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.
Top comments (0)