fido

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 2, 2026 License: BSD-3-Clause Imports: 5 Imported by: 0

README

fido

Speaks the FIDO client-to-authenticator protocol to a security key, in pure Go with CGO_ENABLED=0, on any operating system.

k, err := fido.Open(ctx, transport)   // handshake, channel, capabilities
defer k.Close()
fmt.Println(k)                        // YubiKey FIDO (CTAPHID v2, firmware 5.7.4, wink, ctap2, ctap1)
err = k.Wink(ctx)                     // the key blinks: which one is this?

Nothing here is platform-specific. A Transport moves 64-byte reports to and from one authenticator, and where those come from — IOKit on macOS, hidraw on Linux, WebHID in a browser — is somebody else's problem. The macOS one is go-macos/fido.

The second factor

An operating system already offers the first: Touch ID, a watch, a passcode. Those answer is the person at this machine the one who unlocked it? A security key answers a different question: is the thing they carry present, right now, and did a human touch it? Multi-factor means asking both and getting two independent answers.

Reading the reference first

In pure Go, client-side, there was nothing to reuse. The reference of the field is Yubico's libfido2, written in C; its one serious Go binding wraps it through cgo and has not moved in ten months, and the pure-Go candidates are small and young. So libfido2 and the CTAP specification are read here as documentation, and the code is owned.

Reading them first caught two faults that testing against a key would not have, because a short ping comes back the same either way:

  • CTAPHID_KEEPALIVE is not an answer. A key waiting for a finger sends one about every hundred milliseconds for as long as the person takes. A reader that returns the first complete message hands back a status byte instead of the reply — and does it early, so it reads as the key talking nonsense rather than as a missing feature.
  • The message limit is the framing's, not the length field's. Two length bytes would allow 65535; the framing reaches 7609, because past that the sequence numbers run into the high bit and a key reads them as the start of a new message. An earlier draft used 65535 and carried a comment asserting the framing reached it. The arithmetic was wrong.

What is here

The CTAPHID framing, the handshake, channel negotiation, capabilities, ping and wink — covered to 100%, with a fake transport that reassembles what it is sent, so a key that refuses, a key that stalls, a key that keeps saying it is busy and another program talking on the same key are all ordinary tests.

CBOR, makeCredential, getAssertion and ClientPIN are not here yet.

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

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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

View Source
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

func Split(channel uint32, cmd byte, data []byte) ([][]byte, error)

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.

func ParseInit

func ParseInit(data []byte) (InitReply, error)

ParseInit reads the reply to CmdInit.

It does NOT check the nonce: whether a mismatched nonce is somebody else's reply to ignore or a fault to report is the caller's decision, and this returns what arrived.

type Key

type Key struct {
	// contains filtered or unexported fields
}

Key is an authenticator with a negotiated channel.

func Open

func Open(ctx context.Context, t Transport) (*Key, error)

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) Channel

func (k *Key) Channel() uint32

Channel is the negotiated channel id.

func (*Key) Close

func (k *Key) Close() error

Close releases the transport.

func (*Key) Name

func (k *Key) Name() string

Name is what the key calls itself.

func (*Key) Ping

func (k *Key) Ping(ctx context.Context, data []byte) ([]byte, error)

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) String

func (k *Key) String() string

String renders the key the way a log reads.

func (*Key) Version

func (k *Key) Version() Version

Version is the key's firmware and transport version.

func (*Key) Wink

func (k *Key) Wink(ctx context.Context) error

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

type Message struct {
	Channel uint32
	Cmd     byte
	Data    []byte
}

Message is a CTAPHID message reassembled from its reports.

func (Message) IsError

func (m Message) IsError() bool

IsError reports whether the key refused, rather than answered.

func (Message) String

func (m Message) String() string

String renders the message the way a probe log reads.

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.

type Version

type Version struct {
	// CTAPHID is the transport protocol version, 2 on everything current.
	CTAPHID byte
	// Major, Minor and Build are the key's own firmware version.
	Major, Minor, Build byte
}

Version is what a key answered about itself.

func (Version) String

func (v Version) String() string

String renders the version the way the manufacturer writes it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL