FIDO2 vs WebAuthn est une question de couches plutôt qu’un choix. WebAuthn (W3C Web Authentication) définit l’API JavaScript qu’appellent les parties de confiance (relying parties, RP) et les formats de données signées qu’elles vérifient. CTAP (FIDO Client to Authenticator Protocol) définit la façon dont un navigateur ou un système d’exploitation communique avec un authentificateur externe par USB, NFC, Bluetooth ou le transport hybride par code QR. FIDO2 est le nom générique, introduit en 2018 par la FIDO Alliance, qui désigne WebAuthn plus CTAP 2. Les clés d’accès (passkeys) sont des identifiants FIDO créés au moyen de ces standards.
Vue d’ensemble
| WebAuthn | CTAP | FIDO2 | |
|---|---|---|---|
| De quoi il s’agit | API de navigateur et formats de données | Protocole de communication avec les authentificateurs itinérants | Terme générique qui couvre les deux |
| Éditeur | W3C | FIDO Alliance | FIDO Alliance |
| Version actuelle | Level 3, recommandation du W3C du 25 août 2026 | CTAP 2.3, Proposed Standard du 26 février 2026 (2.2 : juillet 2025) | WebAuthn Level 3 avec CTAP 2.3 |
| Versions antérieures | Level 1 (mars 2019), Level 2 (avril 2021) | CTAP 2.0, 2.1 (2021) ; CTAP1 correspond à U2F | U2F (2014) en est le prédécesseur |
| Qui l’implémente | Navigateurs et plateformes (API), parties de confiance (vérifications côté serveur) | Navigateurs, systèmes d’exploitation, clés de sécurité, téléphones | Tous les maillons de la chaîne |
| Entre | Page web et navigateur ; serveur et données d’authentificateur | Plateforme cliente et authentificateur | De la partie de confiance à l’authentificateur |
| Encodage | Objets JavaScript, clientDataJSON, objets d’attestation CBOR | Commandes CTAP2 en CBOR canonique, comme authenticatorGetAssertion (0x02) | Les deux |
| Transports | Sans objet | USB HID, NFC, Bluetooth Low Energy, hybride (code QR, vérification de proximité, tunnel) | Les deux |
| Principaux points d’entrée | navigator.credentials.create() et .get() | authenticatorMakeCredential (0x01), authenticatorGetInfo (0x04) | Sans objet |
Qui fait quoi lors d’une connexion
- Le site web appelle
navigator.credentials.get()avec un challenge et l’ID de sa partie de confiance (RP ID). Cette étape relève de WebAuthn. - Le navigateur construit
clientDataJSON(type, challenge, origine, indicateur d’origine croisée), le hache et demande à un authentificateur de signer. Pour un authentificateur de plateforme intégré (Touch ID, Windows Hello, Android), cela passe par les API du système d’exploitation ; pour une clé de sécurité ou un téléphone, cela passe par CTAP, sous la forme d’une commandeauthenticatorGetAssertionen CBOR. - L’authentificateur contrôle la présence de l’utilisateur et, sur demande, effectue la vérification de l’utilisateur, puis signe ses données d’authentificateur concaténées au condensat des données client. La réponse revient par CTAP ou par l’API de la plateforme.
- Le navigateur renvoie les octets à la page, et le serveur les vérifie comme le spécifie le §7.2 de WebAuthn.
Le script ci-dessous analyse le vecteur de test officiel §16.2 de WebAuthn Level 3, qui contient un enregistrement et une authentification pour un identifiant ES256, puis vérifie la signature (Node.js 26, cbor-x 1.6.0). Les octets authData et la signature ont été produits par l’authentificateur ; clientDataJSON l’a été par le navigateur.
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
Les indicateurs montrent la présence de l’utilisateur (UP), un identifiant éligible à la sauvegarde et sauvegardé (BE, BS : une clé d’accès synchronisée) et, à l’enregistrement, des données d’identifiant attestées (AT). La clé COSE (type de clé 2 = EC2, algorithme −7 = ES256, courbe 1 = P-256) est le format commun aux deux spécifications ; l’inspecteur WebAuthn décode les mêmes structures à partir d’une vraie réponse de navigateur.
De U2F aux clés d’accès
FIDO U2F (2014) a doté les clés de sécurité d’un protocole simple de second facteur, aujourd’hui appelé CTAP1, et d’une API de navigateur. WebAuthn a remplacé cette API entre 2018 et 2019 (Chrome 67, Firefox 60, Safari 13), et CTAP 2 a ajouté les codes PIN, la vérification de l’utilisateur intégrée et les identifiants détectables, si bien qu’une clé pouvait remplacer le mot de passe au lieu de seulement le compléter. Chrome a retiré l’ancienne API JavaScript U2F dans sa version 104 (2022) ; les identifiants enregistrés par son intermédiaire fonctionnent toujours dans WebAuthn grâce à l’extension appid.
L’annonce des clés d’accès synchronisées par Apple, Google et Microsoft en 2022 n’a exigé aucun nouveau protocole : une clé d’accès est un identifiant détectable dont les indicateurs de sauvegarde sont activés, créé et utilisé via la même API WebAuthn. CTAP 2.2 (juillet 2025) a normalisé le transport hybride, qui permet à un téléphone de servir d’authentificateur à un ordinateur proche après la lecture d’un code QR, ainsi que des politiques de complexité du PIN ; CTAP 2.3 (février 2026) est une révision incrémentale, qui permet par exemple aux authentificateurs d’indiquer quelles commandes de configuration ils prennent en charge.
Attestation et certification
À l’enregistrement, un authentificateur peut inclure une déclaration d’attestation signée par une clé partagée par tout un lot d’appareils, ainsi qu’un AAGUID qui identifie le modèle. WebAuthn définit les formats de déclaration (packed, tpm, android-key, apple, fido-u2f, none) ; la FIDO Alliance certifie les authentificateurs et publie leurs métadonnées, dont les racines d’attestation et les niveaux de certification, dans le service de métadonnées FIDO (FIDO Metadata Service). La plupart des sites grand public devraient ne demander aucune attestation et accepter n’importe quel authentificateur ; les entreprises qui doivent n’autoriser que des clés matérielles certifiées demandent une attestation direct ou enterprise et la confrontent aux métadonnées, comme l’illustrent le vérificateur d’attestation YubiKey et la recherche d’AAGUID.
Ce qu’il faut implémenter
- Site web ou application web : WebAuthn dans le navigateur et une bibliothèque côté serveur qui implémente les étapes de vérification du §7.
- Application native iOS ou Android : les API de clés d’accès de la plateforme (AuthenticationServices, Credential Manager), qui renvoient des données au format WebAuthn liées à votre domaine web par les domaines associés (associated domains) ou par Digital Asset Links.
- Entreprise qui n’autorise que des clés matérielles : WebAuthn avec attestation, confrontée aux entrées du service de métadonnées FIDO.
- Fabricant de clés de sécurité, de téléphones ou de systèmes d’exploitation : CTAP pour les authentificateurs itinérants, certification FIDO pour le produit.
- Achats : demandez le niveau de certification FIDO, la version de CTAP et les transports pris en charge, ainsi que les fonctionnalités WebAuthn dont vous dépendez, plutôt qu’une simple « prise en charge de FIDO2 ».
Notes de migration
- Remplacer les enregistrements U2F : continuez à envoyer l’ancien App ID via l’extension
appidà la connexion, et enregistrez un nouvel identifiant WebAuthn lors de la prochaine connexion des utilisateurs. - Adopter les clés d’accès : demandez
residentKey: "required"etuserVerification: "preferred"ou"required", et stockez les indicateurs de sauvegarde. - Passer à WebAuthn Level 3 fonctionnalité par fonctionnalité : la création conditionnelle,
getClientCapabilities(), les méthodes de signal et les origines associées se dégradent toutes proprement lorsqu’elles ne sont pas prises en charge. - Tests : avant le lancement, éprouvez les authentificateurs de plateforme, une clé USB ou NFC et le parcours hybride depuis un téléphone dans le bac à sable des clés d’accès.
Références
- W3C Web Authentication Level 3 (§6.1 données d’authentificateur, §7 opérations de la partie de confiance, §16 vecteurs de test)
- Spécification FIDO Client to Authenticator Protocol 2.3
- Spécification FIDO Client to Authenticator Protocol 2.2 (§6 commandes, §11 liaisons de transport)
- Service de métadonnées FIDO
- RFC 9052, structures et traitement COSE et RFC 9053, algorithmes COSE