Field course · Nº 30

The Unphishable Login

Why a hardware security key can't be phished

Every phishable credential is one a person can be tricked into giving away, and stacking more of them just gives the attacker more to relay. A hardware security key breaks the pattern by having no secret to surrender and by refusing to sign for a site it isn't really talking to. This course takes you to the exact byte where the phish dies — and to the doors it walks through instead.

Module 01 What phishing really is

Most security programs classify phishing as an email problem and buy a filter. That framing is comfortable and wrong in a way that costs money later, because it points the defense at the delivery vehicle rather than at the mechanism. Nothing executes on the victim's machine in a well-run phishing attack. No vulnerability is exploited. The victim's browser behaves correctly, TLS behaves correctly, the password manager may even be the only participant that notices something is off. What fails is the mapping between the site the person believes they are talking to and the site they are actually talking to.

That reframing is not rhetorical. It yields a test you can apply to any credential — existing, proposed, or invented on a whiteboard — and predict its phishability before anyone runs a pilot. This module builds that test, uses it to draw the boundary between phishing and the attacks it gets confused with, and explains why every behavioral control you layer on top has a ceiling you can calculate in advance. Everything that follows in this course is an application of the test.

Not malware, but misdirection

Phishing succeeds when a human is convinced to authenticate to the attacker's endpoint while believing it is the real one. Read that sentence for what it omits. It says nothing about email — the lure can be a text message, a search advertisement bidding on your own brand name, a calendar invitation, or a QR code taped over the real one in a hotel lobby. It says nothing about exploits, because there is nothing to exploit: the victim types a valid credential into a page they chose to visit, over a connection their browser correctly reports as secure. The compromise is in the target of an otherwise perfectly executed authentication.

This matters because it tells you which defenses are in the right category. An email gateway that quarantines the lure works on the delivery, so its effectiveness is a function of how many delivery channels exist — and the number keeps growing. Endpoint detection works on execution, so it has nothing to detect. Even a browser warning is a race between a freshly registered domain and a reputation system that has not seen it yet. None of these controls touch the actual mechanism, which is why each of them degrades gracefully into “usually works” rather than “cannot be beaten.”

Actor diagram showing that in a phishing attack the victim's trust arrow points at the attacker's endpoint rather than the real server, while the victim's own device is uncompromisedVictimdevice uncompromisedAttacker's endpointcellar-login.example-secure.comReal servercellar.example.comcredential actually goes herevictim believes it goes hereattacker replays itto the real serverno exploit, no malware, no bug
Figure 1.1 — The misdirection, not the machine. Every component behaves correctly: the browser connects where it was told, TLS validates, the credential is genuine. The only thing that is wrong is which party the victim aimed their trust at — which is why controls that inspect the device or the transport have nothing to find.
The load-bearing idea

Phishing is a failure of addressing, not of execution. The attacker does not break authentication; they redirect a correct authentication at themselves. Any fix that does not change what the credential is bound to is a fix aimed at the lure, not the mechanism.

The handover test

Here is the test the rest of this course runs on. A credential is phishable exactly when its normal use consists of the user transferring a value or an approval that the attacker can then reuse against the real server. Call it the handover test. It has two clauses and both must hold: the user must be the one moving the thing, and the thing must retain its power in the attacker's hands.

Apply it to a password: the user reads it out of their head or their manager and types it into a box, and the string works identically wherever it is typed. Both clauses hold — phishable. Apply it to a six-digit authenticator code: the user reads it off a screen and types it into a box, and for the next twenty seconds it works anywhere. Both clauses hold — phishable, on a shorter clock. Apply it to a push notification: the user does not transfer a string, but they transfer an approval, and that approval attaches to whichever login session prompted it, which may not be theirs. Both clauses hold — phishable.

The test's value is that it is predictive rather than historical. When a vendor demonstrates a new factor — a voice confirmation, a one-tap email magic link, a “tap your card to your phone” flow — you do not need an incident to classify it. You ask two questions. What exactly does the user hand over? And is that thing still good if a stranger holds it? Two answers, and you know which side of this course's line the factor falls on.

Worked example — the wine-cellar admin login

The running example throughout this course is a wine-cellar inventory product whose admin console lives at https://cellar.example.com. Today, an admin signs in with a password and a TOTP code from an authenticator app. Run the test on both. Password: the admin transfers a reusable string; a stranger holding it can sign in. Phishable. TOTP: the admin transfers a six-digit code read off a phone; a stranger holding it can sign in within the same 30-second step. Phishable, with a deadline. Two factors, two handovers, one family. Module 3 shows an attacker collecting both in real time without breaking a sweat.

From your other domain

The handover test is the security version of a distinction you already use: the difference between evidence a witness can testify to and evidence that only exists in one place. A bearer instrument — a cashier's check made out to cash — is powerful in whoever's hand holds it; a nominative instrument is not. Every credential in Module 2 is a bearer instrument. The credential in Module 4 is not, and that single change is what the whole course is about.

Neighbors that are not phishing

Three attacks routinely get filed under “phishing” in incident reports, and each needs a different control. Separating them is not pedantry; it decides what you buy and what you report.

AttackWhat actually happensHandover?What stops it
PhishingUser authenticates correctly to the attacker's endpointYes — the user hands the credential overBinding the credential to the real site (this course)
Credential stuffingAttacker replays username/password pairs from a breach dumpNo — the user did nothingUnique passwords, rate limiting, breach-list checks
Infostealer malwareCode on the victim's device reads secrets and cookies out of the browserNo — code takes themEndpoint controls, device health, token binding
Session hijackingAttacker uses a stolen post-login token; authentication is already overNo — the credential is bypassedShort sessions, device-bound tokens, re-auth on sensitive actions

Two of these are worth holding onto. Credential stuffing is the mirror image of phishing: the credential is stolen from a third party and the legitimate user is never involved, which is why a password manager and unique passwords defeat it completely and do nothing whatsoever for phishing. Session hijacking matters for a different reason — it is the tail of the phishing attack you will meet in Module 3. Once an attacker holds a session cookie, the authentication ceremony is finished and irrelevant; hardening the front door does not evict someone already inside.

When the categories blur

Incident reviews often record “phished credentials” when the real path was an infostealer on an unmanaged laptop. The distinction is load-bearing: a phishing finding drives you toward authentication changes, a malware finding drives you toward device controls, and picking the wrong one means the next incident looks exactly like this one. Ask what the user actually did. If they did nothing, it was not phishing.

Why training has a ceiling

Awareness training is real. Programs that run it well see click rates fall, often by an order of magnitude, and that is a genuine reduction in exposure. It is also a control with an arithmetic ceiling, and understanding the arithmetic keeps you from mistaking a good program for a solved problem.

The attacker needs one success. If a plausible lure reaches 4,000 employees and a mature program has driven the credential-entry rate down to 1 in 500, the expected number of successful captures per campaign is eight. Drive it to 1 in 2,000 — an excellent, expensively maintained number — and you still expect two. Neither result is failure; both are non-zero, and non-zero times “attacker only needs one” is compromise on a schedule. Meanwhile the attacker adapts faster than the training cycle: lures now arrive as a reply inside a genuine thread from a supplier whose mailbox was already taken, timed to a real invoice, from a domain registered an hour ago.

There is a second, subtler ceiling. The behavior you are asking for — inspect the address bar before every credential entry — competes directly with the behavior the business rewards, which is dispatching work quickly. Under time pressure, on a phone, in a thread that looks like the eleven before it, human verification degrades. That is not a training deficiency; it is what human attention does. Controls that require sustained vigilance from a distracted person are controls with variance, and variance is what an attacker samples.

The useful role for training

None of this argues for cutting the program. Training still lowers volume, produces the reports that let you see campaigns early, and builds the reflex of escalating rather than complying. Treat it as a rate reducer and a detection channel — not as the control that makes the outcome impossible. That control is mechanical, and it is what Modules 4 through 6 build.

The shape of the rest of the course

The arc from here is deliberate, and knowing it lets you read each module for its job. Module 2 lines up the four factors most organizations actually deploy — password, SMS OTP, TOTP, push — and shows that the handover test collapses them into one family, then states precisely what multi-factor authentication bought and what it did not. Module 3 introduces the adversary the rest of the course must defeat: a real-time reverse proxy that relays an entire login live, harvesting each factor as the victim supplies it and finishing with the session cookie. That module is the reason the course exists; every control before it loses to that attack.

Modules 4 and 5 remove the handover at the mechanism level. Module 4 replaces the shared secret with a key pair and walks the registration ceremony that mints it; Module 5 walks the login ceremony field by field. Module 6 is the heart: origin binding, the property that makes the Module 3 attack fail at exactly one comparison, demonstrated on the same wine-cellar login the attacker already beat once. Modules 7 and 8 refuse the victory lap — attestation and what it really proves, then the soft spots that survive, which is where competent attackers go next and where competent programs still lose.

One promise about tone. This course does not claim that hardware keys make accounts unhackable, and you should be suspicious of any material that does. It claims something narrower and more useful: that a specific attack, which defeats every other second factor in wide deployment, fails against this one for a reason you can point at in a byte string. Then it shows you where the attacker rationally goes instead.

Module 02 The phishable family

The four factors in this module cover the overwhelming majority of deployed authentication. They are usually presented as a ladder: passwords at the bottom, SMS a step up, TOTP better, push best because it is the most convenient and hardest to type into the wrong box. That ladder is real in one dimension — each rung genuinely raises the attacker's cost — and it is completely flat in the dimension that decides whether a live phishing attack works.

This module runs the handover test on each rung and shows the collapse. Then it does the harder and more useful thing: it says precisely what multi-factor authentication did buy, because the case against MFA-as-a-cure is only credible if you can state its real gains without sneering. MFA killed offline password replay at scale. That is an enormous, permanent win, and it is why the attacker's economics shifted from breach dumps to live relay — which is exactly the attack in Module 3.

Passwords: the pure shared secret

A password is a shared secret: a value held identically by the user and (in some derived form) by the server, where possession of the value is the authentication. Everything else in this module is a variation on that theme, so it pays to be exact about why it fails.

The password's fatal property is that it is context-free. The string that authenticates you at cellar.example.com is the same string everywhere it is typed, and nothing in the act of typing it carries information about where it went. The user is the only component in the system with an opinion about the destination, and that opinion is formed by looking at a rendered address bar for a fraction of a second. There is no cryptographic relationship between the password and the site — the binding exists solely in the user's belief.

Storage improvements do not touch this. Hashing with a memory-hard function, salting, peppering, rate limiting — all of these harden the server-side corpus against offline attack, which is a genuinely important problem and a completely different one. A phished password was never in the corpus; it went straight from the user's fingers to the attacker's socket in plaintext. This is worth internalizing because it is the template for every mistake in this module: a control that protects a credential at rest or in bulk says nothing about a credential the user hands over voluntarily.

Where password managers actually help

A password manager's autofill is domain-scoped: on cellar-login.example-secure.com it simply will not offer the credential for cellar.example.com. That silence is a real signal and it stops a meaningful share of attacks — it is, in fact, a primitive form of the origin binding this course builds toward. But it is advisory, not enforced. A user who has been told “our SSO is acting up, you may need to paste it manually” will paste it manually, and the manager has no further vote.

SMS OTP and TOTP: time-boxed, still relayable

One-time codes address a specific weakness: the reusable password. If a value is good for only one login inside a 30-second window, then a value captured today is worthless tomorrow, and the entire breach-dump economy stops working against it. That is a real improvement and it explains the deployment.

TOTP derives a six-digit code from a shared seed and the current time, both sides computing independently. SMS OTP generates a code server-side and delivers it by text. Mechanically they differ in a way worth knowing — TOTP's seed never traverses a carrier network after enrollment, while SMS depends on a delivery channel that has been repeatedly compromised through SIM-swap and signalling attacks, which is why SMS deserves its lower reputation. But that distinction is about who else can obtain the code. Under the handover test the two are identical: the user reads a value off a screen and types it into a box, and the value works for whoever holds it.

The tempting objection is the clock. Thirty seconds is not much time; surely the attacker cannot act fast enough? This is the single most common reasoning error in the field, and it dissolves once you notice that the attacker is not acting after the fact. The attacker is present, live, holding a session with the real server open, waiting. The victim types the code into the attacker's page and the attacker forwards it in under a second. The validity window does not need to be defeated; it needs only to be occupied, and a real-time relay occupies it comfortably. Time-boxing raises tempo, not safety.

Worked example — the wine-cellar admin, act one, prologue

The wine-cellar admin's TOTP step uses a 30-second period and the server accepts one step of drift on either side, giving a code roughly 60–90 seconds of practical validity. An attacker relaying live needs perhaps 400 milliseconds. The margin is not thin — it is three orders of magnitude. Any argument that rests on shortening the window has to contemplate a window shorter than a human can type, at which point the factor is unusable for the reason that made it popular.

Push approval: approving the wrong session

Push approval looks like it escapes the family, and its designers were solving a real problem: if there is no code, the user cannot type a code into the wrong box. That reasoning is correct and insufficient, because it addresses the transferred value while leaving the transferred approval untouched.

Consider what the user sees. A prompt appears on their phone saying that someone is signing in — perhaps with a city name and a browser string, both attacker-controllable to a useful degree. The prompt does not, and structurally cannot, show the user the origin of the browser session it will bless, because the prompt is generated by the identity provider in response to a login attempt, and every login attempt looks the same to the identity provider whether it originated from the victim's laptop or the attacker's server. The user is asked “is this you?” at a moment when they are, in fact, logging in. They say yes. The approval attaches to the attacker's session.

Two failure modes follow. The first is the timing attack just described: the attacker initiates a login at the instant the victim expects one, and the genuine expectation supplies the yes. The second is push fatigue — a prompt storm at 2 a.m. until someone taps approve to make it stop. Number matching, where the login screen displays two digits the user must enter into the prompt, was introduced to fix the second failure mode, and it does: you cannot answer a challenge you have not seen. It does nothing for the first, because an attacker relaying the login in real time can simply read the number off the page they are proxying and show it to the victim on their own lookalike screen.

Diagram showing that a push approval prompt cannot display the origin of the session it will bless, so a genuine user tap approves the attacker's loginAttacker's sessionstarts a login as the adminVictim's browserexpects a login right nowIdentity providerboth look identicalPush prompt“Signing in — is this you?”origin: not shown, not knowablea genuine human taps “approve” — and blesses the red session
Figure 2.1 — The approval with no address. The identity provider cannot tell the two inbound logins apart, and the prompt has no field for the origin of the session it will authorize. The user's tap is honest and correct in intent; it simply lands on whichever login is pending, which is the attacker's.

The taxonomy, resolved

Here is the master frame for the entire course. Three columns, each a yes/no question with a mechanical answer, applied to the four factors above plus the one this course is building toward.

FactorShared secret or relayable approval?Survives a live AiTM proxy?Bound to the real origin?
PasswordYes — a reusable string the user typesNo — relayed verbatimNo — context-free
SMS OTPYes — a code the user reads and typesNo — relayed inside the windowNo
TOTPYes — a code derived from a shared seedNo — relayed inside the windowNo
Push approvalYes — a transferable approvalNo — the tap blesses the attacker's sessionNo — the prompt has no origin to show
FIDO2 security keyNo — the private key never leaves; only a challenge-scoped signature travelsYes — the relayed assertion fails the server's origin checkYes — the browser-supplied origin is inside the signature

Read the rows top to bottom and the ladder disappears. The first four differ in cost, convenience, and resistance to other attacks, and they resolve identically in all three columns. The last row flips all three, and it flips them because of a single design decision — the credential is a per-site key pair whose use is scoped to an origin — rather than because of three separate improvements. That is what “different in kind, not degree” means here, and it is why the rest of the course spends four modules on the last row.

The load-bearing idea

Column two is a consequence of columns one and three, not an independent property. A credential survives a live proxy exactly when there is nothing relayable to harvest and what does travel is scoped to the site it was produced for. Any factor that fails either of the outer columns fails the middle one automatically — which is why you can predict a new factor's phishability without waiting for a demonstration.

What MFA actually bought

Now the honest accounting, because a case that only lists MFA's failures is a case a competent skeptic will dismantle.

Before second factors were common, the dominant credential attack was bulk and asynchronous. Breach dumps were replayed at scale against unrelated services; a password stolen from a forum in 2013 opened a corporate mailbox in 2016. That attack is essentially dead against MFA-protected accounts, and its death is permanent, not a temporary lead. MFA also raised the operational cost of everything else: the attacker can no longer harvest credentials at leisure and use them at their convenience. They must be present, in real time, holding infrastructure, during the victim's actual login. That converts a batch job into a live operation with a narrow window, a per-target cost, and far more exposure to detection.

What MFA did not do is remove the handover. Every factor in the family is still something the user gives away, so the attacker's response was not to defeat MFA but to be there while it happens. This is the correct way to state the situation to an executive who has just funded an MFA rollout: the rollout worked, it eliminated an entire attack class, and the adversary responded by changing tactics in a way that the rollout does not address. Nothing was wasted. Something remains.

Anti-pattern — “all MFA is phishing-resistant”

Symptom: a control framework, vendor datasheet, or audit response that lists “MFA enabled” as satisfying a phishing-resistance requirement, without naming the factor type. Why it is wrong: phishing resistance is a property of origin binding, which only the last row of the taxonomy has; the phrase silently promotes four rows that resolve “no” in every column. Corrective: require the control statement to name the factor and the column — “origin-bound public-key authentication (FIDO2/WebAuthn)” — and treat “MFA” alone as unspecified. Regulators and guidance bodies have moved this way for exactly this reason.

From your GRC background

This is a control-description problem before it is a technology problem. “MFA enforced” is an unfalsifiable control statement: it can be true while the control provides zero resistance to the attack the requirement was written for. The fix is the one you already apply to any vague obligation — make the statement specific enough that an auditor can test it and a reader can be wrong about it. “Phishing-resistant authentication (FIDO2) enforced for all administrative accounts, no phishable fallback enrolled” is a testable sentence. “MFA enforced” is not.

Module 03 The proxy that beats MFA

This is the adversary the rest of the course must defeat, and it deserves to be built precisely rather than gestured at. The AiTM proxy — attacker-in-the-middle, as distinct from the network-level man-in-the-middle that TLS already solved — is a reverse proxy standing between the victim and the genuine site. Every request the victim makes is forwarded to the real server; every response comes back through the proxy and is rewritten just enough to keep the victim on the attacker's domain. The victim sees the real site because they are looking at the real site, one hop late.

Tooling for this has been commodity for years — evilginx and its descendants ship with configuration files for major identity providers — which matters for how you should reason about it. This is not an advanced persistent threat capability. It is a well-documented technique that requires a domain, a certificate, a virtual machine, and a configuration file. Treat it as the baseline an authentication design must survive, not as a worst case.

The reverse proxy as a real site

The attacker registers a domain chosen to survive a glance — a lookalike domain such as cellar-login.example-secure.com, which reads as reassuring and is entirely under their control — and obtains a valid TLS certificate for it in about ninety seconds from a free certificate authority. They point it at a reverse proxy configured with the real site's hostname.

What the proxy does is mechanically simple. An inbound request for /admin/login is rewritten and forwarded to https://cellar.example.com/admin/login; the response is streamed back with any absolute URLs pointing at the real host rewritten to point at the phishing host, so that the victim's next click stays inside the proxy. Nothing is cloned in advance and nothing goes stale. If the real site deploys a new login page at 3 a.m., the phishing site shows the new login page at 3 a.m. If the login flow has an unusual conditional step — a tenant selection screen, a consent prompt, a “your password expires in 4 days” interstitial — the proxy renders it correctly, because it is not simulating the flow but conducting it.

This defeats the entire family of user-facing tells that awareness training teaches. The page is not slightly wrong, because it is not a copy. There are no broken images, no outdated logo, no missing footer link. The connection is genuinely encrypted and the padlock is genuinely valid — the certificate correctly attests that you are securely connected to cellar-login.example-secure.com, which is a true statement about a host you should not be talking to. The only remaining tell is the domain name itself, read carefully, every time, by a person who has other work to do.

Anti-pattern — “look for the padlock”

Symptom: user guidance that lists “check for HTTPS and the padlock” among the phishing indicators. Why it is wrong: the padlock has attested to transport security, not to identity, since domain-validated certificates became free and instant; a phishing proxy always has a valid one. Corrective: remove the item from the guidance entirely — it trains a false confirmation — and replace it with “credentials are entered only on a page reached from your own bookmark or password manager, never from a link.” Then stop relying on the guidance, because Module 1 already established its ceiling.

Relaying the ceremony live

Now walk the login. The victim lands on the proxy and is shown the real login form. They type the admin password; the proxy records it and forwards it to cellar.example.com. The real server evaluates it, finds it correct, and responds with the TOTP prompt. The proxy passes the prompt through. The victim opens their authenticator app, reads 418 902, and types it in. The proxy records the code and forwards it — elapsed time from keystroke to arrival at the real server, a fraction of a second — and the real server validates it, because it is a genuinely valid code inside its window.

Notice what the real server experienced. It saw a login from an IP address (the proxy's) with a correct password and a correct, unexpired second factor. Every check it was designed to perform passed, and passed honestly. There is no anomaly at the protocol level, because there is no protocol anomaly: the credential material was real. Detection, if it happens, has to come from signals outside the authentication decision — an unfamiliar autonomous system, an impossible-travel calculation, a device fingerprint that does not match — all of which are probabilistic and all of which the attacker can degrade by proxying from a residential IP in the victim's city.

This is the moment to be precise about why Module 2's improvements do not help here. The password fell because it is reusable, but the code fell for a different-looking reason that turns out to be the same reason: the user handed over a value, and the attacker only needed it for the length of one relay. Time-boxing constrains when the value is useful, not to whom. Nothing in the ceremony asks the question that would matter — which site is this credential for? — because none of these factors has a way to represent the answer.

The real prize: the session cookie

The credentials are not the objective. They are an inconvenience the attacker tolerates on the way to the thing that actually confers access.

When the real server finishes authenticating, it does what every web application does: it issues a session cookie — a bearer token representing “this browser has already authenticated as this user” — in a Set-Cookie header. That header travels back through the proxy, which reads it, stores it, and passes it on to the victim so the victim lands in a working, logged-in session and notices nothing. The attacker then loads the cookie into their own browser and is inside the account.

The consequences are worth stating individually because incident responders get them wrong under pressure. The second factor is now irrelevant. Authentication has concluded; the cookie is downstream of it. Prompting for MFA again would require the application to re-authenticate, which it has no reason to do for an established session. Rotating the password does nothing. The cookie was not derived from the password and does not become invalid when it changes — a fact that produces a very specific incident-review moment when the team discovers the attacker was still in the account four hours after the “containment” step. Only session revocation evicts them. Invalidating the server-side session, or rotating whatever key signs the tokens, is the action that matters, and it must be taken deliberately.

Three-lane swimlane sequence showing an attacker-in-the-middle proxy relaying a password, a TOTP code, and the session cookie between a victim and the real wine-cellar server, harvesting each in turnVictim (admin)AiTM proxyReal serverbrowsercellar-login.example-secure.comcellar.example.comGET /admin/loginthe genuine login pagerendered, links rewrittenpasswordharvested #1forwarded — validatesTOTP 418 902harvested #2forwarded in ~0.4s — validSet-Cookie: session=…harvested #3 — the prizevictim lands logged in, sees nothing wrong
Figure 3.1 — The AiTM relay, act one of the wine-cellar compromise. Password and TOTP code are captured in passing, but the Set-Cookie on the return path is the objective: it is a bearer token for an already-authenticated session, so the attacker who copies it never has to satisfy a second factor again. The victim's session completes normally, which is why the attack is typically discovered days later from its effects.

Why the whole family falls

Step back from the specific sequence and the generality becomes visible. The proxy does not attack any factor. It attacks the channel, and it does so by occupying a position the factors have no way to notice.

Every credential in Module 2's family reduces, at authentication time, to a value or an approval that the user emits into a page. The proxy is the page. Whatever is emitted is captured and forwarded. The factor's individual improvements all operate on axes the relay is indifferent to: reusability (the relay only needs one use), validity window (the relay operates inside it), possession (the relay never touches the device), user awareness (the page is the real page). A factor could combine every one of these improvements and still lose, because none of them changes the fact that the user is handing something to whoever rendered the form.

That framing tells you what a solution must do, before you know what the solution is. It must either remove the emitted value — so that there is nothing worth capturing — or make the emitted value specific to the destination, so that a value produced for the proxy is useless at the real server. FIDO2 does both, which is why the last row of the taxonomy flips all three columns. Modules 4 and 5 build the first property; Module 6 builds the second, and the second is the one that actually kills this attack.

The load-bearing idea

An AiTM proxy is not a defeat of any particular factor; it is the observation that a credential the user emits into a page belongs to whoever rendered that page. Defeating it requires either nothing worth emitting, or an emission that is scoped to a destination — and only the second survives an attacker who can render anything.

Module 04 The escape: public-key auth

Module 3 ended with a requirement: an authentication mechanism must either emit nothing worth capturing, or emit something scoped to its destination. This module builds the first property and lays the groundwork for the second. The move is to stop transmitting a credential at all and instead transmit a proof that the credential exists — a proof that is freshly manufactured for one login and worthless for any other.

If you have read Guide Nº 19, the primitive is familiar and you can move quickly: a signature over a server-chosen nonce, verified against a stored public key. What this module adds is the WebAuthn ceremony around that primitive — where the key pair comes from, what binds it to a single site and a single user, what the server ends up storing, and why storing it is safe in a way a password database never is. That last point is worth the module on its own for anyone who has ever had to write a breach-notification assessment.

Signing a challenge, not sharing a secret

Challenge-response inverts the transaction. Instead of the user sending a secret that the server compares, the server sends a fresh random value — a nonce, or in WebAuthn's vocabulary a challenge — and the user's device returns a signature over it. The server verifies the signature against a public key it stored earlier. Possession of the private key is demonstrated; the private key is not transmitted, mentioned, or derivable from anything that was.

Two properties follow, and both are worth stating in the terms Module 2's taxonomy uses. First, nothing reusable crosses the wire. The signature is computed over a challenge that the server generated for this attempt and will not accept twice. An attacker who captures it in full holds a proof about a nonce that is already spent — the cryptographic equivalent of a receipt. Second, the emitted value cannot be produced without the key, so there is no path by which a user can be talked into generating the right signature elsewhere. They can be talked into signing, but what they sign is a specific challenge for a specific site, which is where Module 6 will pick this up.

Be precise about what this does and does not yet solve. Challenge-response alone defeats capture-and-replay. It does not, by itself, defeat a live relay — an attacker sitting in the middle could pass the real server's challenge to the victim, collect the signature, and forward it, exactly as they forwarded the TOTP code. Everything in this module is necessary and not yet sufficient. Hold that thought; it is the reason Module 6 exists and the reason this course does not treat “public-key authentication” as a synonym for “phishing-resistant.”

Cross-reference — Guide Nº 19

The signature scheme underneath is the same construction Guide Nº 19 develops: a private key produces a value over a message that anyone holding the public key can verify, and that nobody without the private key can forge. WebAuthn authenticators typically use ECDSA over the P-256 curve or Ed25519. This course treats those as a solved primitive and never opens the algebra — what matters here is the ceremony wrapped around it, which is where the phishing resistance actually comes from.

The key pair that lives in the device

The private key is generated inside the authenticator and never exists anywhere else. This is not a policy or a configuration option; it is a property of the hardware. The secure element — a tamper-resistant chip in a USB key, or the equivalent secure enclave in a phone or laptop — generates the key pair internally, exposes the public half, and offers exactly one operation involving the private half: sign this input. There is no export command, because the command was never implemented.

Run the handover test against that. Clause one asks whether the user transfers a value or approval that the attacker can reuse. The user cannot transfer the private key even if they want to, even if a very convincing person on the telephone explains that the security team needs it. There is no user interface, no file, no string, no QR code that represents it. This is a categorical difference from every factor in Module 2, where the transfer was not merely possible but was the normal mode of use.

What the user can be induced to do is press the button — to authorize a signing operation. That is the remaining human-facing surface, and it is deliberately small: a signature over a challenge, for a site the browser names. Compare the surface areas. With a password, the human decision is “where do I type this reusable string,” with a wrong answer that is permanently costly. With a security key, the human decision is “do I touch this,” and the answer being wrong produces a signature that Module 6 will show is worthless to the attacker anyway.

Two-panel comparison: a password exists identically at client and server so either copy authenticates, while a key pair keeps the private half inside the device and sends only a per-challenge signatureShared secretUserP@ssw…Serverhash(P@ssw…)the same value lives on both sidesthe user can hand it to anyonea captured copy works forevera server dump is a breach of accountswhat travels: the secret itselfKey pairAuthenticatorprivate keyServerpublic keythe halves are different valuesthe private half has no export patha captured signature is already spenta server dump authenticates nothingwhat travels: a per-challenge signature
Figure 4.1 — Symmetry versus asymmetry. The password panel has one value in two places, so compromising either copy is compromising the account. The key-pair panel has two different values, only one of which is secret, and that one has no route out of the hardware — so neither the wire nor the server's database contains anything an attacker can authenticate with.

The registration ceremony

Registration is where the credential comes into existence, and every property the course depends on is decided here. The ceremony runs between three parties: the server (the relying party), the browser, and the authenticator.

The server begins by generating a random challenge and sending it along with the relying-party identifier, information about the user account, and its preferences — which algorithms it accepts, whether it wants user verification, whether it wants attestation. The browser receives this, adds the true origin of the page it is on, and passes a request into the authenticator. The authenticator asks for a gesture — a touch, and possibly a PIN or biometric — and then generates a brand-new key pair, specific to this relying party and this user account. It returns the public key, a credential ID (the handle the server will use to ask for this specific key later), and a signature over the registration data. The private key stays inside.

Two aspects of that description are load-bearing. The key pair is new — nothing is reused across sites, so there is no cross-site correlation and no single credential whose compromise spans services. And it is bound to this relying party, recorded in the authenticator alongside the RP identifier, which is what lets the authenticator refuse to use it elsewhere. A security key holding credentials for forty sites holds forty independent key pairs, each of which will only ever be produced for the site it was minted for. That is the structural fact Module 6 turns into a phishing defense.

Three-lane swimlane of the WebAuthn registration ceremony showing the server challenge, key pair generation inside the authenticator's device boundary, and the public key and credential ID returned for storage while the private key never crosses the boundarydevice boundary — nothing secret crosses itServer (RP)BrowserAuthenticatorchallenge + rpId + user infoclientData: challenge +origin the browser is onuser gesture: touch + PINgenerate key pairfor this RP + this userprivate key sealed herepublic key + credentialIdstored against the account
Figure 4.2 — The registration ceremony. The server's challenge and RP identifier travel inward; the browser adds the true origin; the authenticator mints a key pair specific to this relying party and this user behind the device boundary and returns only the public half and a credential ID. The private key is created inside the boundary and has no route out of it, at registration or ever after.
Cross-reference — Guide Nº 15

Guide Nº 15's survey placed authentication protocols on a spectrum from bearer credentials to proof-of-possession. Registration is the moment a WebAuthn credential is placed at the proof-of-possession end and pinned there: the artifact the server holds is not a credential at all but a verification key, and the artifact that authenticates has no serialized form. If you found the bearer/proof-of-possession distinction abstract in that guide, this ceremony is its most concrete instance.

Why the public key is safe to store

After registration, the wine-cellar server's credential table holds, per credential: a user handle, a credential ID, a public key, a signature counter, and some metadata about the authenticator. Consider what an attacker who exfiltrates that table can do with it.

They can verify signatures. That is the complete list. A public key is, by construction, a verification key — it lets you check that someone else produced a signature, and gives you no ability to produce one. The attacker cannot sign a challenge, cannot derive the private key, cannot use the credential ID for anything except naming a credential they cannot exercise. To be exact about the residual: the table is a list of which accounts exist and roughly what authenticators they use, which is a privacy matter and worth handling accordingly, but it is not an authentication matter.

Compare that to the password case, where a stolen hash table is a race. Good hashing makes the race long and expensive, but the attacker's work is offline, parallelizable, and permanently ongoing, and every weak password in the corpus falls eventually. This asymmetry has an operational consequence you can put in a risk register: a credential-table breach in a WebAuthn deployment does not require a mass password reset, does not require user notification on authentication grounds, and does not create a backlog of accounts at elevated risk. The incident is a privacy incident, sized accordingly.

From your GRC background

You have probably scoped the aftermath of a credential-store exposure — the notification analysis, the forced-reset campaign, the months of elevated monitoring for the accounts nobody reset. Nearly all of that work exists because a password database is a store of authenticating material. A WebAuthn credential table is not. The change in obligations is downstream of a change in what the asset is, which is the kind of argument that survives contact with a regulator far better than a control maturity score.

Anti-pattern — “a security key is just a fancy OTP generator”

Symptom: a design document or migration plan that treats FIDO2 as a drop-in for TOTP, often with the tell “same UX, better hardware,” and that keeps the OTP fallback enrolled “since it's the same category of factor.” Why it is wrong: an OTP generator emits a value the user relays; an authenticator emits a signature it will only produce for a named site. Treating them as the same category is precisely the error the taxonomy's three columns exist to prevent. Corrective: require any migration plan to state which taxonomy column changes and why — and to say what happens to the fallback factor, because a phishable fallback preserves the old row alongside the new one (Module 8).

Module 05 WebAuthn authentication

Registration happens once; this ceremony happens every time the admin signs in, and it is the one an attacker gets to watch. This module walks it in order, names the fields the authenticator returns, and states the full acceptance test the server runs.

One field in that bundle carries the origin the browser is actually connected to. It is named here and deliberately not unpacked — Module 6 opens it, and the deferral is intentional, because origin binding lands harder once you have seen the whole ceremony work without it and noticed that the relay from Module 3 would still succeed. Read this module asking a specific question: if an AiTM proxy sat in the middle of everything below, which step would notice?

The server's challenge

Login begins on the server. It generates a fresh random challenge — 16 bytes or more of cryptographic randomness — records it against this login attempt, and sends it to the browser along with the relying-party identifier and, usually, the list of credential IDs registered for the account being signed into.

The challenge does one job and does it completely: it makes each assertion a statement about this attempt. The server accepts a signature only over the challenge it just issued, only once, and only within a short window. An assertion captured from yesterday's login, or from another session opened seconds ago, covers a different nonce and fails verification. This is the property that retires the entire replay class of attacks — the one that made backup codes and stolen passwords useful for months.

Two implementation details determine whether the property is real. The challenge must be server-generated and server-remembered: a challenge the client picks, or one the server recomputes from a timestamp, is not a nonce and can be anticipated. And it must be consumed — deleted or marked used when the assertion arrives — so that an assertion cannot be submitted twice. Both are easy to get wrong in a hurry, and both fail silently, since the happy path works perfectly either way.

When it misleads

Because a broken challenge implementation still authenticates legitimate users flawlessly, there is no functional signal that anything is wrong. A test that logs in successfully proves nothing about replay resistance. The tests that matter are adversarial: submit the same assertion twice and require the second to fail; submit an assertion against a stale challenge and require rejection. If those two tests do not exist, assume the property does not either.

Three-lane swimlane of the WebAuthn authentication ceremony showing the server's challenge, the browser passing the true origin and client data hash to the authenticator, the user gesture and signature produced inside the secure element, and the server verifying the returned assertion against the stored public key, with nothing reusable crossing the wireAuthenticatorBrowserServerchallenge (nonce), RP ID, credential IDsclient data hash + RP IDthe origin the browser is really onuser gesturepresence, and verificationsign, in the elementprivate key never leavesassertion: authenticatorData,clientDataJSON, signatureforwarded unchangedverify against the storedpublic key; challenge unspent;origin and RP ID matchNothing reusable crosses the wire — the challenge is spent once and the signature is worthless without it
Figure 5.1 — The authentication ceremony. The server issues a nonce; the browser hands the authenticator the client data hash and the origin it is actually connected to; the private key signs inside the element and never moves; the server checks the returned assertion against the public key it stored at registration. Every value on the wire is scoped to this one attempt, so capturing the traffic buys an attacker nothing.

The gesture: presence and verification

The browser passes the challenge to the authenticator, which will not sign anything until a human does something. That requirement comes in two strengths, and confusing them is one of the more consequential mistakes in a deployment.

User presence (UP) means a human was physically there: a finger touched the key's contact, which cannot be satisfied by software. It proves someone is at the device. It does not prove who. User verification (UV) means the authenticator authenticated the human first — a PIN entered into the key, or a fingerprint or face match — before signing. It proves the person is the one who set the device up.

The difference is the difference between one factor and two. A security key that only requires a touch is something-you-have. A stolen key, in a stranger's hands, satisfies UP perfectly. A key that requires UV is something-you-have plus something-you-know-or-are, and the stolen key is inert without the PIN. Both flags are reported in the assertion, and the server can require what it needs — which means the server must actually check them, since an assertion produced with UP only is cryptographically just as valid as one produced with UV.

Worked example — the wine-cellar policy

The wine-cellar console requires UV for admin accounts: the key must have a PIN set, and the server rejects any assertion whose UV flag is clear. The rationale is concrete rather than doctrinal — the four admin keys travel in bags through tasting rooms and trade shows, and the threat of a key going missing for an afternoon is more plausible than a remote attack. Customer accounts require UP only, because the population is eleven thousand people who will not tolerate a PIN and whose threat model is dominated by remote attacks, which UP already handles. The two policies differ because the threats differ, and both are written down with that reasoning attached.

The assertion, field by field

After the gesture, the authenticator signs and returns an assertion with three components. Knowing what is in each is what lets you reason about the next module rather than take its conclusion on faith.

clientDataJSON is assembled by the browser, not by the page and not by the authenticator. It contains the ceremony type (webauthn.get for authentication, webauthn.create for registration), the challenge echoed back, and the origin the browser is connected to. Hold that last one.

authenticatorData is assembled by the authenticator. It contains a hash of the relying-party identifier, a byte of flags including UP and UV, and the sign counter — a value the authenticator increments on each use.

The signature covers both: specifically, the authenticator signs the concatenation of authenticatorData and the SHA-256 hash of clientDataJSON. That construction is the quiet load-bearing detail of the entire course. Because the signature covers the hash of the client data, and the client data contains the origin, the origin is inside the signature. Nothing in the bundle can be altered — not the origin, not the challenge, not the flags — without invalidating it.

Structural diagram of a WebAuthn assertion showing clientDataJSON with type, challenge and origin, authenticatorData with RP ID hash, flags and sign counter, and the signature computed over bothclientDataJSONassembled by the browsertype: "webauthn.get"challenge: "kQ7…"origin: "https://cellar.example.com"authenticatorDataassembled by the authenticatorrpIdHash: sha256("cellar.example.com")flags: UP=1 UV=1signCount: 41hashedsignaturesign(authenticatorData ‖ sha256(clientDataJSON))the origin is inside the signaturechange it and the signature no longer verifies — opened in Module 6
Figure 5.2 — Anatomy of an assertion. The browser supplies the client data (type, challenge, origin); the authenticator supplies its own data (RP ID hash, UP/UV flags, sign counter); the signature covers the authenticator data concatenated with the hash of the client data. Because the origin sits inside the hashed client data, it is covered by the signature and cannot be edited in flight — the fact Module 6 turns into the whole defense.

Server-side verification

An assertion arrives. The server's job is not “check the signature” — that is one line of a checklist, and a deployment that stops there has thrown away most of the guarantee. The full acceptance test:

CheckAgainst whatFailure it prevents
Credential ID is knownThe stored credential for this accountAssertions from an unregistered or wrong-account credential
Signature verifiesThe stored public key for that credential IDForged or tampered assertions
Challenge matches and is unspentThe challenge this server issued for this attemptReplay of a previously captured assertion
Type is webauthn.getThe literal string in clientDataJSONCross-ceremony confusion — a registration signature accepted as a login
Origin matchesThe relying party's expected originAssertions produced on a phishing site — Module 6
RP ID hash matchesSHA-256 of the expected RP IDCredentials being exercised for a different relying party
Flags satisfy policyUP required always; UV required where policy demands itSilent downgrade from two factors to one
Sign counter has advancedThe counter stored at last useUndetected use of a cloned authenticator

Two entries deserve comment. The sign counter is the most commonly dismissed check and the one whose purpose is most often misstated. It does not prevent cloning; it detects it. If two devices hold the same credential, their counters diverge, and a counter that goes backwards or fails to advance is evidence that something is wrong. Note the limits honestly: many authenticators — including most synced passkey implementations — report a constant zero, which is conformant and means the check simply does not apply. A server should treat a counter that moves as a signal it can use and a counter that is always zero as one it cannot, rather than rejecting the latter.

The origin row is the one the next module is about, and it is worth noticing now how ordinary it looks sitting in a checklist. It is a string comparison. That is the entire mechanism — and it is also, precisely because it looks like boilerplate, the check that gets loosened first when someone is debugging a staging environment at eleven at night and needs the login to work.

The load-bearing idea

WebAuthn's guarantees are not properties of the protocol; they are properties of a verifier that performs the checks. Every line in the table above is a guarantee the server can silently decline. A system that verifies the signature and skips the rest is cryptographically sound and phishable.

Module 06 Origin binding: where the phish dies

This is the module the course was built to deliver. Everything so far has been necessary and, by the course's own accounting, insufficient: challenge-response killed replay, the secure element killed the handover, and a live relay would still have walked through both. Module 3's proxy could pass the real server's challenge to the victim, collect a perfectly valid assertion, and forward it — the same trick that beat the TOTP code, applied to a signature.

It does not work, and the reason is a string comparison. To sign with the admin's real credential, the proxy's page has to ask the browser for that credential — and the browser will only hand a credential to the origin it belongs to. The victim's browser is on the proxy's origin, the requested credential is scoped to the real one, and the browser refuses before the authenticator is ever invoked: no prompt, no gesture, no signature. The server keeps its own origin check as a backstop, so that an origin-substituted assertion submitted by some non-browser client is caught — but on this path the server is never reached. Nothing is detected, nothing is blocked, no anomaly engine fires. A value does not match another value.

This module runs the mechanism down to that comparison and then replays the wine-cellar attack against it — act three of the running example — to show exactly which byte kills it and why no relaying strategy recovers.

The browser names the real origin

An origin is scheme, host, and port taken together — https://cellar.example.com — and it is the browser's fundamental unit of trust. Guide Nº 21 developed this at length: cookies, storage, and script access are all partitioned by origin, and the browser maintains that partition against a page that would very much like to escape it. The origin is not something a page asserts about itself; it is something the browser knows about the page, derived from the URL it actually fetched over the connection it actually opened.

When a page calls the WebAuthn API, it supplies several parameters — the challenge, the credential IDs to try, the user-verification preference. It does not supply the origin. The browser assembles clientDataJSON itself and writes in the origin from its own state, and there is no API surface through which script can influence that value. A page served from cellar-login.example-secure.com that calls the API will produce client data reading "origin": "https://cellar-login.example-secure.com", no matter what its JavaScript says, wants, or claims.

This is worth dwelling on because it is where the trust actually rests. The security of the whole scheme reduces to a claim about the browser: that it correctly reports the origin of the page invoking the API and cannot be talked out of it by that page. That claim is exactly the same one the entire web platform already depends on — if it were false, cookie scoping and the same-origin policy would fail first and far more loudly. The phishing defense is not a new trust assumption bolted onto the browser; it is a reuse of the one the browser was built to enforce.

Cross-reference — Guide Nº 21

Guide Nº 21's central point was that the origin is the web's security boundary and that the browser enforces it unilaterally, without the page's cooperation or consent. WebAuthn's phishing resistance is that enforcement applied to authentication: the credential is scoped to an origin exactly as a cookie is, and the browser refuses to let one origin exercise another's credential for the same reason it refuses to let one origin read another's cookies. If Nº 21's model is solid in your head, this module is a corollary rather than a new idea.

Origin is inside the signature

Module 5 established the construction: the authenticator signs authenticatorData concatenated with sha256(clientDataJSON). Combine that with the previous section and the consequence is immediate. The origin is in the client data; the client data is hashed; the hash is signed. The origin is therefore covered by the signature, and any modification to it — by a proxy, a network device, a malicious script, anyone — changes the hash and invalidates the signature.

The semantic shift this produces is the heart of the matter. A password is a claim of the form “I know the secret.” A TOTP code is a claim of the form “I hold the seed, and it is currently 14:32.” Neither says anything about the recipient. A WebAuthn assertion is a claim of the form:

I am credential a7f3…, I was used by a present, verified human,
and I was produced for the site https://cellar.example.com.

That last clause is the difference in kind. The assertion is not a portable proof of identity that happens to be delivered somewhere; it is a proof about a destination, and delivering it elsewhere does not change what it says. An attacker who obtains it holds a signed statement that a user authenticated to the attacker's own site — which is true, and useless, because the real server was never the subject of the claim.

The load-bearing idea

Every phishable credential answers the question “who are you?” A WebAuthn assertion answers “who are you, to whom?” Because the second half of that answer is inside the signature, a credential produced for the wrong recipient cannot be laundered into a credential for the right one. That is the entire mechanism, and there is nothing more to it.

The server's origin and RP-ID checks

The mechanism only exists if the verifier uses it. Two related checks do the work, and they are not redundant.

The origin check compares the origin string in clientDataJSON against the relying party's expected origin — an allow-list the server holds as a constant. This is the phishing defense proper: it answers “was this assertion produced on a page served by us?”

The RP-ID check compares the rpIdHash in authenticatorData against the hash of the expected RP ID. The RP ID is a bare domain — cellar.example.com — rather than a full origin, and it is supplied by the authenticator from its own record of what the credential was registered for. This answers a different question: “is this the credential we minted for this relying party?”

Both exist because they are attested by different parties and can fail independently. The browser vouches for the origin (where the ceremony happened); the authenticator vouches for the RP ID (what the credential is for). The RP ID is also deliberately looser — it may be a registrable suffix of the origin's host, which is what lets one credential work across cellar.example.com and admin.cellar.example.com when the relying party wants that. That flexibility is useful and it is also why the RP-ID check alone is not the phishing defense: it is satisfied by any subdomain, while the origin check pins the exact host, scheme, and port.

Origin checkRP-ID check
Value lives inclientDataJSON.originauthenticatorData.rpIdHash
Asserted byThe browserThe authenticator
GranularityExact scheme + host + portA registrable domain, matching subdomains too
Question answeredWhere did this ceremony happen?Which relying party is this credential for?
If skippedPhishable — a proxy's assertion verifiesCredential confusion across relying parties
Anti-pattern — deriving the expected origin from the request

Symptom: verification code that reads the expected origin from the incoming Origin or Host header, or builds it from a configurable base URL that defaults to the request's own host. It usually appears while making a staging environment work without a second config entry. Why it is wrong: the comparison becomes a self-comparison — the attacker's request supplies both sides — and always passes, silently deleting the entire phishing defense while every functional test continues to pass. Corrective: the expected origin is a build-time constant or an explicit allow-list, and a test asserts specifically on origin mismatch as the rejection reason (Module 5's worksheet). Grep any WebAuthn integration for the expected-origin value and confirm it is a literal.

The proxy, replayed — and where it dies

Act three. The wine-cellar admin has enrolled a security key. The same attacker sends the same lure from the same domain, and the admin — who has had a busy week — clicks it again.

Everything proceeds as before, up to a point. The proxy fetches the real login page and renders it. The admin's browser is connected to https://cellar-login.example-secure.com. The real server generates a challenge; the proxy forwards it faithfully. Then the page — the real login page, conducted through the proxy — calls navigator.credentials.get(), and because it is the real site's own code it asks for the real site's credential: the call carries rpId: "cellar.example.com".

That is the request the browser will not honor. Before it invokes the authenticator — before any prompt appears, before the admin can touch the key or enter a PIN — the browser compares the requested rpId against the origin it is actually on. WebAuthn requires the rpId to equal, or be a registrable-domain suffix of, the caller's origin. cellar.example.com is neither equal to nor a suffix of cellar-login.example-secure.com, so the browser throws a SecurityError and stops. The authenticator is never asked. No gesture is collected, no client data is assembled, no signature is produced — there is nothing for the proxy to forward, because nothing was ever signed.

Notice what this does to the admin's role: she never gets the chance to do anything wrong. The relay does not fail because she scrutinised the domain or declined a prompt — it fails before a prompt could appear, on a comparison the browser makes on her behalf and without her cooperation. And the server's origin check, which the previous section built, is still there — it just never receives an assertion to judge, because none exists. It is the backstop: the line that would catch an origin-substituted assertion from a non-conforming client, a non-browser caller, or a bug, none of which the browser's own refusal covers. Defence in depth means the phish dies at the browser and, if the browser ever failed to hold, would die again at the server. Act three ends at the first of those two.

Closeup of the AiTM relay against WebAuthn: the phishing page requests the real site's credential, and the browser throws a SecurityError because the requested RP ID is not a registrable suffix of the phishing origin, so the authenticator is never invoked and nothing is signed; the server's origin check remains as an unreached backstopAdmin's browserAiTM proxyReal serverconnected to the proxycellar-login.example-secure.comcellar.example.comchallenge kQ7…forwarded faithfullyreal login page calls navigator.credentials.get()rpId: "cellar.example.com"browser checks rpId against its real originorigin: cellar-login.example-secure.comrpId: cellar.example.comnot equal, not a registrable suffixthrow SecurityError — before any signingauthenticator never invoked · nothing signedno assertion — nothing for the proxy to forwardserver origin checkthe backstop — never reached here;it catches origin-substituted / non-browser clients
Figure 6.1 — The exact moment where the phish dies. The proxy relays the challenge faithfully, but the real site's page asks for rpId cellar.example.com while the browser is on cellar-login.example-secure.com. The browser compares the two, finds the RP ID is neither equal to nor a registrable suffix of its origin, and throws a SecurityError before the authenticator is ever invoked — so no assertion exists to forward. The server's origin check remains as the backstop for clients that don't make this refusal; here it is never reached. One string does not match another, and nothing is signed.

Why relaying can't recover

An attacker who has just watched that fail will look for a way around it, and it is worth walking their options, because the completeness of the failure is what distinguishes this from a control that merely raises cost.

Edit the origin in the assertion. Impossible: the signature covers the hash of the client data. Change one byte and verification fails at the signature check instead of the origin check — a different rejection, the same rejection.

Re-sign the modified assertion. Requires the private key, which is in a secure element with no export path and which the proxy has never touched. The proxy can relay bytes; it cannot produce a signature over bytes of its choosing.

Make the victim's browser report the real origin. Requires the victim's browser to be connected to cellar.example.com — at which point the victim is on the real site, there is no proxy in the path, and there is no attack. The browser reports the origin it has, and the attacker's whole position depends on that origin being theirs.

Ask the authenticator for a credential belonging to the real site. The authenticator will not produce it: the credential is registered against that relying party, and the browser will not offer credentials from an origin that does not match the RP ID. The refusal happens before any signing.

Line the requirements up and the structure becomes visible. To succeed, the attacker needs the victim's browser to be connected to the attacker (so the attack exists at all) and the signed origin to be the real server's (so verification passes). Those are the same variable with two required values. No cleverness closes a contradiction — which is why this is a guarantee rather than a mitigation, and why the honest attacker response is not to defeat the login but to go somewhere else entirely. That somewhere else is Module 8.

Two-panel comparison of the same AiTM relay against password plus TOTP, which succeeds, and against WebAuthn, which fails at the origin comparisonPassword + TOTPvictim types password into the proxyproxy forwards it — real server acceptsvictim types TOTP — forwarded, acceptedSet-Cookie harvested at the proxyattacker is inside the accountFIDO2 security keyproxy relays the challenge — finevictim touches the key — fineassertion signed over the proxy's originproxy forwards it unchanged — nothing else to doorigin mismatch → rejectednothing harvested; no cookie ever issued
Figure 6.2 — The same attack, two outcomes. The attacker's behavior is identical in both panels and equally competent. On the left, every step the victim performs yields something reusable and the session cookie ends the story. On the right, every step succeeds until the one comparison that cannot be made to pass — and because the login never completes, no cookie is ever issued to harvest.

Module 07 Attestation and assurance

Module 6 closed the phishing question. This module opens a narrower one that programs in regulated environments have to answer: you know a valid assertion arrived, but what do you know about the thing that produced it?

The honest answer, without attestation, is: less than most people assume. A valid signature proves that the private key matching the stored public key was exercised. It does not prove that key lives in a certified hardware token rather than a software implementation, a virtual authenticator in a browser's developer tools, or a platform credential synced to five devices. For most deployments that gap is acceptable and closing it costs more than it returns. For some — the ones with an approved-device control to enforce — it is the whole question. This module gives you the distinction precisely enough to decide which case you are in, and closes with the verification failures that quietly return a WebAuthn deployment to phishability.

A valid signature is not a known device

Separate two claims that everyday language runs together.

Claim A: “this assertion is valid.” Established by the verification in Module 5 — the signature checks out against the stored public key, the challenge and origin are right, the flags satisfy policy. What it proves: the private key paired with that public key was exercised, by a present (and possibly verified) human, for this site, on this attempt. This is a strong claim and it is sufficient for the phishing question.

Claim B: “a genuine hardware authenticator of a known make produced this.” Not established by any of the above. Verification is indifferent to where the key lives. A software authenticator, a virtual authenticator in a browser's testing tools, an emulated device in a research lab, or a certified token in a tamper-resistant package all produce assertions that verify identically, because verification only ever consults a public key.

The distinction matters wherever the control you are enforcing is about the device rather than the authentication. “Administrative access requires a hardware token that cannot export credentials” is a device claim. If your server accepts any credential that verifies, an admin can satisfy that control with a browser extension implementing WebAuthn in JavaScript, and every log entry will look correct. The control statement will be false and the evidence will look identical to the evidence for its truth.

Anti-pattern — “the user has a hardware key, therefore this assertion came from it”

Symptom: an access review or control narrative that treats hardware-key issuance as proof that authentications used hardware keys. It hides in phrases like “all admins are issued YubiKeys, therefore admin logins are hardware-backed.” Why it is wrong: issuance is a fact about procurement; what enrolled and what signs are facts about credentials, and nothing links them unless attestation was required and verified at enrollment. An admin who found the key inconvenient and enrolled a platform credential instead is indistinguishable in the logs. Corrective: require and verify attestation at enrollment for the populations where the device claim is load-bearing, store the resulting AAGUID with the credential, and make the access review report on enrolled authenticator models rather than on keys handed out.

What attestation asserts

Attestation is a registration-time statement, made by the authenticator and rooted in the manufacturer, saying in effect: this key pair was generated inside a device of this make and model, with these security properties. It is evidence about provenance, delivered alongside the new public key, and it is available only at registration — the moment the credential comes into existence.

The shape is a certificate chain, and if Guide Nº 19 is fresh you already know the pattern. The manufacturer holds a root key and certifies batches of devices; each device carries an attestation certificate that chains to that root; at registration the device signs a statement covering the new credential's public key and the challenge, and the relying party validates the chain against a set of trusted roots. The device also reports an AAGUID — an identifier for the model — and it is the attestation that makes the AAGUID mean anything, since an unattested device can report whatever AAGUID it likes.

Validating the chain requires knowing which roots to trust and what each model's properties are, which is what a metadata service provides: a signed catalogue of authenticator models, their attestation roots, their certification levels, and their known issues. Consuming one is the practical cost of requiring attestation — it is an external dependency you must keep current, and a stale catalogue rejects newly released models that are perfectly good.

One design note worth knowing: attestation certificates are deliberately shared across large batches of identical devices, typically at least 100,000. If each device had a unique attestation certificate it would be a global identifier for that device across every site it registered with. Batching keeps attestation a claim about a model rather than a tracking token for an individual — the deliberate resolution of a real tension between assurance and privacy.

Cross-reference — Guide Nº 19

This is Guide Nº 19's certificate-chain machinery in an unusual application. The familiar case chains a server's identity to a public CA so a browser can trust a website; here the chain runs from a device to its manufacturer so a relying party can trust a hardware claim. Same construction, different subject: TLS certificates attest to who you are talking to, attestation certificates attest to what kind of thing you are talking to.

When you need it, when you don't

Requiring attestation is not free. It adds an external metadata dependency, it rejects authenticators you have not blessed — including new models released after your catalogue was last updated — and in consumer contexts it collides badly with platform credentials, many of which provide no meaningful attestation at all. A consumer flow that requires attestation is a flow that tells a meaningful fraction of users their perfectly good phone is unacceptable, at the exact moment they are trying to enroll.

The decision turns on one question: is there a control that is a claim about the device?

ContextRequire attestation?Reasoning
Consumer sign-in at scaleNoThe threat is remote attack, which origin binding already handles; enrollment friction costs more than the assurance returns
Standard employee accountsUsually noAny origin-bound credential defeats the attack you are defending against; device policy is enforced by endpoint management instead
Administrative or privileged accountsYesThe device claim is load-bearing: an approved-authenticator control cannot be evidenced without it, and the population is small enough that friction is manageable
Regulated high-assurance enrollmentYes, with a model allow-listThe obligation is often written in terms of certified hardware; attestation is the only mechanism that produces evidence of it
Signing or approval of high-value transactionsConsiderDepends on whether the risk decision rests on non-exportability of the key

Notice that attestation and phishing resistance are orthogonal. Every row of that table is equally phishing-resistant, because phishing resistance comes from origin binding and origin binding does not care what kind of device signed. Attestation answers a different question, and conflating the two produces both mistakes: teams that require attestation believing it is what stops phishing, and teams that skip it believing they have therefore weakened their phishing posture.

From your GRC background

This is a control-evidence problem of a kind you have met before. A requirement phrased as “privileged access shall use a FIPS-validated hardware authenticator” is a claim about a device, and the only technical mechanism that can evidence it at authentication time is attestation with a model allow-list checked at enrollment. Without it, the honest control narrative is “we issued hardware tokens and have a policy,” which is a procurement fact plus an intention — testable only by interview. With it, the narrative is “enrollment rejects any authenticator whose attested AAGUID is not on the approved list; the enrolled model is stored per credential and reported in the quarterly access review,” which an auditor can test against data. The gap between those two sentences is the entire value of attestation to a compliance program.

The verification you must not skip

Everything this course has built is a property of a verifier that performs checks. Rushed implementations drop checks, and the dropped ones are predictable — they are the checks whose absence changes nothing about whether legitimate users can log in.

Rank them by what their absence costs. Skipping the origin check is catastrophic and silent: the deployment remains fully phishable, the AiTM proxy from Module 3 succeeds against it exactly as it did against TOTP, and nothing in the user experience or the logs distinguishes it from a correct implementation. Skipping challenge consumption restores replayability, so a captured assertion becomes a reusable credential — the property WebAuthn exists to remove. Skipping the UV flag check silently downgrades two factors to one for anyone whose key has no PIN. Skipping attestation costs the device claim only; the phishing resistance survives intact, which is why it sits at the bottom of this ranking despite being the module's subject.

The pattern is worth naming because it generalizes past WebAuthn: every check whose omission is invisible on the happy path will eventually be omitted by someone under deadline pressure. The countermeasure is not vigilance but adversarial tests that assert on specific rejection reasons — the ones Module 5's worksheet specified. A test that logs in successfully proves the feature works. Only a test that submits a bad assertion and asserts why it was rejected proves the guarantee exists.

Comparison of two claims: a valid assertion proves key possession, presence, and origin, while attestation additionally proves the authenticator's make and security profile“A key signed this”evidence: the assertion signatureprovesthe registered private key was useda human was present (and maybe verified)it was produced for this exact originleaves openwhat kind of thing holds the keywhether it could be copied elsewhere“A known key signed this”evidence: attestation, at registration onlyaddsthe make and model of the deviceits certification and security profilethat the key was generated on itcostsa metadata catalogue to keep currentenrollment friction, refused devices
Figure 7.1 — Two claims, two evidences. The assertion signature settles possession, presence, and destination — everything the phishing question needs. Attestation settles provenance, and only at registration. Neither substitutes for the other: skipping attestation costs you the device claim and leaves phishing resistance untouched, while skipping the origin check costs you phishing resistance no matter how well attested the device is.

Module 08 The honest soft spots

No victory lap. Module 6 established a real guarantee with a precise scope: the login ceremony cannot be relayed. It said nothing about the account, and an attacker who has just watched a phish bounce does not go home — they enumerate the other ways an account changes hands.

There are four, and they are not exotic. Users lose devices, so there must be a recovery path. Recovery paths authenticate humans by other means, and those means are social. Cross-device flows introduce a second channel with its own assumptions. And synced credentials move the root of trust from a chip you can hold to a cloud account you cannot inspect. This module treats each honestly, closes with the deployment sequence and the anti-pattern catalogue, and ends the course where real programs actually lose — which is not the login.

Device loss forces a fallback

People lose keys. They leave them in hotel rooms, snap them off in laptop ports, and hand them back with the laptop when they change jobs. Any deployment that does not plan for this will discover the plan under pressure, at which point someone will make an exception, and the exception will become the process.

The lazy path is to keep a weaker factor enrolled “just in case” — an SMS code or a TOTP app that still works if the key is unavailable. This is the phishable fallback, and its arithmetic is unforgiving. An account's phishing resistance is not the strength of its strongest factor; it is the strength of its weakest usable path. If the account can be entered with an SMS code, then the account can be phished, and the security key has become a preference rather than a control. Worse, it is a preference the attacker gets to override: a phishing page that says “security key unavailable? use your backup code” is a single line of HTML, and the victim who is running late will take the offer.

The correct answer is nearly always a second hardware key, enrolled at the same session as the first. Both are origin-bound, so the weakest usable path stays as strong as the strongest. One lives with the user and one lives in a safe, a desk drawer, or with the person who administers the account. The cost is real — a second piece of hardware per user and the logistics to distribute it — and it is smaller than the cost of any recovery process that has to authenticate a human by other means, which is the next section's subject.

Anti-pattern — the phishable fallback

Symptom: a rollout plan that enrolls security keys while leaving TOTP or SMS enrolled “for continuity,” often phrased as a temporary measure with no removal date. Look for a factor-enrollment report where users have three factors and a policy that requires any one of them. Why it is wrong: phishing resistance is a property of the weakest usable path, so the account remains fully phishable and the attacker simply drives the victim to the fallback. Corrective: enroll two hardware keys per user and remove phishable factors on a dated schedule, tracked as a completion metric per population. If a phishable factor must persist for a transition period, restrict it to non-privileged accounts, alert on every use, and hold a removal date that someone owns.

Attack surface map showing the hardened origin-bound login ceremony in green, off which a phishing attempt bounces, and the four soft edges that survive it in red — account recovery, device provisioning, sync-fabric trust, and any phishable fallback left enrolled — with the attacker's path rerouting around the login to reach themAttackerrational, patientbouncesThe login ceremonyorigin-bound, no shared secrethardened in Modules 4 to 7Account recoverya human verified by other meansDevice provisioninghow did the first key enroll?Sync fabricthe cloud account holds the keysPhishable fallbackSMS or TOTP left enrolledso the attacker reroutes around it
Figure 8.1 — Where the attacker goes instead. Modules 4 through 7 harden one box on this map, and they harden it thoroughly: the phish bounces off the ceremony. The four red edges are what survives, and each one authenticates a person by means other than the key — which is why the rest of this module is about them, and why the account in the next section is taken without the cryptography ever being consulted.

Account recovery: the door left open

Recovery is where an account is handed to someone who cannot currently authenticate. That is its purpose, and it means recovery flows necessarily authenticate a human by means other than the credential — an email link, knowledge-based questions, a document photo, or a conversation with a support agent. Every one of those is social, and social is the thing this course spent seven modules removing from the login.

So the attacker's move after the phish fails is not cryptographic. It is a phone call.

The recovery autopsy — the same account, taken anyway

Three weeks after the failed phish, the wine-cellar support queue receives a ticket from an address resembling the admin's, then a follow-up call. The caller is calm, in a plausible amount of trouble, and knows things: the admin's full name and title, her manager's name, the office address, her start month, and the last four digits of the corporate card on the account — all assembled from a conference bio, a public filing, and the fragments the failed phishing session had already collected. The caller explains that her security key snapped in a laptop port at a trade show and she needs access before the allocation deadline that evening. The agent, following a documented process, verifies identity by asking three questions from the account record — all of which the caller answers — resets the account's authenticators, and emails an enrollment link. The caller enrolls their own key. The login is, at that moment, perfectly unphishable and belongs to the attacker.

Read what failed. Not the cryptography — it was never consulted. Not the agent, who followed the process correctly and had no way to distinguish this call from the four legitimate ones that week. The process failed, in three specific ways: the verification questions tested knowledge that is obtainable rather than possession of anything; a single agent could complete a full authenticator reset on a privileged account without a second party; and urgency was allowed to compress the process, which is the lever every social engineer reaches for first. Notice also that the failed phishing attempt three weeks earlier was the reconnaissance for this one, and nobody connected them because the failed assertion was logged and never read — which is precisely the alerting gap Module 6's worksheet closed.

Swimlane sequence of a help-desk recovery bypass in which an attacker social-engineers an authenticator reset and enrolls their own key on an account whose login is unphishableAttackerSupport agentAccountlogin: unphishable throughout“my key broke, deadline is tonight”three identity questionsall answered — from public sourcesprocess followed correctlyreset all authenticatorsenrollment link emailedattacker enrolls their own security keyaccount takenthe login is still unphishable — for them
Figure 8.2 — The recovery autopsy. The cryptography is never consulted; the agent follows the documented process correctly; the account changes hands. Three process defects do the work — verification by obtainable knowledge, single-agent authority to reset a privileged account, and urgency permitted to compress the flow — and the attacker ends up holding an unphishable login of their own.

Cross-device and hybrid flows

You will often authenticate on a laptop using a credential held on a phone. This is the hybrid transport — a QR code on the laptop screen, scanned by the phone, followed by a proximity-checked channel between them — and it deserves a careful assessment rather than either of the two easy verdicts.

The origin binding survives. The relying party's origin travels to the phone over the hybrid channel, the phone's authenticator signs over it, and the server checks it exactly as before. A proxy that renders a QR code on a phishing page does not get an assertion for the real site; it gets one for its own origin, and the story ends the way Module 6 ended. The design was built with this attack in mind.

What is added is a channel and its assumptions. The QR code carries a key that secures the phone-to-laptop channel, and the protocol requires a proximity check — a Bluetooth advertisement establishing that the two devices are physically near each other — specifically to stop an attacker from relaying a QR code to a remote victim. That check is the load-bearing addition, and it is where the honest caveats live: it depends on the platform implementing proximity correctly, and it is a newer, less battle-tested surface than the origin comparison that does the main work. Treat it as sound and as the newer part of the system.

The reasonable posture: hybrid is a legitimate, phishing-resistant flow and appropriate for most populations. For the highest-assurance accounts, prefer a directly attached authenticator, not because hybrid is known to be broken but because it has more moving parts, and for four admin accounts the cost of carrying a physical key is negligible. That is a defensible line to hold in a design review, and it avoids both the overclaim (“hybrid is just as good, always”) and the dismissal (“QR codes can be phished”), which is false as stated.

Synced passkeys versus bound hardware keys

A synced passkey is a WebAuthn credential that a platform replicates across a user's devices through a cloud account. It is origin-bound and it is phishing-resistant — this must be said first and without hedging, because the most common error in this area is dismissing passkeys as a weaker thing. Against the attack this course is about, a synced passkey and a hardware key are equivalent.

They differ on a different axis: where the credential can exist and what secures that. A hardware key's private key exists in one chip and has no export path, so compromising the credential requires the physical object. A synced passkey's private key exists on every device in the sync fabric and, in encrypted form, in the provider's infrastructure — so its security is the security of the cloud account and the sync fabric's key management. That is usually strong, and it is a different and larger trust base than a chip in your pocket.

Bound hardware keySynced passkey
Origin-bound / phishing-resistantYesYes — identically
Private key existsIn one secure elementOn every synced device, encrypted in the provider's fabric
Compromise requiresThe physical device (plus the PIN, if UV is enforced)The cloud account, or the sync fabric
Attestation availableYes, with model provenanceOften minimal or absent
Recovery on device lossA second enrolled keyThe provider's account recovery — which you do not control
Best forPrivileged accounts, device-provenance controlsLarge populations, adoption at scale

That last row is the practical judgment. For eleven thousand customers, a synced passkey is decisively the right answer: it is phishing-resistant, it survives a lost phone without a support call, and its adoption curve is the only one that gets a consumer population off passwords. For four admins with a device-provenance control, the hardware key is right. Both statements are true at once, and a program that picks one credential type for everyone is answering the wrong question.

Anti-pattern — the unstated downgrade

Symptom: a control narrative or risk acceptance that says “all privileged users authenticate with FIDO2 credentials,” where some of those credentials are synced passkeys backed by personal cloud accounts. The claim is technically true and the assurance differs materially from what the reader will assume. Why it matters: the recovery path for a synced credential is the provider's account recovery, which your program does not run, cannot test, and cannot evidence — so the account's weakest usable path lies outside your control boundary entirely. Corrective: state the credential type in the control, not just the protocol; where the device claim matters, enforce it with attestation at enrollment (Module 7) rather than with an expectation.

Deployment, and the anti-pattern catalogue

The order of operations in a rollout determines whether it produces security or an outage.

Enroll before you remove. Every user gets their credentials — two hardware keys for privileged accounts, a passkey for consumer populations — and enrollment is confirmed by a real authentication before any weaker factor is disabled. Removing first produces a lockout event and a hastily approved exception process, which becomes the permanent recovery backdoor.

Harden recovery before you announce. The moment the login becomes unphishable, recovery becomes the cheapest path in. If recovery hardening lags the rollout, the program has moved the attacker rather than stopped them, and it has done so while telling everyone the problem is solved.

Make removal a dated, owned metric. “Percentage of privileged accounts with zero phishable factors enrolled” is the number that measures the program. “Percentage with a security key enrolled” measures the beginning of it and will happily read 100% while every account remains phishable.

Alert on the events that mean something. Origin mismatch (Module 6), factor enrollment on an existing account, and authenticator reset. All three are low-volume, high-signal, and all three are routinely logged and unrouted.

The catalogue, with the symptom and corrective for each:

Anti-patternSymptom in the wildCorrective
“All MFA is phishing-resistant”A control statement reading “MFA enforced” mapped to a phishing-resistance requirementName the factor and the property: “origin-bound public-key authentication (FIDO2), no phishable factor enrolled”
TOTP or push “solved” AiTMA phishing playbook retired after an MFA rolloutTest it: run a red-team AiTM against the deployed factor and watch the session cookie leave
Key treated as a fancy OTP generatorA migration plan with “same UX, better hardware” and the OTP fallback retainedRequire the plan to state which taxonomy column changes and what happens to the fallback
Phishable fallbackUsers with three enrolled factors and a policy requiring any oneTwo hardware keys; dated removal of phishable factors, tracked per population
Unhardened recoveryLogin hardened; help-desk reset process unchanged since 2019Two-party authorization for privileged resets, possession-based verification, mandatory delay
“User has a key” = “this assertion came from it”Access review citing key issuance as evidence of hardware-backed loginsAttestation verified at enrollment; report enrolled AAGUIDs from data
Synced passkey assumed equivalent to bound hardware“All privileged users use FIDO2” where some credentials are personal-cloud-syncedState the credential type in the control; enforce with attestation where the device claim matters
Skipping origin or RP-ID verificationExpected origin read from a request header or a base-URL config defaultBuild-time constant or explicit allow-list; a CI test asserting specifically on origin mismatch
The load-bearing idea

The login became unphishable and the attacker did not stop attacking; they moved to the door left open. A program that hardens authentication without hardening enrollment, recovery, and factor management has not removed the risk — it has relocated it into a queue where the control is a person following a script, and where nobody is measuring.

Concept index

Phishing
Convincing a person to authenticate to the attacker's endpoint believing it is the legitimate one.
Handover test
A credential is phishable exactly when its use is the user transferring a value or approval the attacker can reuse.
Credential stuffing
Replaying username/password pairs from a breach dump — theft by reuse, not by misdirection.
Shared secret
A credential held identically by user and server, so either copy can authenticate.
SMS OTP
A one-time code delivered by text message; relayable, and additionally exposed to SIM-swap and carrier interception.
TOTP
A time-based one-time code derived from a shared seed; short-lived but still a relayable value.
Push approval
Authenticating by tapping “approve” on a prompt whose true origin the user cannot see.
Number matching
A push variant that shows a code on screen to be typed into the prompt; raises relay effort, does not stop it.
AiTM proxy
A real-time attacker-in-the-middle reverse proxy that relays a live login and harvests every credential and the session cookie.
Session cookie
The post-authentication token whose theft lets an attacker skip the second factor entirely.
Lookalike domain
A deceptive (combosquatted or typosquatted) domain chosen to survive a glance — the delivery vehicle for an AiTM proxy. Distinct from a true homograph, or IDN, domain, which imitates the real name using confusable Unicode characters rather than plain ASCII.
Challenge-response
Proving possession of a private key by signing a fresh server nonce, transmitting nothing reusable.
Public-key credential
A per-site key pair whose private half stays in the authenticator and whose public half the server stores.
Registration ceremony
The WebAuthn flow that mints a key pair bound to one relying party and user and returns the public key.
Secure element
The tamper-resistant component that generates and seals the private key so it never leaves the device.
Credential ID
The handle the server stores and echoes back so the authenticator knows which key pair to use.
Relying party
The site or service the credential is bound to — the “RP” in the WebAuthn ceremonies.
Authentication ceremony
The WebAuthn login flow: server challenge, user gesture, signed assertion, server verification.
Assertion
The signed bundle an authenticator returns at login: authenticatorData, clientDataJSON, and the signature.
clientDataJSON
The client data — including type, challenge, and origin — hashed and covered by the signature.
authenticatorData
The authenticator's own bytes: RP ID hash, flag bits (UP/UV), and the sign counter.
User presence / verification
UP (a touch) proves someone is there; UV (PIN or biometric) proves it is the enrolled user.
Sign counter
A monotonic counter in authenticatorData used to detect cloned authenticators.
Origin binding
The browser-supplied true origin, signed into the assertion and checked by the server, that scopes a signature to one site.
Origin
Scheme, host, and port taken together — the browser's unit of trust and the value written into clientDataJSON.
RP ID
The relying-party identifier whose hash the authenticator signs, checked alongside origin.
Attestation
A registration-time statement vouching that a key lives in a genuine authenticator of a known make.
AAGUID
The identifier in authenticatorData naming the authenticator model — meaningful only when attestation vouches for it.
Metadata service
The catalogue of authenticator models and their security properties a verifier checks attestation against.
Phishable fallback
A weaker recovery factor (SMS or OTP) that an attacker targets instead of the hardware key.
Account recovery
The out-of-band flow to regain access after device loss — the phishable path that survives an unphishable login.
Synced passkey
A public-key credential replicated across devices by a cloud fabric, whose assurance depends on that fabric.
Hybrid transport
The cross-device flow that lets a phone authenticate a laptop over a proximity-checked channel.

This is the complete text of the course. With JavaScript enabled, this same page runs the interactive edition — a self-diagnostic that reorders the syllabus around your gaps, knowledge checks, applied worksheets with model answers, and a spaced-repetition review queue — with progress saved locally in your browser.