Documentation
¶
Overview ¶
Package pcscid identifies any smart card presented to a reader registered with the local pcscd: NFC, mifare, RFID, contact smart cards and everything else the daemon manages.
It is pure Go without cgo, on Unix it speaks the pcscd daemon wire protocol directly. The identifier of a card, its btag, is a short hash over the card type and the unique tag of the individual card, its UID read through the PC/SC part 3 GET DATA APDU. A btag always requires a valid UID: when no UID can be read (a failed probe or an ISO/IEC 14443-3 random UID that identifies nothing) the card presentation carries no btag and no insertion event is reported — silently in normal operation, with the full diagnostic trace under a debug level logger.
Reader unit identity: the per-unit discriminator of a reader.
The reader name alone identifies the model, not the unit, so the watch loop probes the driver for two per-unit facts while a card is connected, in one connection:
- SCARD_ATTR_VENDOR_IFD_SERIAL_NO, the USB iSerial string burned into the reader hardware. Portable: the same unit carries it on every machine, port and daemon restart. Some reader families, for example the ACS ACR122U, ship a constant placeholder ("0" on every unit), which is filtered here.
- SCARD_ATTR_CHANNEL_ID, the USB bus and device address, resolved through sysfs to the kernel physical port path (devpath, "2-1.3"). The port path anchors the unit to the port it sits on: stable across daemon restarts and reboots, but not portable, moving the reader to another port changes it. It is the opt-in fallback (Options.USBPathID, PCSCID_USB_PATH_ID in the sample app) when no usable serial exists.
When the driver serves neither usable serial nor channel id, the sysfs USB tree is scanned for the reader's device directly: the USB manufacturer and product strings must match the pcscd reader name (pcscd derives that name from the same vendor and product strings) and the device must carry a CCID interface. Exactly one matching device identifies the port, two identical models cannot be told apart without the channel id. With USBPathID enabled an unresolved port is reported as a qualified error, not swallowed silently: the reader then keeps the model level tag, which two identical units share.
MachineID (readerid.go bottom) is the machine level identity, the stable hardware MAC addresses of the physical ethernet ports, mixed into the reader tag when enabled (Options.MACID, PCSCID_MAC_ID in the sample app).
Reader inventory: the startup evaluation of every reader currently registered with the local pcscd.
IdentifyReaders answers the operator question "what exactly is attached here and which tags will it serve" before the first card is presented: it enumerates the daemon's reader state array and derives every identity tier of each reader — the model level tag from the reader name, the per-unit facts (hardware serial, physical USB port path) when a card is present to probe them with, and the effective tag under the configured Options, including the machine component of Options.MACID.
The per-unit facts need an open card connection (SCardGetAttrib talks to the driver through the card), so a reader reported without a card carries the model level tag only; its Tier says "model". The same limitation applies to Watch's insert events, the report just makes it visible at startup instead of after the first scan.
Index ¶
- func Btag(cardType string, tag []byte) string
- func DetectType(atr []byte) string
- func MachineID() string
- func ReaderTag(reader string) string
- func ReaderTagWithMachine(reader, serial, port, machine string) string
- func ReaderTagWithSerial(reader, serial string) string
- func ReaderTagWithUnit(reader, serial, port string) string
- func RequireLoopback(addr string, allowRemote bool) error
- func Version() string
- func Watch(ctx context.Context, opts *Options) (<-chan Event, error)
- type ATR
- type Bridge
- type BridgeEvent
- type BridgeOptions
- type Card
- type Event
- type Kind
- type Options
- type ReaderInfo
- type Signer
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Btag ¶ added in v0.0.104
Btag derives the btag, the short unique identifier of a card, from its type and the unique tag of the individual card, its UID. The btag is 10 characters from the digits and lower case letters in three dash separated groups, xxx-xxx-xxxx, stable across readers and re-presentations, and different for two cards of the same type with different tags. It always requires a valid UID: a card whose UID cannot be read is never served a btag derived from its ATR, which would only identify the card type, not the individual card.
func DetectType ¶
DetectType returns a card type name for an answer to reset. The name distinguishes card families and models, the unique identifier of an individual card comes from its UID, see Identify.
func MachineID ¶ added in v0.0.141
func MachineID() string
MachineID returns the stable hardware identity of this machine: the MAC addresses of its physical ethernet network interfaces, sorted, joined with "|" (for example "aa:bb:cc:dd:ee:01|aa:bb:cc:dd:ee:02"), or the empty string when the machine has no such interface. It reads the network stack through sysfs, pure Go: an interface counts as a stable hardware ethernet port when it is of type ethernet (ARPHRD_ETHER), is backed by a physical device (the sysfs device symlink, absent on loopback, bridges, bonds, vlans, veth and every other virtual interface), carries no wireless phy (the phy80211 entry, present on every wifi interface) and has a real, non zero MAC burned in. MACs of different physical ports never repeat on one machine, and the MAC is the most stable identifier commodity hardware carries, it survives reboots, OS reinstalls and software reconfiguration.
func ReaderTag ¶ added in v0.0.122
ReaderTag derives the stable short unique identifier of a reader from its pcscd reader name. The tag is 8 characters from the digits and lower case letters in three dash separated groups, xx-xxxx-xx, the same for the same reader across restarts, sockets, machines and USB ports.
pcscd reader names end in volatile hotplug indices, for example "ACS ACR122U 01 00 00", where the trailing number groups change with the USB port, the boot order and the machine. Those groups are stripped before hashing, so the tag follows the reader hardware, not its point of attachment. Two identical reader models whose names carry no serial number share one tag, ReaderTagWithSerial fixes that with the serial from the driver when one is available.
func ReaderTagWithMachine ¶ added in v0.0.141
ReaderTagWithMachine adds the machine identity to the reader tag: the machine component (MachineID or an explicit override, wired by Options.MACID / PCSCID_MAC_ID) is mixed into every derivation when it is not empty, so identical readers on different machines serve distinct tags. The price is portability: a tag derived with a machine component does not follow the reader to another machine. The hash domain is pcscid/reader/m1 with the unit fact kind prefixed ("serial:...", "port:...", or "model"), so a serial and a port path of the same text can never collide. An empty machine falls back to ReaderTagWithUnit.
func ReaderTagWithSerial ¶ added in v0.0.137
ReaderTagWithSerial derives the reader tag from the reader name and the hardware serial number of the unit (Event.ReaderSerial, the USB iSerial string from SCARD_ATTR_VENDOR_IFD_SERIAL_NO). Readers of the same model, whose names hash to the same ReaderTag, get distinct tags, one per physical unit, still stable across machines, sockets, USB ports and daemon restarts: the serial is burned into the reader hardware. An empty serial falls back to the plain name based tag.
func ReaderTagWithUnit ¶ added in v0.0.139
ReaderTagWithUnit derives the reader tag from every per-unit fact the driver could serve, in decreasing portability:
- a usable hardware serial (ReaderTagWithSerial semantics, hash domain pcscid/reader/v2): one tag per unit, portable across machines and USB ports.
- the kernel physical USB port path (Event.ReaderPort, hash domain pcscid/reader/v3): one tag per unit as long as it stays in its port, stable across daemon restarts and reboots, but moving the reader or re-plugging it into another port changes the tag.
- neither: the model level ReaderTag.
func RequireLoopback ¶ added in v0.0.127
RequireLoopback guards the default deployment posture of a Bridge: card identities must never leave the kiosk. Only a loopback address (or the literal localhost) is accepted without the explicit allowRemote escape hatch.
func Version ¶
func Version() string
Version returns the semantic version of the pcscid library or binary. The value is injected at build time via the go linker (-ldflags "-X paepcke.de/pcscid.version=<semver>"). When not injected, for plain go builds, it falls back to the VCS revision recorded in the Go build info, and finally to defaultVersion, the current release tag.
func Watch ¶
Watch starts watching all readers of the local pcscd and reports card insertions and removals on the returned channel. Cards that are already present when Watch starts are reported as inserted.
When the pcscd socket cannot be reached, Watch fails with that error, the pcscd service is a hard requirement.
The channel is closed once ctx is cancelled.
Types ¶
type ATR ¶
type ATR struct {
// TS is the initial character, 0x3B direct or 0x3F inverse
// convention.
TS byte
// T0 carries the presence mask of the first interface byte group
// in its high nibble and the number of historical bytes in its
// low nibble.
T0 byte
// InterfaceBytes lists the TA, TB, TC and TD bytes of each
// interface byte group in order of appearance.
InterfaceBytes [][]byte
// Historical is the historical byte sequence.
Historical []byte
// Protocols are the offered transmission protocols, starting with
// the default T=0, deduplicated in order of first appearance.
Protocols []byte
// HasTCK reports whether a checksum byte is present, which is the
// case as soon as any protocol other than T=0 is offered.
HasTCK bool
// TCK is the checksum byte, XOR of all bytes after TS.
TCK byte
}
ATR is a parsed answer to reset as defined by ISO/IEC 7816-3.
func ParseATR ¶
ParseATR parses the answer to reset of a card. It tolerates the slightly non-conformant pseudo ATRs some contactless readers emit.
func (ATR) ChecksumOK ¶
ChecksumOK reports whether the TCK matches the XOR of all bytes after TS and before the TCK. It is true when the ATR carries no checksum. The atr argument is the raw ATR the receiver was parsed from.
type Bridge ¶ added in v0.0.127
type Bridge struct {
// contains filtered or unexported fields
}
Bridge feeds Watch events through the tag derivation and the dedup filter and serves the result as SSE and polling JSON over HTTP. The zero value is not usable, use NewBridge.
func NewBridge ¶ added in v0.0.127
func NewBridge(opts *BridgeOptions) *Bridge
NewBridge returns a Bridge recording presentations with the given options.
func (*Bridge) Feed ¶ added in v0.0.127
Feed applies one Watch event. Insertions with an identified card become one bridge presentation, {reader tag, card btag, line signature}, filtered by the dedup guard; removals and card-less events are ignored — one presentation per scan. The reader tag is Event.ReaderTag as derived by Watch (unit facts and machine identity per the Watch Options); events without one, for example from a hand built event loop, fall back to the local derivation. Feed never blocks.
func (*Bridge) Handler ¶ added in v0.0.127
Handler returns the HTTP surface of the bridge: the liveness probe, the SSE stream and the polling fallback, all with permissive CORS headers so a kiosk page served from an HTTPS origin may read them from a loopback http origin.
func (*Bridge) Serve ¶ added in v0.0.127
Serve listens on addr and serves the bridge HTTP surface until ctx is cancelled (graceful shutdown, a nil return) or the listener fails. RequireLoopback should be consulted first, Serve itself does not guard the address.
func (*Bridge) ServeListener ¶ added in v0.0.128
ServeListener serves the bridge HTTP surface on an already bound listener until ctx is cancelled (graceful shutdown, a nil return) or the listener fails. Binding the listener in the caller makes a bad address fail fast instead of asynchronously.
type BridgeEvent ¶ added in v0.0.127
type BridgeEvent struct {
ID int64 `json:"id"`
Reader string `json:"reader"`
Card string `json:"card"`
Sig string `json:"sig,omitempty"`
}
BridgeEvent is one card presentation served over the bridge HTTP surface. Reader is the reader tag (xx-xxxx-xx), Card the card btag (xxx-xxx-xxxx). ID is the monotonic cursor the /pending polling fallback replays with (after=last seen ID). Sig is the base64 SSHSIG line signature of the exact output line "#<Reader>:<Card>" when the bridge runs with a Signer (BridgeOptions.Signer, the PCSCID_SIGN_KEY of the sample app): byte-identical to the signature the binary prints after the '$' separator on stdout, so a consumer verifying the bridge event (chrony's POST /terminal/punch, for example) verifies the same bytes ssh-keygen -Y verify would check against the key's public half under the namespace "pcscid". Without a Signer the field is omitted.
type BridgeOptions ¶ added in v0.0.127
type BridgeOptions struct {
// Capacity is how many presentations the /pending ring retains,
// default 64.
Capacity int
// DedupWindow is how long the same reader+card pair is suppressed
// after one presentation, default 3s.
DedupWindow time.Duration
// StartupGrace is how long every event is suppressed after
// NewBridge, default 2s. It must cover the initial "already
// present" reports of Watch.
StartupGrace time.Duration
// Signer, when set, signs every served presentation: each BridgeEvent
// carries the base64 SSHSIG signature of its exact output line
// "#<reader>:<card>" in Sig. The sample app wires the PCSCID_SIGN_KEY
// signer here so the bridge delivers the same signature the stdout
// lines carry — a kiosk page can then forward the event verbatim to a
// consumer that enforces signed punches. Without a signer the events
// stay unsigned (Sig omitted).
Signer *Signer
}
BridgeOptions tunes NewBridge. A nil *BridgeOptions selects every default.
type Card ¶
type Card struct {
// ID is the btag, the short unique identifier, the output of Btag.
ID string
// Type is the detected card type name, see DetectType.
Type string
// UID is the unique tag of the individual card, nil when it
// could not be read.
UID []byte
// ATR is the answer to reset of the card.
ATR []byte
// Reader is the reader name the card was seen on. The same card
// reports the same ID on every reader.
Reader string
// Source is what the ID was derived from: "uid" when the card
// served its unique tag, "none" when it did not (a failed UID
// read or an ISO/IEC 14443-3 random UID that identifies nothing).
// "none" means ID is empty: no btag is served without a valid
// UID, the presentation is skipped instead of degraded to an ATR
// derived type level tag.
Source string
}
Card is the identification of one presented smart card.
type Event ¶
type Event struct {
// Kind is what happened.
Kind Kind
// Card is set for KindInsert.
Card *Card
// Reader is the reader name the event belongs to.
Reader string
// ReaderTag is the reader tag as derived by Watch from every
// identity source the Options enabled: the reader name, the unit
// facts (ReaderSerial, ReaderPort) and the machine identity
// (Options.MACID). It is set for KindInsert events, empty for
// KindRemove, which carries no card connection to probe facts
// with. It equals ReaderTagWithUnit(ev.Reader, ev.ReaderSerial,
// ev.ReaderPort) unless MACID mixed the machine in.
ReaderTag string
// ReaderSerial is the hardware serial number of the reader
// (SCARD_ATTR_VENDOR_IFD_SERIAL_NO, the USB iSerial string of the
// unit) when the driver serves a usable one, empty otherwise.
// Constant vendor placeholders, for example the all zero serial of
// the ACS ACR122U family, are filtered and also answer empty. It is
// probed with the card connection of an insertion, so only
// insertions carry it. See ReaderTagWithUnit.
ReaderSerial string
// ReaderPort is the kernel physical USB port path of the reader
// (sysfs devpath, for example "2-1.3"), resolved through its
// SCARD_ATTR_CHANNEL_ID bus/device address when the driver serves
// no usable serial and Options.USBPathID allows it. It anchors the
// unit to its port: stable across daemon restarts and reboots, but
// moving the reader to another port changes it. Only insertions
// carry it.
ReaderPort string
}
Event is one reader state change.
type Options ¶
type Options struct {
// Logger receives the verbose debug trace. In normal operation a
// discarded logger keeps the library silent. The cmd/pcscid
// sample app wires DEBUG=1 to it.
Logger *slog.Logger
// SocketPath overrides the pcscd socket path. Empty selects the
// platform default, PCSCLITE_CSOCK_NAME overrides it on Unix.
SocketPath string
// SysfsUSBRoot overrides the sysfs root scanned to resolve a
// reader's USB bus/device address to its physical port path,
// default /sys/bus/usb/devices. It exists to test the port based
// reader identity fallback against a fake device tree.
SysfsUSBRoot string
// USBPathID allows the physical USB port path as the per-unit
// fallback for readers serving no usable hardware serial (the ACS
// ACR122U family). Off by default, because the port path is stable
// only as long as the reader stays in its port: moving the reader
// to another port or machine changes the tag. The sample app wires
// PCSCID_USB_PATH_ID to it.
USBPathID bool
// MACID mixes the machine identity, the stable hardware MAC
// addresses of the physical ethernet ports (MachineID), into every
// reader tag: identical readers on different machines get distinct
// tags, at the price of the tag no longer following the reader to
// another machine. Off by default. The sample app wires
// PCSCID_MAC_ID to it.
MACID bool
// MachineID overrides the machine identity used when MACID is
// enabled. Empty uses MachineID(), which is empty again on machines
// without a physical ethernet port; those then keep the unit level
// tags. It exists as the injection point of a caller chosen
// machine identity and for tests.
MachineID string
}
Options tunes Watch. A nil Options selects every default.
type ReaderInfo ¶ added in v0.0.148
type ReaderInfo struct {
// Reader is the pcscd reader name, for example
// "ACS ACR122U 00 00".
Reader string
// ModelTag is the model level tag (ReaderTag): derived from the
// normalized reader name alone, identical for two units of the
// same model.
ModelTag string
// Serial is the hardware serial number of the unit
// (SCARD_ATTR_VENDOR_IFD_SERIAL_NO) when the driver serves a
// usable one, empty otherwise. It is only probeable while a
// card is present.
Serial string
// Port is the kernel physical USB port path of the unit
// (sysfs devpath, for example "2-1.3") when Options.USBPathID
// allows it and the reader serves no usable serial. It is only
// probeable while a card is present.
Port string
// Tier names the portability tier of the per-unit facts that
// produced Tag: "serial" (one tag per unit, portable), "port"
// (one tag per USB port, not portable) or "model" (no per-unit
// fact readable, name only).
Tier string
// Tag is the effective reader tag under the configured Options:
// ReaderTagWithMachine over the probed facts, the tag every
// insertion event of this reader will carry in Event.ReaderTag.
Tag string
// CardPresent reports whether a card sits on the reader.
CardPresent bool
// Card is the identification of the presented card, the same
// derivation an insertion event carries, nil when CardPresent
// is false. A card whose UID could not be read carries Source
// "none" and an empty ID (no btag without a valid UID), only
// the type and ATR facts are reported.
Card *Card
}
ReaderInfo is the evaluated identity of one reader registered with the local pcscd, one entry of the IdentifyReaders inventory.
func IdentifyReaders ¶ added in v0.0.148
func IdentifyReaders(opts *Options) ([]ReaderInfo, error)
IdentifyReaders evaluates every reader currently registered with the local pcscd and answers the inventory of their identities: the reader name, every derivable tag and the per-unit facts behind it, plus the identification of a card that is already present. The evaluation uses the same options and derivations as Watch, so the reported Tag is exactly the tag the reader's insertion events will carry. Readers with a presented card are probed over a full card connection (serial, port, card UID). Readers without a card cannot open the connection the driver attributes need, but with USBPathID enabled their USB port is still pinned from the sysfs device scan when it is unambiguous (or every other identical unit is already owned by its own probe); they report the model level tier otherwise.
When the pcscd socket cannot be reached, IdentifyReaders fails with that error, the pcscd service is a hard requirement.
type Signer ¶ added in v0.0.132
type Signer struct {
// contains filtered or unexported fields
}
Signer signs output lines with an ed25519 private key in the OpenSSH SSHSIG format.
func NewSigner ¶ added in v0.0.132
NewSigner loads an OpenSSH ed25519 private key from path and returns a Signer over it. The key must be usable for signing: an unencrypted ("openssh-key-v1" container, cipher "none") ed25519 key whose private half matches its public half. Encrypted keys, keys of other types and damaged files fail with an error instead of silently producing unsigned output.
func (*Signer) Sign ¶ added in v0.0.132
Sign returns the SSHSIG signature of msg as base64. The SSHSIG blob is the binary signature format inside the armored block of `ssh-keygen -Y sign`; encoding it as plain base64 keeps one signed output line a single line, it re-arms into the PEM style block by wrapping it at 70 columns between the armor lines.
func (*Signer) SignLine ¶ added in v0.0.132
SignLine returns line with the separator and the base64 encoded SSHSIG signature of the line itself appended: line$<base64>. The signature covers exactly the bytes of line as emitted, so a signature over "#xx-xxxx-xx:xxx-xxx-xxxx" is stable text a verifier can re-sign and compare.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
pcscid
command
Command pcscid prints a short unique identifier for every smart card presented to any reader registered with the local pcscd.
|
Command pcscid prints a short unique identifier for every smart card presented to any reader registered with the local pcscd. |
|
internal
|
|
|
pcscfake
Package pcscfake implements an in-process fake pcscd daemon.
|
Package pcscfake implements an in-process fake pcscd daemon. |
|
Package pcsc implements a pure Go client for the pcscd IPC protocol on Linux.
|
Package pcsc implements a pure Go client for the pcscd IPC protocol on Linux. |