code wiki / (root) / nx_kyber_sample_ntt_wasm.nx

nx_kyber_sample_ntt_wasm.nx source

↩ module page · 74 lines · 2658 B

1// nx_kyber_sample_ntt_wasm.nx -- FIPS 203 §4.2.1 SampleNTT (rejection sampling). 2// 3// Turns a SHAKE128 byte stream into a uniformly-random NTT-domain 4// polynomial: 256 coefficients in Z_3329. The matrix A used in 5// ML-KEM-768 K-PKE keygen / encrypt is built from 9 such polynomials 6// (k * k = 9, k=3 for ML-KEM-768), one per (i, j) pair seeded by the 7// (rho || j || i) byte string SHAKE128'd into bytes. 8// 9// Algorithm (FIPS 203 §4.2.1 Algorithm 4): 10// Read 3 bytes at a time from the SHAKE stream. Split into two 12-bit 11// values d1 = b0 | ((b1 & 0xf) << 8), d2 = (b1 >> 4) | (b2 << 4). 12// If d < q, accept as next coefficient. Otherwise discard and continue. 13// Stop when 256 coefficients have been accepted. 14// 15// Browser-side glue: 16// 1. SHAKE128.absorb(rho || j_byte || i_byte) 17// 2. SHAKE128.squeeze(buf, buf_len) -- ~600 bytes usually enough 18// 3. nx_kyber_sample_ntt(poly_out, buf, buf_len) -> n_consumed (>=0) 19// OR -1 if buffer too short (caller should squeeze more + retry) 20// 21// API: 22// nx_kyber_sample_ntt(poly_out, buf, buf_len) -> i64 23// returns number of bytes consumed on success, -1 if buffer 24// too short to reach 256 accepted coefficients. 25// 26// Verified: KAT against PQClean reference (a known SHAKE stream 27// produces a known polynomial). 28// 29// license_tier: INDEPENDENT_REDERIVE 30// genealogy_id: international-research-sources/nist/fips_203 31// lineage_id: nishi_kyber_sample_ntt_wasm_q1 32// safe_shift_audit: no 64-bit rotations in this module 33 34const KYBER_Q: i64 = 3329 35const KYBER_N: i64 = 256 36 37func _pstore(p: *u8, i: i64, v: i64) -> i64 { 38 var vv: i64 = v 39 if vv < 0 { vv = vv + 65536 } 40 p[i * 2] = vv & 0xff 41 p[i * 2 + 1] = (vv >> 8) & 0xff 42 return 0 43} 44 45// SampleNTT: parse 3 bytes at a time, accept candidates < q. 46// Returns bytes consumed (>=3*128 = 384 minimum) on success, -1 if 47// buf_len exhausted before 256 acceptances. 48func nx_kyber_sample_ntt(poly: *u8, buf: *u8, buf_len: i64) -> i64 { 49 var i: i64 = 0 // accepted coefficient index (0..256) 50 var pos: i64 = 0 // byte cursor into buf 51 52 while i < KYBER_N { 53 if pos + 3 > buf_len { return -1 } 54 let b0: i64 = buf[pos] 55 let b1: i64 = buf[pos + 1] 56 let b2: i64 = buf[pos + 2] 57 pos = pos + 3 58 59 let d1: i64 = b0 | ((b1 & 0x0f) << 8) 60 let d2: i64 = (b1 >> 4) | (b2 << 4) 61 62 if d1 < KYBER_Q { 63 _pstore(poly, i, d1) 64 i = i + 1 65 } 66 if i < KYBER_N { 67 if d2 < KYBER_Q { 68 _pstore(poly, i, d2) 69 i = i + 1 70 } 71 } 72 } 73 return pos 74}