feat(passkey): add WebAuthn login/assertion crypto adapter
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.
This commit is contained in:
3 files changed
+237
-20
No files matched your search
@@ -16,12 +16,16 @@ import (
|
||||
// ceremony — and manages the credentials they have bound. Email-OTP (handlers_email_otp.go)
|
||||
// stays the fallback factor, so a player with no passkey is never locked out.
|
||||
//
|
||||
// Scope: ENROLLMENT only. The login/assertion path (proving a passkey to mint or
|
||||
// elevate a session from an unauthenticated state) is a deferred slice — the panel.*
|
||||
// passkey relying-party boundary is a later decision (see migration 0007). So every
|
||||
// ceremony here rides on a known principal: the challenge is bound to the caller's
|
||||
// user_id and the finish verifies against the server-stashed SessionData, never a
|
||||
// client-echoed challenge.
|
||||
// Scope of the HANDLERS in this file: ENROLLMENT only. Every ceremony here rides on a
|
||||
// known principal — the challenge is bound to the caller's user_id and the finish
|
||||
// verifies against the server-stashed SessionData, never a client-echoed challenge. The
|
||||
// login/assertion path (proving a passkey to mint or elevate a session from an
|
||||
// UNauthenticated state) has its cryptographic half built and Oracle-verified in the
|
||||
// adapter (internal/passkey BeginLogin/FinishLogin, against a virtual authenticator),
|
||||
// and its persist-ready output shape is VerifiedAssertion below — but the login HTTP
|
||||
// handlers, the session minting, and the panel.* passkey relying-party boundary/tier
|
||||
// decision (see migration 0007) are a deferred slice: this file adds no unauthenticated
|
||||
// login route.
|
||||
//
|
||||
// The cryptographic half is a seam (PasskeyVerifier) so this package never imports
|
||||
// go-webauthn: ceremony state crosses the boundary as opaque bytes, the attestation
|
||||
@@ -85,6 +89,22 @@ type VerifiedCredential struct {
|
||||
AAGUID string
|
||||
}
|
||||
|
||||
// VerifiedAssertion is the output of a finished LOGIN (assertion) ceremony: which of the
|
||||
// user's bound credentials proved itself and the signature counter the authenticator
|
||||
// reported. Like VerifiedCredential it carries no secret. SignCount is the raw ceremony
|
||||
// fact, NOT a policy verdict: the handler that eventually consumes this holds the
|
||||
// previously-stored counter and decides whether a non-increase is a cloned-authenticator
|
||||
// signal — the verifier deliberately does not, so clone policy lives in one place with
|
||||
// the stored state. SignCount is legitimately 0 for authenticators that keep no counter.
|
||||
//
|
||||
// The login handlers do not exist yet (see the file header): this is the stable seam
|
||||
// output the production adapter (internal/passkey) already produces and its Oracle test
|
||||
// already asserts on, so wiring the handlers later needs no reshaping here.
|
||||
type VerifiedAssertion struct {
|
||||
CredentialID string // base64url(raw credential id) — which bound credential signed
|
||||
SignCount uint32
|
||||
}
|
||||
|
||||
// errPasskeyUnavailable is returned when the WebAuthn verifier is not configured on
|
||||
// this api instance, so the begin/finish ceremony routes answer 503 rather than panic.
|
||||
var errPasskeyUnavailable = newError(http.StatusServiceUnavailable, "passkey_unavailable",
|
||||
|
||||
Reference in new issue
Block a user