Fourth Maven project in the repository. Registration and authentication run end to end with no browser and no hardware key: VirtualAuthenticator emits real CBOR attestation objects and real ES256 assertion signatures, and tools/PasskeyCeremony.java drives the live HTTP endpoints with them. Profiles cover userVerification REQUIRED, DIRECT attestation, a disallowed origin and JDBC persistence. Eleven doc chapters and twelve captured transcripts under docs/passkeys and docs/output/pk-*.txt, all regenerated by passkeys/scripts/run-all.sh.
4.3 KiB
← 03 — The two ceremonies · index · next: 05 — The defaults
A software authenticator, so the ceremonies can be executed
Every claim in these chapters comes from a run, which means something had to play the part of
the security key. Chrome's DevTools virtual authenticator can do it interactively; it cannot
be scripted into run-all.sh, and it cannot be checked into a repository.
VirtualAuthenticator
is about two hundred lines and does the whole job. Spring Security and WebAuthn4J verify its
output without knowing that no hardware was involved — which is itself worth noticing.
What it has to produce
Authenticator data, a fixed binary layout:
rpIdHash 32 bytes SHA-256 of the rpId string, not of the origin
flags 1 byte UP 0x01, UV 0x04, BE 0x08, BS 0x10, AT 0x40
signCount 4 bytes big endian
attestedCredData variable registration only: aaguid(16) || credIdLen(2) || credId || COSE key
A COSE public key, CBOR, for ES256:
new Cbor().map(5)
.num(1).num(2) // kty: EC2
.num(3).num(-7) // alg: ES256
.num(-1).num(1) // crv: P-256
.num(-2).bytes(x) // 32 bytes, left-padded
.num(-3).bytes(y) // 32 bytes, left-padded
.toByteArray();
The left-padding matters. BigInteger.toByteArray() returns a two's-complement encoding: it
prepends a zero byte when the top bit is set, and it drops leading zero bytes when they are
not. Either way you get 31 or 33 bytes roughly half the time, and the relying party rejects
the key without telling you why.
An attestation object, also CBOR:
new Cbor().map(3)
.text("fmt").text("none")
.text("attStmt").map(0)
.text("authData").bytes(authData)
.toByteArray();
178 bytes, in this module's case. Cbor
is a forty-line encoder covering the four CBOR major types WebAuthn needs; pulling in a
library for this would hide the structure rather than explain it.
A signature over authenticatorData || SHA-256(clientDataJSON), using
SHA256withECDSA, which emits the DER encoding WebAuthn expects.
What it deliberately does not do
It sets the UP and UV flags because it was asked to, not because anything happened. There is no user presence test, no biometric, no secure element, and the private key sits in the heap next to everything else.
That is not a shortcoming, it is the point. A relying party cannot tell the difference between this and a real authenticator unless it verifies attestation — and by default Spring Security does not. See 05 — The defaults.
Driving it
tools/PasskeyCeremony.java is a single-file
source program (JEP 458, so it compiles its dependencies from the same directory on the fly).
It uses java.net.http.HttpClient with a real cookie jar and drives the actual HTTP endpoints,
CSRF tokens and all — not the operations bean directly — so filter ordering,
session handling and response codes are exercised too.
java --class-path "target/classes:$(cat target/deps.txt)" tools/PasskeyCeremony.java register-and-login
| scenario | what it shows |
|---|---|
register-and-login |
both ceremonies end to end |
clone-counter |
the signature counter, and what is done with it |
no-uv |
a credential whose UV flag was never set |
wrong-origin / wrong-origin-login |
a phishing attempt, in each ceremony |
duplicate |
excludeCredentials, and a client that ignores it |
bootstrap |
asking for registration options with nobody logged in |
ott |
the one-time-token fallback |
filters |
the live filter chain |
The contract tests
use the same authenticator against Webauthn4JRelyingPartyOperations directly, with no HTTP
and no Spring context, so a failure there is the framework's behaviour rather than a filter
accident.
← 03 — The two ceremonies · index · next: 05 — The defaults