Help us shape the future of Go! Take the 2026 Go Developer Survey

keyfactor

package module
v0.2.1 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.

f := keyfactor.New(linuxfido.Transport, 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.
  • 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.

f := keyfactor.New(linuxfido.Transport, 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