webauthn

package module
v0.1.0 Latest Latest
Warning

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

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

README

webauthn

Go Reference License CI

Asks Windows to authenticate somebody, through webauthn.dll — the same API browsers use. Pure Go, CGO_ENABLED=0, no cgo and no SDK.

a, err := webauthn.Assert(ctx, webauthn.Request{
    RPID:         "example.test",
    Origin:       "https://example.test",
    Challenge:    challenge,               // fresh, always
    Allow:        []webauthn.Credential{{ID: credentialID}},
    Attachment:   webauthn.CrossPlatform,  // a security key, not Hello
    Verification: webauthn.VerificationRequired,
})
fmt.Println(a.Transport)   // "usb" — what actually answered

Where it sits in go-mswin

go-mswin/winrt already answers the OTHER half: RequireUserConsent asks Windows Hello, through UserConsentVerifier. That is the authenticator built into the machine. This package is for the one somebody carries — and for the case where you need a signed assertion rather than a yes.

go-mswin/win32 is where windows come from, which matters here: the dialog is modal to an HWND. Pass your own through Request.Window; leaving it zero borrows the foreground, which is what a console program has to do and what Teleport does in production.

Why this is not a CTAP transport

Its siblings, go-macos/fido and go-gnulinux/fido, move 64-byte reports and let go-authn/fido speak the protocol. This one cannot, and the reason is not a preference.

Since Windows 10 version 1903, opening a FIDO HID device requires elevation. Microsoft closed that door and pointed everyone at this API. A CTAP transport here would work only for administrators, which is not a program anybody should ship. The measure of that wall is what it takes to get around it: the one project that does installs an elevated Windows service and relays CTAPHID over a named pipe.

So Windows speaks the protocol, shows its own dialog, collects the PIN or the fingerprint itself, and hands back an assertion. go-authn/fido is not underneath this package. That the three platforms end up with different shapes is fine — go-authn/mfa asks for a Factor, not for a transport.

And it cuts the other way too: on Windows the platform half is winrt, not this, so a future go-mswin/factors will draw its two kinds from two different packages. That is what the layering was for.

What was proved, and what only somebody could tell you

An assertion says a credential answered, and its flags say whether the authenticator verified who was holding it. It does not say what convinced it. Windows Hello accepts a face, a fingerprint, or a PIN — and a PIN is something known, not something anyone is. Reporting "user verified" as biometrics would be reporting the wrong kind of proof, which is the one thing a multi-factor policy exists to prevent.

Two things narrow it, and both are here:

  • Attachment constrains what may answer. CrossPlatform asks for a security key — something carried. Platform asks for the one built into this machine.
  • Assertion.Transport reports what actually answered. Asking is a request; this is an observation, and they can differ.

Transport.Carried() returns two values, deliberately. Windows fills the transport in only from version 4 of its assertion structure, and an older one says nothing at all. Reading silence as "not carried" would quietly downgrade every security key on those machines to something built in, so silence is reported as silence.

What it refuses to confuse

  • Dismissing the dialog is not failing. Error.Cancelled() is separate: nobody was refused, somebody changed their mind, and saying otherwise accuses them.
  • Nothing there to ask is not a refusal. Error.Unavailable() covers NTE_DEVICE_NOT_FOUND, NTE_NOT_FOUND and NTE_NOT_SUPPORTED.
  • Cancelling the context cancels the operation through Windows. The package asks for a cancellation id and uses WebAuthNCancelCurrentOperation, so the dialog comes down. Abandoning the wait instead would leave it on the person's screen with nothing behind it.
  • The client data is returned, not describable. What is signed is those exact bytes; a verifier that rebuilds them and gets one space different sees every signature as forged. It is also built with a JSON encoder rather than a format string, so an origin containing a quote does not produce a call Windows rejects with a parameter error that names no parameter.

The two things a compiler cannot catch

dwVersion tells the DLL how many fields were laid out. It does not tell it where they are. A field in the wrong place is read as whatever sits at that offset — a length taken from a pointer, a pointer taken from a length — and nothing reports it. A test pins every size and offset against webauthn.h and the C alignment rules.

A status table is exactly the kind of thing that looks right and is not. A sibling package shipped one written from memory with five wrong entries, and a real device caught it rather than a test. So the HRESULTs here are checked, on the Windows lane, against golang.org/x/sys/windows's own constants — which are generated from the SDK headers. The set is the one libfido2's winhello.c translates.

What is not here

Registration. WebAuthNAuthenticatorMakeCredential is not bound yet, so this package asserts credentials registered somewhere else. Its structure is larger and its layout carries the same silent risk, so it gets its own pass rather than being tacked on.

A run against a real authenticator. Everything that needs no dialog is covered to 100%, on Linux, and the layout and error tables are checked on the Windows lane. But a CI runner has no security key and nobody to touch it: the call itself has never been made. That is the honest state, and it wants one run on a Windows machine with a key in it.

Documentation

Overview

Package webauthn asks Windows to authenticate somebody, in pure Go with CGO_ENABLED=0.

It calls the Win32 WebAuthn API in webauthn.dll — the same one browsers use. Windows finds the authenticator, shows its own dialog, collects a PIN or a fingerprint, and hands back a signed assertion.

Why not CTAP, as on Linux and macOS

Because Windows will not let a normal program talk to a security key at all. Since Windows 10 version 1903, opening a FIDO HID device requires elevation: Microsoft closed that door and pointed everyone at this API. A CTAP transport here would work only for administrators, which is not a program anybody should ship. (The one project that does it anyway installs an elevated Windows service and relays CTAPHID over a named pipe — the size of that workaround is the measure of the wall.)

So this package is shaped differently from its siblings, and deliberately. github.com/go-authn/fido is not underneath it: the protocol is Windows's business here, and what comes back is an assertion, not a CTAP reply.

What was proved, and what only somebody could tell you

An assertion says a credential answered and, through its flags, whether the authenticator verified who was holding it. It does NOT say what convinced it. Windows Hello accepts a face, a fingerprint, or a **PIN** — and a PIN is something known, not something anyone is. A caller that read "user verified" as "biometrics" would be reporting the wrong kind of proof.

Two things narrow it, and this package offers both:

  • Request.Attachment constrains what may answer at all. CrossPlatform asks for a security key — something carried. Platform asks for the one built into this machine.
  • Assertion.Transport reports what actually answered, when Windows says. Asking is a request; this is an observation, and they can differ.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnsupported is returned off Windows, and on a Windows too old to have
	// the API -- it arrived in version 1903.
	ErrUnsupported = errors.New("webauthn: the Windows WebAuthn API is not available here")

	// ErrCancelled is returned when the person dismissed the dialog, or the
	// context ended and the operation was cancelled.
	ErrCancelled = errors.New("webauthn: the operation was cancelled")
)

Functions

func APIVersion

func APIVersion() uint32

APIVersion is zero off Windows.

func Available

func Available() bool

Available is false off Windows.

Types

type Assertion

type Assertion struct {
	// AuthenticatorData is the signed statement: relying-party hash, flags,
	// signature counter, and any extension output.
	AuthenticatorData []byte
	// Signature covers AuthenticatorData followed by the SHA-256 of
	// ClientDataJSON.
	Signature []byte
	// ClientDataJSON is what was hashed into the signature. It is returned
	// because a verifier needs the exact bytes, not a reconstruction: rebuild
	// it and one different space makes every signature look forged.
	ClientDataJSON []byte
	// CredentialID says which credential answered, which matters when the
	// request allowed several.
	CredentialID []byte
	// UserID is the user handle, present for a discoverable credential.
	UserID []byte
	// Transport is what actually answered, or [TransportUnknown] when this
	// Windows did not say.
	Transport Transport
}

Assertion is what Windows returns.

func Assert

func Assert(context.Context, Request) (*Assertion, error)

Assert is unavailable off Windows.

It reports ErrUnsupported rather than a failure: a Mac has not refused anybody, it has no Windows WebAuthn API. The macOS and Linux siblings are go-macos/fido and go-gnulinux/fido, over github.com/go-authn/fido.

type Attachment

type Attachment uint32

Attachment says which authenticators may answer.

const (
	// AnyAttachment lets Windows offer whatever it has. The answer does not
	// say which kind was used unless [Assertion.Transport] does.
	AnyAttachment Attachment = 0
	// Platform is the authenticator built into this machine: Windows Hello.
	Platform Attachment = 1
	// CrossPlatform is something carried and plugged in: a security key.
	CrossPlatform Attachment = 2
)

The attachments, numbered as webauthn.h numbers them.

func (Attachment) String

func (a Attachment) String() string

type Credential

type Credential struct {
	// ID is what a registration returned.
	ID []byte
}

Credential names one registered credential.

type Error

type Error struct {
	// Op is the call that failed.
	Op string
	// HResult is the code Windows returned.
	HResult uint32
	// Name is what WebAuthNGetErrorName calls it, when it could be asked.
	Name string
}

Error is a failure reported by Windows, carrying the name Windows gives it.

func (*Error) Cancelled

func (e *Error) Cancelled() bool

Cancelled reports whether Windows says the person dismissed the dialog.

It is not a failure of authentication: nobody was refused, somebody changed their mind. A caller that reported it as a refusal would be accusing them.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unavailable

func (e *Error) Unavailable() bool

Unavailable reports whether there was nothing here to ask.

NTE_DEVICE_NOT_FOUND and NTE_NOT_FOUND mean no authenticator could serve the request -- no key plugged in, or none holding the credential that was asked for. NTE_NOT_SUPPORTED means this Windows cannot do what was asked. None of those is somebody failing to authenticate, and a policy counts them separately.

type Request

type Request struct {
	// RPID is the relying party: the domain the credential belongs to.
	RPID string
	// Origin is what goes in the client data, and Windows checks that it
	// matches RPID. For a program that is not a web page, use
	// "https://" + RPID.
	Origin string
	// Challenge is the bytes to be signed over. It must be fresh: a repeated
	// challenge makes a recorded assertion replayable for ever.
	Challenge []byte
	// Allow lists the credentials that may answer. Empty asks the
	// authenticator for a discoverable one, which it has only if a
	// registration asked for that.
	Allow []Credential
	// Attachment constrains what may answer. See the package documentation for
	// why this is how a caller keeps a claim about KINDS honest.
	Attachment Attachment
	// Verification says whether the authenticator must establish who is
	// holding it, rather than only that somebody is.
	Verification UserVerification
	// TimeoutMilliseconds is guidance to Windows, which may override it. Zero
	// leaves the choice to Windows.
	TimeoutMilliseconds uint32
	// Window is the HWND the Windows dialog belongs to. Zero borrows whatever
	// is in the foreground, which is what a console program has to do.
	//
	// A program that HAS a window should pass its own. Borrowing means the
	// modal dialog parents to somebody else's window: it can end up behind
	// theirs, or move when they move. go-mswin/win32 makes windows; this field
	// is a uintptr rather than a win32.HWND because Request is compiled on
	// every platform and that type is not.
	Window uintptr
}

Request is one authentication.

type Transport

type Transport uint32

Transport is how an authenticator was reached. Windows reports it in the assertion, from version 4 of that structure onwards.

const (
	TransportUnknown   Transport = 0
	TransportUSB       Transport = 0x01
	TransportNFC       Transport = 0x02
	TransportBLE       Transport = 0x04
	TransportTest      Transport = 0x08
	TransportInternal  Transport = 0x10
	TransportHybrid    Transport = 0x20
	TransportSmartCard Transport = 0x40
)

The transports, as the WEBAUTHN_CTAP_TRANSPORT_* bits.

func (Transport) Carried

func (t Transport) Carried() (carried, ok bool)

Carried reports whether the authenticator was a separate object rather than part of this machine.

It is the honest half of the question a policy wants to ask. "internal" is this computer; usb, nfc, ble, smart-card and hybrid are all something brought to it. An UNREPORTED transport answers neither way, and this says so through ok rather than guessing -- older Windows fills in no transport at all, and treating silence as "not carried" would quietly downgrade every security key on those machines.

func (Transport) String

func (t Transport) String() string

String uses the names the specification gives these, which are the ones the header spells out as WEBAUTHN_CTAP_TRANSPORT_*_STRING.

type UserVerification

type UserVerification uint32

UserVerification says how hard Windows should insist on knowing who is there.

const (
	VerificationAny         UserVerification = 0
	VerificationRequired    UserVerification = 1
	VerificationPreferred   UserVerification = 2
	VerificationDiscouraged UserVerification = 3
)

The requirements, numbered as webauthn.h numbers them.

func (UserVerification) String

func (v UserVerification) String() string

Jump to

Keyboard shortcuts

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