code wiki / (root) / nx_pq_hybrid.nx

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}