LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Passkeys and FIDO2 Explained

cryptographypasskeysfido2webauthnauthenticationsecurity

The password is the worst piece of infrastructure on the internet, and we have known this for thirty years. It is reused across sites because humans cannot remember sixty unique strings. It is weak because the ones we can remember are guessable. It is phished because the protocol that exchanges it does not know which site is asking. It is breached because relying parties keep storing it badly, sometimes in plaintext, occasionally in a SQL dump that ends up on a forum. Multi-factor authentication helped at the margin, but a six-digit code typed into a phishing page is still a six-digit code the attacker now possesses, and SMS-based second factors leak through SIM swaps. The honest fix was always going to require a different protocol, not a stronger password policy. Passkeys are that protocol, finally shipped to billions of devices and consumer-facing accounts, and on World Passkey Day 2026 the FIDO Alliance reported that roughly five billion passkeys are now in active use globally, with 75 percent of surveyed consumers having enabled a passkey on at least one account. This post walks the FIDO2 stack from the bottom up, explains the registration and authentication ceremonies in enough detail that the design choices make sense, looks honestly at the sync model that nearly broke the alliance, and gives a defensible answer to the question every reader will actually have, which is whether to turn passkeys on for their bank.


What a passkey actually is

A passkey is, at the protocol layer, a public-private key pair generated on a device, scoped to a specific relying party (a website or app), and used to sign challenges that prove possession of the private key without ever transmitting it. That is the whole idea. The phrase “passkey” is a marketing wrapper that the FIDO Alliance and the three major platform vendors agreed on in 2022 to describe a FIDO2 credential that is either discoverable (the relying party can list it without the user typing a username) or synced across the user’s devices, or both. Underneath the brand name, a passkey is just a WebAuthn credential. The cryptographic primitives are not exotic. The default algorithm in 2026 is ECDSA over the NIST P-256 curve (COSE algorithm identifier -7), which most authenticators have supported since FIDO U2F in 2014. Newer authenticators and some platform implementations support EdDSA over Ed25519 (COSE -8), which is faster and has a cleaner security story but is not yet universal. A small minority of older hardware tokens fall back to RSASSA-PKCS1-v1_5 with SHA-256 (COSE -257) for relying parties that have not updated their verification stack. Almost everything you interact with is P-256.

The relying party never sees the private key. It receives the public key once, during registration, and stores it against the user account. On every subsequent login, it sends a fresh random challenge (the spec requires at least 16 bytes; most implementations use 32 bytes, the output size of SHA-256) and verifies the signature the authenticator returns. Because the signature covers both the challenge and a hash of the relying party’s origin, a signature produced for google.com is not valid at g00gle.com. That single property is what makes passkeys phishing-resistant, and it is the whole reason the protocol was designed this way.

The relationship to the cryptography underneath is worth making explicit. If you have read our piece on elliptic-curve cryptography, the EC math here is the same math: a private scalar, a public point on the curve, ECDSA for signing. The novelty is not in the primitives but in the binding of those primitives to an origin and to a piece of hardware, and in the user experience that wraps it.


The FIDO2 stack, end to end

FIDO2 is an umbrella for two specifications that together describe how a browser, an authenticator, and a relying party cooperate. The W3C maintains WebAuthn, the JavaScript API that pages call to register and authenticate credentials. The FIDO Alliance maintains CTAP2, the binary protocol that the browser uses to talk to an external authenticator over USB-HID, NFC, or Bluetooth Low Energy. Platform authenticators (a device’s built-in secure element) skip CTAP2 entirely; the operating system mediates between the browser and the secure element directly.

Layer What it is Who specifies it Where it runs
Relying party server Generates challenges, stores public keys, verifies signatures Application code; libraries like SimpleWebAuthn, py_webauthn, Microsoft’s FIDO2 .NET lib Backend
WebAuthn JS API navigator.credentials.create() for register, .get() for auth W3C Level 3 (2024) Browser
Client (browser + platform) Marshals WebAuthn calls into CTAP2 or platform calls; enforces origin binding Chromium, WebKit, Gecko, Edge OS + browser
CTAP2 Binary CBOR-encoded protocol over HID, NFC, BLE FIDO Alliance CTAP2.1 / 2.2 Wire between client and roaming authenticator
Authenticator Generates keys, signs challenges, holds private keys in a secure element YubiKey, Pixel Titan M2, Apple Secure Enclave, Windows Hello TPM Hardware

The chain of trust runs the other way. When the relying party verifies a signature, it is trusting the authenticator to have actually checked that the user is present (a touch sensor, a biometric scan, a PIN entry). The optional attestation chain produced at registration time lets the relying party verify, with a hardware-rooted certificate, that the authenticator is a specific make and model from a known manufacturer. In practice, attestation is rarely enforced for consumer flows; enterprises and regulated industries are the main consumers of the AAGUID (Authenticator Attestation GUID) and its associated certificate chain, which they use to enforce policies like “only FIPS 140-3 validated tokens allowed.” The FIDO Alliance publishes the Metadata Service (MDS3), a signed JSON blob of every certified authenticator’s AAGUID, key protection mode, and certification level, and most enterprise WebAuthn libraries can consume it directly.

The underlying hardware story is the same one we covered in hardware security modules and secure enclaves: a tamper-resistant chip with its own CPU and key storage, with a narrow interface that lets the host ask it to sign things but never to extract the private key. Apple’s Secure Enclave on the A-series and M-series chips, Google’s Titan M2 on Pixel 6 and later, Microsoft’s Pluton on recent x86 laptops, and the secure elements in YubiKey 5 and YubiKey Bio devices are all variants of the same idea.


The registration ceremony, step by step

When a user clicks “Set up a passkey” on a site, the browser and authenticator perform what the spec calls a registration ceremony. The relying party drives the protocol, but the cryptography happens entirely on the user’s device. The choreography looks like this:

   Browser (Client)             Authenticator (e.g., Secure Enclave, YubiKey)
        |                                     |
        |  1. POST /webauthn/register/begin   |
        |------------------------------------>|
        |        Relying party server         |
        |        |                            |
        |        |  generates random          |
        |        |  challenge (32 bytes)      |
        |        |  + user.id, user.name,     |
        |        |    rp.id = "example.com",  |
        |        |    pubKeyCredParams,       |
        |        |    authenticatorSelection  |
        |<-------|                            |
        |                                     |
        |  2. navigator.credentials.create()  |
        |    clientDataJSON = {               |
        |      type: "webauthn.create",       |
        |      challenge, origin,             |
        |      crossOrigin: false             |
        |    }                                |
        |    clientDataHash = SHA-256(...)    |
        |------------------------------------>| 3. Verify user (touch / FaceID
        |                                     |    / fingerprint / PIN)
        |                                     |
        |                                     | 4. Generate EC P-256 keypair
        |                                     |    bound to (rp.id, user.id)
        |                                     |
        |                                     | 5. Sign attestation object:
        |                                     |    authData || clientDataHash
        |                                     |    with attestation key
        |                                     |
        |  attestationObject + clientDataJSON |
        |<------------------------------------|
        |                                     |
        |  6. POST /webauthn/register/finish  |
        |    { attestationObject,             |
        |      clientDataJSON, credentialId } |
        |------------------------------------>|
        |        Relying party server         |
        |        |                            |
        |        |  - Verify clientData       |
        |        |    challenge matches       |
        |        |  - Verify origin matches   |
        |        |  - Verify attestation sig  |
        |        |    against MDS3 cert       |
        |        |  - Store credentialId,     |
        |        |    public key, sign count, |
        |        |    AAGUID against user     |

A few details are worth pulling out. The rp.id is the effective domain (typically example.com), and the browser refuses to send it to an authenticator if the page’s origin is not a registrable suffix of that id. This is the origin binding that makes the protocol phishing-resistant; an attacker page at examp1e.com cannot ask the authenticator for a credential scoped to example.com. The clientDataJSON is a JSON blob that includes the challenge, the actual origin the browser is running on, and a crossOrigin flag; the authenticator signs a hash of it, so any tampering invalidates the signature. The authData blob the authenticator returns includes flags for user presence (UP) and user verification (UV), the AAGUID, a sign counter, and the public key in COSE format. The relying party stores the public key, the credential id, and ideally the AAGUID and sign counter; it does not need anything else to verify future logins.


The authentication ceremony

Authentication is the part the user does every day, and it is deliberately simpler. The relying party sends a fresh challenge, the authenticator signs it with the private key associated with the user’s credential, the browser ships the signature back, and the server verifies. There is no attestation step; the trust was established at registration. For discoverable credentials (the kind that show up in a system “Sign in with passkey” picker without the user typing a username), the authenticator returns the user handle along with the signature, and the relying party uses it to look up the account.

   Browser                           Authenticator
       |                                   |
       |  1. POST /login/begin             |
       |---------------------------------->|
       |   Relying party server            |
       |   challenge (32 bytes)            |
       |   allowCredentials? (list of      |
       |     credentialIds, optional for   |
       |     discoverable creds)           |
       |<----------------------------------|
       |                                   |
       |  2. navigator.credentials.get()   |
       |    clientDataJSON = {             |
       |      type: "webauthn.get",        |
       |      challenge, origin            |
       |    }                              |
       |---------------------------------->| 3. User verification
       |                                   |    (FaceID / touch / PIN)
       |                                   |
       |                                   | 4. Sign with private key:
       |                                   |    authData || SHA-256(clientData)
       |                                   |
       |   assertion: { credentialId,      |
       |   authenticatorData, signature,   |
       |   userHandle }                    |
       |<----------------------------------|
       |                                   |
       |  5. POST /login/finish            |
       |---------------------------------->|
       |   Relying party server            |
       |   - lookup public key by credId   |
       |   - verify signature              |
       |   - check sign counter increases  |
       |   - check origin matches rp.id    |
       |   - issue session                 |

This is the entire authentication exchange. There is no shared secret, no replayable token in transit, no password to phish. The signature is fresh per login because the challenge is fresh per login. The signature is bound to the origin because the clientDataJSON includes the origin and is hashed into the signed bytes. The signature is bound to a user-present, user-verified event because the authenticator only produces it after the local gesture (a touch, a biometric, a PIN). The sign counter, which most authenticators increment monotonically with each signature, exists so the relying party can detect a cloned authenticator: if a signature comes in with a counter lower than the last seen value, something is wrong. (Apple’s Secure Enclave keeps the counter at zero deliberately, because they argue cloning the Secure Enclave is not in the threat model, and the counter would leak usage patterns across relying parties.)

If you have spent time inside the Signal protocol’s double ratchet, this will look almost embarrassingly simple by comparison. There is no forward secrecy story, because there is no shared session to compromise. There is no post-compromise recovery story, because the private key never leaves the authenticator. The protocol’s whole job is “prove possession of this key, for this origin, right now,” and almost everything else falls out for free.


Platform vs roaming authenticators

The WebAuthn spec distinguishes two kinds of authenticators, and the distinction matters more for the UX and recovery story than for the cryptography. A platform authenticator is built into the device the user is browsing from: the Secure Enclave on a Mac or iPhone, the StrongBox / Titan M2 on a Pixel, the TPM behind Windows Hello on a Windows 11 PC. A roaming authenticator is an external device that can be carried between machines: a YubiKey 5 plugged into USB-A or USB-C, a YubiKey 5 NFC tapped against a phone, a security key over Bluetooth LE. The browser presents them as different choices in the credential creation dialog, and the relying party can hint a preference via the authenticatorSelection.authenticatorAttachment field ("platform" or "cross-platform").

Property Platform authenticator Roaming authenticator
Examples Apple Secure Enclave, Pixel Titan M2, Windows Hello TPM, Microsoft Pluton YubiKey 5 / 5C / 5 NFC / Bio, Google Titan Security Key, Feitian ePass
Transport OS-internal USB-HID, NFC, BLE (CTAP2)
User gesture FaceID, TouchID, Windows Hello biometric, device PIN Capacitive touch, on-key fingerprint (Bio), PIN over CTAP2 PIN/UV protocol
Sync across devices Yes, via iCloud Keychain / Google Password Manager / Microsoft Authenticator (E2EE) No, by design; the key is the device
Portable to a different OS / vendor Cross-Device Auth (QR + BLE proximity) Plug it in or tap it on any compliant client
Recovery if lost Restore from cloud sync, account recovery flow Re-register from a spare key; no recovery if sole key is lost
Phishing resistance Yes (origin-bound) Yes (origin-bound)
Malware resistance Strong (Secure Enclave isolation) but OS is in the trust path Strongest (key never touches host CPU)
Typical price Free, built in $50-$95 (YubiKey 5 series, 2026 pricing)
Best for Most consumer accounts; daily-use sign-ins High-value accounts: root domain, payroll, crypto custody, admin SSO

The reason both kinds exist is that they make different trade-offs on a real axis: convenience versus assurance. A platform authenticator is always with the user because the user’s phone is always with the user, and it syncs across the user’s other devices because the cloud account ties them together. A roaming authenticator is a separate physical object that can be locked in a safe, kept off the network, and used as the only credential for a high-value account, but the user has to actually carry it. Most consumers, in practice, end up with platform passkeys for everyday sites and one or two YubiKeys as the recovery / high-value tier. That is the architecture the FIDO Alliance now publicly recommends, after spending a few years arguing about it internally.


The sync model that scared everyone

For the first few years of FIDO U2F and FIDO2, the private key never left the device. That was the entire pitch: a hardware-bound credential is unphishable, unclonable, and irrecoverable. When Apple announced in 2022 that iCloud Keychain would sync passkeys across devices, and Google and Microsoft followed with similar models, a meaningful chunk of the security community lost their composure. The argument was that a synced passkey is no longer hardware-bound; it is a credential that lives in a cloud account, and if the cloud account is compromised, every passkey it holds is compromised. The FIDO Alliance had spent a decade telling people that this was exactly the failure mode passkeys were supposed to eliminate, and now they were endorsing it.

The pragmatic answer, which the alliance and the platform vendors landed on, was that hardware-bound credentials are the right model for the security-conscious 5 percent of users who will tolerate the recovery pain, and they are an unworkable model for the other 95 percent who lose their phone every couple of years. A single-device passkey that can never be recovered if the phone falls in a lake is not a consumer authentication story; it is a guarantee that the user will keep using their password as a fallback. The sync model accepts a worse cryptographic worst case (a cloud-account compromise breaches everything) in exchange for a dramatically better median outcome (the user actually uses passkeys, on every account, and stops typing passwords).

The mitigations are real and worth understanding. iCloud Keychain end-to-end encrypts the passkey blob with a key derived from the user’s device passcode and the iCloud Keychain Security Code; Apple’s servers cannot decrypt it. Google’s Password Manager uses a similar end-to-end-encrypted sync, gated on the device lock screen. Microsoft Authenticator’s passkey sync uses Microsoft account credentials plus the device’s local biometric. All three require that the user’s cloud account itself be protected by, at minimum, a strong password and a second factor; in practice that second factor is increasingly another passkey, which creates a chicken-and-egg recovery problem that the platforms solve with recovery contacts, printed recovery codes, and an account-recovery flow that takes days. The FIDO Alliance ratified the Credential Exchange Protocol (CXP) in 2025 to let users export a passkey from one provider (say, iCloud Keychain) and import it into another (say, 1Password 8 or Bitwarden), which addresses the older complaint that passkeys would lock users into a single vendor.

There is still a real residual question, which is whether a single compromise of the user’s Apple ID or Google account is now the entire game. The answer, honestly, is yes in the same sense that a single compromise of the user’s email account was already the entire game under the password regime, because email is the master password reset channel for everything else. Passkey sync did not introduce a new single point of failure; it made the existing one more legible.


Where this actually shipped

By mid-2026, the deployment landscape is no longer a curiosity. Google reports more than 800 million accounts using passkeys for sign-in, and has begun defaulting new accounts to passkey-first. Microsoft has been pushing personal Microsoft accounts toward passwordless since 2021 and now reports the majority of consumer sign-ins are passkey or Windows Hello, with the password field hidden by default. Apple turned on passkey support in iCloud Keychain in iOS 16 and macOS Ventura, and the feature is now on by default on every modern Apple device. Amazon rolled out passkeys to consumer accounts in 2023; the FIDO Alliance reported 175 million Amazon users created passkeys in the first year. GitHub supports passkeys for both sign-in and as a 2FA factor; PayPal, X, Shopify, eBay, Best Buy, Home Depot, the New York Times, Adobe, TikTok, LinkedIn, WhatsApp, and most major U.S. banks now support passkeys on at least their consumer web flows. The FIDO Alliance’s running estimate, drawn from its 2026 Sapio Research surveys, is that roughly half of the Tylenol-list top 1000 web properties now offer passkey authentication, and that five billion passkeys are registered globally, with about 49 percent of surveyed consumers using passkeys regularly when offered.

The enterprise picture is messier but moving. Sixty-eight percent of organizations surveyed for the FIDO Alliance 2026 Workforce Study report deploying or actively deploying passkeys for employee sign-in. Okta, Microsoft Entra ID, Google Workspace, and Ping Identity all support passkey enrollment as a first-class factor. The remaining friction is mostly around lifecycle: how IT assigns passkeys at onboarding, how it revokes them when an employee leaves, how it handles a lost YubiKey at 11 p.m. on a Friday. Microsoft Entra and Okta have shipped reasonable answers, but the operational maturity is still well behind, say, password rotation policy, simply because the tooling is newer.

The password managers have, mostly, made their peace. 1Password 8 stores passkeys natively across desktop and mobile and acts as a sync provider in its own right. Bitwarden added passkey support in 2023 and supports both creation and authentication in its browser extension. Dashlane and KeePassXC followed. The Credential Exchange Protocol means a user can in principle migrate their passkeys between any two compliant providers, which is the property the FIDO Alliance has been promising since the start.


The honest trade-offs

The case for turning passkeys on is strong but not infinite, and it is worth being specific about what they fix and what they do not. On the win column: phishing resistance is real, and it is the single most valuable property of the protocol. The relying party cannot be tricked into accepting a signature for a different origin, and the user cannot be tricked into producing one, because the browser does the origin check and the authenticator does the signing. A user who only ever signs in with passkeys cannot, by construction, be phished into giving up their account, full stop. The credential cannot be reused across sites because each is scoped to its own relying party. There is no shared secret on the server side, so a server breach yields a list of public keys that are useless to the attacker. The login UX is faster than password-plus-TOTP, which matters more than security professionals like to admit because it is the only reason normal humans adopt anything.

On the cost column: device-loss recovery is still imperfect. A user whose phone is lost or destroyed before they enrolled a second authenticator is going through an account-recovery flow, and that flow is still mediated by email, phone number, or a recovery contact, all of which are weaker than the passkey itself. Many sites still keep a password fallback in place because their support teams do not yet trust passkeys as the sole credential, which means the password is still phishable and the passkey only raises the bar for the easy attacks. The sync model trades device-bound assurance for usability, and that trade is the right call for most consumer accounts but the wrong call for accounts that hold serious value; a bank or a crypto custody account is a defensible reason to use a hardware-bound roaming authenticator like a YubiKey 5 or YubiKey Bio, with a second YubiKey locked in a safe as the recovery. Enterprise lifecycle tooling is still catching up, and IT departments that have not yet rolled out passkeys for their workforce should not assume the operational story is as mature as the consumer one.

The FIDO Alliance’s original ambition was a world without passwords. The 2026 reality is a world where passwords still exist, mostly as a fallback, but where the dominant sign-in method on the largest consumer services is no longer the password. That is, by any reasonable measure, the largest authentication shift the consumer internet has ever undergone, and it happened more or less on schedule. The remaining work is closing the recovery story, getting enterprise tooling to parity, and convincing the long tail of relying parties to retire their password field. None of those are easy, but none of them require new cryptography.


Verdict

Passkeys are the real thing. They are not a marketing wrapper around the same broken model; they are a different protocol with different properties, and the most important of those properties (phishing resistance via origin binding) is exactly the property the password regime could never have. The sync model is the right call for consumer accounts, where the dominant failure mode is users abandoning anything that does not survive a phone upgrade. The hardware-bound model, via a YubiKey 5 or YubiKey Bio, is the right call for accounts where the dominant failure mode is targeted attack and the user can be trusted to keep a spare key in a safe. Most readers should turn on passkeys for their Google, Apple, Microsoft, GitHub, Amazon, and PayPal accounts today, set up two YubiKeys for their bank and any account that holds custody of money, and keep the recovery codes printed and stored somewhere physical. The password is not dead, but for the first time in thirty years, there is a credible path to its retirement, and the path is no longer hypothetical.


Sources

Comments