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