# Assertion verifier ## Purpose Implements `verifyAssertion`, the cryptographic check at the heart of the WebAuthn authentication ceremony (W3C WebAuthn L3 §7.2). The function ingests the authenticator's assertion (authenticatorData, clientDataJSON, signature) plus the relying-party's stored credential state (public key, expected origin, expected rpIdHash, sign counter) and returns a discriminated-union `VerifyResult`. The dispatcher routes to per-algorithm verifiers backed by `@suaveplan/crypto-curve` and `@suaveplan/crypto-rsa` — every supported COSE algorithm (ES256, ES384, ES512, EdDSA, RS256) goes through real cryptographic verification. ## Features - Discriminated-union return: `{ ok: true } | { ok: false, code }` — callers branch without unwrapping thrown errors. - Five algorithms supported: ES256 (P-256 + SHA-256), ES384 (P-384 + SHA-384), ES512 (P-521 + SHA-512), EdDSA (Ed25519), RS256 (RSA-PKCS1-v1.5 + SHA-256). - DER ECDSA decoding via `@suaveplan/x509#decodeEcdsaSignature` — WebAuthn emits DER-encoded ECDSA signatures, which we convert to the compact `r||s` form expected by `@suaveplan/crypto-curve`. - Constant-time `rpIdHash` compare via `constantTimeEqual` — a mismatched RP hash is the canonical phishing signal and the compare time must not leak the byte index of the first difference. - Sign-counter regression check via `detectSignCounterRegression` — `{ ok: false, code: "WEBAUTHN.SIGN_COUNTER_REGRESSION" }` on monotonicity violation. - Client-data revalidation via `validateClientDataJSON` so type / origin / challenge are also checked. - `verifyOverCoseKey(key, message, signature)` helper exported for per-format attestation verifiers (packed self-attestation, fido-u2f, tpm). ## Usage: Basic Example ```ts import { verifyAssertion } from "@suaveplan/webauthn/verifier"; const result = await verifyAssertion({ authenticatorData, clientDataJSON, signature, storedPublicKey, expectedChallenge, expectedOrigin: "https://example.com", expectedRpIdHash, signCounter: stored.signCounter, }); if (!result.ok) { if (result.code === "WEBAUTHN.SIGN_COUNTER_REGRESSION") { revokeCredential(); } throw new Error(result.code); } ``` ## Usage: Advanced Example ```ts import { verifyAssertion, verifyOverCoseKey, } from "@suaveplan/webauthn/verifier"; // Direct signature check (used by per-format attestation verifiers): const ok = await verifyOverCoseKey( storedKey, concatBytes(authData, sha256(clientDataJSON)), derSignatureBytes, ); ``` ## API Reference - `VerifyResult` — `{ ok: true } | { ok: false, code: "WEBAUTHN.SIGNATURE_INVALID" | "WEBAUTHN.RPID_HASH_MISMATCH" | "WEBAUTHN.SIGN_COUNTER_REGRESSION" }`. - `VerifyAssertionInput` — `{ authenticatorData, clientDataJSON, signature, storedPublicKey, expectedChallenge, expectedOrigin, expectedRpIdHash, signCounter }`. - `verifyAssertion(input): Promise` — the main entry point. - `verifyOverCoseKey(key, message, signature): Promise` — per-algorithm signature check, exported for attestation verifiers. ## Implementation Notes - WebAuthn ECDSA signatures are ASN.1 DER-encoded, not the JOSE-form `r||s` that `@suaveplan/crypto-curve` accepts. The package delegates DER decoding to `decodeEcdsaSignature` from `@suaveplan/x509` (the same routine used in TLS / X.509) so the structural validation is shared with the rest of the security family. - EC2 public keys are reconstructed in uncompressed form `0x04 || x || y` before passing to `noble-curves`; the COSE key labels `x` and `y` as separate byte strings. - RSA verification consumes the COSE `n` and `e` byte strings via `os2ip` to produce `bigint` modulus + exponent, then calls `rsaVerifyPkcs1v15` with `hash: "SHA-256"`. Any structural failure (wrong sig length, modulus mismatch) collapses to `false` — semantic failures never throw out of `verifyOverCoseKey`. - The dispatch order is: parse authData → validate client data → constant-time rpIdHash compare → sign-counter check → signature verify. The order matters: a mismatched rpIdHash is the strongest phishing signal so it surfaces before the expensive signature operation.