4.1 KiB
4.1 KiB
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 compactr||sform expected by@suaveplan/crypto-curve. - Constant-time
rpIdHashcompare viaconstantTimeEqual— 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
validateClientDataJSONso 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
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
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||sthat@suaveplan/crypto-curveaccepts. The package delegates DER decoding todecodeEcdsaSignaturefrom@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 || ybefore passing tonoble-curves; the COSE key labelsxandyas separate byte strings. - RSA verification consumes the COSE
nandebyte strings viaos2ipto producebigintmodulus + exponent, then callsrsaVerifyPkcs1v15withhash: "SHA-256". Any structural failure (wrong sig length, modulus mismatch) collapses tofalse— semantic failures never throw out ofverifyOverCoseKey. - 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.