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.
This commit is contained in:
96
docs/passkeys/04-virtual-authenticator.md
Normal file
96
docs/passkeys/04-virtual-authenticator.md
Normal file
@@ -0,0 +1,96 @@
|
||||
[← 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)
|
||||
Reference in New Issue
Block a user