nx_x25519mlkem768.nx source
↩ module page · 140 lines · 7180 B
1// nx_x25519mlkem768.nx -- the MODERN post-quantum hybrid KEM X25519MLKEM768 (TLS 1.3 named group
2// 0x11EC) per draft-ietf-tls-ecdhe-mlkem-05 (banked knowledge/fetched/pq_hybrid_ietf_draft.raw).
3//
4// COMPLETES the post-quantum story: retires the UNWIRED constant-only NG_X25519_KYBER768_DRAFT00
5// (0x6399) "draft" by composing two COMPLETE primitives into the FINAL standardized hybrid --
6// * FIPS-203 ML-KEM-768 (nx_ml_kem_768_wasm: nx_mlkem_keygen/encaps/decaps)
7// * X25519 ECDH (nx_x25519: x25519 Montgomery ladder)
8// by CONCATENATION with the ML-KEM part FIRST (the spec's deliberately-reversed order, sec 3+4.3):
9// client key_exchange = ML-KEM-768 ek (1184) || X25519 pubkey (32) = 1216 bytes
10// server key_exchange = ML-KEM ciphertext (1088) || X25519 pubkey (32) = 1120 bytes
11// shared secret = ML-KEM shared secret (32) || X25519 shared secret (32) = 64 bytes
12// The 64-byte hybrid secret is quantum-safe iff EITHER component is (harvest-now-decrypt-later proof).
13//
14// This is the KEM engine; the TLS wiring (offer 0x11EC in supported_groups + carry the shares in
15// key_share + feed the 64B secret to the key schedule) composes these three functions. Proven END-TO-
16// END by the round-trip KAT in main (client_keygen -> server -> client_finish => identical 64B secret).
17//
18// license_tier: INDEPENDENT_REDERIVE
19// genealogy_id: international-research-sources/ietf/draft-ietf-tls-ecdhe-mlkem + nist/fips_203 + rfc_7748
20// lineage_id: nishi_x25519mlkem768_q10
21import "nx_syscalls.nx"
22import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
23import "nx_x25519.nx"
24import "nx_ml_kem_768_wasm.nx"
25const XM_MAGIC_65536: i64 = 65536
26
27const XM_X_PUB: i64 = 32
28const XM_X_PRIV: i64 = 32
29const XM_X_SS: i64 = 32
30const XM_EK: i64 = 1184
31const XM_CT: i64 = 1088
32const XM_SS_K: i64 = 32
33const XM_DK: i64 = 2400
34const XM_CLIENT_SHARE: i64 = 1216 // ek(1184) || x_pub(32)
35const XM_SERVER_SHARE: i64 = 1120 // ct(1088) || x_pub(32)
36const XM_CLIENT_SECRET: i64 = 2432 // x_priv(32) || dk(2400)
37const XM_SHARED: i64 = 64 // mlkem_ss(32) || x_ss(32)
38
39func xm_cpy(dst: *u8, src: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { dst[i] = src[i]; i = i + 1 } return 0 }
40func xm_off(p: *u8, k: i64) -> *u8 { return (p as i64 + k) as *u8 }
41// X25519 basepoint u=9 (32 bytes little-endian: 0x09 then zeros).
42func xm_basepoint(bp: *u8) -> i64 { bp[0] = 9 as u8; var i: i64 = 1; while i < 32 { bp[i] = 0 as u8; i = i + 1 } return 0 }
43
44func xm_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
45// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
46// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
47// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
48// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
49func xm_putn(v: i64) -> i64 { nxi_out(v); return 0 }
50
51// CLIENT keygen: rand96 = x_priv(32) || mlkem_seed(64). Emits client_share(1216) + client_secret(2432).
52func x25519mlkem768_client_keygen(rand96: *u8, client_share: *u8, client_secret: *u8) -> i64 {
53 let scratch: *u8 = sys_mmap(XM_MAGIC_65536)
54 let bp: *u8 = sys_mmap(32); xm_basepoint(bp)
55 let x_priv: *u8 = rand96
56 let x_pub: *u8 = sys_mmap(32)
57 x25519(x_priv, bp, x_pub)
58 let mlkem_seed: *u8 = xm_off(rand96, 32)
59 let ek: *u8 = sys_mmap(XM_EK)
60 let dk: *u8 = sys_mmap(XM_DK)
61 nx_mlkem_keygen(mlkem_seed, scratch, ek, dk)
62 xm_cpy(client_share, ek, XM_EK) // ML-KEM first
63 xm_cpy(xm_off(client_share, XM_EK), x_pub, XM_X_PUB)
64 xm_cpy(client_secret, x_priv, XM_X_PRIV)
65 xm_cpy(xm_off(client_secret, XM_X_PRIV), dk, XM_DK)
66 return 0
67}
68
69// SERVER: parse client_share(1216); rand64 = x_priv_s(32) || mlkem_msg(32). Emits server_share(1120) + shared(64).
70func x25519mlkem768_server(client_share: *u8, rand64: *u8, server_share: *u8, shared: *u8) -> i64 {
71 let scratch: *u8 = sys_mmap(XM_MAGIC_65536)
72 let bp: *u8 = sys_mmap(32); xm_basepoint(bp)
73 let peer_ek: *u8 = client_share
74 let peer_x_pub: *u8 = xm_off(client_share, XM_EK)
75 let x_priv_s: *u8 = rand64
76 let x_pub_s: *u8 = sys_mmap(32)
77 x25519(x_priv_s, bp, x_pub_s)
78 let x_ss: *u8 = sys_mmap(32)
79 x25519(x_priv_s, peer_x_pub, x_ss)
80 let mlkem_msg: *u8 = xm_off(rand64, 32)
81 let ct: *u8 = sys_mmap(XM_CT)
82 let mlkem_ss: *u8 = sys_mmap(32)
83 nx_mlkem_encaps(peer_ek, mlkem_msg, scratch, ct, mlkem_ss)
84 xm_cpy(server_share, ct, XM_CT) // ML-KEM first
85 xm_cpy(xm_off(server_share, XM_CT), x_pub_s, XM_X_PUB)
86 xm_cpy(shared, mlkem_ss, XM_SS_K) // ML-KEM first
87 xm_cpy(xm_off(shared, XM_SS_K), x_ss, XM_X_SS)
88 return 0
89}
90
91// CLIENT finish: parse server_share(1120) + client_secret(2432). Emits shared(64).
92func x25519mlkem768_client_finish(server_share: *u8, client_secret: *u8, shared: *u8) -> i64 {
93 let scratch: *u8 = sys_mmap(XM_MAGIC_65536)
94 let ct: *u8 = server_share
95 let peer_x_pub_s: *u8 = xm_off(server_share, XM_CT)
96 let x_priv: *u8 = client_secret
97 let dk: *u8 = xm_off(client_secret, XM_X_PRIV)
98 let x_ss: *u8 = sys_mmap(32)
99 x25519(x_priv, peer_x_pub_s, x_ss)
100 let mlkem_ss: *u8 = sys_mmap(32)
101 nx_mlkem_decaps(dk, ct, scratch, mlkem_ss)
102 xm_cpy(shared, mlkem_ss, XM_SS_K) // ML-KEM first
103 xm_cpy(xm_off(shared, XM_SS_K), x_ss, XM_X_SS)
104 return 0
105}
106
107// Round-trip KAT: client_keygen -> server -> client_finish; assert the two 64B secrets are IDENTICAL.
108func main() -> i64 {
109 let rand96: *u8 = sys_mmap(96)
110 var i: i64 = 0; while i < 96 { rand96[i] = ((i * 7 + 3) & 0xff) as u8; i = i + 1 }
111 let rand64: *u8 = sys_mmap(64)
112 i = 0; while i < 64 { rand64[i] = ((i * 5 + 11) & 0xff) as u8; i = i + 1 }
113
114 let client_share: *u8 = sys_mmap(XM_CLIENT_SHARE)
115 let client_secret: *u8 = sys_mmap(XM_CLIENT_SECRET)
116 x25519mlkem768_client_keygen(rand96, client_share, client_secret)
117
118 let server_share: *u8 = sys_mmap(XM_SERVER_SHARE)
119 let ss_server: *u8 = sys_mmap(XM_SHARED)
120 x25519mlkem768_server(client_share, rand64, server_share, ss_server)
121
122 let ss_client: *u8 = sys_mmap(XM_SHARED)
123 x25519mlkem768_client_finish(server_share, client_secret, ss_client)
124
125 var eq: i64 = 1
126 i = 0; while i < XM_SHARED { if ss_server[i] != ss_client[i] { eq = 0 } i = i + 1 }
127 var nz: i64 = 0
128 i = 0; while i < XM_SHARED { if ss_server[i] != (0 as u8) { nz = 1 } i = i + 1 }
129
130 xm_puts("=== nx_x25519mlkem768 round-trip KAT (TLS group 0x11EC, draft-ietf-tls-ecdhe-mlkem) ===\n" as *u8)
131 xm_puts("client_share=" as *u8); xm_putn(XM_CLIENT_SHARE)
132 xm_puts(" server_share=" as *u8); xm_putn(XM_SERVER_SHARE)
133 xm_puts(" hybrid_secret=" as *u8); xm_putn(XM_SHARED); xm_puts(" bytes\n" as *u8)
134 if eq == 1 { if nz == 1 {
135 xm_puts("PASS: client + server derived the IDENTICAL 64B hybrid secret (ML-KEM-768 || X25519)\n" as *u8)
136 return 0
137 } }
138 xm_puts("FAIL: hybrid secrets differ or all-zero\n" as *u8)
139 return 1
140}