feat: Add ML-KEM-768 (FIPS 203) post-quantum KEM support#28
Open
feat: Add ML-KEM-768 (FIPS 203) post-quantum KEM support#28
Conversation
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
2151784 to
6abc898
Compare
…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)
Collaborator
Author
Updated: X-Wing spec compliance (draft-connolly-cfrg-xwing-kem-09)Commit Key changes from previous commit
Spec references
Tests: 22/22 passing |
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
133133133Implementation
RNG.rand()viaMLK_CONFIG_CUSTOM_RANDOMBYTESctap_buffer[7609]as scratch — SK at[0..2399], CT arrives at[5497..6584], no overlap, no new static allocationsRAM cost
320 bytes — from
PACKET_BUFFER_SIZEbump (768→1088) to accommodate ML-KEM ciphertext in the multi-packet transport.LARGE_BUFFER_SIZEalso bumped 1024→1088 but this costs zero extra RAM (it shifts a pointer withinctap_buffer).Files changed
Modified (firmware hooks):
onlykey/okcore.h—KEYTYPE_MLKEM768, size constants, buffer bumps, flash function declarationsonlykey/okcore.cpp—okcore_flashset_mlkem_sk()/okcore_flashget_mlkem_sk()onlykey/okcrypto.h— ML-KEM function declarationsonlykey/okcrypto.cpp—okcrypto_mlkem_keygen(),okcrypto_mlkem_decaps(),okcrypto_mlkem_getpubkey(), dispatch hooksAdded (vendored library):
mlkem_native/— mlkem-native C-only source tree (FIPS 203 compliant)mlkem_native/test/— 12-test suite (run withcd mlkem_native/test && make test)Testing
Notes
HKDF-SHA256(X25519_ss || ML-KEM_ss)