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 Assertion
- type Attestation
- type AuthData
- type Capabilities
- type Credential
- type Flags
- type GetAssertionRequest
- type Info
- type InitReply
- type Key
- func (k *Key) CBOR(ctx context.Context, cmd byte, params []byte) ([]byte, error)
- func (k *Key) Capabilities() Capabilities
- func (k *Key) Channel() uint32
- func (k *Key) Close() error
- func (k *Key) GetAssertion(ctx context.Context, r GetAssertionRequest) (*Assertion, error)
- func (k *Key) GetInfo(ctx context.Context) (Info, error)
- func (k *Key) KeyAgreement(ctx context.Context, proto PINProtocol) (*ecdh.PublicKey, error)
- func (k *Key) MakeCredential(ctx context.Context, r MakeCredentialRequest) (*Attestation, error)
- func (k *Key) Name() string
- func (k *Key) PINRetries(ctx context.Context) (Retries, error)
- func (k *Key) PINToken(ctx context.Context, pin string, proto PINProtocol, perms Permissions, ...) (Token, error)
- 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 MakeCredentialRequest
- type Message
- type PINProtocol
- type Permissions
- type Reassembler
- type RelyingParty
- type Retries
- type Status
- type Token
- type Transport
- type User
- type Version
Constants ¶
const ( CmdMakeCredential byte = 0x01 CmdGetAssertion byte = 0x02 )
The CTAP2 commands that make and use credentials.
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 AlgES256 int64 = -7
AlgES256 is the COSE identifier for ECDSA over P-256 with SHA-256. It is the one algorithm every FIDO authenticator supports, which is why it is the default here: asking for something else and being refused is a worse first experience than asking for the thing that always works.
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 CmdClientPIN byte = 0x06
CmdClientPIN is authenticatorClientPIN.
const ( // CmdGetInfo asks the authenticator to describe itself. It takes no // parameters at all -- the message is the command byte and nothing else -- // which is why it is the first CTAP2 command worth having: it needs a CBOR // DECODER and no encoder. CmdGetInfo byte = 0x04 )
The CTAP2 commands. Only the ones this package sends are named; a constant for something unsent would be a claim about behaviour nothing here has.
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 Assertion ¶ added in v0.3.0
type Assertion struct {
// Credential says which credential answered. An authenticator may leave it
// out when the request named exactly one.
Credential Credential
AuthData []byte
Parsed AuthData
// Signature covers the authenticator data followed by the client data
// hash, in that order. Verifying it is the caller's, with the public key
// from the registration.
Signature []byte
// UserID is the user handle, present for a discoverable credential.
UserID []byte
// Available is how many credentials could have answered, when the
// authenticator said. More than one means the caller may ask for the next.
Available uint
}
Assertion is what an authenticator answers with.
type Attestation ¶ added in v0.3.0
type Attestation struct {
// Format names the attestation statement's shape: "packed", "none", and
// others.
Format string
// AuthData is the raw authenticator data, kept because a signature is over
// these bytes and re-encoding [Parsed] would not reproduce them.
AuthData []byte
Parsed AuthData
// Statement is the attestation statement, left as CBOR: verifying it means
// choosing a certificate library, and that choice is not this package's.
Statement cbor.RawMessage
}
Attestation is what a registration returns.
type AuthData ¶ added in v0.3.0
type AuthData struct {
// RPIDHash is SHA-256 of the relying party id. It is a hash, not a name:
// the authenticator never learns the name.
RPIDHash [32]byte
Flags Flags
// SignCount rises with use on authenticators that keep one. A count that
// goes BACKWARDS is the classic sign of a cloned credential -- and a count
// that stays at zero means this authenticator does not keep one, which is
// allowed and is not evidence of anything.
SignCount uint32
// The rest is present only when [FlagAT] is set, which is a registration.
AAGUID [16]byte
CredentialID []byte
// COSEKey is the credential's public key, as COSE_Key CBOR. It is kept
// encoded because that is what arrived; [AuthData.PublicKey] turns it into
// an ECDSA key when it is one.
COSEKey cbor.RawMessage
// Extensions is present only when [FlagED] is set.
Extensions cbor.RawMessage
}
AuthData is the authenticator data an authenticator signs over.
It is the part of a FIDO answer that says what happened: which relying party it was for, whether a person was present and whether they were verified. The signature covers it, so reading it is how a caller learns anything at all beyond "the key answered".
func ParseAuthData ¶ added in v0.3.0
ParseAuthData reads authenticator data.
The layout is fixed-width up to the flag byte and then conditional on it, which is where the mistakes live: a parser that assumes attested data is present reads a credential id out of an assertion's extensions, and one that assumes it is absent silently ignores a registration. Both flags are obeyed here, and a length that does not add up is an error rather than a slice.
func (AuthData) PublicKey ¶ added in v0.3.0
PublicKey turns the credential's COSE key into an ECDSA public key.
Only P-256 with ES256 is decoded, which is not a limitation in practice: it is the one algorithm every FIDO authenticator supports and the one this package asks for by default. Anything else is refused by name rather than half-decoded, because a key of the wrong curve that verified nothing would be worse than no key at all.
This is a CONVERSION and not a verification. Checking a signature needs a decision about what counts as valid -- which algorithms, what to do with a sign counter that went backwards -- and that decision belongs to whoever is protecting something. crypto/ecdsa is the standard library, so returning one of its keys imposes no choice on anybody.
The signature an assertion carries is over the authenticator data followed by the client data hash, in that order, hashed with SHA-256:
signed := append(append([]byte{}, a.AuthData...), clientDataHash...)
digest := sha256.Sum256(signed)
ok := ecdsa.VerifyASN1(pub, digest[:], a.Signature)
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 Credential ¶ added in v0.3.0
type Credential struct {
// Type is "public-key" for everything current.
Type string
// ID is the credential id the authenticator gave out.
ID []byte
// Transports, when known, say how the credential can be reached.
Transports []string
}
Credential names one credential an authenticator holds.
type Flags ¶ added in v0.3.0
type Flags byte
Flags are the authenticator data's flag byte: what the authenticator did before it signed.
const ( // FlagUP means a person was PRESENT: something touched the key. It says // nothing about who. FlagUP Flags = 1 << 0 // FlagUV means the person was VERIFIED -- a PIN, a fingerprint on the key // itself. This is the bit that makes an assertion worth more than // possession. FlagUV Flags = 1 << 2 // FlagBE means the credential may be backed up, and FlagBS that it // currently is. A backed-up credential is not confined to this one device, // which matters to anyone counting it as "something you have". FlagBE Flags = 1 << 3 FlagBS Flags = 1 << 4 // FlagAT means attested credential data follows, which a registration has // and an assertion does not. FlagAT Flags = 1 << 6 // FlagED means extension data follows. FlagED Flags = 1 << 7 )
The flag bits, as the specification numbers them.
type GetAssertionRequest ¶ added in v0.3.0
type GetAssertionRequest struct {
RPID string
ClientDataHash []byte
// Allow narrows the request to particular credentials. Leaving it empty
// asks the authenticator to use a DISCOVERABLE credential, which it only
// has if one was registered with the "rk" option.
Allow []Credential
Options map[string]bool
// Token, when valid, authorises the request. See
// [MakeCredentialRequest.Token].
Token Token
}
GetAssertionRequest asks an authenticator to prove it holds a credential.
type Info ¶ added in v0.2.0
type Info struct {
// Versions are the protocols it speaks, such as "U2F_V2", "FIDO_2_0" and
// "FIDO_2_1".
Versions []string
// Extensions it supports, such as "hmac-secret".
Extensions []string
// AAGUID identifies the MODEL of authenticator, not the individual one.
AAGUID [16]byte
// Options are the capabilities it declares, such as "rk" (it can store
// credentials), "up" (it can test for a person's presence), "uv" (it can
// verify who they are) and "clientPin".
Options map[string]bool
// MaxMsgSize is the largest message it will accept, or zero when it did
// not say.
MaxMsgSize uint
// PINProtocols are the PIN protocol versions it supports.
PINProtocols []uint
// Transports it can be reached over, such as "usb" or "nfc".
Transports []string
}
Info is what an authenticator says about itself, from authenticatorGetInfo.
Only the fields with a caller are decoded. The rest of the reply is left alone rather than half-read: a struct field that is always the zero value because nothing fills it is worse than no field, since it reads as "this authenticator does not have one".
func (Info) Has ¶ added in v0.2.0
Has reports whether the authenticator declares an option AND says it is true. An option a key does not mention is not the same as one it sets to false -- the first means "not applicable", the second "supported but off" -- and both answer no here, which is what a caller deciding whether to try something needs.
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) CBOR ¶ added in v0.2.0
CBOR sends one CTAP2 command and returns the reply's body, with the status byte already checked.
params is the command's CBOR-encoded parameters, or nil for a command that takes none. The command byte is prepended here, because the CTAPHID payload of a CBOR message is the command followed by the parameters and getting that join wrong is the sort of thing each caller should not repeat.
func (*Key) Capabilities ¶
func (k *Key) Capabilities() Capabilities
Capabilities is what the key said it can do.
func (*Key) GetAssertion ¶ added in v0.3.0
GetAssertion asks the authenticator to sign, proving it holds the credential.
func (*Key) GetInfo ¶ added in v0.2.0
GetInfo asks the authenticator to describe itself.
A key that does not declare CapCBOR is not asked: it would answer CTAPHID_ERROR, and the caller would have to tell that refusal from a real fault.
func (*Key) KeyAgreement ¶ added in v0.5.0
KeyAgreement asks the authenticator for a public key to agree with.
It is exported because it is the one half of the PIN dance that needs no PIN: a key with none set still answers, so this is how a caller checks that the whole chain -- CBOR out, COSE in, ECDH -- works before anybody is asked for a secret.
func (*Key) MakeCredential ¶ added in v0.3.0
func (k *Key) MakeCredential(ctx context.Context, r MakeCredentialRequest) (*Attestation, error)
MakeCredential registers a new credential.
The authenticator will ask for a person: a touch, and a PIN or a fingerprint when the request or the authenticator demands verification. It sends CTAPHID_KEEPALIVE the whole time it waits, which the transport skips, so the only bound on how long this takes is the context.
func (*Key) PINRetries ¶ added in v0.5.0
PINRetries asks how many attempts are left.
It needs no PIN and no key agreement, which makes it the one clientPIN subcommand that can be asked of a key nobody has configured -- and the right thing to ask BEFORE prompting somebody, so a prompt is never the last one they get.
func (*Key) PINToken ¶ added in v0.5.0
func (k *Key) PINToken(ctx context.Context, pin string, proto PINProtocol, perms Permissions, rpID string) (Token, error)
PINToken exchanges a PIN for a token.
permissions is what the token will be allowed to authorise, and rpID narrows it further. Both are CTAP 2.1; an authenticator that does not list "pinUvAuthToken" in its options is asked the CTAP 2.0 way instead, where a token authorises everything and neither argument is sent.
The PIN never leaves in the clear: what is sent is the first sixteen bytes of its SHA-256, encrypted under a secret agreed for this exchange alone.
A wrong PIN COSTS a retry, and an authenticator that runs out locks until it is unplugged, then permanently. Key.PINRetries before prompting is not politeness.
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 MakeCredentialRequest ¶ added in v0.3.0
type MakeCredentialRequest struct {
// ClientDataHash is SHA-256 of the client data the caller built. It is
// hashed here by nobody: what goes into the client data is the caller's
// business, and the authenticator signs the hash it is given.
ClientDataHash []byte
RP RelyingParty
User User
// Algorithms are COSE identifiers, most preferred first. Empty asks for
// [AlgES256].
Algorithms []int64
// Exclude lists credentials that must NOT be registered again, which is how
// a relying party stops one key holding two credentials for one account.
Exclude []Credential
// Options are the authenticator options for this request, such as "rk" for
// a discoverable credential and "uv" to demand verification rather than
// mere presence.
Options map[string]bool
// Token, when valid, authorises the request: it is what turns "somebody
// touched the key" into "somebody who knows its PIN touched the key", and
// it is the only way to get the verified bit set on an authenticator whose
// verification IS a PIN.
Token Token
}
MakeCredentialRequest is a registration.
type Message ¶
Message is a CTAPHID message reassembled from its reports.
type PINProtocol ¶ added in v0.5.0
type PINProtocol int
PINProtocol is a PIN/UV auth protocol version.
Two exist and they differ in more than a number: protocol one hashes the ECDH output once and truncates its HMAC to sixteen bytes, protocol two derives two separate keys through HKDF and truncates nothing. An authenticator says which it supports in Info.PINProtocols.
const ( PINProtocolOne PINProtocol = 1 PINProtocolTwo PINProtocol = 2 )
The protocols. Higher is better where an authenticator offers both: protocol two separates the encryption key from the authentication key and uses a fresh initialisation vector per message.
func (PINProtocol) String ¶ added in v0.5.0
func (p PINProtocol) String() string
String names the protocol.
type Permissions ¶ added in v0.5.0
type Permissions uint
Permissions are what a token is allowed to authorise. An authenticator that speaks CTAP 2.1 wants them; one that only speaks 2.0 has no notion of them and is asked the older way.
const ( PermMakeCredential Permissions = 0x01 PermGetAssertion Permissions = 0x02 )
The permissions this package can ask for.
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 RelyingParty ¶ added in v0.3.0
type RelyingParty struct {
// ID is the domain the credential is scoped to. An authenticator hashes it
// and never learns it.
ID string
// Name is for a person to read on the authenticator's own screen, when it
// has one.
Name string
}
RelyingParty is the site or program a credential belongs to.
type Retries ¶ added in v0.5.0
type Retries struct {
// PIN is how many PIN attempts remain before the authenticator locks. It
// falls on a wrong PIN and is restored by a correct one.
PIN int
// PowerCycle is true when the authenticator will accept no more PINs until
// it is unplugged and plugged back in -- which is not the same as being
// locked for good, and telling a person the difference matters.
PowerCycle bool
}
Retries is what an authenticator says about how many attempts are left.
type Status ¶ added in v0.2.0
type Status byte
Status is the byte a CTAP2 reply begins with. Zero is success.
const StatusOK Status = 0x00
StatusOK means the authenticator did what was asked.
type Token ¶ added in v0.5.0
type Token struct {
// contains filtered or unexported fields
}
Token is a pinUvAuthToken: permission, for a while, to do the things it was asked for.
It is deliberately not a plain []byte. A token authenticates every later request, so it must be paired with the protocol it was obtained under -- authenticating with the wrong one produces sixteen bytes where thirty-two are expected, which an authenticator reports as a bad PIN rather than as a mismatched protocol.
func (Token) Protocol ¶ added in v0.5.0
func (t Token) Protocol() PINProtocol
Protocol is the protocol this token belongs to.
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 User ¶ added in v0.3.0
type User struct {
// ID is an opaque handle chosen by the relying party. It comes BACK in an
// assertion, so it is how a caller knows who signed in -- and for that
// reason it must not be an email address or anything else that identifies
// a person to whoever holds the key.
ID []byte
// Name and DisplayName are shown to a person choosing between credentials.
Name string
DisplayName string
}
User is who the credential is for, as the authenticator will remember them.