Documentation
¶
Overview ¶
Package fido speaks the FIDO client-to-authenticator protocol to a security key, in pure Go with CGO_ENABLED=0 and on any operating system.
Nothing here is platform-specific. A Transport carries 64-byte reports to and from one authenticator, and where those reports come from -- IOKit on macOS, hidraw on Linux, WebHID in a browser -- is somebody else's problem. See go-macos/fido for the macOS one.
The second factor ¶
An operating system already offers the first factor: Touch ID, a watch, a passcode. Those answer one question -- is the person at this machine the one who unlocked it? A security key answers a different one: is the thing they carry present, right now, and did a human touch it? Multi-factor means asking both, and getting two independent answers.
CTAPHID ¶
A message is cut into 64-byte reports: one INITIALISATION packet carrying the command and the total length, then CONTINUATION packets numbered from zero. A channel id is negotiated first, with CmdInit, and every later message carries it.
Yubico's libfido2 and the CTAP specification were read as documentation for this, and the reading caught two faults that testing against a key would not have: see CmdKeepalive and MaxMessage.
Index ¶
- Constants
- Variables
- func Split(channel uint32, cmd byte, data []byte) ([][]byte, error)
- type Capabilities
- type InitReply
- type Key
- func (k *Key) Capabilities() Capabilities
- func (k *Key) Channel() uint32
- func (k *Key) Close() error
- func (k *Key) Name() string
- func (k *Key) Ping(ctx context.Context, data []byte) ([]byte, error)
- func (k *Key) String() string
- func (k *Key) Version() Version
- func (k *Key) Wink(ctx context.Context) error
- type Message
- type Reassembler
- type Transport
- type Version
Constants ¶
const ( // CmdPing echoes its payload back. It is the honest way to check that a // channel works, because a key that answers a ping with the same bytes has // received, framed and returned them. CmdPing byte = 0x01 // CmdInit negotiates a channel. Sent on [BroadcastChannel] with an // eight-byte nonce, it comes back with that nonce, a fresh channel id, and // what the key can do. CmdInit byte = 0x06 // CmdWink makes the key blink or flash. It changes nothing and stores // nothing, and it is the cheapest way to ask a person WHICH of the keys in // front of them is this one. CmdWink byte = 0x08 // CmdCBOR carries a CTAP2 message. Not used yet; named so the capability // below is not a number without a meaning. CmdCBOR byte = 0x10 // CmdMsg carries an older CTAP1/U2F message. CmdMsg byte = 0x03 // CmdLock reserves the key for one channel. Optional, and not used here. CmdLock byte = 0x04 // CmdCancel abandons a request the key is still working on -- which for // anything needing a touch means the person never touched it. CmdCancel byte = 0x11 // CmdKeepalive is what a key sends WHILE it works, and it is not an answer. // A key waiting for a finger sends one about every hundred milliseconds for // as long as the person takes, and a reader that returns the first complete // message it sees returns THAT instead of the reply. libfido2 skips them in // its receive loop and keeps waiting; so does this. CmdKeepalive byte = 0x3B // CmdError is what a key answers with when it will not do something. CmdError byte = 0x3F )
The CTAPHID commands used here. The command byte travels with its high bit set, which is what distinguishes an initialisation packet from a continuation one -- a continuation packet carries a sequence number in the same position, and sequence numbers never reach 0x80.
const BroadcastChannel uint32 = 0xFFFFFFFF
BroadcastChannel is the channel a key is addressed on before it has given one out. Only CmdInit may be sent there.
const MaxContinuations = 128
MaxContinuations is how many continuation packets one message may use. The sequence number shares its byte with the command, and a command is marked by its high bit, so a sequence number may never reach 0x80. libfido2 checks the same thing when it sends.
const MaxMessage = initPayload + MaxContinuations*contPayload
MaxMessage is the largest message CTAPHID can actually carry.
The two length bytes would allow 65535, and an earlier version of this file used that -- with a comment claiming the framing reached it. It does not: 57 bytes in the initialisation packet plus 128 continuations of 59 is 7609, and a longer message would need sequence numbers past 0x7F, which the key would read as initialisation packets. The arithmetic was wrong and the comment asserted it anyway.
const ReportSize = 64
ReportSize is the size of every CTAPHID report, in bytes. The specification fixes it at 64 and every authenticator observed publishes exactly that in both directions.
Variables ¶
var ( // ErrNoKey means no FIDO authenticator is attached. ErrNoKey = errors.New("fido: no security key is attached") // ErrTooLong means the message will not fit in a CTAPHID transfer. ErrTooLong = errors.New("fido: the message is longer than CTAPHID can carry") // ErrShortPacket means a report arrived that is not a whole CTAPHID packet. ErrShortPacket = errors.New("fido: a report was shorter than a CTAPHID packet") // ErrWrongChannel means a report arrived for a different channel, which is // what happens when two programs talk to one key at once. ErrWrongChannel = errors.New("fido: the report belongs to another channel") // ErrOutOfOrder means continuation packets did not arrive in sequence. ErrOutOfOrder = errors.New("fido: a continuation packet arrived out of order") // ErrTruncated means the key stopped sending before the length it promised. ErrTruncated = errors.New("fido: the key sent less than it said it would") )
Errors this package returns for what a caller can act on.
Functions ¶
func Split ¶
Split cuts a message into the reports that carry it: one initialisation packet then as many continuation packets as are needed, each exactly ReportSize bytes and zero-padded.
Every packet is padded to the full report size deliberately. A short output report is legal HID and some keys accept it, but the specification says the transfer is report-sized and a key that reads past the bytes given would read whatever the previous transfer left there.
Types ¶
type Capabilities ¶
type Capabilities byte
Capabilities is what a key said it can do, in the CTAPHID_INIT reply.
const ( // CapWink means [CmdWink] does something visible. CapWink Capabilities = 0x01 // CapCBOR means the key speaks CTAP2. CapCBOR Capabilities = 0x04 // CapNoMsg means the key does NOT speak the older CTAP1/U2F messages. It is // spelled as an absence in the specification, and kept that way here rather // than inverted, so a reader comparing this with the specification does not // have to hold two conventions at once. CapNoMsg Capabilities = 0x08 )
The capability bits.
func (Capabilities) Has ¶
func (c Capabilities) Has(want Capabilities) bool
Has reports whether every bit in c is set.
func (Capabilities) String ¶
func (c Capabilities) String() string
String lists what the key can do, in the order the bits are defined.
type InitReply ¶
type InitReply struct {
// Nonce is the nonce sent, echoed back. A reply whose nonce does not match
// belongs to somebody else's INIT, which happens when two programs
// initialise one key at the same time.
Nonce [8]byte
// Channel is the channel id to use from now on.
Channel uint32
Version Version
Caps Capabilities
}
InitReply is what CTAPHID_INIT answers.
type Key ¶
type Key struct {
// contains filtered or unexported fields
}
Key is an authenticator with a negotiated channel.
func Open ¶
Open negotiates a channel on t and returns the key behind it.
The handshake is not optional and is done here rather than left to the caller: every later message carries the channel id, so a Key without one could not be used for anything, and returning one would be handing back a half-built object to be misused.
The Key takes ownership of t: Key.Close closes it, and so does a failed Open, so a caller never has to unwind a half-open device.
func (*Key) Capabilities ¶
func (k *Key) Capabilities() Capabilities
Capabilities is what the key said it can do.
func (*Key) Ping ¶
Ping sends data and returns what came back, which a working channel returns unchanged. It is how to check a key is still there without asking it to do anything.
func (*Key) Wink ¶
Wink makes the key blink, when it said it can.
It is the one thing here a PERSON can see, which is what makes it useful for more than diagnostics: asked to prove which key is which, or that the key is the one on the desk rather than one left in a hub across the room, a blink answers.
A key without CapWink is not asked, because a key that does not wink answers CTAPHID_ERROR and the caller would have to tell that refusal from a real fault.
type Message ¶
Message is a CTAPHID message reassembled from its reports.
type Reassembler ¶
type Reassembler struct {
// contains filtered or unexported fields
}
Reassembler puts a message back together from the reports a key sends.
It is a type rather than a function because the reports arrive one at a time, from a callback, and the caller needs to know after each one whether the message is complete.
func NewReassembler ¶
func NewReassembler(channel uint32) *Reassembler
NewReassembler collects reports for one channel, refusing any other.
func (*Reassembler) Feed ¶
func (r *Reassembler) Feed(report []byte) (msg Message, done bool, err error)
Feed takes one report. done is true when the message is whole, and the message is then returned and the reassembler is ready for the next one.
A report for another channel is an error rather than something to ignore: two programs talking to one key is a real situation, and silently dropping the other one's traffic would leave a caller waiting for a reply that was already discarded.
type Transport ¶
type Transport interface {
// Send writes one report of exactly [ReportSize] bytes.
Send(report []byte) error
// Receive returns the next report.
Receive(ctx context.Context) ([]byte, error)
// Name is what the device calls itself, for error messages a person reads.
Name() string
// Close releases the device.
Close() error
}
Transport carries CTAPHID reports to and from ONE authenticator.
It is deliberately this small. Everything above it -- framing, the handshake, keepalives, the commands -- is the same on every operating system, so an implementation has only to move 64 bytes at a time and say what the device is called.
Receive must return the NEXT report, blocking until one arrives, the context ends, or the device goes away. It must not drop reports between calls: a key answers a ping in under a millisecond, and an implementation that only listens while asked will miss it.