Documentation
¶
Overview ¶
Package keyfactor turns a security key into a factor a policy can ask.
It is the piece between github.com/go-authn/fido, which speaks CTAP and knows about no operating system, and github.com/go-authn/mfa, which decides how much proof is enough and knows about no device. Neither should import the other; this does both, and nothing else does.
// From the registration: the credential ID and its public key.
pub, err := attestation.Parsed.PublicKey()
open := func(context.Context) (fido.Transport, error) { return linuxfido.Transport() }
f := keyfactor.New(open, keyfactor.Options{
RPID: "example.test",
CredentialID: id,
PublicKey: pub,
})
r, err := mfa.Verify(ctx, mfa.Policy{Count: 2}, somethingElse, f)
What is checked ¶
Whatever answers on the transport is a device, not necessarily THE key: a programmable USB board can speak CTAP as well as a security key can. So the answer is verified against what the registration recorded -- the relying party hash, the credential ID when the device names one, and the signature over the authenticator data and a fresh random challenge, with the credential's public key. A device that does not hold the credential's private key cannot produce that signature, whatever flags it claims.
What is not, and cannot be ¶
A PIN is sent to whatever device answers, before anything is signed: CTAP has the platform hand the key LEFT(SHA-256(PIN), 16), encrypted under a secret agreed with the device itself. A forged device holding no credential is refused here, but it has still received that hash, and a short PIN falls to an offline search in moments. That is inherent to CTAP, not something this package can fix: use a long PIN, and do not type one into a key whose provenance is unknown.
Why it is not in a platform package ¶
It was, twice — once for macOS and once, nearly, for Linux. The two copies differed in one line: which fido.Transport to open. Everything else — the assertion, the flag checks, the refusal to spend somebody's last PIN attempt — is the same everywhere CTAP is, which is everywhere.
So the platform package supplies an Opener and keeps what is genuinely platform-specific: how a key is found, and what it means when there is none.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Unavailable ¶
Unavailable marks an error as "there was nothing here to ask".
Types ¶
type Opener ¶
Opener produces a transport to the attached key.
It is where the platform lives: go-macos/fido and go-gnulinux/fido each have one. An Opener that finds nothing should wrap its error with mfa.ErrUnavailable — through Unavailable — because an empty USB port has refused nobody, and a policy counts "could not be asked" separately from "said no".
type Options ¶
type Options struct {
// RPID is the relying party the credential belongs to. Required.
RPID string
// CredentialID is what a registration returned. Empty asks the key for a
// discoverable credential, which it has only if one was registered with
// the "rk" option. When set, an answer naming any other credential is
// refused.
CredentialID []byte
// PublicKey is the credential's public key, from the registration
// (fido.AuthData.PublicKey on the attestation). Required: the signature
// it verifies is the only thing that tells the key that was registered
// from any device that answers CTAP. Only P-256 is accepted.
PublicKey *ecdsa.PublicKey
// PIN, when set, asks the key to establish WHO is holding it rather than
// only that somebody is.
//
// It travels no further than the key: what goes over the wire is the first
// sixteen bytes of its SHA-256, encrypted under a secret agreed for that
// one exchange. It is still a secret in a Go string, so a caller that can
// avoid holding one should.
PIN string
// Name is what to call this key when talking to a person. Empty picks a
// sensible default.
Name string
}
Options describes what to ask the key.