{"id":1253,"date":"2026-10-28T11:29:59","date_gmt":"2026-10-28T10:29:59","guid":{"rendered":"https:\/\/remivernier.com\/?p=1253"},"modified":"2026-10-28T13:28:28","modified_gmt":"2026-10-28T12:28:28","slug":"passkeys-fido2-entra-id-ceremonie-webauthn","status":"publish","type":"post","link":"https:\/\/remivernier.com\/index.php\/2026\/10\/28\/passkeys-fido2-entra-id-ceremonie-webauthn\/","title":{"rendered":"Passkeys FIDO2 dans Entra ID: c\u00e9r\u00e9monie WebAuthn en d\u00e9tail"},"content":{"rendered":"<h2>Pourquoi une passkey r\u00e9siste au phishing?<\/h2>\n<p>Un utilisateur qui saisit son mot de passe et son code OTP sur un faux portail de connexion donne \u00e0 l&rsquo;attaquant tout ce qu&rsquo;il faut pour rejouer l&rsquo;authentification. Est-ce qu&rsquo;il se passe la m\u00eame chose s&rsquo;il valide sa passkey sur ce m\u00eame faux portail? Non. La signature produite par l&rsquo;authentificateur est li\u00e9e au nom de domaine du service l\u00e9gitime et \u00e0 un challenge \u00e0 usage unique, et c&rsquo;est le navigateur qui renseigne l&rsquo;origine, pas la page.<\/p>\n<p>Pour comprendre d&rsquo;o\u00f9 vient cette propri\u00e9t\u00e9, il faut regarder ce qui circule pendant les deux c\u00e9r\u00e9monies WebAuthn, l&rsquo;enregistrement puis l&rsquo;authentification, ce qu&rsquo;Entra ID conserve de son c\u00f4t\u00e9 et ce qui reste enferm\u00e9 dans l&rsquo;authentificateur.<\/p>\n<p>Trois acteurs participent \u00e0 la c\u00e9r\u00e9monie: la Relying Party (RP), ici Entra ID, identifi\u00e9e par un RP ID qui est un nom de domaine (<code>login.microsoft.com<\/code> pour Entra), le client (le navigateur ou l&rsquo;OS, qui expose l&rsquo;API <code>navigator.credentials<\/code>) et l&rsquo;authentificateur (cl\u00e9 de s\u00e9curit\u00e9 USB\/NFC, Windows Hello, Microsoft Authenticator, gestionnaire de passkeys synchronis\u00e9es, etc\u2026).<\/p>\n<p>Le point de d\u00e9part de la r\u00e9sistance au phishing est simple: une paire de cl\u00e9s est g\u00e9n\u00e9r\u00e9e pour chaque couple RP ID \/ compte, et le navigateur refuse qu&rsquo;une page demande un RP ID qui ne correspond pas \u00e0 son propre domaine. Une page h\u00e9berg\u00e9e sur <code>login-micros0ft.com<\/code> ne peut donc pas demander une signature pour <code>login.microsoft.com<\/code>. Si l&rsquo;attaquant monte un proxy de type adversary-in-the-middle qui relaie la page l\u00e9gitime, l&rsquo;origine que le navigateur inscrit dans le <code>clientDataJSON<\/code> est celle du proxy, elle est couverte par la signature, et Entra ID la rejette.<\/p>\n<p>Comparons avec une r\u00e9ponse NTLM: comme nous l&rsquo;avions vu dans l&rsquo;article sur les <a href=\"https:\/\/remivernier.com\/index.php\/2018\/08\/26\/protocoles-nbt-ns-llmnr-et-exploitation-des-failles\/\">protocoles NBT-NS, LLMNR et l&rsquo;exploitation des failles<\/a>, rien dans cette r\u00e9ponse ne dit \u00e0 qui elle \u00e9tait destin\u00e9e, d&rsquo;o\u00f9 la possibilit\u00e9 de la relayer. WebAuthn ferme cette porte au niveau du protocole.<\/p>\n<h2>Enregistrement: ce que le serveur stocke<\/h2>\n<p>\u00c0 l&rsquo;enregistrement, Entra ID envoie au navigateur des options de cr\u00e9ation: un challenge al\u00e9atoire, le RP ID, l&rsquo;identifiant de l&rsquo;utilisateur, les algorithmes accept\u00e9s, la pr\u00e9f\u00e9rence d&rsquo;attestation et la liste des credentials d\u00e9j\u00e0 enregistr\u00e9s pour \u00e9viter les doublons. Le navigateur construit alors un <code>clientDataJSON<\/code> qui contient le type (<code>webauthn.create<\/code>), le challenge et l&rsquo;origine qu&rsquo;il a lui-m\u00eame constat\u00e9e, puis transmet \u00e0 l&rsquo;authentificateur le hash SHA-256 de cet objet.<\/p>\n<p>L&rsquo;authentificateur v\u00e9rifie la pr\u00e9sence de l&rsquo;utilisateur (un appui sur la cl\u00e9), et g\u00e9n\u00e9ralement sa v\u00e9rification (PIN ou biom\u00e9trie), g\u00e9n\u00e8re une nouvelle paire de cl\u00e9s et renvoie un <code>attestationObject<\/code> encod\u00e9 en CBOR. Il contient trois \u00e9l\u00e9ments:<\/p>\n<ul>\n<li><code>fmt<\/code>: le format de l&rsquo;attestation (<code>packed<\/code>, <code>tpm<\/code>, <code>android-key<\/code>, <code>apple<\/code>, <code>fido-u2f<\/code> ou <code>none<\/code>),<\/li>\n<li><code>attStmt<\/code>: la d\u00e9claration d&rsquo;attestation, c&rsquo;est \u00e0 dire une signature produite avec une cl\u00e9 d&rsquo;attestation du fabricant et sa cha\u00eene de certificats,<\/li>\n<li><code>authData<\/code>: les donn\u00e9es de l&rsquo;authentificateur, dont la structure est d\u00e9taill\u00e9e ci dessous.<\/li>\n<\/ul>\n<figure class=\"wp-block-image\">\n<svg viewBox=\"0 0 720 210\" width=\"100%\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" font-family=\"sans-serif\" font-size=\"13\">\n<rect x=\"10\" y=\"40\" width=\"150\" height=\"50\" fill=\"#e8f0fe\" stroke=\"#333\"\/>\n<text x=\"85\" y=\"62\" text-anchor=\"middle\">rpIdHash<\/text>\n<text x=\"85\" y=\"80\" text-anchor=\"middle\">32 octets<\/text>\n<rect x=\"160\" y=\"40\" width=\"70\" height=\"50\" fill=\"#fdf3e1\" stroke=\"#333\"\/>\n<text x=\"195\" y=\"62\" text-anchor=\"middle\">flags<\/text>\n<text x=\"195\" y=\"80\" text-anchor=\"middle\">1<\/text>\n<rect x=\"230\" y=\"40\" width=\"90\" height=\"50\" fill=\"#fdf3e1\" stroke=\"#333\"\/>\n<text x=\"275\" y=\"62\" text-anchor=\"middle\">signCount<\/text>\n<text x=\"275\" y=\"80\" text-anchor=\"middle\">4<\/text>\n<rect x=\"320\" y=\"40\" width=\"300\" height=\"50\" fill=\"#e6f4ea\" stroke=\"#333\"\/>\n<text x=\"470\" y=\"62\" text-anchor=\"middle\">attestedCredentialData<\/text>\n<text x=\"470\" y=\"80\" text-anchor=\"middle\">pr\u00e9sent si le bit AT est lev\u00e9<\/text>\n<rect x=\"620\" y=\"40\" width=\"90\" height=\"50\" fill=\"#fff\" stroke=\"#333\" stroke-dasharray=\"4 3\"\/>\n<text x=\"665\" y=\"62\" text-anchor=\"middle\">extensions<\/text>\n<text x=\"665\" y=\"80\" text-anchor=\"middle\">si bit ED<\/text>\n<line x1=\"320\" y1=\"90\" x2=\"160\" y2=\"140\" stroke=\"#333\"\/>\n<line x1=\"620\" y1=\"90\" x2=\"710\" y2=\"140\" stroke=\"#333\"\/>\n<rect x=\"160\" y=\"140\" width=\"130\" height=\"50\" fill=\"#e6f4ea\" stroke=\"#333\"\/>\n<text x=\"225\" y=\"162\" text-anchor=\"middle\">AAGUID<\/text>\n<text x=\"225\" y=\"180\" text-anchor=\"middle\">16 octets<\/text>\n<rect x=\"290\" y=\"140\" width=\"90\" height=\"50\" fill=\"#e6f4ea\" stroke=\"#333\"\/>\n<text x=\"335\" y=\"162\" text-anchor=\"middle\">longueur L<\/text>\n<text x=\"335\" y=\"180\" text-anchor=\"middle\">2<\/text>\n<rect x=\"380\" y=\"140\" width=\"150\" height=\"50\" fill=\"#e6f4ea\" stroke=\"#333\"\/>\n<text x=\"455\" y=\"162\" text-anchor=\"middle\">credentialId<\/text>\n<text x=\"455\" y=\"180\" text-anchor=\"middle\">L octets<\/text>\n<rect x=\"530\" y=\"140\" width=\"180\" height=\"50\" fill=\"#e6f4ea\" stroke=\"#333\"\/>\n<text x=\"620\" y=\"162\" text-anchor=\"middle\">cl\u00e9 publique<\/text>\n<text x=\"620\" y=\"180\" text-anchor=\"middle\">COSE_Key (CBOR)<\/text>\n<text x=\"10\" y=\"25\" font-size=\"14\">authenticatorData<\/text>\n<\/svg><figcaption>Structure de l&rsquo;authenticatorData renvoy\u00e9e par l&rsquo;authentificateur<\/figcaption><\/figure>\n<p>Le premier champ est le hash SHA-256 du RP ID: le serveur v\u00e9rifie qu&rsquo;il correspond au sien. L&rsquo;octet de flags porte les bits UP (bit 0, pr\u00e9sence), UV (bit 2, v\u00e9rification), BE et BS (bits 3 et 4, credential \u00e9ligible \u00e0 la sauvegarde et effectivement sauvegard\u00e9, autrement dit synchronisable), AT (bit 6, donn\u00e9es de credential pr\u00e9sentes) et ED (bit 7, extensions). Le <code>signCount<\/code> est un compteur de signatures. \u00c0 l&rsquo;enregistrement viennent ensuite l&rsquo;AAGUID, un identifiant de 128 bits qui d\u00e9signe le mod\u00e8le d&rsquo;authentificateur, le credential ID et la cl\u00e9 publique.<\/p>\n<p>\u00c0 l&rsquo;issue de la c\u00e9r\u00e9monie, Entra ID conserve pour ce compte le credential ID, la cl\u00e9 publique, l&rsquo;AAGUID, le dernier compteur et les informations tir\u00e9es de l&rsquo;attestation (mod\u00e8le, niveau d&rsquo;attestation). Nous pouvons consulter une partie de ces donn\u00e9es avec Microsoft Graph:<\/p>\n<pre class=\"wp-block-code\"><code>GET https:\/\/graph.microsoft.com\/v1.0\/users\/alice@test.onmicrosoft.com\/authentication\/fido2Methods\n\n{\n  \"id\": \"Y2Ek...\",\n  \"displayName\": \"Cl\u00e9 USB bureau\",\n  \"aaguid\": \"de1e552d-db1d-4423-a619-566b625cdc84\",\n  \"model\": \"Microsoft Authenticator for Android\",\n  \"attestationLevel\": \"attested\",\n  \"createdDateTime\": \"2026-10-12T08:41:03Z\"\n}<\/code><\/pre>\n<p>Aucun secret dans cette liste. La cl\u00e9 priv\u00e9e ne quitte jamais l&rsquo;authentificateur, et le PIN ou l&#8217;empreinte non plus: le serveur ne voit que le bit UV. Une fuite de la base c\u00f4t\u00e9 RP livre des cl\u00e9s publiques, rien de rejouable, alors qu&rsquo;une fuite d&rsquo;un annuaire livre des hashs exploitables, m\u00eame lorsque ce sont des hashs de hashs comme nous l&rsquo;avions vu dans l&rsquo;article sur <a href=\"https:\/\/remivernier.com\/index.php\/2019\/11\/23\/azure-ad-office-365-password-hash-sync-et-seamless-sso-en-profondeur\/\">Password Hash Sync et Seamless SSO<\/a>.<\/p>\n<h2>Authentification: signature et compteur<\/h2>\n<p>La seconde c\u00e9r\u00e9monie suit la m\u00eame logique, sans cr\u00e9ation de cl\u00e9. Entra ID envoie un nouveau challenge, le navigateur construit un <code>clientDataJSON<\/code> de type <code>webauthn.get<\/code>, et l&rsquo;authentificateur signe la concat\u00e9nation de l&rsquo;<code>authenticatorData<\/code> (cette fois sans le bloc attestedCredentialData) et du hash SHA-256 du <code>clientDataJSON<\/code>.<\/p>\n<figure class=\"wp-block-image\">\n<svg viewBox=\"0 0 720 380\" width=\"100%\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" font-family=\"sans-serif\" font-size=\"13\">\n<defs><marker id=\"fl\" markerWidth=\"10\" markerHeight=\"8\" refX=\"9\" refY=\"4\" orient=\"auto\"><path d=\"M0,0 L10,4 L0,8 z\" fill=\"#333\"\/><\/marker><\/defs>\n<rect x=\"20\" y=\"10\" width=\"170\" height=\"40\" fill=\"#e8f0fe\" stroke=\"#333\"\/>\n<text x=\"105\" y=\"35\" text-anchor=\"middle\">Entra ID (RP)<\/text>\n<rect x=\"275\" y=\"10\" width=\"170\" height=\"40\" fill=\"#fdf3e1\" stroke=\"#333\"\/>\n<text x=\"360\" y=\"35\" text-anchor=\"middle\">Navigateur<\/text>\n<rect x=\"530\" y=\"10\" width=\"170\" height=\"40\" fill=\"#e6f4ea\" stroke=\"#333\"\/>\n<text x=\"615\" y=\"35\" text-anchor=\"middle\">Authentificateur<\/text>\n<line x1=\"105\" y1=\"50\" x2=\"105\" y2=\"370\" stroke=\"#333\" stroke-dasharray=\"4 3\"\/>\n<line x1=\"360\" y1=\"50\" x2=\"360\" y2=\"370\" stroke=\"#333\" stroke-dasharray=\"4 3\"\/>\n<line x1=\"615\" y1=\"50\" x2=\"615\" y2=\"370\" stroke=\"#333\" stroke-dasharray=\"4 3\"\/>\n<line x1=\"105\" y1=\"85\" x2=\"355\" y2=\"85\" stroke=\"#333\" marker-end=\"url(#fl)\"\/>\n<text x=\"230\" y=\"78\" text-anchor=\"middle\">1. challenge, rpId, allowCredentials<\/text>\n<line x1=\"360\" y1=\"135\" x2=\"610\" y2=\"135\" stroke=\"#333\" marker-end=\"url(#fl)\"\/>\n<text x=\"487\" y=\"113\" text-anchor=\"middle\">2. le navigateur ajoute l&rsquo;origine<\/text>\n<text x=\"487\" y=\"128\" text-anchor=\"middle\">rpId + SHA-256(clientDataJSON)<\/text>\n<rect x=\"545\" y=\"155\" width=\"140\" height=\"55\" fill=\"#fff\" stroke=\"#333\"\/>\n<text x=\"615\" y=\"177\" text-anchor=\"middle\">3. PIN \/ biom\u00e9trie<\/text>\n<text x=\"615\" y=\"196\" text-anchor=\"middle\">signature cl\u00e9 priv\u00e9e<\/text>\n<line x1=\"610\" y1=\"245\" x2=\"365\" y2=\"245\" stroke=\"#333\" marker-end=\"url(#fl)\"\/>\n<text x=\"487\" y=\"238\" text-anchor=\"middle\">4. authenticatorData, signature<\/text>\n<line x1=\"355\" y1=\"290\" x2=\"110\" y2=\"290\" stroke=\"#333\" marker-end=\"url(#fl)\"\/>\n<text x=\"230\" y=\"275\" text-anchor=\"middle\">5. clientDataJSON, authenticatorData,<\/text>\n<text x=\"230\" y=\"288\" text-anchor=\"middle\">signature, credentialId<\/text>\n<rect x=\"30\" y=\"310\" width=\"150\" height=\"55\" fill=\"#fff\" stroke=\"#333\"\/>\n<text x=\"105\" y=\"332\" text-anchor=\"middle\">6. cl\u00e9 publique,<\/text>\n<text x=\"105\" y=\"351\" text-anchor=\"middle\">challenge, origine<\/text>\n<\/svg><figcaption>C\u00e9r\u00e9monie d&rsquo;authentification WebAuthn avec Entra ID<\/figcaption><\/figure>\n<p>C\u00f4t\u00e9 serveur, Entra ID retrouve la cl\u00e9 publique gr\u00e2ce au credential ID, v\u00e9rifie la signature, puis contr\u00f4le le type, le challenge (il doit \u00eatre celui qu&rsquo;il vient d&rsquo;\u00e9mettre), l&rsquo;origine, le <code>rpIdHash<\/code>, les bits UP et UV et le compteur. Si le compteur re\u00e7u n&rsquo;est pas sup\u00e9rieur au dernier stock\u00e9 alors qu&rsquo;il n&rsquo;est pas nul, cela peut trahir un clone de l&rsquo;authentificateur.<\/p>\n<p>Attention: les passkeys synchronis\u00e9es (iCloud Keychain, Google Password Manager\u2026) renvoient en g\u00e9n\u00e9ral un compteur \u00e0 0, puisque la m\u00eame cl\u00e9 vit sur plusieurs appareils. La d\u00e9tection de clonage par compteur ne vaut donc que pour les authentificateurs li\u00e9s \u00e0 un appareil.<\/p>\n<p>Pour observer ces objets sur un compte de test, le plus simple est d&rsquo;intercepter la r\u00e9ponse de l&rsquo;API dans la console du navigateur, sur la page de connexion, avant de lancer la connexion par passkey:<\/p>\n<pre class=\"wp-block-code\"><code>const b = x =&gt; btoa(String.fromCharCode(...new Uint8Array(x)))\n  .replace(\/\\+\/g, '-').replace(\/\\\/\/g, '_').replace(\/=+$\/, '');\nconst get = navigator.credentials.get.bind(navigator.credentials);\nnavigator.credentials.get = async o =&gt; {\n  const c = await get(o);\n  console.log(c.id, b(c.response.clientDataJSON), b(c.response.authenticatorData));\n  return c;\n};<\/code><\/pre>\n<p>Pour l&rsquo;enregistrement, la m\u00eame technique s&rsquo;applique \u00e0 <code>navigator.credentials.create<\/code>, et la m\u00e9thode <code>getAuthenticatorData()<\/code> de la r\u00e9ponse \u00e9vite d&rsquo;avoir \u00e0 d\u00e9coder le CBOR de l&rsquo;<code>attestationObject<\/code>. Attention, ce hook dispara\u00eet au moindre rechargement de la page: il faut le coller une fois la page de choix de la m\u00e9thode affich\u00e9e. L&rsquo;onglet \u00ab WebAuthn \u00bb des outils de d\u00e9veloppement de Chrome permet aussi de simuler un authentificateur virtuel. Une capture Fiddler fonctionne \u00e9galement, comme nous l&rsquo;avions fait dans l&rsquo;article sur <a href=\"https:\/\/remivernier.com\/index.php\/2018\/09\/16\/exploration-des-entetes-http-www-authenticate\/\">les ent\u00eates WWW-Authenticate et Authorization<\/a>, mais il faut alors retrouver les champs dans le corps du POST.<\/p>\n<p>Le script Python ci dessous d\u00e9code les deux objets r\u00e9cup\u00e9r\u00e9s (encod\u00e9s en base64url):<\/p>\n<pre class=\"wp-block-code\"><code>import base64, hashlib, json, struct, sys, uuid\n\ndef b64u_decode(s):\n    return base64.urlsafe_b64decode(s + \"=\" * (-len(s) % 4))\n\nFLAGS = {0: \"UP\", 2: \"UV\", 3: \"BE\", 4: \"BS\", 6: \"AT\", 7: \"ED\"}\n\ndef parse_client_data(b64):\n    cd = json.loads(b64u_decode(b64))\n    for k, v in cd.items():\n        print(f\"clientDataJSON.{k:&lt;12}: {v}\")\n    return cd\n\ndef parse_auth_data(raw, rp_id=\"login.microsoft.com\"):\n    rp_hash, flags, count = raw[:32], raw[32], struct.unpack(\"&gt;I\", raw[33:37])[0]\n    attendu = hashlib.sha256(rp_id.encode()).digest()\n    print(f\"rpIdHash            : {rp_hash.hex()[:16]}... ({'OK' if rp_hash == attendu else 'DIFFERENT'} pour {rp_id})\")\n    print(f\"flags               : 0x{flags:02x} -&gt; {' '.join(n for b, n in FLAGS.items() if flags &gt;&gt; b &amp; 1)}\")\n    print(f\"signCount           : {count}\")\n    if flags &gt;&gt; 6 &amp; 1:\n        aaguid = uuid.UUID(bytes=raw[37:53])\n        l = struct.unpack(\"&gt;H\", raw[53:55])[0]\n        cred_id = raw[55:55 + l]\n        print(f\"aaguid              : {aaguid}\")\n        print(f\"credentialId        : {base64.urlsafe_b64encode(cred_id).rstrip(b'=').decode()[:24]}... ({l} octets)\")\n        print(f\"credentialPublicKey : {len(raw) - 55 - l} octets (COSE_Key)\")\n\nif __name__ == \"__main__\":\n    parse_client_data(sys.argv[1])\n    parse_auth_data(b64u_decode(sys.argv[2]))<\/code><\/pre>\n<p>Voici la sortie pour une assertion d&rsquo;exemple:<\/p>\n<pre class=\"wp-block-code\"><code>python3 webauthn_decode.py eyJ0eXBlIjoid2ViYXV0aG4uZ2V0Iiwi... NWye1KCTIblpXx6vkYID8bVfaJ2mH7yWGEwVfdpoDIEFAAAAKg\n\nclientDataJSON.type        : webauthn.get\nclientDataJSON.challenge   : Qm9uam91ciB0ZXN0LmxvY2Fs\nclientDataJSON.origin      : https:\/\/login.microsoft.com\nclientDataJSON.crossOrigin : False\nrpIdHash            : 356c9ed4a09321b9... (OK pour login.microsoft.com)\nflags               : 0x05 -&gt; UP UV\nsignCount           : 42<\/code><\/pre>\n<p>-> <code>origin<\/code> est \u00e9crite par le navigateur et couverte par la signature via le hash du <code>clientDataJSON<\/code>: le proxy d&rsquo;un attaquant ne peut pas la modifier.<\/p>\n<p>-> <code>0x05<\/code>: pr\u00e9sence et v\u00e9rification de l&rsquo;utilisateur, pas de bit BE\/BS, nous sommes sur un authentificateur li\u00e9 \u00e0 l&rsquo;appareil, et le compteur est incr\u00e9ment\u00e9.<\/p>\n<p>Et pour l&rsquo;enregistrement d&rsquo;une passkey dans Microsoft Authenticator sur Android:<\/p>\n<pre class=\"wp-block-code\"><code>clientDataJSON.type        : webauthn.create\nclientDataJSON.challenge   : Qm9uam91ciB0ZXN0LmxvY2Fs\nclientDataJSON.origin      : https:\/\/login.microsoft.com\nclientDataJSON.crossOrigin : False\nrpIdHash            : 356c9ed4a09321b9... (OK pour login.microsoft.com)\nflags               : 0x45 -&gt; UP UV AT\nsignCount           : 0\naaguid              : de1e552d-db1d-4423-a619-566b625cdc84\ncredentialId        : -cfkJ7NsbAtA4siGwhrxE0eM... (32 octets)\ncredentialPublicKey : 77 octets (COSE_Key)<\/code><\/pre>\n<p>Le bit AT est lev\u00e9 et l&rsquo;AAGUID appara\u00eet. C&rsquo;est cette valeur qu&rsquo;Entra ID utilise pour ses restrictions de cl\u00e9s.<\/p>\n<h2>AAGUID et attestation dans Entra ID<\/h2>\n<p>Entra ID regroupe d\u00e9sormais ces r\u00e9glages dans des profils de passkeys, que l&rsquo;on attribue \u00e0 des groupes. <a href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity\/authentication\/how-to-authentication-passkeys-fido2\">La documentation Microsoft<\/a> pr\u00e9sente des exigences sp\u00e9cifiques comme l&rsquo;attestation, le type de passkey (li\u00e9e \u00e0 l&rsquo;appareil ou synchronis\u00e9e) ou des restrictions par AAGUID, avec des profils distincts selon les populations, par exemple les administrateurs d&rsquo;un c\u00f4t\u00e9 et les op\u00e9rateurs de terrain de l&rsquo;autre.<\/p>\n<p>Les deux r\u00e9glages ne s&rsquo;appliquent pas au m\u00eame moment, et ce point est tr\u00e8s important. L&rsquo;application de l&rsquo;attestation ne d\u00e9termine si une passkey est accept\u00e9e qu&rsquo;au moment de l&rsquo;enregistrement. Une passkey enregistr\u00e9e sans attestation continue donc de fonctionner si nous activons l&rsquo;option plus tard. Les restrictions de cl\u00e9s, elles, conditionnent l&rsquo;utilisation des mod\u00e8les ou fournisseurs \u00e0 la fois pour l&rsquo;enregistrement et pour l&rsquo;authentification: retirer un AAGUID de la liste autoris\u00e9e coupe imm\u00e9diatement les connexions des passkeys de ce mod\u00e8le d\u00e9j\u00e0 enregistr\u00e9es. Il vaut mieux le savoir avant de \u00ab nettoyer \u00bb une liste en production.<\/p>\n<p>Le d\u00e9codage plus haut explique le reste. L&rsquo;AAGUID se trouve dans l&rsquo;<code>authData<\/code>, que l&rsquo;authentificateur remplit lui-m\u00eame. Sans attestation, rien ne prouve que cette valeur est vraie: un authentificateur logiciel peut y \u00e9crire l&rsquo;AAGUID d&rsquo;une cl\u00e9 YubiKey. Avec l&rsquo;attestation, la d\u00e9claration <code>attStmt<\/code> est sign\u00e9e par une cl\u00e9 du fabricant dont la cha\u00eene de certificats peut \u00eatre v\u00e9rifi\u00e9e dans les m\u00e9tadonn\u00e9es de confiance. Microsoft le dit sans d\u00e9tour: sans attestation impos\u00e9e, Entra ID ne peut garantir aucun attribut d&rsquo;une passkey, y compris son caract\u00e8re synchronis\u00e9 ou li\u00e9 \u00e0 un appareil. Une liste d&rsquo;AAGUID sans attestation reste utile pour orienter les utilisateurs vers les bons mod\u00e8les, pas pour bloquer un attaquant.<\/p>\n<p>Le cas de Microsoft Authenticator illustre la combinaison des deux. La <a href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity\/authentication\/how-to-enable-authenticator-passkey\">page d\u00e9di\u00e9e<\/a> donne les AAGUID de l&rsquo;application, dont <code>de1e552d-db1d-4423-a619-566b625cdc84<\/code> pour Android, et pr\u00e9cise une contrainte: l&rsquo;enregistrement de passkeys attest\u00e9es n&rsquo;est possible que directement dans l&rsquo;application Authenticator, les parcours inter-appareils ne le supportent pas.<\/p>\n<p>Un dernier niveau de contr\u00f4le se place dans l&rsquo;acc\u00e8s conditionnel: les <a href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity\/authentication\/concept-authentication-strength-advanced-options\">options avanc\u00e9es des niveaux d&rsquo;authentification<\/a> acceptent elles aussi une liste d&rsquo;AAGUID. Nous pouvons ainsi laisser tous les utilisateurs enregistrer une passkey, puis n&rsquo;exiger un mod\u00e8le attest\u00e9 que pour les r\u00f4les d&rsquo;administration ou les applications sensibles.<\/p>\n<p>Attention, la passkey prot\u00e8ge uniquement la c\u00e9r\u00e9monie d&rsquo;authentification: les jetons et cookies de session \u00e9mis ensuite restent expos\u00e9s comme avec n&rsquo;importe quelle autre m\u00e9thode. Un poste compromis par un voleur de cookies reste un poste compromis. Pour les comptes \u00e0 privil\u00e8ges, la combinaison que je retiens est un profil limit\u00e9 aux passkeys li\u00e9es \u00e0 l&rsquo;appareil, avec attestation impos\u00e9e et une liste courte d&rsquo;AAGUID de cl\u00e9s mat\u00e9rielles valid\u00e9es, compl\u00e9t\u00e9e par un niveau d&rsquo;authentification qui reprend cette m\u00eame liste.<\/p>\n<h2>Compl\u00e9ment: maquette PHP avec lbuchs\/webauthn<\/h2>\n<p>Pour suivre ces deux c\u00e9r\u00e9monies sans tenant Entra ID, j&rsquo;avais test\u00e9 la biblioth\u00e8que <a href=\"https:\/\/github.com\/lbuchs\/webauthn\">lbuchs\/webauthn<\/a> de Lukas Buchs. Elle tient dans une classe principale, WebAuthn.php, accompagn\u00e9e de ses d\u00e9codeurs CBOR et d&rsquo;attestation, et son dossier \u00ab _test \u00bb contient une d\u00e9mo compl\u00e8te: une page client.html et un server.php qui jouent les deux r\u00f4les. Nous la remontons ici sur un Apache avec PHP 8.3, dans un conteneur Docker nomm\u00e9 srvphp01, avec localhost comme Relying Party.<\/p>\n<p>Attention: le navigateur n&rsquo;expose l&rsquo;API WebAuthn que dans un contexte s\u00e9curis\u00e9, c&rsquo;est \u00e0 dire en HTTPS ou sur localhost. Sur un vrai serveur il faut un certificat, et un RP ID \u00e9gal au nom de domaine du site ou \u00e0 l&rsquo;un de ses suffixes.<\/p>\n<h3>Installation sur le serveur PHP<\/h3>\n<p>La biblioth\u00e8que demande PHP 8.0 au minimum avec les extensions OpenSSL et mbstring, et sodium pour les cl\u00e9s Ed25519. L&rsquo;image officielle php:8.3-apache les fournit d\u00e9j\u00e0. Nous y ajoutons unzip pour Composer et jq pour lire les logs, et nous envoyons les logs d&rsquo;Apache dans des fichiers, l&rsquo;image les dirigeant par d\u00e9faut vers la sortie du conteneur:<\/p>\n<pre class=\"wp-block-code\"><code>docker run -d --name webauthn-lab --hostname srvphp01 -p 127.0.0.1:8090:80 php:8.3-apache\ndocker exec -it webauthn-lab bash\napt-get update &amp;&amp; apt-get install -y unzip jq\nrm \/var\/log\/apache2\/access.log \/var\/log\/apache2\/error.log &amp;&amp; apache2ctl -k graceful\nphp -v\nphp -m | grep -E 'openssl|mbstring|sodium'<\/code><\/pre>\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/remivernier.com\/wp-content\/uploads\/2026\/10\/webauthn-php-1-extensions.png\" width=\"760\" height=\"173\" alt=\"Sortie de php -v et des extensions openssl, mbstring et sodium sur srvphp01\"\/><figcaption>PHP 8.3.36 et les trois extensions demand\u00e9es par la biblioth\u00e8que<\/figcaption><\/figure>\n<p>Composer s&rsquo;installe avec le script officiel, puis la biblioth\u00e8que arrive depuis Packagist, en version 2.2.0 \u00e0 l&rsquo;heure o\u00f9 j&rsquo;\u00e9cris ces lignes:<\/p>\n<pre class=\"wp-block-code\"><code>curl -sS https:\/\/getcomposer.org\/installer | php -- --install-dir=\/usr\/local\/bin --filename=composer\ncd \/var\/www\ncomposer require lbuchs\/webauthn<\/code><\/pre>\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/remivernier.com\/wp-content\/uploads\/2026\/10\/webauthn-php-2-composer.png\" width=\"760\" height=\"414\" alt=\"Installation de Composer puis composer require lbuchs\/webauthn\"\/><figcaption>Installation de lbuchs\/webauthn 2.2.0 avec Composer<\/figcaption><\/figure>\n<p>La d\u00e9mo est livr\u00e9e avec le paquet, dans vendor\/lbuchs\/webauthn\/_test. Nous la copions dans le site et rempla\u00e7ons son <code>require_once '..\/src\/WebAuthn.php'<\/code> par l&rsquo;autoload de Composer:<\/p>\n<pre class=\"wp-block-code\"><code>cp -r vendor\/lbuchs\/webauthn\/_test html\/webauthn\nsed -i \"s#require_once '..\/src\/WebAuthn.php';#require_once __DIR__ . '\/..\/..\/vendor\/autoload.php';#\" html\/webauthn\/server.php\nchown -R www-data: html\/webauthn<\/code><\/pre>\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/remivernier.com\/wp-content\/uploads\/2026\/10\/webauthn-php-3-demo.png\" width=\"760\" height=\"206\" alt=\"Copie de la d\u00e9mo dans html\/webauthn et remplacement du require_once\"\/><figcaption>La d\u00e9mo copi\u00e9e dans le site et branch\u00e9e sur l&rsquo;autoload de Composer<\/figcaption><\/figure>\n<p>La d\u00e9mo ne journalise rien. Pour retrouver dans les logs ce que le serveur a re\u00e7u et v\u00e9rifi\u00e9, nous ajoutons dans server.php un <code>error_log()<\/code> juste apr\u00e8s l&rsquo;appel \u00e0 processCreate, et un autre apr\u00e8s processGet:<\/p>\n<pre class=\"wp-block-code\"><code>function b64u($b) { return rtrim(strtr(base64_encode($b), \"+\/\", \"-_\"), \"=\"); }\n\n\/\/ apr\u00e8s $data = $WebAuthn-&gt;processCreate(...)\nerror_log('webauthn processCreate ' . json_encode(array(\n    'clientData' =&gt; json_decode($clientDataJSON), 'fmt' =&gt; $data-&gt;attestationFormat,\n    'aaguid' =&gt; bin2hex($data-&gt;AAGUID), 'UP' =&gt; $data-&gt;userPresent, 'UV' =&gt; $data-&gt;userVerified,\n    'BE' =&gt; $data-&gt;isBackupEligible, 'BS' =&gt; $data-&gt;isBackedUp, 'signCount' =&gt; $data-&gt;signatureCounter,\n    'rootValid' =&gt; $data-&gt;rootValid, 'credentialId' =&gt; b64u($data-&gt;credentialId)), JSON_UNESCAPED_SLASHES));\n\n\/\/ apr\u00e8s $WebAuthn-&gt;processGet(...)\nerror_log('webauthn processGet ' . json_encode(array(\n    'clientData' =&gt; json_decode($clientDataJSON), 'signCount' =&gt; $WebAuthn-&gt;getSignatureCounter(),\n    'clientDataJSON_b64u' =&gt; b64u($clientDataJSON), 'authenticatorData_b64u' =&gt; b64u($authenticatorData)), JSON_UNESCAPED_SLASHES));<\/code><\/pre>\n<h3>Enr\u00f4lement<\/h3>\n<p>La page http:\/\/localhost:8090\/webauthn\/client.html expose les param\u00e8tres de la c\u00e9r\u00e9monie. Nous cochons les passkeys (credentials d\u00e9couvrables c\u00f4t\u00e9 client), nous imposons la v\u00e9rification de l&rsquo;utilisateur et nous laissons l&rsquo;attestation activ\u00e9e, avec tous les formats propos\u00e9s:<\/p>\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/remivernier.com\/wp-content\/uploads\/2026\/10\/webauthn-php-4-formulaire.png\" width=\"960\" height=\"864\" alt=\"Page client.html de la d\u00e9mo lbuchs\/webauthn avec les passkeys coch\u00e9es et la v\u00e9rification de l&#x27;utilisateur obligatoire\"\/><figcaption>La page de d\u00e9mo, RP ID localhost, v\u00e9rification de l&rsquo;utilisateur obligatoire<\/figcaption><\/figure>\n<p>Pour ne pas d\u00e9pendre d&rsquo;un mat\u00e9riel, l&rsquo;authentificateur est celui que Chromium sait simuler, le m\u00eame que dans l&rsquo;onglet \u00ab WebAuthn \u00bb des outils de d\u00e9veloppement. Nous le pilotons avec Playwright par le protocole DevTools: un authentificateur interne en CTAP2, qui conserve ses cl\u00e9s et r\u00e9pond \u00e0 la v\u00e9rification de l&rsquo;utilisateur sans intervention.<\/p>\n<pre class=\"wp-block-code\"><code>await cdp.send('WebAuthn.enable');\nawait cdp.send('WebAuthn.addVirtualAuthenticator', { options: {\n  protocol: 'ctap2', transport: 'internal', hasResidentKey: true,\n  hasUserVerification: true, isUserVerified: true, automaticPresenceSimulation: true } });<\/code><\/pre>\n<p>Le bouton \u00ab new registration \u00bb encha\u00eene les deux appels du sch\u00e9ma de la biblioth\u00e8que. Le premier, <code>GET server.php?fn=getCreateArgs<\/code>, renvoie les options de cr\u00e9ation:<\/p>\n<pre class=\"wp-block-code\"><code>{\n  \"publicKey\": {\n    \"rp\": {\n      \"name\": \"WebAuthn Library\",\n      \"id\": \"localhost\"\n    },\n    \"authenticatorSelection\": {\n      \"userVerification\": \"required\",\n      \"requireResidentKey\": true,\n      \"residentKey\": \"required\"\n    },\n    \"user\": {\n      \"id\": \"=?BINARY?B?ZGVtb2RlbW8=?=\",\n      \"name\": \"demo\",\n      \"displayName\": \"Demo Demolin\"\n    },\n    \"pubKeyCredParams\": [\n      {\n        \"type\": \"public-key\",\n        \"alg\": -8\n      },\n      {\n        \"type\": \"public-key\",\n        \"alg\": -7\n      },\n      {\n        \"type\": \"public-key\",\n        \"alg\": -257\n      }\n    ],\n    \"attestation\": \"direct\",\n    \"extensions\": {\n      \"exts\": true\n    },\n    \"timeout\": 240000,\n    \"challenge\": \"=?BINARY?B?ySlWh2+Z8VeQ5PdXk0ZbSks0olxsDV3YO7J6OwOuwPM=?=\",\n    \"excludeCredentials\": []\n  }\n}<\/code><\/pre>\n<p>Nous y retrouvons ce que nous avons d\u00e9crit pour Entra ID: le RP ID, l&rsquo;identifiant de l&rsquo;utilisateur, les algorithmes accept\u00e9s (-8 pour Ed25519, -7 pour ES256, -257 pour RS256), residentKey \u00e0 required pour obtenir une passkey, et une attestation direct, que la biblioth\u00e8que demande d\u00e8s que des certificats racines sont charg\u00e9s (ici le dossier du FIDO Metadata Service, coch\u00e9 par d\u00e9faut dans la page). Les champs binaires voyagent sous une forme assez inhabituelle, <code>=?BINARY?B?\u2026?=<\/code>, que client.html reconvertit en ArrayBuffer avant d&rsquo;appeler navigator.credentials.create. Le second appel, <code>POST fn=processCreate<\/code>, transmet le clientDataJSON et l&rsquo;attestationObject.<\/p>\n<p>La page r\u00e9pond <code>registration ok, but certificate does not match any of the selected root ca.<\/code> et le tableau des donn\u00e9es que le serveur a conserv\u00e9es (server.php?fn=getStoredDataHtml) montre pourquoi:<\/p>\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/remivernier.com\/wp-content\/uploads\/2026\/10\/webauthn-php-5-donnees-serveur.png\" width=\"591\" height=\"698\" alt=\"Tableau des donn\u00e9es conserv\u00e9es par le serveur: format packed, certificat Batch Certificate, rootValid no\"\/><figcaption>Ce que le serveur conserve apr\u00e8s l&rsquo;enr\u00f4lement<\/figcaption><\/figure>\n<p>L&rsquo;attestation est au format packed, mais son certificat a le m\u00eame \u00e9metteur et le m\u00eame sujet, \u00ab Batch Certificate (Chromium Authenticator Attestation) \u00bb: il est auto-sign\u00e9, ne remonte \u00e0 aucune racine de confiance, et rootValid vaut no. La d\u00e9mo accepte malgr\u00e9 tout l&rsquo;enregistrement, car elle appelle processCreate avec son dernier param\u00e8tre, $failIfRootMismatch, \u00e0 false. Nous sommes dans le cas d\u00e9crit plus haut pour Entra ID: l&rsquo;authentificateur annonce un AAGUID, ici 01020304-0506-0708-0102-030405060708, celui de l&rsquo;authentificateur virtuel de Chromium, et rien ne permet de croire cette valeur.<\/p>\n<p>-&gt; la cl\u00e9 publique commence par MCowBQYDK2Vw, l&rsquo;en-t\u00eate d&rsquo;une cl\u00e9 Ed25519: l&rsquo;authentificateur a retenu le premier algorithme de la liste (-8), que la biblioth\u00e8que ne propose que si sodium ou OpenSSL savent v\u00e9rifier ce type de signature.<\/p>\n<p>-&gt; UP et UV sont lev\u00e9s, BE et BS ne le sont pas: la cl\u00e9 est li\u00e9e \u00e0 l&rsquo;appareil, et son compteur d\u00e9marre \u00e0 1.<\/p>\n<p>Le tableau laisse l&rsquo;AAGUID vide car ses octets ne sont pas imprimables. Le log de l&rsquo;enr\u00f4lement le donne en hexad\u00e9cimal, avec le clientDataJSON d\u00e9cod\u00e9 et l&rsquo;origine http:\/\/localhost:8090 que le navigateur a lui-m\u00eame inscrite:<\/p>\n<pre class=\"wp-block-code\"><code>awk '{sub(\/&amp;.*\/, \"\", $7); print $4, $6, $7, $9}' \/var\/log\/apache2\/access.log | grep server.php\ngrep processCreate \/var\/log\/apache2\/error.log | tail -1 | grep -o '{.*}' | jq .<\/code><\/pre>\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/remivernier.com\/wp-content\/uploads\/2026\/10\/webauthn-php-6-log-enregistrement.png\" width=\"760\" height=\"334\" alt=\"Log JSON de processCreate format\u00e9 avec jq\"\/><figcaption>La c\u00e9r\u00e9monie d&rsquo;enregistrement vue depuis le log du serveur<\/figcaption><\/figure>\n<h3>Authentification<\/h3>\n<p>Le bouton \u00ab check registration \u00bb encha\u00eene fn=getGetArgs puis fn=processGet. Nous le lan\u00e7ons deux fois. Le journal d&rsquo;acc\u00e8s d&rsquo;Apache, r\u00e9duit \u00e0 l&rsquo;heure, \u00e0 la m\u00e9thode, \u00e0 la fonction appel\u00e9e et au code retour, montre les deux c\u00e9r\u00e9monies:<\/p>\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/remivernier.com\/wp-content\/uploads\/2026\/10\/webauthn-php-7-journal-acces.png\" width=\"760\" height=\"157\" alt=\"Journal d&#x27;acc\u00e8s Apache: getCreateArgs, processCreate puis deux fois getGetArgs et processGet\"\/><figcaption>Enregistrement puis deux authentifications dans le journal d&rsquo;acc\u00e8s<\/figcaption><\/figure>\n<p>Le log du premier processGet contient un clientDataJSON de type webauthn.get avec un nouveau challenge, le compteur, et l&rsquo;authenticatorData encod\u00e9e en base64url:<\/p>\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/remivernier.com\/wp-content\/uploads\/2026\/10\/webauthn-php-8-log-authentification.png\" width=\"760\" height=\"238\" alt=\"Log JSON de processGet format\u00e9 avec jq\"\/><figcaption>La premi\u00e8re authentification vue depuis le log du serveur<\/figcaption><\/figure>\n<p>Le script de d\u00e9codage vu plus haut, appliqu\u00e9 au clientDataJSON et \u00e0 l&rsquo;authenticatorData de cette connexion, donne:<\/p>\n<pre class=\"wp-block-code\"><code>$ python3 webauthn_decode.py eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxl... SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAg\nclientDataJSON.type        : webauthn.get\nclientDataJSON.challenge   : zKiu6qgkfyN8OA2t7ggfHfaik9AN8i0BgzYcqFSiUA8\nclientDataJSON.origin      : http:\/\/localhost:8090\nclientDataJSON.crossOrigin : False\nrpIdHash            : 49960de5880e8c68... (DIFFERENT pour login.microsoft.com)\nflags               : 0x05 -&gt; UP UV\nsignCount           : 2<\/code><\/pre>\n<p>-&gt; le rpIdHash est le SHA-256 de \u00ab localhost \u00bb (49960de5880e8c68\u2026). Le script, \u00e9crit pour Entra ID, le compare \u00e0 login.microsoft.com et le trouve diff\u00e9rent: c&rsquo;est la liaison au domaine d\u00e9crite au d\u00e9but de l&rsquo;article, vue depuis une autre Relying Party.<\/p>\n<p>-&gt; flags 0x05 (UP UV) et compteur \u00e0 2, puis \u00e0 3 lors de la seconde connexion: l&rsquo;authentificateur virtuel incr\u00e9mente son compteur \u00e0 chaque signature.<\/p>\n<p>Lors d&rsquo;un premier passage, Chrome avait ajout\u00e9 au clientDataJSON une cl\u00e9 other_keys_can_be_added_here, dont la valeur demande de ne pas comparer cet objet \u00e0 un mod\u00e8le. Le serveur doit donc lire ce JSON champ par champ, comme le fait processGet, et jamais le comparer \u00e0 une cha\u00eene attendue.<\/p>\n<p>Attention, la d\u00e9mo appelle processGet avec null \u00e0 la place du compteur pr\u00e9c\u00e9dent et ne met jamais \u00e0 jour celui qu&rsquo;elle a stock\u00e9: le contr\u00f4le de clonage d\u00e9crit plus haut n&rsquo;y est pas actif. Dans une vraie application, il faut enregistrer apr\u00e8s chaque connexion la valeur renvoy\u00e9e par getSignatureCounter() et la passer au processGet suivant, la biblioth\u00e8que levant alors l&rsquo;exception \u00ab signature counter not valid \u00bb si le compteur n&rsquo;augmente pas. La d\u00e9mo garde aussi ses enregistrements dans la session PHP. En production ils vont en base, rattach\u00e9s au compte, et les premi\u00e8res \u00e9tapes de la v\u00e9rification (le credential ID fait-il partie de ceux attendus, le userHandle correspond-il au compte?) restent \u00e0 notre charge: les commentaires du code de la biblioth\u00e8que les laissent explicitement \u00e0 l&rsquo;impl\u00e9mentation.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>C\u00e9r\u00e9monie WebAuthn dans Entra ID: challenge, credential ID, AAGUID, attestation et compteur d\u00e9cod\u00e9s en Python, puis le lien avec les restrictions de cl\u00e9s.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[20,3,37],"tags":[81,82,80,78,77,83,79],"class_list":["post-1253","post","type-post","status-publish","format-standard","hentry","category-azure-ad","category-protocoles","category-securite","tag-aaguid","tag-attestation","tag-entra-id","tag-fido2","tag-passkey","tag-phishing","tag-webauthn"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Passkeys FIDO2 dans Entra ID: c\u00e9r\u00e9monie WebAuthn en d\u00e9tail - R\u00e9mi VERNIER<\/title>\n<meta name=\"description\" content=\"C\u00e9r\u00e9monie WebAuthn dans Entra ID: challenge, credential ID, AAGUID, attestation et compteur d\u00e9cod\u00e9s en Python, puis le lien avec les restrictions de cl\u00e9s.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/remivernier.com\/index.php\/2026\/10\/28\/passkeys-fido2-entra-id-ceremonie-webauthn\/\" \/>\n<meta property=\"og:locale\" content=\"fr_FR\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Passkeys FIDO2 dans Entra ID: c\u00e9r\u00e9monie WebAuthn en d\u00e9tail - R\u00e9mi VERNIER\" \/>\n<meta property=\"og:description\" content=\"C\u00e9r\u00e9monie WebAuthn dans Entra ID: challenge, credential ID, AAGUID, attestation et compteur d\u00e9cod\u00e9s en Python, puis le lien avec les restrictions de cl\u00e9s.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/remivernier.com\/index.php\/2026\/10\/28\/passkeys-fido2-entra-id-ceremonie-webauthn\/\" \/>\n<meta property=\"og:site_name\" content=\"R\u00e9mi VERNIER\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-28T10:29:59+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-28T12:28:28+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/remivernier.com\/wp-content\/uploads\/2026\/10\/webauthn-php-1-extensions.png\" \/>\n<meta name=\"author\" content=\"R\u00e9mi\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"\u00c9crit par\" \/>\n\t<meta name=\"twitter:data1\" content=\"R\u00e9mi\" \/>\n\t<meta name=\"twitter:label2\" content=\"Dur\u00e9e de lecture estim\u00e9e\" \/>\n\t<meta name=\"twitter:data2\" content=\"18 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/remivernier.com\\\/index.php\\\/2026\\\/10\\\/28\\\/passkeys-fido2-entra-id-ceremonie-webauthn\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/remivernier.com\\\/index.php\\\/2026\\\/10\\\/28\\\/passkeys-fido2-entra-id-ceremonie-webauthn\\\/\"},\"author\":{\"name\":\"R\u00e9mi\",\"@id\":\"https:\\\/\\\/remivernier.com\\\/#\\\/schema\\\/person\\\/a4c76c63a5a3d94454484845e42fc906\"},\"headline\":\"Passkeys FIDO2 dans Entra ID: c\u00e9r\u00e9monie WebAuthn en d\u00e9tail\",\"datePublished\":\"2026-10-28T10:29:59+00:00\",\"dateModified\":\"2026-10-28T12:28:28+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/remivernier.com\\\/index.php\\\/2026\\\/10\\\/28\\\/passkeys-fido2-entra-id-ceremonie-webauthn\\\/\"},\"wordCount\":2976,\"commentCount\":0,\"image\":{\"@id\":\"https:\\\/\\\/remivernier.com\\\/index.php\\\/2026\\\/10\\\/28\\\/passkeys-fido2-entra-id-ceremonie-webauthn\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/remivernier.com\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/webauthn-php-1-extensions.png\",\"keywords\":[\"aaguid\",\"attestation\",\"entra id\",\"fido2\",\"passkey\",\"phishing\",\"webauthn\"],\"articleSection\":[\"Azure AD\",\"Protocoles\",\"S\u00e9curit\u00e9\"],\"inLanguage\":\"fr-FR\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/remivernier.com\\\/index.php\\\/2026\\\/10\\\/28\\\/passkeys-fido2-entra-id-ceremonie-webauthn\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/remivernier.com\\\/index.php\\\/2026\\\/10\\\/28\\\/passkeys-fido2-entra-id-ceremonie-webauthn\\\/\",\"url\":\"https:\\\/\\\/remivernier.com\\\/index.php\\\/2026\\\/10\\\/28\\\/passkeys-fido2-entra-id-ceremonie-webauthn\\\/\",\"name\":\"Passkeys FIDO2 dans Entra ID: c\u00e9r\u00e9monie WebAuthn en d\u00e9tail - R\u00e9mi VERNIER\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/remivernier.com\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/remivernier.com\\\/index.php\\\/2026\\\/10\\\/28\\\/passkeys-fido2-entra-id-ceremonie-webauthn\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/remivernier.com\\\/index.php\\\/2026\\\/10\\\/28\\\/passkeys-fido2-entra-id-ceremonie-webauthn\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/remivernier.com\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/webauthn-php-1-extensions.png\",\"datePublished\":\"2026-10-28T10:29:59+00:00\",\"dateModified\":\"2026-10-28T12:28:28+00:00\",\"author\":{\"@id\":\"https:\\\/\\\/remivernier.com\\\/#\\\/schema\\\/person\\\/a4c76c63a5a3d94454484845e42fc906\"},\"description\":\"C\u00e9r\u00e9monie WebAuthn dans Entra ID: challenge, credential ID, AAGUID, attestation et compteur d\u00e9cod\u00e9s en Python, puis le lien avec les restrictions de cl\u00e9s.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/remivernier.com\\\/index.php\\\/2026\\\/10\\\/28\\\/passkeys-fido2-entra-id-ceremonie-webauthn\\\/#breadcrumb\"},\"inLanguage\":\"fr-FR\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/remivernier.com\\\/index.php\\\/2026\\\/10\\\/28\\\/passkeys-fido2-entra-id-ceremonie-webauthn\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"fr-FR\",\"@id\":\"https:\\\/\\\/remivernier.com\\\/index.php\\\/2026\\\/10\\\/28\\\/passkeys-fido2-entra-id-ceremonie-webauthn\\\/#primaryimage\",\"url\":\"https:\\\/\\\/remivernier.com\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/webauthn-php-1-extensions.png\",\"contentUrl\":\"https:\\\/\\\/remivernier.com\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/webauthn-php-1-extensions.png\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/remivernier.com\\\/index.php\\\/2026\\\/10\\\/28\\\/passkeys-fido2-entra-id-ceremonie-webauthn\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Accueil\",\"item\":\"https:\\\/\\\/remivernier.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Passkeys FIDO2 dans Entra ID: c\u00e9r\u00e9monie WebAuthn en d\u00e9tail\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/remivernier.com\\\/#website\",\"url\":\"https:\\\/\\\/remivernier.com\\\/\",\"name\":\"R\u00e9mi VERNIER\",\"description\":\"Identit\u00e9, s\u00e9curit\u00e9, SAP et IA : Active Directory, Azure &amp; co...\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/remivernier.com\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"fr-FR\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/remivernier.com\\\/#\\\/schema\\\/person\\\/a4c76c63a5a3d94454484845e42fc906\",\"name\":\"R\u00e9mi\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"fr-FR\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/f750448bc07f5d1d9c1189344283777ec0d33275c184599b199fb6b305bc6fba?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/f750448bc07f5d1d9c1189344283777ec0d33275c184599b199fb6b305bc6fba?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/f750448bc07f5d1d9c1189344283777ec0d33275c184599b199fb6b305bc6fba?s=96&d=mm&r=g\",\"caption\":\"R\u00e9mi\"},\"sameAs\":[\"http:\\\/\\\/www.linkedin.com\\\/in\\\/remi-vernier\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Passkeys FIDO2 dans Entra ID: c\u00e9r\u00e9monie WebAuthn en d\u00e9tail - R\u00e9mi VERNIER","description":"C\u00e9r\u00e9monie WebAuthn dans Entra ID: challenge, credential ID, AAGUID, attestation et compteur d\u00e9cod\u00e9s en Python, puis le lien avec les restrictions de cl\u00e9s.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/remivernier.com\/index.php\/2026\/10\/28\/passkeys-fido2-entra-id-ceremonie-webauthn\/","og_locale":"fr_FR","og_type":"article","og_title":"Passkeys FIDO2 dans Entra ID: c\u00e9r\u00e9monie WebAuthn en d\u00e9tail - R\u00e9mi VERNIER","og_description":"C\u00e9r\u00e9monie WebAuthn dans Entra ID: challenge, credential ID, AAGUID, attestation et compteur d\u00e9cod\u00e9s en Python, puis le lien avec les restrictions de cl\u00e9s.","og_url":"https:\/\/remivernier.com\/index.php\/2026\/10\/28\/passkeys-fido2-entra-id-ceremonie-webauthn\/","og_site_name":"R\u00e9mi VERNIER","article_published_time":"2026-10-28T10:29:59+00:00","article_modified_time":"2026-10-28T12:28:28+00:00","og_image":[{"url":"https:\/\/remivernier.com\/wp-content\/uploads\/2026\/10\/webauthn-php-1-extensions.png","type":"","width":"","height":""}],"author":"R\u00e9mi","twitter_card":"summary_large_image","twitter_misc":{"\u00c9crit par":"R\u00e9mi","Dur\u00e9e de lecture estim\u00e9e":"18 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/remivernier.com\/index.php\/2026\/10\/28\/passkeys-fido2-entra-id-ceremonie-webauthn\/#article","isPartOf":{"@id":"https:\/\/remivernier.com\/index.php\/2026\/10\/28\/passkeys-fido2-entra-id-ceremonie-webauthn\/"},"author":{"name":"R\u00e9mi","@id":"https:\/\/remivernier.com\/#\/schema\/person\/a4c76c63a5a3d94454484845e42fc906"},"headline":"Passkeys FIDO2 dans Entra ID: c\u00e9r\u00e9monie WebAuthn en d\u00e9tail","datePublished":"2026-10-28T10:29:59+00:00","dateModified":"2026-10-28T12:28:28+00:00","mainEntityOfPage":{"@id":"https:\/\/remivernier.com\/index.php\/2026\/10\/28\/passkeys-fido2-entra-id-ceremonie-webauthn\/"},"wordCount":2976,"commentCount":0,"image":{"@id":"https:\/\/remivernier.com\/index.php\/2026\/10\/28\/passkeys-fido2-entra-id-ceremonie-webauthn\/#primaryimage"},"thumbnailUrl":"https:\/\/remivernier.com\/wp-content\/uploads\/2026\/10\/webauthn-php-1-extensions.png","keywords":["aaguid","attestation","entra id","fido2","passkey","phishing","webauthn"],"articleSection":["Azure AD","Protocoles","S\u00e9curit\u00e9"],"inLanguage":"fr-FR","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/remivernier.com\/index.php\/2026\/10\/28\/passkeys-fido2-entra-id-ceremonie-webauthn\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/remivernier.com\/index.php\/2026\/10\/28\/passkeys-fido2-entra-id-ceremonie-webauthn\/","url":"https:\/\/remivernier.com\/index.php\/2026\/10\/28\/passkeys-fido2-entra-id-ceremonie-webauthn\/","name":"Passkeys FIDO2 dans Entra ID: c\u00e9r\u00e9monie WebAuthn en d\u00e9tail - R\u00e9mi VERNIER","isPartOf":{"@id":"https:\/\/remivernier.com\/#website"},"primaryImageOfPage":{"@id":"https:\/\/remivernier.com\/index.php\/2026\/10\/28\/passkeys-fido2-entra-id-ceremonie-webauthn\/#primaryimage"},"image":{"@id":"https:\/\/remivernier.com\/index.php\/2026\/10\/28\/passkeys-fido2-entra-id-ceremonie-webauthn\/#primaryimage"},"thumbnailUrl":"https:\/\/remivernier.com\/wp-content\/uploads\/2026\/10\/webauthn-php-1-extensions.png","datePublished":"2026-10-28T10:29:59+00:00","dateModified":"2026-10-28T12:28:28+00:00","author":{"@id":"https:\/\/remivernier.com\/#\/schema\/person\/a4c76c63a5a3d94454484845e42fc906"},"description":"C\u00e9r\u00e9monie WebAuthn dans Entra ID: challenge, credential ID, AAGUID, attestation et compteur d\u00e9cod\u00e9s en Python, puis le lien avec les restrictions de cl\u00e9s.","breadcrumb":{"@id":"https:\/\/remivernier.com\/index.php\/2026\/10\/28\/passkeys-fido2-entra-id-ceremonie-webauthn\/#breadcrumb"},"inLanguage":"fr-FR","potentialAction":[{"@type":"ReadAction","target":["https:\/\/remivernier.com\/index.php\/2026\/10\/28\/passkeys-fido2-entra-id-ceremonie-webauthn\/"]}]},{"@type":"ImageObject","inLanguage":"fr-FR","@id":"https:\/\/remivernier.com\/index.php\/2026\/10\/28\/passkeys-fido2-entra-id-ceremonie-webauthn\/#primaryimage","url":"https:\/\/remivernier.com\/wp-content\/uploads\/2026\/10\/webauthn-php-1-extensions.png","contentUrl":"https:\/\/remivernier.com\/wp-content\/uploads\/2026\/10\/webauthn-php-1-extensions.png"},{"@type":"BreadcrumbList","@id":"https:\/\/remivernier.com\/index.php\/2026\/10\/28\/passkeys-fido2-entra-id-ceremonie-webauthn\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Accueil","item":"https:\/\/remivernier.com\/"},{"@type":"ListItem","position":2,"name":"Passkeys FIDO2 dans Entra ID: c\u00e9r\u00e9monie WebAuthn en d\u00e9tail"}]},{"@type":"WebSite","@id":"https:\/\/remivernier.com\/#website","url":"https:\/\/remivernier.com\/","name":"R\u00e9mi VERNIER","description":"Identit\u00e9, s\u00e9curit\u00e9, SAP et IA : Active Directory, Azure &amp; co...","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/remivernier.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"fr-FR"},{"@type":"Person","@id":"https:\/\/remivernier.com\/#\/schema\/person\/a4c76c63a5a3d94454484845e42fc906","name":"R\u00e9mi","image":{"@type":"ImageObject","inLanguage":"fr-FR","@id":"https:\/\/secure.gravatar.com\/avatar\/f750448bc07f5d1d9c1189344283777ec0d33275c184599b199fb6b305bc6fba?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/f750448bc07f5d1d9c1189344283777ec0d33275c184599b199fb6b305bc6fba?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/f750448bc07f5d1d9c1189344283777ec0d33275c184599b199fb6b305bc6fba?s=96&d=mm&r=g","caption":"R\u00e9mi"},"sameAs":["http:\/\/www.linkedin.com\/in\/remi-vernier"]}]}},"_links":{"self":[{"href":"https:\/\/remivernier.com\/index.php\/wp-json\/wp\/v2\/posts\/1253","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/remivernier.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/remivernier.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/remivernier.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/remivernier.com\/index.php\/wp-json\/wp\/v2\/comments?post=1253"}],"version-history":[{"count":3,"href":"https:\/\/remivernier.com\/index.php\/wp-json\/wp\/v2\/posts\/1253\/revisions"}],"predecessor-version":[{"id":1264,"href":"https:\/\/remivernier.com\/index.php\/wp-json\/wp\/v2\/posts\/1253\/revisions\/1264"}],"wp:attachment":[{"href":"https:\/\/remivernier.com\/index.php\/wp-json\/wp\/v2\/media?parent=1253"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/remivernier.com\/index.php\/wp-json\/wp\/v2\/categories?post=1253"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/remivernier.com\/index.php\/wp-json\/wp\/v2\/tags?post=1253"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}