FIDO2 frente a WebAuthn es una cuestión de capas, no una elección. WebAuthn (Web Authentication, del W3C) define la API de JavaScript a la que llaman las relying parties (RP, las partes que confían en la autenticación) y los formatos de datos firmados que estas verifican. CTAP (Client to Authenticator Protocol, de FIDO) define cómo se comunica un navegador o un sistema operativo con un autenticador externo por USB, NFC, Bluetooth o el transporte híbrido con código QR. FIDO2 es el nombre genérico que la FIDO Alliance introdujo en 2018 para WebAuthn junto con CTAP 2. Las llaves de acceso son credenciales FIDO creadas mediante estos estándares.
De un vistazo
| WebAuthn | CTAP | FIDO2 | |
|---|---|---|---|
| Qué es | API del navegador y formatos de datos | Protocolo de comunicación con autenticadores externos | Término genérico para ambos |
| Publicado por | W3C | FIDO Alliance | FIDO Alliance |
| Versión actual | Level 3, recomendación del W3C del 25 de agosto de 2026 | CTAP 2.3, estándar propuesto del 26 de febrero de 2026 (2.2: julio de 2025) | WebAuthn Level 3 con CTAP 2.3 |
| Versiones anteriores | Level 1 (marzo de 2019), Level 2 (abril de 2021) | CTAP 2.0, 2.1 (2021); CTAP1 es U2F | U2F (2014) es su predecesor |
| Quién lo implementa | Navegadores y plataformas (API), relying parties (comprobaciones en el servidor) | Navegadores, sistemas operativos, llaves de seguridad, teléfonos | Todos los eslabones de la cadena |
| Entre | Página web y navegador; servidor y datos del autenticador | Plataforma cliente y autenticador | De la relying party al autenticador |
| Codificación | Objetos JavaScript, clientDataJSON, objetos de atestación CBOR | Comandos CBOR canónicos de CTAP2, como authenticatorGetAssertion (0x02) | Ambas |
| Transportes | No aplica | USB HID, NFC, Bluetooth Low Energy, híbrido (código QR, comprobación de proximidad, túnel) | Ambos |
| Puntos de entrada principales | navigator.credentials.create() y .get() | authenticatorMakeCredential (0x01), authenticatorGetInfo (0x04) | No aplica |
Quién hace qué durante un inicio de sesión
- El sitio web llama a
navigator.credentials.get()con un desafío y el ID de su relying party (RP ID). Esto es WebAuthn. - El navegador construye
clientDataJSON(tipo, desafío, origen, indicador de origen cruzado), calcula su hash y pide a un autenticador que firme. Con un autenticador de plataforma integrado (Touch ID, Windows Hello, Android), esto pasa por las API del sistema operativo; con una llave de seguridad o un teléfono, va por CTAP como un comandoauthenticatorGetAssertionen CBOR. - El autenticador comprueba la presencia del usuario y, si se le pide, la verificación del usuario, y después firma sus datos del autenticador concatenados con el hash de los datos del cliente. La respuesta vuelve por CTAP o por la API de la plataforma.
- El navegador devuelve los bytes a la página y el servidor los verifica como especifica WebAuthn §7.2.
El script siguiente analiza el vector de prueba oficial §16.2 de WebAuthn Level 3, que contiene un registro y una autenticación de una credencial ES256, y verifica la firma (Node.js 26, cbor-x 1.6.0). authData y la firma son lo que produjo el autenticador; clientDataJSON es lo que produjo el navegador.
import { createHash, createPublicKey, verify } from "node:crypto";
import { decode } from "cbor-x";
// WebAuthn Level 3 §16.2 test vector: ES256 credential, "none" attestation
const hex = (s) => Buffer.from(s, "hex");
const attestationObject = hex("a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b559000000008446ccb9ab1db374750b2367ff6f3a1f0020f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4a5010203262001215820afefa16f97ca9b2d23eb86ccb64098d20db90856062eb249c33a9b672f26df61225820930a56b87a2fca66334b03458abf879717c12cc68ed73290af2e2664796b9220");
const authenticatorData = hex("bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b51900000000");
const clientDataJSON = hex("7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224f63446e55685158756c5455506f334a5558543049393770767a7a59425039745a63685879617630314167222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d");
const signature = hex("3046022100f50a4e2e4409249c4a853ba361282f09841df4dd4547a13a87780218deffcd380221008480ac0f0b93538174f575bf11a1dd5d78c6e486013f937295ea13653e331e87");
const sha256 = (b) => createHash("sha256").update(b).digest();
const flagNames = { 0x01: "UP", 0x04: "UV", 0x08: "BE", 0x10: "BS", 0x40: "AT", 0x80: "ED" };
const flags = (b) => Object.entries(flagNames).filter(([bit]) => b & bit).map(([, n]) => n).join(" ");
// Registration: authenticator data produced by the authenticator (CTAP authenticatorMakeCredential)
const { fmt, authData } = decode(attestationObject);
const credIdLen = authData.readUInt16BE(53);
const coseKey = decode(authData.subarray(55 + credIdLen));
console.log("attestation format:", fmt);
console.log("rpIdHash == SHA-256('example.org'):", authData.subarray(0, 32).equals(sha256("example.org")));
console.log("flags:", flags(authData[32]), "| signCount:", authData.readUInt32BE(33));
console.log("AAGUID:", authData.subarray(37, 53).toString("hex"), "| credential ID bytes:", credIdLen);
console.log("COSE key: kty", coseKey[1], "alg", coseKey[3], "crv", coseKey[-1]);
// Authentication: verify the assertion with the registered public key
const publicKey = createPublicKey({ key: { kty: "EC", crv: "P-256",
x: Buffer.from(coseKey[-2]).toString("base64url"), y: Buffer.from(coseKey[-3]).toString("base64url") }, format: "jwk" });
console.log("\nclientDataJSON (built by the browser):", clientDataJSON.toString());
console.log("assertion flags:", flags(authenticatorData[32]));
const ok = verify("sha256", Buffer.concat([authenticatorData, sha256(clientDataJSON)]), publicKey, signature);
console.log("signature over authenticatorData || SHA-256(clientDataJSON):", ok ? "valid" : "invalid");
attestation format: none
rpIdHash == SHA-256('example.org'): true
flags: UP BE BS AT | signCount: 0
AAGUID: 8446ccb9ab1db374750b2367ff6f3a1f | credential ID bytes: 32
COSE key: kty 2 alg -7 crv 1
clientDataJSON (built by the browser): {"type":"webauthn.get","challenge":"OcDnUhQXulTUPo3JUXT0I97pvzzYBP9tZchXyav01Ag","origin":"https://example.org","crossOrigin":false}
assertion flags: UP BE BS
signature over authenticatorData || SHA-256(clientDataJSON): valid
Los indicadores muestran la presencia del usuario (UP), una credencial apta para copia de seguridad y ya respaldada (BE, BS: una llave de acceso sincronizada) y, en el registro, datos de credencial atestados (AT). La clave COSE (tipo de clave 2 = EC2, algoritmo −7 = ES256, curva 1 = P-256) es el formato que comparten ambas especificaciones; el inspector de WebAuthn decodifica las mismas estructuras a partir de una respuesta real del navegador.
De U2F a las llaves de acceso
FIDO U2F (2014) dio a las llaves de seguridad un protocolo sencillo de segundo factor, hoy llamado CTAP1, y una API para el navegador. WebAuthn sustituyó esa API entre 2018 y 2019 (Chrome 67, Firefox 60, Safari 13), y CTAP 2 añadió el PIN, la verificación del usuario integrada y las credenciales detectables, de modo que una llave podía sustituir a la contraseña en lugar de complementarla. Chrome eliminó la antigua API de JavaScript de U2F en la versión 104 (2022); las credenciales registradas con ella siguen funcionando en WebAuthn gracias a la extensión appid.
El anuncio de las llaves de acceso sincronizadas por parte de Apple, Google y Microsoft en 2022 no necesitó un protocolo nuevo: una llave de acceso es una credencial detectable con los indicadores de copia de seguridad activados, creada y usada mediante la misma API de WebAuthn. CTAP 2.2 (julio de 2025) estandarizó el transporte híbrido, que permite a un teléfono actuar como autenticador de un equipo cercano tras escanear un código QR, junto con políticas de complejidad del PIN; CTAP 2.3 (febrero de 2026) es una revisión incremental que, por ejemplo, permite a los autenticadores indicar qué comandos de configuración admiten.
Atestación y certificación
En el registro, un autenticador puede incluir una declaración de atestación firmada con una clave compartida por un lote de dispositivos, y un AAGUID que identifica el modelo. WebAuthn define los formatos de la declaración (packed, tpm, android-key, apple, fido-u2f, none); la FIDO Alliance certifica los autenticadores y publica sus metadatos, incluidas las raíces de atestación y los niveles de certificación, en el servicio de metadatos de FIDO. La mayoría de los sitios para el público general no deberían solicitar atestación y deberían aceptar cualquier autenticador; las empresas que solo deben permitir llaves de hardware certificadas solicitan la atestación direct o enterprise y la contrastan con los metadatos, como ilustran el verificador de atestación de YubiKey y la búsqueda de AAGUID.
Qué implementar
- Sitio web o aplicación web: WebAuthn en el navegador y una biblioteca de servidor que implemente los pasos de verificación del §7.
- Aplicación nativa de iOS o Android: las API de llaves de acceso de la plataforma (AuthenticationServices, Credential Manager), que devuelven datos en formato WebAuthn vinculados a tu dominio web mediante dominios asociados o Digital Asset Links.
- Empresa que solo admite llaves de hardware: WebAuthn con atestación, contrastada con las entradas del servicio de metadatos de FIDO.
- Fabricante de llaves de seguridad, teléfonos o sistemas operativos: CTAP para los autenticadores externos y certificación FIDO para el producto.
- Compras: pide el nivel de certificación FIDO, la versión de CTAP y los transportes admitidos, y las funciones de WebAuthn de las que dependes, en lugar de solo «compatibilidad con FIDO2».
Notas de migración
- Sustitución de los registros U2F: sigue enviando el App ID antiguo mediante la extensión
appidal iniciar sesión, y registra una credencial WebAuthn nueva la próxima vez que cada usuario inicie sesión. - Adopción de las llaves de acceso: solicita
residentKey: "required"yuserVerification: "preferred"o"required", y guarda los indicadores de copia de seguridad. - Paso a las funciones de Level 3, una a una: la creación condicional,
getClientCapabilities(), los métodos de señal y los orígenes relacionados se degradan sin romper nada cuando no se admiten. - Pruebas: antes del lanzamiento, prueba los autenticadores de plataforma, una llave USB o NFC y el flujo híbrido desde un teléfono en el entorno de pruebas de llaves de acceso.
Referencias
- Web Authentication Level 3 del W3C (§6.1 datos del autenticador, §7 operaciones de la relying party, §16 vectores de prueba)
- CTAP 2.3 de FIDO (Client to Authenticator Protocol)
- CTAP 2.2 de FIDO (Client to Authenticator Protocol) (§6 comandos, §11 vinculaciones con los transportes)
- Servicio de metadatos de FIDO
- RFC 9052, estructuras y procesos de COSE y RFC 9053, algoritmos de COSE