Skip to content

Add post-quantum (age ML-KEM-768 / X-Wing) scaffold for WebCrypt/onlyagent#39

Closed
0c-coder wants to merge 6 commits into
onlykey:heroku-deployfrom
0c-coder:pqc-age-pr
Closed

Add post-quantum (age ML-KEM-768 / X-Wing) scaffold for WebCrypt/onlyagent#39
0c-coder wants to merge 6 commits into
onlykey:heroku-deployfrom
0c-coder:pqc-age-pr

Conversation

@0c-coder

Copy link
Copy Markdown

Host-side post-quantum KEM scaffold for the WebCrypt/onlyagent app, mirroring the age PQC support in python-onlykey#90 + firmware libraries#29 (ML-KEM-768 = keytype 5, X-Wing = keytype 6).

New files only, no edits to existing files:

  • src/onlykey-fido2/onlykey/xwing.js -- ML-KEM-768 / X-Wing encapsulation + age stanza helpers (host side)
    • src/onlykey-fido2/onlykey/onlykey-pqc.js -- device getPubKey / decapsulate over the existing OKCONNECT derive flow (DERIVE_PUBLIC_KEY / DERIVE_SHARED_SECRET), keyed by derivation label (no slots)
    • src/plugins/age/age-pqc.js -- age encrypt/decrypt plugin
    • src/plugins/age/INTEGRATION.md -- wiring (plugin.js DI, package.json deps), keytype map, and remaining TODO(verify) items
      Design: the web path has no key slots -- keys are derived per-identity from the reserved web-derivation key. The 32-byte derived secret feeds X-Wing directly (its private key is a 32-byte seed) and expands to ML-KEM's (d,z). Only the keytype byte (5/6) is added to the derive request.

Compatibility with #38 (OnlyAgent reskin): disjoint file sets. #38 only touches src/app-src.html, src/index-src.html, and two asset files; this only adds device-layer JS and a new src/plugins/age/ folder. They merge cleanly in either order, and the reskin's app_pages loop auto-styles the new age plugin page.

Status: scaffold / WIP. Encapsulation and the derive-flow shape are real; the TODO(verify) items in INTEGRATION.md (identity->keyhandle encoding, decaps op, ML-KEM seed expansion, age stanza/HPKE) must be matched byte-for-byte against python-onlykey#90 (tests/test_age_wire.py) before interop.

@0c-coder

Copy link
Copy Markdown
Author

Related post-quantum (ML-KEM-768 / X-Wing) work across the stack:

- xwing.js: verified split-decaps crypto (mlkem seed expansion, recipient build,
  encapsulate, xwingSplitDecapsulate) against @noble/post-quantum
- onlykey-pqc.js: 64-byte derive wrappers (getRecipient / decapsulate) over the
  existing FIDO2 derive flow; device returns [ss_X|mlkem_seed], sk_X never leaves
- age-pqc.js: recipient/encrypt/decrypt wired to the verified KEM (age container
  framing left as scoped TODOs to byte-match the age mlkem768x25519 format)
- INTEGRATION.md: pinned spec (HKDF domain separation, 64-byte layout, combiner)
- test/xwing-split.test.mjs: proves split decaps == standard encaps shared secret
@0c-coder

0c-coder commented Jul 1, 2026

Copy link
Copy Markdown
Author

Firmware counterpart (the 64-byte derive response, UNTESTED): trustcrypto/libraries#30.

@0c-coder

0c-coder commented Jul 4, 2026

Copy link
Copy Markdown
Author

age container framing implemented (the last open TODO; KEM was already done).

Added on pqc-age-pr:

  • src/plugins/age/age-format.js — age mlkem768x25519 container: bech32 age1onlykey recipient, HPKE base wrap (X-Wing KEM 0x647A / HKDF-SHA256 / ChaCha20-Poly1305, kem_context=enc, info="", 16-byte file key → 32-byte body), and the full age-v1 file (header + HMAC-SHA256 header MAC + ChaCha20-Poly1305 STREAM payload) since the browser has no age binary.
    • src/plugins/age/age-pqc.js — wired encrypt/decrypt to it (resolved the assembleAgeFile/parseStanzaCiphertext/openAgeFile TODOs).
    • test/age-format.test.mjs (npm run test:age) — 5/5. package.json — declares the @noble/* deps (kept test:pqc). AGE-FORMAT.md — spec.
      HPKE seal + bech32 recipient are byte-identical to python-onlykey#90 (frozen KATs, cross-checked vs #90's Python). Full split-custody round-trip passes end-to-end (device X25519 + browser ML-KEM → encaps → seal → age file → parse → decap → unwrap → multi-chunk STREAM).

Note: the web app addresses keys by derivation label, not a slot, so it doesn't emit #90's slot-based identity string — but the recipient (age1onlykey1…) is byte-compatible, so files interoperate. Remaining: firmware #30 hardware test.

@0c-coder

0c-coder commented Jul 4, 2026

Copy link
Copy Markdown
Author

Withdrawing this PR. The web app has no age workflow, and its key derivation works differently from the CLI: the web app derives keys per-label from the reserved web-derivation key (no slots), whereas python-onlykey#90 / the CLI use on-device ECC slots (101–116). So this scaffold implements an age client the web app isn't — and its label-derived keys wouldn't be the CLI's slot identities anyway, so the earlier "interoperates with the CLI" framing doesn't hold.

The KEM/container code and tests remain on branch pqc-age-pr for reference if PQC-in-web is later scoped correctly (more likely against the WebCrypt/PGP path than age). Firmware #30 is unaffected as a device-side X-Wing derive primitive. Reopen / new PR if we take a different approach.

@0c-coder 0c-coder closed this Jul 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants