nx_pq_hybrid.nx source
↩ module page · 312 lines · 15810 B
1// nx_pq_hybrid.nx -- C8 of the comms lane: POST-QUANTUM HYBRID KEY TRANSPORT for the C7 ratchet.
2// Contract symbol pqh_kex == the /compare/comms C8 watch. REFEREE: nx_pq_hybrid_gate.
3// DONE-RULE (comms.plan): hybrid classical+KEM ratcheting in the C7 tree (Signal SPQR / triple-ratchet
4// class): the gate proves a classical-only decryption fails AND a KEM-only decryption fails -- both
5// families must break.
6//
7// WHAT IT DOES: C7 distributes each epoch's commit_secret to the remaining members over X25519-only
8// ECDH. C8 replaces that transport with the X25519MLKEM768 HYBRID KEM (TLS 1.3 named group 0x11EC,
9// draft-ietf-tls-ecdhe-mlkem): the wrap key is derived from a 64-byte secret that is
10// ml_kem_768_shared(32) || x25519_shared(32), so recovering it needs BOTH the elliptic-curve secret
11// AND the lattice KEM secret. An adversary who breaks X25519 (a future quantum computer solving ECDH)
12// still cannot read the commit secret without also breaking ML-KEM-768 -- and vice versa. Because the
13// commit secret drives every subsequent epoch, this defeats harvest-now-decrypt-later on the whole
14// forward chain, which is exactly the property Signal's SPQR triple ratchet added in Oct-2025.
15//
16// PURE COMPOSITION, no new crypto: it forks NOTHING and imports the sovereign hybrid KEM
17// (nx_x25519mlkem768, itself x25519 + ml_kem_768) plus sha256 (HMAC/HKDF) and chacha20 (RFC 8439).
18// No WebRTC, no OpenSSL, no liboqs -- the ML-KEM here is the estate's own FIPS 203 implementation.
19//
20// THE PROOF, mechanical: unwrap takes a `corrupt` argument that damages ONE half of the recovered
21// 64-byte hybrid secret before deriving the wrap key -- corrupt=pq models an attacker who has the
22// classical half but not the lattice half (broke ECDH, not ML-KEM), corrupt=classical the reverse.
23// Either damaged half fails the HMAC and decrypts NOTHING; only corrupt=none succeeds. That is the
24// two-independent-families claim made falsifiable.
25//
26// DEBT NAMED, not hidden: mg_hmac/mg_derive/hex/file helpers here are the same shapes as nx_mls_group.
27// Extracting them to a shared nx_kdf_lib that BOTH C7 and C8 import is the correct DRY move and is
28// filed (comms.plan debt kdf-lib-extract); shipping C8 independent now lands the verified exceed
29// without re-opening the just-gated C7 binary in the same breath.
30// license_tier: ORIGINAL No hw writes (Rule 26).
31import "nx_syscalls.nx"
32import "nx_itoa_lib.nx"
33import "nx_x25519mlkem768.nx"
34import "nx_sha256.nx"
35import "nx_chacha20.nx"
36
37const PQH_EXIT_OK: i64 = 0
38const PQH_EXIT_USAGE: i64 = 2
39const PQH_EXIT_REFUSED: i64 = 3
40const PQH_EXIT_CORRUPT: i64 = 5
41// wire sizes of X25519MLKEM768 (algorithm constants of the standard, named):
42const PQH_SHARE: i64 = 1216 // ek(1184) || x_pub(32) -- a member's hybrid public share
43const PQH_SECRET: i64 = 2432 // x_priv(32) || dk(2400) -- a member's hybrid secret
44const PQH_SRVSHARE: i64 = 1120 // ct(1088) || x_pub_s(32) -- the encapsulation to a member
45const PQH_SS: i64 = 64 // mlkem_ss(32) || x_ss(32) -- the hybrid shared secret
46const PQH_RAND96: i64 = 96
47const PQH_RAND64: i64 = 64
48const PQH_KEY: i64 = 32
49const PQH_MAC: i64 = 32
50const PQH_NONCE: i64 = 12
51const PQH_BLOCK: i64 = 64
52const PQH_IPAD: i64 = 54
53const PQH_OPAD: i64 = 92
54const PQH_NL: i64 = 10
55const PQH_PIPE: i64 = 124
56const PQH_FILECAP: i64 = 262144
57
58func pqh_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
59func pqh_num(v: i64) -> i64 { nxi_out(v); return 0 }
60func pqh_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
61func pqh_eq(a: *u8, b: *u8) -> i64 {
62 var i: i64 = 0
63 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
64 if b[i] != (0 as u8) { return 0 }
65 return 1
66}
67func pqh_memeq(a: *u8, b: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { if a[i] != b[i] { return 0 } i = i + 1 } return 1 }
68func pqh_hexe(src: *u8, n: i64, dst: *u8) -> i64 {
69 var i: i64 = 0
70 while i < n {
71 let b: i64 = src[i] as i64
72 let hi: i64 = b / 16
73 let lo: i64 = b % 16
74 if hi < 10 { dst[i*2] = (48 + hi) as u8 } else { dst[i*2] = (87 + hi) as u8 }
75 if lo < 10 { dst[i*2+1] = (48 + lo) as u8 } else { dst[i*2+1] = (87 + lo) as u8 }
76 i = i + 1
77 }
78 dst[n*2] = 0 as u8
79 return n * 2
80}
81func pqh_hexv(c: i64) -> i64 {
82 if c >= 48 { if c <= 57 { return c - 48 } }
83 if c >= 97 { if c <= 102 { return c - 87 } }
84 if c >= 65 { if c <= 70 { return c - 55 } }
85 return 0 - 1
86}
87func pqh_hexd(src: *u8, n: i64, dst: *u8) -> i64 {
88 if n % 2 != 0 { return 0 - 1 }
89 var i: i64 = 0
90 while i < n {
91 let h: i64 = pqh_hexv(src[i] as i64)
92 let l: i64 = pqh_hexv(src[i+1] as i64)
93 if h < 0 { return 0 - 1 }
94 if l < 0 { return 0 - 1 }
95 dst[i/2] = (h * 16 + l) as u8
96 i = i + 2
97 }
98 return n / 2
99}
100func pqh_read(path: *u8, buf: *u8, cap: i64) -> i64 {
101 let fd: i64 = sys_openat_rd(path)
102 if fd < 0 { return 0 - 1 }
103 var tot: i64 = 0
104 while tot < cap { let r: i64 = sys_read(fd, (buf as i64 + tot) as *u8, cap - tot); if r <= 0 { break } tot = tot + r }
105 sys_close(fd)
106 return tot
107}
108func pqh_write(path: *u8, buf: *u8, n: i64) -> i64 {
109 let fd: i64 = sys_openat_wr(path, 420)
110 if fd < 0 { return 0 - 1 }
111 var w: i64 = 0
112 while w < n { let r: i64 = sys_write(fd, (buf as i64 + w) as *u8, n - w); if r <= 0 { break } w = w + r }
113 sys_fsync(fd)
114 sys_close(fd)
115 return 0
116}
117func pqh_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { d[o] = s[i]; o = o + 1; i = i + 1 } return o }
118// pipe-separated field <idx> of a buffer into out; len or -1
119func pqh_field(line: *u8, n: i64, idx: i64, out: *u8) -> i64 {
120 var f: i64 = 0
121 var i: i64 = 0
122 var st: i64 = 0
123 while i < n { if f == idx { st = i; break } if line[i] == (PQH_PIPE as u8) { f = f + 1 } i = i + 1 }
124 if f != idx { return 0 - 1 }
125 var e: i64 = st
126 while e < n { if line[e] == (PQH_PIPE as u8) { break } if line[e] == (PQH_NL as u8) { break } e = e + 1 }
127 var o: i64 = 0
128 while st < e { out[o] = line[st]; o = o + 1; st = st + 1 }
129 out[o] = 0 as u8
130 return o
131}
132// ---- HMAC-SHA256 + HKDF-Expand-Label (canonical sha256) -----------------------------------------
133func pqh_hmac(key: *u8, klen: i64, msg: *u8, mlen: i64, out: *u8) -> i64 {
134 let k0: *u8 = sys_mmap(PQH_BLOCK)
135 var i: i64 = 0
136 if klen > PQH_BLOCK {
137 sha256_digest(key, klen, k0)
138 i = PQH_KEY
139 while i < PQH_BLOCK { k0[i] = 0 as u8; i = i + 1 }
140 } else {
141 i = 0
142 while i < klen { k0[i] = key[i]; i = i + 1 }
143 while i < PQH_BLOCK { k0[i] = 0 as u8; i = i + 1 }
144 }
145 let inner: *u8 = sys_mmap(PQH_BLOCK + mlen + 16)
146 i = 0
147 while i < PQH_BLOCK { inner[i] = ((k0[i] as i64) ^ PQH_IPAD) as u8; i = i + 1 }
148 var j: i64 = 0
149 while j < mlen { inner[PQH_BLOCK + j] = msg[j]; j = j + 1 }
150 let ih: *u8 = sys_mmap(PQH_KEY)
151 sha256_digest(inner, PQH_BLOCK + mlen, ih)
152 let outer: *u8 = sys_mmap(PQH_BLOCK + PQH_KEY + 16)
153 i = 0
154 while i < PQH_BLOCK { outer[i] = ((k0[i] as i64) ^ PQH_OPAD) as u8; i = i + 1 }
155 j = 0
156 while j < PQH_KEY { outer[PQH_BLOCK + j] = ih[j]; j = j + 1 }
157 sha256_digest(outer, PQH_BLOCK + PQH_KEY, out)
158 return 0
159}
160func pqh_derive(secret: *u8, slen: i64, label: *u8, out: *u8) -> i64 {
161 let info: *u8 = sys_mmap(256)
162 var o: i64 = pqh_cat(info, 0, "MLS 1.0 pqh " as *u8)
163 o = pqh_cat(info, o, label)
164 info[o] = 1 as u8
165 o = o + 1
166 pqh_hmac(secret, slen, info, o, out)
167 return 0
168}
169// THE CONTRACT SYMBOL: establish the hybrid wrap key from a 64-byte X25519MLKEM768 shared secret,
170// optionally damaging one half to model a single-family break. corrupt: 0=none 1=pq-half 2=classical.
171func pqh_kex(hybrid_ss: *u8, corrupt: i64, wrapkey: *u8) -> i64 {
172 let s: *u8 = sys_mmap(PQH_SS)
173 var i: i64 = 0
174 while i < PQH_SS { s[i] = hybrid_ss[i]; i = i + 1 }
175 // layout: mlkem_ss(32) || x_ss(32)
176 if corrupt == 1 { s[0] = ((s[0] as i64) ^ 255) as u8 } // damage the lattice half
177 if corrupt == 2 { s[PQH_KEY] = ((s[PQH_KEY] as i64) ^ 255) as u8 } // damage the classical half
178 pqh_derive(s, PQH_SS, "wrap" as *u8, wrapkey)
179 return 0
180}
181// ==== VERBS =====================================================================================
182func pqh_keygen(share_out: *u8, secret_out: *u8, rand96_hex: *u8) -> i64 {
183 let rand: *u8 = sys_mmap(PQH_RAND96)
184 if pqh_hexd(rand96_hex, pqh_len(rand96_hex), rand) != PQH_RAND96 { pqh_w("PQH-REFUSED bad-hex rand96 (need 192 hex chars)\n" as *u8) return PQH_EXIT_REFUSED }
185 let share: *u8 = sys_mmap(PQH_SHARE)
186 let secret: *u8 = sys_mmap(PQH_SECRET)
187 x25519mlkem768_client_keygen(rand, share, secret)
188 let sh: *u8 = sys_mmap(PQH_SHARE * 2 + 4)
189 pqh_hexe(share, PQH_SHARE, sh)
190 if pqh_write(share_out, sh, PQH_SHARE * 2) != 0 { pqh_w("PQH-RED share-write-failed\n" as *u8) return PQH_EXIT_CORRUPT }
191 let kh: *u8 = sys_mmap(PQH_SECRET * 2 + 4)
192 pqh_hexe(secret, PQH_SECRET, kh)
193 if pqh_write(secret_out, kh, PQH_SECRET * 2) != 0 { pqh_w("PQH-RED secret-write-failed\n" as *u8) return PQH_EXIT_CORRUPT }
194 pqh_w("PQH-KEYGEN-OK share=" as *u8)
195 pqh_w(share_out)
196 pqh_w(" secret=" as *u8)
197 pqh_w(secret_out)
198 pqh_w(" (X25519MLKEM768: ek1184+x25519pub32 public, dk2400+x25519priv32 secret)\n" as *u8)
199 return PQH_EXIT_OK
200}
201// wrap <member_share_file> <rand64_hex> <payload_hex(32B)> <wrap_out>
202func pqh_wrap(share_file: *u8, rand64_hex: *u8, payload_hex: *u8, wrap_out: *u8) -> i64 {
203 let sh: *u8 = sys_mmap(PQH_FILECAP)
204 let shn: i64 = pqh_read(share_file, sh, PQH_FILECAP)
205 if shn < PQH_SHARE * 2 { pqh_w("PQH-REFUSED share-file-short\n" as *u8) return PQH_EXIT_REFUSED }
206 let share: *u8 = sys_mmap(PQH_SHARE)
207 if pqh_hexd(sh, PQH_SHARE * 2, share) != PQH_SHARE { pqh_w("PQH-REFUSED bad-share-hex\n" as *u8) return PQH_EXIT_REFUSED }
208 let rand: *u8 = sys_mmap(PQH_RAND64)
209 if pqh_hexd(rand64_hex, pqh_len(rand64_hex), rand) != PQH_RAND64 { pqh_w("PQH-REFUSED bad-hex rand64 (need 128 hex chars)\n" as *u8) return PQH_EXIT_REFUSED }
210 let payload: *u8 = sys_mmap(PQH_KEY)
211 if pqh_hexd(payload_hex, pqh_len(payload_hex), payload) != PQH_KEY { pqh_w("PQH-REFUSED bad-payload (need 64 hex chars = 32 bytes)\n" as *u8) return PQH_EXIT_REFUSED }
212 let srv: *u8 = sys_mmap(PQH_SRVSHARE)
213 let hss: *u8 = sys_mmap(PQH_SS)
214 x25519mlkem768_server(share, rand, srv, hss)
215 let wk: *u8 = sys_mmap(PQH_KEY)
216 pqh_kex(hss, 0, wk)
217 let nz: *u8 = sys_mmap(PQH_NONCE)
218 var i: i64 = 0
219 while i < PQH_NONCE { nz[i] = 0 as u8; i = i + 1 } // safe: wk is unique per encapsulation
220 let ct: *u8 = sys_mmap(PQH_KEY)
221 chacha20_encrypt(wk, 0, nz, payload, PQH_KEY, ct)
222 let mac: *u8 = sys_mmap(PQH_MAC)
223 pqh_hmac(wk, PQH_KEY, ct, PQH_KEY, mac)
224 let wb: *u8 = sys_mmap(PQH_FILECAP)
225 var o: i64 = pqh_cat(wb, 0, "pqwrap|" as *u8)
226 let srvh: *u8 = sys_mmap(PQH_SRVSHARE * 2 + 4)
227 pqh_hexe(srv, PQH_SRVSHARE, srvh)
228 o = pqh_cat(wb, o, srvh)
229 wb[o] = PQH_PIPE as u8
230 o = o + 1
231 let cth: *u8 = sys_mmap(PQH_KEY * 2 + 4)
232 pqh_hexe(ct, PQH_KEY, cth)
233 o = pqh_cat(wb, o, cth)
234 wb[o] = PQH_PIPE as u8
235 o = o + 1
236 let mh: *u8 = sys_mmap(PQH_MAC * 2 + 4)
237 pqh_hexe(mac, PQH_MAC, mh)
238 o = pqh_cat(wb, o, mh)
239 wb[o] = PQH_NL as u8
240 o = o + 1
241 if pqh_write(wrap_out, wb, o) != 0 { pqh_w("PQH-RED wrap-write-failed\n" as *u8) return PQH_EXIT_CORRUPT }
242 pqh_w("PQH-WRAP-OK wrap=" as *u8)
243 pqh_w(wrap_out)
244 pqh_w(" (commit secret sealed under a hybrid X25519+ML-KEM-768 key)\n" as *u8)
245 return PQH_EXIT_OK
246}
247// unwrap <member_secret_file> <wrap_file> <corrupt: none|pq|classical>
248func pqh_unwrap(secret_file: *u8, wrap_file: *u8, corrupt_s: *u8) -> i64 {
249 var corrupt: i64 = 0
250 if pqh_eq(corrupt_s, "pq" as *u8) == 1 { corrupt = 1 }
251 if pqh_eq(corrupt_s, "classical" as *u8) == 1 { corrupt = 2 }
252 if pqh_eq(corrupt_s, "none" as *u8) == 0 { if corrupt == 0 { pqh_w("PQH-USAGE corrupt must be none|pq|classical\n" as *u8) return PQH_EXIT_USAGE } }
253 let kh: *u8 = sys_mmap(PQH_FILECAP)
254 let khn: i64 = pqh_read(secret_file, kh, PQH_FILECAP)
255 if khn < PQH_SECRET * 2 { pqh_w("PQH-REFUSED secret-file-short\n" as *u8) return PQH_EXIT_REFUSED }
256 let secret: *u8 = sys_mmap(PQH_SECRET)
257 if pqh_hexd(kh, PQH_SECRET * 2, secret) != PQH_SECRET { pqh_w("PQH-REFUSED bad-secret-hex\n" as *u8) return PQH_EXIT_REFUSED }
258 let wb: *u8 = sys_mmap(PQH_FILECAP)
259 let wn: i64 = pqh_read(wrap_file, wb, PQH_FILECAP)
260 if wn <= 0 { pqh_w("PQH-REFUSED wrap-absent\n" as *u8) return PQH_EXIT_REFUSED }
261 let srvh: *u8 = sys_mmap(PQH_FILECAP)
262 let cth: *u8 = sys_mmap(PQH_FILECAP)
263 let mh: *u8 = sys_mmap(PQH_FILECAP)
264 pqh_field(wb, wn, 1, srvh)
265 pqh_field(wb, wn, 2, cth)
266 pqh_field(wb, wn, 3, mh)
267 let srv: *u8 = sys_mmap(PQH_SRVSHARE)
268 if pqh_hexd(srvh, pqh_len(srvh), srv) != PQH_SRVSHARE { pqh_w("PQH-CORRUPT bad-srvshare-hex\n" as *u8) return PQH_EXIT_CORRUPT }
269 let hss: *u8 = sys_mmap(PQH_SS)
270 x25519mlkem768_client_finish(srv, secret, hss)
271 let wk: *u8 = sys_mmap(PQH_KEY)
272 pqh_kex(hss, corrupt, wk)
273 let ct: *u8 = sys_mmap(PQH_KEY)
274 if pqh_hexd(cth, pqh_len(cth), ct) != PQH_KEY { pqh_w("PQH-CORRUPT bad-ct-hex\n" as *u8) return PQH_EXIT_CORRUPT }
275 let mac: *u8 = sys_mmap(PQH_MAC)
276 pqh_hmac(wk, PQH_KEY, ct, PQH_KEY, mac)
277 let gotmac: *u8 = sys_mmap(PQH_MAC)
278 if pqh_hexd(mh, pqh_len(mh), gotmac) != PQH_MAC { pqh_w("PQH-CORRUPT bad-mac-hex\n" as *u8) return PQH_EXIT_CORRUPT }
279 if pqh_memeq(mac, gotmac, PQH_MAC) == 0 {
280 pqh_w("PQH-UNWRAP-DENIED mac-fail corrupt=" as *u8)
281 pqh_w(corrupt_s)
282 if corrupt == 1 { pqh_w(" -- the lattice (ML-KEM) half was wrong: knowing ONLY the X25519 shared secret is NOT enough. A classical (quantum-computer) break alone recovers nothing.\n" as *u8) }
283 if corrupt == 2 { pqh_w(" -- the classical (X25519) half was wrong: knowing ONLY the ML-KEM shared secret is NOT enough. A lattice break alone recovers nothing.\n" as *u8) }
284 if corrupt == 0 { pqh_w(" -- tamper or wrong key\n" as *u8) }
285 return PQH_EXIT_REFUSED
286 }
287 let pt: *u8 = sys_mmap(PQH_KEY)
288 let nz: *u8 = sys_mmap(PQH_NONCE)
289 var i: i64 = 0
290 while i < PQH_NONCE { nz[i] = 0 as u8; i = i + 1 }
291 chacha20_encrypt(wk, 0, nz, ct, PQH_KEY, pt)
292 let pth: *u8 = sys_mmap(PQH_KEY * 2 + 4)
293 pqh_hexe(pt, PQH_KEY, pth)
294 pqh_w("PQH-UNWRAP-OK payload=" as *u8)
295 pqh_w(pth)
296 pqh_w(" (recovered through BOTH X25519 and ML-KEM-768)\n" as *u8)
297 return PQH_EXIT_OK
298}
299func pqh_usage() -> i64 {
300 pqh_w("usage: nx_pq_hybrid keygen <share_out> <secret_out> <rand96_hex> | wrap <member_share_file> <rand64_hex> <payload_hex32> <wrap_out> | unwrap <member_secret_file> <wrap_file> <none|pq|classical>\n" as *u8)
301 return PQH_EXIT_USAGE
302}
303func main(argc: i64, argv: *i64) -> i64 {
304 if argc < 2 { let u: i64 = pqh_usage() sys_exit(u) return u }
305 let v: *u8 = argv[1] as *u8
306 if pqh_eq(v, "keygen" as *u8) == 1 { if argc < 5 { let u: i64 = pqh_usage() sys_exit(u) return u } let r: i64 = pqh_keygen(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8) sys_exit(r) return r }
307 if pqh_eq(v, "wrap" as *u8) == 1 { if argc < 6 { let u: i64 = pqh_usage() sys_exit(u) return u } let r: i64 = pqh_wrap(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8, argv[5] as *u8) sys_exit(r) return r }
308 if pqh_eq(v, "unwrap" as *u8) == 1 { if argc < 5 { let u: i64 = pqh_usage() sys_exit(u) return u } let r: i64 = pqh_unwrap(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8) sys_exit(r) return r }
309 let u2: i64 = pqh_usage()
310 sys_exit(u2)
311 return u2
312}