Build the assertion (login) half of the WebAuthn ceremony crypto in the
internal/passkey adapter, Oracle-verified against a virtual authenticator.
- BeginLogin/FinishLogin over go-webauthn BeginLogin/ValidateLogin,
username-first (allowCredentials scoped to the known user's bound
passkeys). Discoverable/usernameless login stays out of scope: the
enrolled credentials are non-resident and the challenge store is
user-keyed (migration 0007), so it would need a future migration.
- WebAuthnCredentials() now populates the stored COSE public key and
signature counter (assertion validation needs both to verify the
signature and detect clones); enrollment ignores them, so the change
is backward-compatible and the enrollment tests guard it.
- VerifiedAssertion seam output: which credential signed plus the raw
signature counter. Clone/regression policy is deliberately NOT here —
the counter is a ceremony fact and the future handler, which holds the
previously stored counter, decides reject/warn.
Scope: crypto adapter only. The login HTTP handlers, session minting,
and the panel.* relying-party boundary/tier decision remain a deferred
slice (no unauthenticated login route is added). BeginLogin/FinishLogin
live on the concrete adapter, not the api.PasskeyVerifier interface,
which grows only when a handler consumes them.
Tests (virtualwebauthn): a real enrollment chained into a real assertion
exercises the COSE public-key decode path and surfaces the advanced
signature counter, plus origin-mismatch and unbound-credential rejection.