Profile
Back to NewsBack
Dev.to 6 min
Reader Mode
Passkeys are a trap? I decoded two real ones to see what a website learns

Passkeys are a trap? I decoded two real ones to see what a website learns

12 hours ago

One of the most upvoted posts on Hacker News this weekend was an essay titled "Passkeys are a trap. Do not use them." Its argument is not that passkeys are weak. It concedes they can be far more secure than passwords and resist man-in-the-middle attacks. The worry is control: WebAuthn lets a website ask what kind of authenticator you used, and with attestation it can demand proof, which could one day lock out custom operating systems, unofficial passkey managers or anyone a site decides not to trust.

That is a policy argument, and it is worth having. But most of the people deciding whether passkeys become a trap are not standards bodies. They are developers writing a registration endpoint. So I generated two real passkeys and decoded exactly what they tell a website.

The setup

Chromium has a virtual authenticator you can drive over the DevTools protocol, so you can create real WebAuthn credentials in a headless browser without any hardware. I registered two:

  1. A synced passkey, the kind iCloud Keychain or Google Password Manager creates, with the site asking for attestation: "none".
  2. A hardware security key, with the site asking for attestation: "direct".

Then I decoded each attestationObject with this script. It needs one dependency, cbor-x.

// Decode a WebAuthn attestationObject and print what the authenticator claimed.
// Usage: node flags.mjs <base64url attestationObject>
import { decode } from "cbor-x";

const att = decode(Buffer.from(process.argv[2], "base64url"));
const auth = att.authData;           // rpIdHash(32) | flags(1) | signCount(4) | ...
const f = auth[32];

const FLAGS = [
  [0x01, "UP", "user present (someone tapped or touched)"],
  [0x04, "UV", "user verified (biometric or PIN)"],
  [0x08, "BE", "backup eligible (can be synced to other devices)"],
  [0x10, "BS", "backed up (is currently synced)"],
  [0x40, "AT", "attested credential data included"],
  [0x80, "ED", "extension data included"],
];

console.log(`attestation format: ${att.fmt}`);
console.log(`attestation statement keys: [${Object.keys(att.attStmt).join(", ")}]`);
console.log(`flags byte: 0x${f.toString(16).padStart(2, "0")}`);
for (const [bit, name, meaning] of FLAGS) {
  console.log(`  ${f & bit ? "x" : " "} ${name}  ${meaning}`);
}
console.log(`  reserved bits 0x02 and 0x20: ${f & 0x22 ? "set" : "unset"}`);

What the synced passkey said

attestation format: none
attestation statement keys: []
flags byte: 0x5d
  x UP  user present (someone tapped or touched)
  x UV  user verified (biometric or PIN)
  x BE  backup eligible (can be synced to other devices)
  x BS  backed up (is currently synced)
  x AT  attested credential data included
    ED  extension data included

What the security key said

attestation format: packed
attestation statement keys: [alg, sig, x5c]
flags byte: 0x45
  x UP  user present (someone tapped or touched)
  x UV  user verified (biometric or PIN)
    BE  backup eligible (can be synced to other devices)
    BS  backed up (is currently synced)
  x AT  attested credential data included
    ED  extension data included

What a website learns from each passkey registration

Three things stand out.

The flags are self-reported. With fmt: none the statement is empty. Nothing signs the claim that the user was verified or that the key is synced. The site simply takes the authenticator's word for it. That is the article's point: without attestation, software can say anything, and a site that wants certainty has to ask for attestation.

AT does not mean attested. This trips people up. The AT bit means the authenticator data contains the new credential's ID and public key. It is set on every registration, including the one with no attestation at all.

Attestation is a certificate chain. The security key returned x5c, an X.509 chain that identifies the authenticator's maker and, through its AAGUID, its model. A site can check it against the FIDO Metadata Service and refuse anything it does not recognise. That is the lever the article worries about.

Who actually pulls the lever

You do, in one line. Here is the default in SimpleWebAuthn, a widely used WebAuthn server library for Node:

import { generateRegistrationOptions } from "@simplewebauthn/server";

const options = await generateRegistrationOptions({
  rpName: "Acme",
  rpID: "acme.com",
  userName: user.email,
  attestationType: "none", // the default in v14; "direct" asks for the certificate chain
  authenticatorSelection: {
    residentKey: "preferred",
    userVerification: "preferred",
  },
});

The WebAuthn standard's own default for attestation conveyance is also none. Level 3 of the spec, which added the backup flags, became a W3C Recommendation on 25 August 2026. And even if you ask for attestation, Apple's synced passkeys do not provide it, according to passkey vendor Corbado. A consumer site that required attestation would lock out a large share of iPhone users on day one. So for consumer products today, the trap is mostly theoretical. Enterprises are the exception: requiring attested hardware keys for employees is a legitimate, common policy.

The four decisions that make your passkey login a trap, or not

The rules I use for client apps

  1. Leave attestation at none for consumers. Only ask for it when a contract or regulator requires specific hardware.
  2. Never make passkeys the only way in. Keep a recovery path: a second passkey, TOTP or a verified email link. The article recommends TOTP and hardware keys, and supporting them costs you little.
  3. Accept any authenticator. Do not allowlist by AAGUID unless you have a reason you can write down. An allowlist quietly breaks every passkey manager you did not think of, and the user only finds out at the login screen. Bitwarden, 1Password, KeePassXC and the platform managers should all work.
  4. Read BE and BS, do not enforce them. They are useful for UX. If a user's only passkey has BS unset, it lives on one device, and losing that device means losing the account. Prompt them to add a backup or a second factor. Do not reject the credential.

Passkeys are the biggest phishing fix in a decade, and most of the control the article fears sits in the options object you write. Write it so your users can leave.

I set up authentication like this in every MVP we build at Axionry, and the rest of my default stack is in the SaaS MVP tech stack for 2026.

Sources: Passkeys are a trap (Non-Functional, 10 October 2026), Hacker News discussion, W3C Web Authentication Level 3, SimpleWebAuthn, Corbado on platforms without passkey attestation. Registrations generated with Chromium's virtual authenticator on 12 October 2026. I used an AI assistant while drafting this.

Chat with me