first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
# 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<VerifyResult>` — the main entry point.
- `verifyOverCoseKey(key, message, signature): Promise<boolean>` — 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.