keyfactor

package module
v0.2.3 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 2026 License: BSD-3-Clause Imports: 7 Imported by: 0

README

keyfactor

Go Reference License CI

Turns a security key into a factor a policy can ask. Pure Go, CGO_ENABLED=0, no platform.

// An Opener takes the attempt's context; go-gnulinux/fido's Transport does not.
open := func(context.Context) (fido.Transport, error) { return linuxfido.Transport() }
f := keyfactor.New(open, keyfactor.Options{
    RPID:         "example.test",
    CredentialID: id,
})
r, err := mfa.Verify(ctx, mfa.Policy{Count: 2}, somethingElse, f)

It is the piece between go-authn/fido, which speaks CTAP and knows about no operating system, and go-authn/mfa, which decides how much proof is enough and knows about no device. Neither should import the other.

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

What it refuses to confuse

  • A key answering is not a person being there. The UP flag is checked, and UV when a PIN was used. A request for verification is not proof of it: a key may be asked and simply not do it.
  • A PIN is not a second factor. The kind stays Possession. A PIN entered into the key never reaches this machine and identifies nobody to us — it protects the key. Counting it would let one object masquerade as two.
  • The last PIN attempt is not spent on a guess. A wrong PIN costs a retry, a key that runs out locks until it is unplugged and then permanently, taking every credential on it. The count is asked for first, and a key that will not say is refused rather than tried.
  • An empty port is not a refusal. The Opener decides — it is the only thing that knows — and its verdict is passed through untouched. An Opener that finds nothing wraps its error with keyfactor.Unavailable, which marks it mfa.ErrUnavailable, so a policy counts "could not be asked" apart from "said no".
  • The challenge is random. Nothing here verifies the signature, so there is no protocol to bind one to, and a fixed challenge would let a recorded assertion be replayed here for ever.

Covered to 100%, with no key

The tests fake the authenticator, not the transport: a CTAPHID handshake, a CTAP2 assertion, and a real ClientPIN exchange, built out of go-authn/fido's own exported framing. Stopping at the wire would have left everything this package does untested.

⭐ The PIN half plays the authenticator's side of protocol two independently — its own ECDH, its own HKDF, its own AES-CBC, written from the specification rather than from the code under test. If the two derivations disagreed by a byte the exchange would fail. A fake that reused the implementation would agree with it for free, including when both were wrong.

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.

open := func(context.Context) (fido.Transport, error) { return linuxfido.Transport() }
f := keyfactor.New(open, keyfactor.Options{
    RPID:         "example.test",
    CredentialID: id,
})
r, err := mfa.Verify(ctx, mfa.Policy{Count: 2}, somethingElse, f)

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 New

func New(open Opener, o Options) mfa.Factor

New makes a factor.

func Unavailable

func Unavailable(err error) error

Unavailable marks an error as "there was nothing here to ask".

Types

type Opener

type Opener func(context.Context) (fido.Transport, error)

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.
	CredentialID []byte
	// 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.

Jump to

Keyboard shortcuts

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