[← 03 — The two ceremonies](03-the-two-ceremonies.md) · [index](README.md) · next: [05 — The defaults](05-defaults.md) # 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`](../../passkeys/src/main/java/com/ankurm/passkeys/virtual/VirtualAuthenticator.java) 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: ```java 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: ```java 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`](../../passkeys/src/main/java/com/ankurm/passkeys/virtual/Cbor.java) 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](05-defaults.md). ## Driving it [`tools/PasskeyCeremony.java`](../../passkeys/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. ```bash 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](../../passkeys/src/test/java/com/ankurm/passkeys/PasskeyContractTests.java) 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](03-the-two-ceremonies.md) · [index](README.md) · next: [05 — The defaults](05-defaults.md)