1
0
Files
spring-auth-demo/docs/passkeys/04-virtual-authenticator.md
Ankur Mhatre f6dd692177 Add passkeys project: WebAuthn ceremonies, a software authenticator and the one-time-token fallback
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.
2026-08-25 23:00:27 +05:30

97 lines
4.3 KiB
Markdown

[← 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)