Debugging WebAuthn Signature Verification Failures
The signature step is the deepest and most failure-prone check in server-side verification: when it fails, everything up to it looked fine, and the error — “signature invalid” — points nowhere. Almost every case is one of four things: the signed data was reconstructed wrong, the stored COSE public key was converted wrong, an ES256 signature’s ASN.1 encoding was mishandled, or an RS256 padding scheme mismatched. This page gives a reproducible fix for each, within the server-side debugging cluster that is its parent. For the signing internals, cross-reference handling WebAuthn signature verification in Node.js.
Failure Signatures
| Root cause | Symptom | Fix class |
|---|---|---|
| Wrong signed data | Verify fails for all users, all algs | Rebuild authData ‖ SHA-256(clientDataJSON) from raw bytes |
| Bad COSE→SPKI conversion | Fails for one alg family | Re-derive public key from COSE map (kty, crv, x, y / n, e) |
| ES256 ASN.1 mismatch | Fails only for ES256 | Match DER vs raw r‖s to the verifier |
| RS256 padding mismatch | Fails only for RS256 | Use PKCS#1 v1.5 with SHA-256 |
| Re-encoded authenticatorData | Intermittent / library-specific | Use the original received bytes, never a round-trip |
The signed data definition is fixed by WebAuthn L2 §7.2 step 20: the signature covers authenticatorData || SHA-256(clientDataJSON).
Root Cause Analysis
1. Reconstructed signed data differs by a byte. Re-serialising authenticatorData (e.g. through a CBOR round-trip) changes bytes; the signature was made over the original bytes, so it must be verified against them.
2. COSE key converted incorrectly. The stored public key is a COSE_Key map; converting it to the SPKI/PEM your crypto library expects must map the right fields (-2/-3 = x/y for EC2, -1/-2 = n/e for RSA). A swapped or mis-length field silently yields a wrong key.
3. ES256 signature format. WebAuthn ES256 signatures are ASN.1 DER (SEQUENCE { INTEGER r, INTEGER s }). Node’s crypto.verify for 'sha256' with an EC key expects DER; Web Crypto’s ECDSA expects raw r||s. Feeding the wrong one fails.
4. RS256 padding. RS256 uses RSASSA-PKCS1-v1_5 with SHA-256; using PSS fails.
Step-by-Step Resolution
Step 1 — Rebuild the signed data from the received bytes
import { createHash } from 'crypto';
// authenticatorData: the ORIGINAL Buffer as received (do not re-encode)
const signedData = Buffer.concat([
authenticatorData,
createHash('sha256').update(clientDataJSON).digest(),
]);
Step 2 — Convert the COSE key correctly (ES256 example)
import { createPublicKey } from 'crypto';
// cose: decoded COSE_Key map. For EC2 P-256: kty=2, crv=1, x=-2, y=-3
function coseEs256ToKeyObject(cose: Map<number, Buffer>) {
const x = cose.get(-2)!, y = cose.get(-3)!; // 32 bytes each
const jwk = { kty: 'EC', crv: 'P-256', x: x.toString('base64url'), y: y.toString('base64url') };
return createPublicKey({ key: jwk, format: 'jwk' });
}
Step 3 — Verify with the matching signature encoding
import { verify } from 'crypto';
// Node expects DER for EC; WebAuthn ES256 signatures are already DER.
const ok = verify('sha256', signedData, coseEs256ToKeyObject(cose), signature);
if (!ok) throw new Error('ES256 signature invalid — check signed data and key mapping');
Step 4 — Confirm the algorithm matches the stored COSE alg
Read alg (-7 = ES256, -257 = RS256, -8 = EdDSA) from the stored key and dispatch to the correct verifier and padding, aligning with cryptographic algorithms supported by WebAuthn.
Verification and Testing
Use a known-good vector (register a credential with a virtual authenticator, capture the assertion) and assert each stage:
expect(signedData.length).toBe(authenticatorData.length + 32); // structure check
expect(verify('sha256', signedData, pubKey, signature)).toBe(true); // end-to-end
Cross-check by verifying the same assertion with @simplewebauthn/server; if the library passes and your code fails, the delta is in your signed-data reconstruction or key conversion. Add regression tests per algorithm (ES256, RS256, EdDSA) using captured vectors.
Pitfalls
1. Re-encoding authenticatorData. Verify against the original received bytes only.
2. Swapped COSE fields. Map EC x/y (-2/-3) and RSA n/e (-1/-2) exactly; check byte lengths.
3. ES256 DER vs raw confusion. Match the signature encoding to your verifier’s expectation.
Related
- Debugging and Observability for WebAuthn Servers — the parent pipeline and per-step telemetry
- Handling WebAuthn Signature Verification in Node.js — the full signature verification implementation
- Cryptographic Algorithms Supported by WebAuthn — COSE
algidentifiers and per-algorithm encoding