Saltar al contenido
ecdsa.com

FIDO2, WebAuthn y CTAP: capas de las llaves de acceso

No son alternativas. WebAuthn es la API del W3C a la que llaman los sitios web, CTAP es el protocolo de FIDO entre el dispositivo y una llave de seguridad o un teléfono, y FIDO2 es el nombre del conjunto de ambos.

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

WebAuthnCTAPFIDO2
Qué esAPI del navegador y formatos de datosProtocolo de comunicación con autenticadores externosTérmino genérico para ambos
Publicado porW3CFIDO AllianceFIDO Alliance
Versión actualLevel 3, recomendación del W3C del 25 de agosto de 2026CTAP 2.3, estándar propuesto del 26 de febrero de 2026 (2.2: julio de 2025)WebAuthn Level 3 con CTAP 2.3
Versiones anterioresLevel 1 (marzo de 2019), Level 2 (abril de 2021)CTAP 2.0, 2.1 (2021); CTAP1 es U2FU2F (2014) es su predecesor
Quién lo implementaNavegadores y plataformas (API), relying parties (comprobaciones en el servidor)Navegadores, sistemas operativos, llaves de seguridad, teléfonosTodos los eslabones de la cadena
EntrePágina web y navegador; servidor y datos del autenticadorPlataforma cliente y autenticadorDe la relying party al autenticador
CodificaciónObjetos JavaScript, clientDataJSON, objetos de atestación CBORComandos CBOR canónicos de CTAP2, como authenticatorGetAssertion (0x02)Ambas
TransportesNo aplicaUSB HID, NFC, Bluetooth Low Energy, híbrido (código QR, comprobación de proximidad, túnel)Ambos
Puntos de entrada principalesnavigator.credentials.create() y .get()authenticatorMakeCredential (0x01), authenticatorGetInfo (0x04)No aplica

Quién hace qué durante un inicio de sesión

  1. El sitio web llama a navigator.credentials.get() con un desafío y el ID de su relying party (RP ID). Esto es WebAuthn.
  2. 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 comando authenticatorGetAssertion en CBOR.
  3. 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.
  4. 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.

Análisis y verificación del vector de prueba §16.2 de WebAuthn Level 3
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");
Salida
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

  1. Sustitución de los registros U2F: sigue enviando el App ID antiguo mediante la extensión appid al iniciar sesión, y registra una credencial WebAuthn nueva la próxima vez que cada usuario inicie sesión.
  2. Adopción de las llaves de acceso: solicita residentKey: "required" y userVerification: "preferred" o "required", y guarda los indicadores de copia de seguridad.
  3. 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.
  4. 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

Todas las comparaciones