go-ctap/pcsc is a cgo-free Go library for PC/SC smart-card readers. It
supports Windows, macOS and Linux.
Warning
This module is under active development. Its public API may change during v0.x.
The package supports:
- reader enumeration and connection events;
- shared, exclusive and direct connections;
- card status and ATR;
- APDU and reader control commands;
- transactions and reconnect;
- reader and card attributes.
It uses the native PC/SC service on each platform:
| Platform | Backend |
|---|---|
| Windows | winscard.dll |
| macOS | PCSC.framework |
| Linux | libpcsclite.so.1 |
The native library is loaded on the first PC/SC operation, not during package
import. If it is not available, the operation returns pcsc.ErrUnavailable.
This lets an application keep PC/SC support optional.
go get github.com/go-ctap/pcsc@latestSee go.mod for the required Go version.
This example lists all readers and opens the first one:
package main
import (
"fmt"
"log"
"github.com/go-ctap/pcsc"
)
func main() {
var readerName string
for reader, err := range pcsc.Enumerate() {
if err != nil {
log.Fatal(err)
}
fmt.Printf("reader: %s, state: 0x%x, ATR: %x\n",
reader.Name, reader.State, reader.ATR)
if readerName == "" {
readerName = reader.Name
}
}
if readerName == "" {
log.Fatal("no PC/SC readers found")
}
card, err := pcsc.Open(readerName)
if err != nil {
log.Fatal(err)
}
defer card.Close()
status, err := card.Status()
if err != nil {
log.Fatal(err)
}
fmt.Printf("protocol: %d, ATR: %x\n", status.Protocol, status.ATR)
}Open uses a shared connection, offers T=0 and T=1, and leaves the card
unchanged on Close. These settings can be changed with options:
card, err := pcsc.Open(
readerName,
pcsc.WithShareMode(pcsc.ShareModeExclusive),
pcsc.WithPreferredProtocols(pcsc.ProtocolT1),
pcsc.WithDisconnectDisposition(pcsc.DispositionResetCard),
)In direct mode, the default preferred protocol is ProtocolUndefined.
Transmit sends one raw APDU. The returned data includes the final SW1 and SW2
status bytes:
response, err := card.Transmit(ctx, []byte{
0x00, 0x84, 0x00, 0x00, 0x08, // GET CHALLENGE
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("response: %x\n", response)Control sends a reader-specific control command. The last argument is the
expected response-buffer size:
response, err := card.Control(ctx, pcsc.ControlCode(3400), nil, 4096)Control commands are not retried automatically because they may have side effects.
The package also provides:
Statusfor reader names, card state, protocol and ATR;BeginTransactionandEndTransactionfor exclusive card access;Reconnectfor new connection settings or card reset;GetAttributeandSetAttributefor PC/SC attributes.
Events first sends the current reader and card state. It then sends live
changes in order:
DeviceEventReaderConnected;DeviceEventReaderDisconnected;DeviceEventCardInserted;DeviceEventCardRemoved.
Each event receiver has its own PC/SC context and must be closed:
receiver, err := pcsc.Events()
if err != nil {
log.Fatal(err)
}
defer receiver.Close()
for event := range receiver.Listen() {
if event.Err != nil {
log.Printf("PC/SC events stopped: %v", event.Err)
break
}
fmt.Printf("%s: %s\n", event.Type, event.ReaderInfo.Name)
}If the receiver stops because of a PC/SC error, the last event contains that
error in DeviceEvent.Err.
Transactions stop other PC/SC applications from sending commands between your operations:
if err := card.BeginTransaction(ctx); err != nil {
log.Fatal(err)
}
defer card.EndTransaction(pcsc.DispositionLeaveCard)A transaction does not provide rollback. The disposition passed to
EndTransaction only tells PC/SC what to do with the card.
One Card runs one operation at a time. A call waiting for another operation
can be canceled through its context.
On Windows, the package asks PC/SC to cancel an active card operation. On
macOS and Linux, an APDU, control or transaction call may continue in the
background after the Go method returns ctx.Err(). The card stays locked until
the native call ends, and Close waits for it.
Do not retry a canceled APDU or control command automatically. The card or reader may already have processed it.
PC/SC errors support errors.Is with the package error values:
if errors.Is(err, pcsc.ErrNoCard) {
log.Print("insert a smart card")
}The original PC/SC result is available as *pcsc.Error. It contains the failed
operation and the native result code:
var pcscErr *pcsc.Error
if errors.As(err, &pcscErr) {
log.Printf("%s failed with 0x%08x", pcscErr.Operation, pcscErr.Code)
}Common errors include ErrUnavailable, ErrNoReaders, ErrNoCard,
ErrCardReset, ErrCardRemoved and ErrClosed.
CardStatevalues are platform-specific. Windows uses an enum. macOS and Linux use a bitmask.ProtocolRawhas a different native value on Windows and Unix systems.- Direct connections have small platform differences. Use the Go constants instead of hard-coded native values.
- Linux needs pcsc-lite, a running
pcscdservice and permission to access the reader.
This package provides low-level PC/SC access. It does not provide:
- ISO 7816 APDU encoding;
- status-word processing;
GET RESPONSEor command chaining;- application protocols such as CTAP, PIV or OpenPGP.
Use go-ctap/ctap for FIDO2 commands and
go-ctap/token2 for Token2 device support.
Run the normal test suite without hardware:
go test ./...The generic hardware test is read-only and works with any connected smart card:
PCSC_TEST=1 go test -run TestPCSCLifecycle -vThe CTAP-over-NFC test needs a FIDO authenticator connected through a PC/SC reader:
PCSC_TEST_CTAPNFC=1 go test -run TestCTAPNFC -vApache License 2.0. See LICENSE.