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 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.
Five Spec Bugs Found
The spec's own example contradicts its own grammar. Appendix A's passkey carries
fido2Extensions.hmacSecretwith an"HS256"algorithm - a structure and value that appear nowhere in the CDDL, which defineshmacCredentialswithhmac-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.The example breaks the spec's formatting rules twice more. It uses
"CA"wheresubdivision-coderequires the ISO 3166-2 formUS-CA, and"WPA2"where the wifi enum sayswpa2-personal.A CDDL typo makes an enum into a map.
WIFINetworkSecurityTypeis 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.A grammar contradiction: an array that's simultaneously required-non-empty and defaulted-to-empty.
CustomFields.extensionsis 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.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 withindex.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.
Implementation Highlights
Some things I'm proud of in the implementation:
- Lossless round-trip guarantee -
parse โ serialize โ parseis 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)
- CLI security - the
inspectcommand 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
Getting Started
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.
Comments
No comments yet. Start the discussion.