Dev.to Security 🔐 Cybersecurity 👁 0 📖 2 min read

Implementing FIDO's new passkey-export format — and the five spec bugs I found

The FIDO Alliance recently published the Credential Exchange Format (CXF) — the first standard way to move passkeys, passwords, and TOTP secrets between credential managers. Apple ships it in iOS 26, Bitwarden was the fi

The FIDO Alliance recently published the Credential Exchange Format (CXF) — the first standard way to move passkeys, passwords, and TOTP secrets between credential managers. Apple ships it in iOS 26, Bitwarden was the first third-party manager to support it, and until now, exporting your vault meant a plaintext CSV. CXF is genuinely important infrastructure.

There was no TypeScript implementation. So I built one: cxf-kit — data models, parser, serializer, conformance validator, and a CLI, built against the Proposed Standard of 2025-08-14 with the spec's CDDL grammar vendored as the single source of truth, and interop-tested against Bitwarden's Rust implementation.
Implementing a spec line-by-line is the deepest way to read it. Here's what I found.

1. The spec's own example contradicts its own grammar. Appendix A's passkey carries fido2Extensions.hmacSecret with an "HS256" algorithm — a structure and value that appear nowhere in the CDDL, which defines hmacCredentials with hmac-sha256. Real exporters copy examples, so my validator treats this shape as a warning rather than an error: a validator that fails the spec's own example would be useless against real-world exports.

2. The example breaks the spec's formatting rules twice more. It uses "CA" where subdivision-code requires the ISO 3166-2 form US-CA, and "WPA2" where the wifi enum says wpa2-personal.

3. A CDDL typo makes an enum into a map. WIFINetworkSecurityType is written with braces — { "unsecured" / ... } — which in CDDL means a map, though it's plainly meant as a string enum like every other enum in the spec.

4. A grammar contradiction: an array that's simultaneously required-non-empty and defaulted-to-empty. CustomFields.extensions is declared [ + Extension ] .default [] — "one or more" with a default of none. My serializer had to make preservation the default and normalization opt-in, because the spec's own example wouldn't survive its own exporter rules.

5. The spec defines no packaging. File credentials carry an id, size, and integrity hash — but the spec never says where the bytes live in an export. Every implementation must invent its own convention (mine: a ZIP with index.json + documents/, documented as kit-defined, not spec).

All five are documented with evidence in the repo's SPEC_NOTES.md, and I'll be reporting them upstream.

Some things I'm proud of in the implementation: a lossless round-trip guarantee (parse → serialize → parse is value-identical for any input, even nonconforming ones), hardened archive reading (decompression caps, path-traversal rejection, strict UTF-8 — silently corrupting a secret is worse than failing), and a CLI whose inspect command cannot print credential values — enforced by a test that greps the output of every mode for eleven planted secrets, on every CI run, on Linux and Windows.

npm install cxf-kit
npx cxf validate export.json

It's MIT, on GitHub, and v0.1.1 today. If you're implementing CXF — or you maintain a credential manager and want an independent validator for your exports — issues and interop reports are very welcome.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.