Skip to content

feat: Add ML-KEM-768 (FIPS 203) post-quantum KEM support#28

Open
onlykey wants to merge 5 commits intomasterfrom
feature/mlkem-768
Open

feat: Add ML-KEM-768 (FIPS 203) post-quantum KEM support#28
onlykey wants to merge 5 commits intomasterfrom
feature/mlkem-768

Conversation

@onlykey
Copy link
Collaborator

@onlykey onlykey commented Mar 15, 2026

Add ML-KEM-768 (FIPS 203) Post-Quantum KEM Support

Summary

Adds ML-KEM-768 key encapsulation to the OnlyKey, enabling post-quantum key exchange on the hardware token. Uses mlkem-native — the same ML-KEM implementation used by AWS-LC, rustls, and OpenTitan.

What this enables

Operation Slot Payload Response
Keygen 133 PK (1184 bytes)
Get public key 133 PK (1184 bytes)
Decapsulate 133 CT (1088 bytes) Shared secret (32 bytes)

Implementation

  • Library: mlkem-native v1.0.0 (Apache-2.0 / MIT / ISC, C90, CBMC memory-safe verified)
  • Backend: Pure portable C (no assembly — Cortex-M4 not covered by existing native backends)
  • RNG: Bridged to ArduinoLibs RNG.rand() via MLK_CONFIG_CUSTOM_RANDOMBYTES
  • Key storage: 2400-byte secret key stored AES-GCM encrypted in flash sectors 10-11 (repurposed from FIDO2 resident key slots 5-8)
  • Runtime memory: Uses existing ctap_buffer[7609] as scratch — SK at [0..2399], CT arrives at [5497..6584], no overlap, no new static allocations

RAM cost

320 bytes — from PACKET_BUFFER_SIZE bump (768→1088) to accommodate ML-KEM ciphertext in the multi-packet transport.

LARGE_BUFFER_SIZE also bumped 1024→1088 but this costs zero extra RAM (it shifts a pointer within ctap_buffer).

Files changed

Modified (firmware hooks):

  • onlykey/okcore.hKEYTYPE_MLKEM768, size constants, buffer bumps, flash function declarations
  • onlykey/okcore.cppokcore_flashset_mlkem_sk() / okcore_flashget_mlkem_sk()
  • onlykey/okcrypto.h — ML-KEM function declarations
  • onlykey/okcrypto.cppokcrypto_mlkem_keygen(), okcrypto_mlkem_decaps(), okcrypto_mlkem_getpubkey(), dispatch hooks

Added (vendored library):

  • mlkem_native/ — mlkem-native C-only source tree (FIPS 203 compliant)
  • mlkem_native/test/ — 12-test suite (run with cd mlkem_native/test && make test)

Testing

  • Compile with Teensyduino for MK20DX256
  • Keygen produces valid keypair (verify with host-side encaps/decaps round-trip)
  • Decaps recovers correct shared secret
  • Key persists across power cycles
  • Existing ECC/RSA operations unaffected

Notes

  • FIDO2 resident key slots 5-8 (flash sectors 10-11) are repurposed for ML-KEM SK storage
  • For hybrid PQ+classical, combine with existing X25519: HKDF-SHA256(X25519_ss || ML-KEM_ss)

cr7pt0 added 2 commits March 14, 2026 21:52
Integrates mlkem-native (Apache-2.0/MIT/ISC) for ML-KEM-768 key
encapsulation on the OnlyKey hardware token.

New capabilities:
- ML-KEM-768 keypair generation (KEYTYPE_MLKEM768 = 5)
- ML-KEM-768 decapsulation (recover shared secret from ciphertext)
- ML-KEM-768 public key retrieval from stored keypair

Implementation details:
- Uses mlkem-native v1.0.0 C-only portable backend (no assembly)
- All C code is CBMC-verified memory-safe
- RNG bridged to ArduinoLibs RNG.rand() via MLK_CONFIG_CUSTOM_RANDOMBYTES
- Secret key (2400 bytes) stored AES-GCM encrypted in flash sectors 10-11
  (repurposed from FIDO2 resident key slots 5-8)
- Runtime scratch uses existing ctap_buffer (no new static allocations)
- PACKET_BUFFER_SIZE bumped 768->1088 for ML-KEM ciphertext transport
- LARGE_BUFFER_SIZE bumped 1024->1088 (no extra RAM, shifts within ctap_buffer)
- Total new RAM cost: 320 bytes

Protocol:
- Keygen:  OKGENKEY    slot=133 -> returns PK (1184 bytes)
- Get PK:  OKGETPUBKEY slot=133 -> returns PK (1184 bytes)
- Decaps:  OKDECRYPT   slot=133, payload=CT (1088 bytes) -> returns SS (32 bytes)

Files added:
- mlkem_native/ - mlkem-native library (C90, FIPS 203 compliant)

Files modified:
- onlykey/okcore.h   - KEYTYPE_MLKEM768, size defines, buffer size bumps
- onlykey/okcore.cpp - Flash storage for ML-KEM secret key
- onlykey/okcrypto.h - ML-KEM function declarations
- onlykey/okcrypto.cpp - ML-KEM operations and dispatch hooks
Implements combined X25519 + ML-KEM-768 key encapsulation following
NIST/CNSA 2.0 hybrid recommendations. Both classical and post-quantum
components must be compromised to break the shared secret.

Protocol:
  Combined PK: X25519_pk(32) || ML-KEM_pk(1184) = 1216 bytes
  Decaps input: X25519_eph_pk(32) || ML-KEM_ct(1088) = 1120 bytes
  Combined SS: SHA256(X25519_ss || ML-KEM_ss) = 32 bytes

New operations (slot 134 = RESERVED_KEY_HYBRID_PQ):
  - okcrypto_hybrid_keygen: generates both keypairs, persists to flash
  - okcrypto_hybrid_decaps: X25519 ECDH + ML-KEM decaps + SHA256 combine
  - okcrypto_hybrid_getpubkey: returns combined 1216-byte public key

Storage:
  - Both keys share flash sectors 10-11 with standalone ML-KEM
  - Features byte (offset 352 in sector 11) distinguishes key type:
    KEYTYPE_MLKEM768(5) vs KEYTYPE_HYBRID_PQ(6)
  - X25519 SK (32 bytes) stored AES-GCM encrypted at sector 11 offset 353

Buffer sizes bumped to 1120 (from 1088) for hybrid payload.

Test suite expanded to 16 tests:
  - 8 ML-KEM-768 standalone tests
  - 6 hybrid tests (combiner, wrong-component rejection, full flow)
  - 2 performance/stress tests
@onlykey onlykey force-pushed the feature/mlkem-768 branch from 2151784 to 6abc898 Compare March 15, 2026 01:52
…kem-09)

BREAKING: Replaces custom hybrid combiner with X-Wing KEM per
draft-connolly-cfrg-xwing-kem-09, compatible with age v1.3.0
mlkem768x25519 recipient type.

Key changes:
- Combiner: SHA3-256(ss_M||ss_X||ct_X||pk_X||XWingLabel) replaces
  SHA-256(x25519_ss||mlkem_ss)
- XWingLabel: ASCII \.//^\ (hex 5c2e2f2f5e5c)
- PK order: pk_M(1184)||pk_X(32) (was x25519_pk||mlkem_pk)
- CT order: ct_M(1088)||ct_X(32) (was x25519_eph||mlkem_ct)
- Keygen: SHAKE256(seed,96) seed expansion per spec
- X25519: crypto_scalarmult (tweetnacl) replaces Curve25519::eval
- Flash: stores 2464-byte expanded SK as single encrypted blob
- SHA3-256 and SHAKE256 from mlkem-native FIPS202 (zero extra code)
- Renamed KEYTYPE_HYBRID_PQ(6) -> KEYTYPE_XWING(6)
- Renamed all hybrid_* functions to xwing_*

Test suite: 22/22 passing (ML-KEM + X-Wing combiner + stress)
@onlykey
Copy link
Collaborator Author

onlykey commented Mar 15, 2026

Updated: X-Wing spec compliance (draft-connolly-cfrg-xwing-kem-09)

Commit 4126ece aligns the hybrid KEM implementation to the X-Wing spec, compatible with age v1.3.0 mlkem768x25519 recipient type.

Key changes from previous commit

Before (custom hybrid) After (X-Wing spec)
SHA-256(x25519_ss || mlkem_ss) SHA3-256(ss_M || ss_X || ct_X || pk_X || XWingLabel)
PK: x25519_pk(32) || pk_M(1184) PK: pk_M(1184) || pk_X(32)
CT: x25519_eph(32) || ct_M(1088) CT: ct_M(1088) || ct_X(32)
Curve25519::eval() crypto_scalarmult (tweetnacl)
crypto_box_keypair() random SHAKE256(seed, 96) expansion
Separate encrypted blobs Single 2464-byte encrypted SK
KEYTYPE_HYBRID_PQ KEYTYPE_XWING

Spec references

  • X-Wing KEM: draft-connolly-cfrg-xwing-kem-09
  • XWingLabel: \\.//^\\ = hex 5c2e2f2f5e5c
  • Combiner: SHA3-256 and SHAKE256 from mlkem-native FIPS202 module (zero extra code)

Tests: 22/22 passing

cr7pt0 added 2 commits March 15, 2026 16:35
Uses standard ECC slot infrastructure (slots 101-132) for ML-KEM
and X-Wing key storage. No new flash code needed.

Key changes:
- Keygen stores 32-byte seed via ecc_priv_flash() with new keytypes
- Decaps/getpubkey read seed from ecc_private_key (loaded by
  okcore_flashget_ECC in dispatch), then SHAKE256-expand on demand
- Dispatch routes by keytype after flashget: KEYTYPE_MLKEM768(5)
  and KEYTYPE_XWING(6) branch to PQ handlers
- Removed RESERVED_KEY_MLKEM/XWING (133/134) — use any ECC slot
- Removed okcore_flashset/get_pqseed — zero custom flash code
- Removed XWING_SK_SIZE — no expanded key stored
- Net -131 lines

ECC slot stores: 32-byte seed + EEPROM type byte (5 or 6)
On use: SHAKE256(seed) -> keypair_derand() -> decaps/getpubkey
- Fix keygen feature bits: 0xD0 -> 0x20 (bit 5 = decrypt)
  0xD0 incorrectly set bits 4,6,7 (sign+backup+garbage)
  0x20 correctly sets only bit 5 (decrypt feature)
- Add early return in okcrypto_compute_pubkey() for KEYTYPE_MLKEM768
  and KEYTYPE_XWING — PQ seeds don't produce traditional ECC pubkeys
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