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.
97 lines
4.3 KiB
Markdown
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)
|