SimpleWebAuthn divide WebAuthn en dos paquetes: @simplewebauthn/browser envuelve navigator.credentials, y @simplewebauthn/server genera las opciones que envía una relying party y verifica lo que vuelve: formatos de atestación, claves COSE, contadores y firmas. Para la comprobación, el paquete del servidor recibió las respuestas de un autenticador por software que producía JSON estándar de registro y de aserción: los mismos bytes que se usaron en las páginas de Python, Go y Java.
El error de integración más habitual es una discrepancia entre lo que piden las opciones y lo que exige la verificación. generateRegistrationOptions() usa por defecto userVerification: "preferred", pero ambos verificadores usan por defecto requireUserVerification: true: una aserción de un autenticador que solo demostró la presencia del usuario —una llave de seguridad sin PIN— se rechazó con User verification required, but user could not be verified. Decide una vez y haz que ambos lados digan lo mismo, como hace el ejemplo.
Las firmas de aserción ES256 deben ser DER ASN.1, como exige la especificación WebAuthn. Una firma r‖s en bruto de 64 bytes llega igualmente al analizador ASN.1 y falla con un mensaje que depende de los bytes de la firma —Too big integer, End of input reached before message was fully decoded o Data does not match to ECDSASigValue ASN1 schema. fueron los más frecuentes con 60 credenciales nuevas—, y ninguno menciona la codificación. Los autenticadores y los proveedores de llaves de acceso producen DER, así que las firmas en bruto suelen indicar un cliente escrito a mano o un arnés de pruebas que llamó directamente al sign() de WebCrypto; conviértelas con el conversor DER ⇄ en bruto. Una firma DER high-S se verificó.
Un contador de firmas menor que el almacenado es un error, no una advertencia: la verificación lanzó Response counter value 3 was lower than expected 5, y un contador de 0 tras un 5 almacenado falló de la misma manera. Solo una credencial que siempre ha devuelto 0 —como hacen las llaves de acceso sincronizadas— pasa con 0. Guarda el contador recibido y decide tú si un retroceso bloquea la credencial o solo genera una alerta. La versión 14 pone ML-DSA-44 (COSE -48) en primer lugar en los pubKeyCredParams predeterminados, por delante de Ed25519 (-8), ES256 (-7) y RS256 (-257), y la credencial Ed25519 se verificó de principio a fin.
Decodifica lo que envió realmente un navegador con el inspector de WebAuthn, prueba flujos sin un autenticador real en el entorno de pruebas de llaves de acceso e identifica modelos de autenticador por su AAGUID con la búsqueda de AAGUID.