SimpleWebAuthn splits WebAuthn into two packages: @simplewebauthn/browser wraps navigator.credentials, and @simplewebauthn/server generates the options a relying party sends and verifies what comes back — attestation formats, COSE keys, counters and signatures. The check drove the server package with a software authenticator that produced standard registration and assertion JSON, the same bytes used for the Python, Go and Java pages.
The most common integration bug is a mismatch between what the options ask for and what verification demands. generateRegistrationOptions() defaults to userVerification: "preferred", but both verifiers default to requireUserVerification: true: an assertion from an authenticator that only proved user presence — a security key without a PIN — was refused with User verification required, but user could not be verified. Decide once and make both sides say the same thing, as the sample does.
Assertion signatures for ES256 must be ASN.1 DER, as the WebAuthn specification requires. A raw 64-byte r‖s signature is fed to the ASN.1 parser anyway and fails with a message that depends on the signature bytes — Too big integer, End of input reached before message was fully decoded or Data does not match to ECDSASigValue ASN1 schema. were the most frequent over 60 fresh credentials — none of which mentions the encoding. Authenticators and passkey providers produce DER, so raw signatures usually mean a hand-written client or a test harness that called WebCrypto's sign() directly; convert them with the DER ⇄ raw converter. A high-S DER signature verified.
A sign counter lower than the stored one is an error, not a warning: verification threw Response counter value 3 was lower than expected 5, and a counter of 0 after a stored 5 failed the same way. Only a credential that has always reported 0 — as synced passkeys do — passes with 0. Store the counter you received, and decide yourself whether a regression locks the credential or only alerts. Version 14 lists ML-DSA-44 (COSE -48) first in the default pubKeyCredParams, ahead of Ed25519 (-8), ES256 (-7) and RS256 (-257), and the Ed25519 credential verified end to end.
Decode what a browser actually sent with the WebAuthn inspector, try flows without a real authenticator in the passkey sandbox, and identify authenticator models by AAGUID with AAGUID lookup.