code wiki / (root) / nx_ml_dsa_65_wasm.nx

nx_ml_dsa_65_wasm.nx source

↩ module page · 2340 lines · 103017 B

1// nx_ml_dsa_65_wasm.nx -- Sovereign single-WASM FIPS 204 ML-DSA-65 (foundation). 2// 3// L131 iteration 1: math foundation only. Ships: 4// * Constants + i32 polynomial helpers (256 i32 LE = 1024 B per poly) 5// * Keccak-f[1600] + SHA-3-256 + SHAKE256 sponges (inlined verbatim from L126) 6// * Montgomery + Barrett reductions for Z_8380417 7// * Full ML-DSA NTT + INVNTT with 256-entry zeta table (Montgomery form, 8// primitive 512th root zeta=1753 mod q, bit-reversed indexed) 9// * Pointwise basemul + montgomery acc 10// * KAT export: nx_mldsa_ntt_round_trip_test(poly_ptr) returns 0 if 11// INVNTT(NTT(poly)) ≡ original (mod q, accounting for the 12// Montgomery factor R=2^32 that invntt_tomont leaves behind) 13// 14// Next iterations layer on: 15// L132: sampling (rej_uniform for matrix A, rej_eta for s1/s2, sample_in_ball) 16// L133: power-of-2 decompose + make_hint / use_hint 17// L134: keygen + tobytes/frombytes for pk/sk 18// L135: sign with rejection loop 19// L136: verify 20// L137: end-to-end round-trip self-test 21// 22// Final public API (after L137): 23// nx_mldsa_keygen(seed_32, scratch, pk_out, sk_out) 24// nx_mldsa_sign(sk, msg, msg_len, scratch, sig_out, sig_len_out) 25// nx_mldsa_verify(pk, msg, msg_len, sig, sig_len, scratch) -> i64 1/0 26// 27// Pillar 4 alignment: uses the same scratch-layout annotation pattern as 28// L126 (F-meta-8 grader-compatible). 29// 30// license_tier: INDEPENDENT_REDERIVE 31// genealogy_id: international-research-sources/nist/fips_204 32// lineage_id: nishi_ml_dsa_65_wasm_q1 33// safe_shift_audit: shared Keccak _rotl64 uses gold-standard mask 34 35// ============================================================================ 36// SECTION 0: ML-DSA-65 parameters (FIPS 204 §4 Table 1) 37// ============================================================================ 38 39const MLDSA_Q: i64 = 8380417 40const MLDSA_QINV: i64 = 58728449 // q^{-1} mod 2^32 (unsigned) 41const MLDSA_N: i64 = 256 42const MLDSA_K: i64 = 6 // matrix rows 43const MLDSA_L: i64 = 5 // matrix cols 44const MLDSA_ETA: i64 = 4 45const MLDSA_TAU: i64 = 49 46const MLDSA_BETA: i64 = 196 // tau * eta 47const MLDSA_GAMMA1: i64 = 524288 // 2^19 48const MLDSA_GAMMA2: i64 = 261888 // (q-1)/32 49const MLDSA_OMEGA: i64 = 55 50const MLDSA_D: i64 = 13 51 52const MLDSA_PK_BYTES: i64 = 1952 53const MLDSA_SK_BYTES: i64 = 4032 54const MLDSA_SIG_BYTES: i64 = 3309 55 56// Polynomial in-memory size (256 i32 LE) 57const POLY_BUF: i64 = 1024 58 59// ============================================================================ 60// SECTION 1: i32 polynomial I/O + reductions 61// ============================================================================ 62 63func _i32_load_le(p: *u8, i: i64) -> i64 { 64 let b0: i64 = p[i * 4] 65 let b1: i64 = p[i * 4 + 1] 66 let b2: i64 = p[i * 4 + 2] 67 let b3: i64 = p[i * 4 + 3] 68 let raw: i64 = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24) 69 if raw >= 0x80000000 { return raw - 0x100000000 } 70 return raw 71} 72 73func _i32_store_le(p: *u8, i: i64, v: i64) -> i64 { 74 var vv: i64 = v 75 if vv < 0 { vv = vv + 0x100000000 } 76 p[i * 4] = vv & 0xff 77 p[i * 4 + 1] = (vv >> 8) & 0xff 78 p[i * 4 + 2] = (vv >> 16) & 0xff 79 p[i * 4 + 3] = (vv >> 24) & 0xff 80 return 0 81} 82 83// Montgomery reduction for q = 8380417, R = 2^32. 84// input: a in (-q * 2^31, q * 2^31) 85// output: a * R^{-1} mod q in (-q, q) 86// Per FIPS 204 / PQClean: 87// t = (a * QINV) mod 2^32 // int32_t truncation 88// return (a - t * q) >> 32 89func _mont_reduce(a: i64) -> i64 { 90 var t: i64 = (a * MLDSA_QINV) & 0xffffffff 91 if t >= 0x80000000 { t = t - 0x100000000 } 92 return (a - t * MLDSA_Q) >> 32 93} 94 95// Barrett-style reduce32 per PQClean: input any i32, output in (-(q-1)/2, q/2]. 96func _reduce32(a: i64) -> i64 { 97 let t: i64 = (a + (1 << 22)) >> 23 98 return a - t * MLDSA_Q 99} 100 101// Modular multiplication via Montgomery: result = a*b*R^{-1} mod q. 102func _fqmul(a: i64, b: i64) -> i64 { return _mont_reduce(a * b) } 103 104// 64-bit rotate left (Keccak helper). 105func _rotl64(x: i64, n: i64) -> i64 { 106 let nn: i64 = n & 63 107 if nn == 0 { return x } 108 let shr_amt: i64 = 64 - nn 109 let mask: i64 = (1 << nn) - 1 110 return ((x << nn) | ((x >> shr_amt) & mask)) & 0xffffffffffffffff 111} 112 113// ============================================================================ 114// SECTION 2: Keccak-f[1600] + sponges (verbatim from L126) 115// ============================================================================ 116 117func _lane_load(p: *u8) -> i64 { 118 let b0: i64 = p[0]; let b1: i64 = p[1]; let b2: i64 = p[2]; let b3: i64 = p[3] 119 let b4: i64 = p[4]; let b5: i64 = p[5]; let b6: i64 = p[6]; let b7: i64 = p[7] 120 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24) | (b4 << 32) | (b5 << 40) | (b6 << 48) | (b7 << 56) 121} 122func _lane_store(p: *u8, v: i64) -> i64 { 123 p[0] = v & 0xff; p[1] = (v >> 8) & 0xff; p[2] = (v >> 16) & 0xff; p[3] = (v >> 24) & 0xff 124 p[4] = (v >> 32) & 0xff; p[5] = (v >> 40) & 0xff; p[6] = (v >> 48) & 0xff; p[7] = (v >> 56) & 0xff 125 return 0 126} 127func _keccak_rc(i: i64) -> i64 { 128 if i == 0 { return 0x0000000000000001 } if i == 1 { return 0x0000000000008082 } 129 if i == 2 { return 0x800000000000808a } if i == 3 { return 0x8000000080008000 } 130 if i == 4 { return 0x000000000000808b } if i == 5 { return 0x0000000080000001 } 131 if i == 6 { return 0x8000000080008081 } if i == 7 { return 0x8000000000008009 } 132 if i == 8 { return 0x000000000000008a } if i == 9 { return 0x0000000000000088 } 133 if i == 10 { return 0x0000000080008009 } if i == 11 { return 0x000000008000000a } 134 if i == 12 { return 0x000000008000808b } if i == 13 { return 0x800000000000008b } 135 if i == 14 { return 0x8000000000008089 } if i == 15 { return 0x8000000000008003 } 136 if i == 16 { return 0x8000000000008002 } if i == 17 { return 0x8000000000000080 } 137 if i == 18 { return 0x000000000000800a } if i == 19 { return 0x800000008000000a } 138 if i == 20 { return 0x8000000080008081 } if i == 21 { return 0x8000000000008080 } 139 if i == 22 { return 0x0000000080000001 } 140 return 0x8000000080008008 141} 142func _rho_off(lane_idx: i64) -> i64 { 143 if lane_idx == 0 { return 0 } if lane_idx == 1 { return 1 } 144 if lane_idx == 2 { return 62 } if lane_idx == 3 { return 28 } 145 if lane_idx == 4 { return 27 } if lane_idx == 5 { return 36 } 146 if lane_idx == 6 { return 44 } if lane_idx == 7 { return 6 } 147 if lane_idx == 8 { return 55 } if lane_idx == 9 { return 20 } 148 if lane_idx == 10 { return 3 } if lane_idx == 11 { return 10 } 149 if lane_idx == 12 { return 43 } if lane_idx == 13 { return 25 } 150 if lane_idx == 14 { return 39 } if lane_idx == 15 { return 41 } 151 if lane_idx == 16 { return 45 } if lane_idx == 17 { return 15 } 152 if lane_idx == 18 { return 21 } if lane_idx == 19 { return 8 } 153 if lane_idx == 20 { return 18 } if lane_idx == 21 { return 2 } 154 if lane_idx == 22 { return 61 } if lane_idx == 23 { return 56 } 155 return 14 156} 157func _keccak_f1600(state_ptr: *u8) -> i64 { 158 let A: *u8 = state_ptr 159 let C: *u8 = (state_ptr as i64 + 200) as *u8 160 let B: *u8 = (state_ptr as i64 + 240) as *u8 161 var round: i64 = 0 162 while round < 24 { 163 var x: i64 = 0 164 while x < 5 { 165 let c0: i64 = _lane_load((A as i64 + 8 * (x + 0)) as *u8) 166 let c1: i64 = _lane_load((A as i64 + 8 * (x + 5)) as *u8) 167 let c2: i64 = _lane_load((A as i64 + 8 * (x + 10)) as *u8) 168 let c3: i64 = _lane_load((A as i64 + 8 * (x + 15)) as *u8) 169 let c4: i64 = _lane_load((A as i64 + 8 * (x + 20)) as *u8) 170 _lane_store((C as i64 + 8 * x) as *u8, c0 ^ c1 ^ c2 ^ c3 ^ c4) 171 x = x + 1 172 } 173 var x2: i64 = 0 174 while x2 < 5 { 175 let xm: i64 = (x2 + 4) % 5; let xp: i64 = (x2 + 1) % 5 176 let cl: i64 = _lane_load((C as i64 + 8 * xm) as *u8) 177 let cr: i64 = _lane_load((C as i64 + 8 * xp) as *u8) 178 let d: i64 = cl ^ _rotl64(cr, 1) 179 var y: i64 = 0 180 while y < 5 { 181 let off: i64 = 8 * (x2 + 5 * y) 182 let v: i64 = _lane_load((A as i64 + off) as *u8) ^ d 183 _lane_store((A as i64 + off) as *u8, v) 184 y = y + 1 185 } 186 x2 = x2 + 1 187 } 188 var y3: i64 = 0 189 while y3 < 5 { 190 var x3: i64 = 0 191 while x3 < 5 { 192 let src_idx: i64 = x3 + 5 * y3 193 let rot: i64 = _rho_off(src_idx) 194 let lane: i64 = _lane_load((A as i64 + 8 * src_idx) as *u8) 195 let rotated: i64 = _rotl64(lane, rot) 196 let new_x: i64 = y3 197 let new_y: i64 = (2 * x3 + 3 * y3) % 5 198 let dst_idx: i64 = new_x + 5 * new_y 199 _lane_store((B as i64 + 8 * dst_idx) as *u8, rotated) 200 x3 = x3 + 1 201 } 202 y3 = y3 + 1 203 } 204 var y4: i64 = 0 205 while y4 < 5 { 206 var x4: i64 = 0 207 while x4 < 5 { 208 let xp1: i64 = (x4 + 1) % 5; let xp2: i64 = (x4 + 2) % 5 209 let b0: i64 = _lane_load((B as i64 + 8 * (x4 + 5 * y4)) as *u8) 210 let b1: i64 = _lane_load((B as i64 + 8 * (xp1 + 5 * y4)) as *u8) 211 let b2: i64 = _lane_load((B as i64 + 8 * (xp2 + 5 * y4)) as *u8) 212 let nb1: i64 = (~b1) & 0xffffffffffffffff 213 let v: i64 = b0 ^ (nb1 & b2) 214 _lane_store((A as i64 + 8 * (x4 + 5 * y4)) as *u8, v) 215 x4 = x4 + 1 216 } 217 y4 = y4 + 1 218 } 219 let a00: i64 = _lane_load(A) ^ _keccak_rc(round) 220 _lane_store(A, a00) 221 round = round + 1 222 } 223 return 0 224} 225// Generic sponge: rate, dom_byte, out_len. state_ptr needs 440 bytes. 226func _sponge_one_shot(msg: *u8, msg_len: i64, rate: i64, dom_byte: i64, 227 state: *u8, out: *u8, out_len: i64) -> i64 { 228 var i: i64 = 0 229 while i < 200 { state[i] = 0; i = i + 1 } 230 var pos: i64 = 0 231 while pos + rate <= msg_len { 232 var b: i64 = 0 233 while b < rate { state[b] = (state[b] ^ msg[pos + b]) & 0xff; b = b + 1 } 234 _keccak_f1600(state) 235 pos = pos + rate 236 } 237 let tail: i64 = msg_len - pos 238 var t: i64 = 0 239 while t < tail { state[t] = (state[t] ^ msg[pos + t]) & 0xff; t = t + 1 } 240 state[tail] = (state[tail] ^ dom_byte) & 0xff 241 state[rate - 1] = (state[rate - 1] ^ 0x80) & 0xff 242 _keccak_f1600(state) 243 var written: i64 = 0 244 while written < out_len { 245 let remaining: i64 = out_len - written 246 var take: i64 = rate 247 if remaining < rate { take = remaining } 248 var k: i64 = 0 249 while k < take { out[written + k] = state[k]; k = k + 1 } 250 written = written + take 251 if written < out_len { _keccak_f1600(state) } 252 } 253 return 0 254} 255func _sha3_256(msg: *u8, msg_len: i64, state: *u8, out: *u8) -> i64 { 256 return _sponge_one_shot(msg, msg_len, 136, 0x06, state, out, 32) 257} 258func _shake256(msg: *u8, msg_len: i64, state: *u8, out: *u8, out_len: i64) -> i64 { 259 return _sponge_one_shot(msg, msg_len, 136, 0x1f, state, out, out_len) 260} 261func _shake128(msg: *u8, msg_len: i64, state: *u8, out: *u8, out_len: i64) -> i64 { 262 return _sponge_one_shot(msg, msg_len, 168, 0x1f, state, out, out_len) 263} 264 265// ============================================================================ 266// SECTION 3: ML-DSA zetas table (Montgomery form, 256 entries) 267// ============================================================================ 268// Generated from primitive 512th root of unity ζ=1753 mod q=8380417: 269// zetas[i] = (ζ^bitrev8(i) * R) mod q signed to (-q/2, q/2] 270// where R = 2^32 mod q = 4193792. 271func _zeta(i: i64) -> i64 { 272 if i == 0 { return -4186625 } if i == 1 { return 25847 } if i == 2 { return -2608894 } if i == 3 { return -518909 } 273 if i == 4 { return 237124 } if i == 5 { return -777960 } if i == 6 { return -876248 } if i == 7 { return 466468 } 274 if i == 8 { return 1826347 } if i == 9 { return 2353451 } if i == 10 { return -359251 } if i == 11 { return -2091905 } 275 if i == 12 { return 3119733 } if i == 13 { return -2884855 } if i == 14 { return 3111497 } if i == 15 { return 2680103 } 276 if i == 16 { return 2725464 } if i == 17 { return 1024112 } if i == 18 { return -1079900 } if i == 19 { return 3585928 } 277 if i == 20 { return -549488 } if i == 21 { return -1119584 } if i == 22 { return 2619752 } if i == 23 { return -2108549 } 278 if i == 24 { return -2118186 } if i == 25 { return -3859737 } if i == 26 { return -1399561 } if i == 27 { return -3277672 } 279 if i == 28 { return 1757237 } if i == 29 { return -19422 } if i == 30 { return 4010497 } if i == 31 { return 280005 } 280 if i == 32 { return 2706023 } if i == 33 { return 95776 } if i == 34 { return 3077325 } if i == 35 { return 3530437 } 281 if i == 36 { return -1661693 } if i == 37 { return -3592148 } if i == 38 { return -2537516 } if i == 39 { return 3915439 } 282 if i == 40 { return -3861115 } if i == 41 { return -3043716 } if i == 42 { return 3574422 } if i == 43 { return -2867647 } 283 if i == 44 { return 3539968 } if i == 45 { return -300467 } if i == 46 { return 2348700 } if i == 47 { return -539299 } 284 if i == 48 { return -1699267 } if i == 49 { return -1643818 } if i == 50 { return 3505694 } if i == 51 { return -3821735 } 285 if i == 52 { return 3507263 } if i == 53 { return -2140649 } if i == 54 { return -1600420 } if i == 55 { return 3699596 } 286 if i == 56 { return 811944 } if i == 57 { return 531354 } if i == 58 { return 954230 } if i == 59 { return 3881043 } 287 if i == 60 { return 3900724 } if i == 61 { return -2556880 } if i == 62 { return 2071892 } if i == 63 { return -2797779 } 288 if i == 64 { return -3930395 } if i == 65 { return -1528703 } if i == 66 { return -3677745 } if i == 67 { return -3041255 } 289 if i == 68 { return -1452451 } if i == 69 { return 3475950 } if i == 70 { return 2176455 } if i == 71 { return -1585221 } 290 if i == 72 { return -1257611 } if i == 73 { return 1939314 } if i == 74 { return -4083598 } if i == 75 { return -1000202 } 291 if i == 76 { return -3190144 } if i == 77 { return -3157330 } if i == 78 { return -3632928 } if i == 79 { return 126922 } 292 if i == 80 { return 3412210 } if i == 81 { return -983419 } if i == 82 { return 2147896 } if i == 83 { return 2715295 } 293 if i == 84 { return -2967645 } if i == 85 { return -3693493 } if i == 86 { return -411027 } if i == 87 { return -2477047 } 294 if i == 88 { return -671102 } if i == 89 { return -1228525 } if i == 90 { return -22981 } if i == 91 { return -1308169 } 295 if i == 92 { return -381987 } if i == 93 { return 1349076 } if i == 94 { return 1852771 } if i == 95 { return -1430430 } 296 if i == 96 { return -3343383 } if i == 97 { return 264944 } if i == 98 { return 508951 } if i == 99 { return 3097992 } 297 if i == 100 { return 44288 } if i == 101 { return -1100098 } if i == 102 { return 904516 } if i == 103 { return 3958618 } 298 if i == 104 { return -3724342 } if i == 105 { return -8578 } if i == 106 { return 1653064 } if i == 107 { return -3249728 } 299 if i == 108 { return 2389356 } if i == 109 { return -210977 } if i == 110 { return 759969 } if i == 111 { return -1316856 } 300 if i == 112 { return 189548 } if i == 113 { return -3553272 } if i == 114 { return 3159746 } if i == 115 { return -1851402 } 301 if i == 116 { return -2409325 } if i == 117 { return -177440 } if i == 118 { return 1315589 } if i == 119 { return 1341330 } 302 if i == 120 { return 1285669 } if i == 121 { return -1584928 } if i == 122 { return -812732 } if i == 123 { return -1439742 } 303 if i == 124 { return -3019102 } if i == 125 { return -3881060 } if i == 126 { return -3628969 } if i == 127 { return 3839961 } 304 if i == 128 { return 2091667 } if i == 129 { return 3407706 } if i == 130 { return 2316500 } if i == 131 { return 3817976 } 305 if i == 132 { return -3342478 } if i == 133 { return 2244091 } if i == 134 { return -2446433 } if i == 135 { return -3562462 } 306 if i == 136 { return 266997 } if i == 137 { return 2434439 } if i == 138 { return -1235728 } if i == 139 { return 3513181 } 307 if i == 140 { return -3520352 } if i == 141 { return -3759364 } if i == 142 { return -1197226 } if i == 143 { return -3193378 } 308 if i == 144 { return 900702 } if i == 145 { return 1859098 } if i == 146 { return 909542 } if i == 147 { return 819034 } 309 if i == 148 { return 495491 } if i == 149 { return -1613174 } if i == 150 { return -43260 } if i == 151 { return -522500 } 310 if i == 152 { return -655327 } if i == 153 { return -3122442 } if i == 154 { return 2031748 } if i == 155 { return 3207046 } 311 if i == 156 { return -3556995 } if i == 157 { return -525098 } if i == 158 { return -768622 } if i == 159 { return -3595838 } 312 if i == 160 { return 342297 } if i == 161 { return 286988 } if i == 162 { return -2437823 } if i == 163 { return 4108315 } 313 if i == 164 { return 3437287 } if i == 165 { return -3342277 } if i == 166 { return 1735879 } if i == 167 { return 203044 } 314 if i == 168 { return 2842341 } if i == 169 { return 2691481 } if i == 170 { return -2590150 } if i == 171 { return 1265009 } 315 if i == 172 { return 4055324 } if i == 173 { return 1247620 } if i == 174 { return 2486353 } if i == 175 { return 1595974 } 316 if i == 176 { return -3767016 } if i == 177 { return 1250494 } if i == 178 { return 2635921 } if i == 179 { return -3548272 } 317 if i == 180 { return -2994039 } if i == 181 { return 1869119 } if i == 182 { return 1903435 } if i == 183 { return -1050970 } 318 if i == 184 { return -1333058 } if i == 185 { return 1237275 } if i == 186 { return -3318210 } if i == 187 { return -1430225 } 319 if i == 188 { return -451100 } if i == 189 { return 1312455 } if i == 190 { return 3306115 } if i == 191 { return -1962642 } 320 if i == 192 { return -1279661 } if i == 193 { return 1917081 } if i == 194 { return -2546312 } if i == 195 { return -1374803 } 321 if i == 196 { return 1500165 } if i == 197 { return 777191 } if i == 198 { return 2235880 } if i == 199 { return 3406031 } 322 if i == 200 { return -542412 } if i == 201 { return -2831860 } if i == 202 { return -1671176 } if i == 203 { return -1846953 } 323 if i == 204 { return -2584293 } if i == 205 { return -3724270 } if i == 206 { return 594136 } if i == 207 { return -3776993 } 324 if i == 208 { return -2013608 } if i == 209 { return 2432395 } if i == 210 { return 2454455 } if i == 211 { return -164721 } 325 if i == 212 { return 1957272 } if i == 213 { return 3369112 } if i == 214 { return 185531 } if i == 215 { return -1207385 } 326 if i == 216 { return -3183426 } if i == 217 { return 162844 } if i == 218 { return 1616392 } if i == 219 { return 3014001 } 327 if i == 220 { return 810149 } if i == 221 { return 1652634 } if i == 222 { return -3694233 } if i == 223 { return -1799107 } 328 if i == 224 { return -3038916 } if i == 225 { return 3523897 } if i == 226 { return 3866901 } if i == 227 { return 269760 } 329 if i == 228 { return 2213111 } if i == 229 { return -975884 } if i == 230 { return 1717735 } if i == 231 { return 472078 } 330 if i == 232 { return -426683 } if i == 233 { return 1723600 } if i == 234 { return -1803090 } if i == 235 { return 1910376 } 331 if i == 236 { return -1667432 } if i == 237 { return -1104333 } if i == 238 { return -260646 } if i == 239 { return -3833893 } 332 if i == 240 { return -2939036 } if i == 241 { return -2235985 } if i == 242 { return -420899 } if i == 243 { return -2286327 } 333 if i == 244 { return 183443 } if i == 245 { return -976891 } if i == 246 { return 1612842 } if i == 247 { return -3545687 } 334 if i == 248 { return -554416 } if i == 249 { return 3919660 } if i == 250 { return -48306 } if i == 251 { return -1362209 } 335 if i == 252 { return 3937738 } if i == 253 { return 1400424 } if i == 254 { return -846154 } if i == 255 { return 1976782 } 336} 337 338// ============================================================================ 339// SECTION 4: NTT / INVNTT in Z_8380417 340// ============================================================================ 341// Per FIPS 204 / PQClean Dilithium: 342// ntt walks len = 128, 64, 32, ... 1; k starts at 0, ++k before each zeta read 343// so zetas[1..255] are used (zetas[0] is the unused sentinel). 344 345func nx_mldsa_ntt(poly: *u8) -> i64 { 346 var k: i64 = 0 347 var len: i64 = 128 348 while len > 0 { 349 var start: i64 = 0 350 while start < MLDSA_N { 351 k = k + 1 352 let zeta: i64 = _zeta(k) 353 var j: i64 = start 354 while j < start + len { 355 let t: i64 = _fqmul(zeta, _i32_load_le(poly, j + len)) 356 let aj: i64 = _i32_load_le(poly, j) 357 _i32_store_le(poly, j + len, aj - t) 358 _i32_store_le(poly, j, aj + t) 359 j = j + 1 360 } 361 start = j + len 362 } 363 len = len >> 1 364 } 365 return 0 366} 367 368// INVNTT: walks zetas backward, final scale by f = mont^2 * 256^{-1} mod q. 369// Per PQClean dilithium ntt.c invntt: 370// f = 41978 (= mont^2 / 256 mod q signed) 371func nx_mldsa_invntt(poly: *u8) -> i64 { 372 let f: i64 = 41978 373 var k: i64 = 256 374 var len: i64 = 1 375 while len < MLDSA_N { 376 var start: i64 = 0 377 while start < MLDSA_N { 378 k = k - 1 379 let zeta: i64 = -_zeta(k) 380 var j: i64 = start 381 while j < start + len { 382 let aj: i64 = _i32_load_le(poly, j) 383 let ajl: i64 = _i32_load_le(poly, j + len) 384 _i32_store_le(poly, j, aj + ajl) 385 _i32_store_le(poly, j + len, _fqmul(zeta, aj - ajl)) 386 j = j + 1 387 } 388 start = j + len 389 } 390 len = len << 1 391 } 392 // Final scale 393 var i: i64 = 0 394 while i < MLDSA_N { 395 let v: i64 = _i32_load_le(poly, i) 396 _i32_store_le(poly, i, _fqmul(v, f)) 397 i = i + 1 398 } 399 return 0 400} 401 402// Pointwise multiplication in NTT domain. 403func nx_mldsa_basemul(out: *u8, a: *u8, b: *u8) -> i64 { 404 var i: i64 = 0 405 while i < MLDSA_N { 406 let r: i64 = _fqmul(_i32_load_le(a, i), _i32_load_le(b, i)) 407 _i32_store_le(out, i, r) 408 i = i + 1 409 } 410 return 0 411} 412 413// ============================================================================ 414// SECTION 4.5: Sampling primitives (L132) 415// ============================================================================ 416 417// rej_uniform: parse SHAKE128 byte stream into uniform [0, q) i32 coefs. 418// Per FIPS 204 §4.2 / PQClean Dilithium rej_uniform: 419// read 3 bytes -> t = b0 | (b1<<8) | ((b2 & 0x7F)<<16) // 23-bit value 420// accept if t < q 421// continue until 256 coefs accepted 422// Returns bytes consumed on success, -1 if buf exhausted. 423func nx_mldsa_rej_uniform(poly: *u8, buf: *u8, buf_len: i64) -> i64 { 424 var i: i64 = 0 425 var pos: i64 = 0 426 while i < MLDSA_N { 427 if pos + 3 > buf_len { return -1 } 428 let b0: i64 = buf[pos] 429 let b1: i64 = buf[pos + 1] 430 let b2: i64 = buf[pos + 2] & 0x7f // mask top bit 431 pos = pos + 3 432 let t: i64 = b0 | (b1 << 8) | (b2 << 16) 433 if t < MLDSA_Q { 434 _i32_store_le(poly, i, t) 435 i = i + 1 436 } 437 } 438 return pos 439} 440 441// rej_bounded_eta4: parse SHAKE256 stream into [-4, 4] coefs. 442// Per FIPS 204 §4.3 / PQClean rej_eta when ETA == 4: 443// read 1 byte -> two nibbles, each accepted if < 9, output = 4 - nibble. 444// Returns bytes consumed on success, -1 if buf exhausted. 445func nx_mldsa_rej_eta4(poly: *u8, buf: *u8, buf_len: i64) -> i64 { 446 var i: i64 = 0 447 var pos: i64 = 0 448 while i < MLDSA_N { 449 if pos + 1 > buf_len { return -1 } 450 let byte: i64 = buf[pos] 451 pos = pos + 1 452 let t0: i64 = byte & 0x0f 453 let t1: i64 = (byte >> 4) & 0x0f 454 if t0 < 9 { 455 _i32_store_le(poly, i, 4 - t0) 456 i = i + 1 457 } 458 if i < MLDSA_N { 459 if t1 < 9 { 460 _i32_store_le(poly, i, 4 - t1) 461 i = i + 1 462 } 463 } 464 } 465 return pos 466} 467 468// sample_in_ball: build challenge polynomial c with exactly tau (=49) 469// non-zero coefficients each in {-1, +1}, rest zero. 470// Per FIPS 204 §4.4 / PQClean Dilithium poly_challenge: 471// 1. SHAKE256(c_tilde_32, 8) -> sign_bits (8 bytes = 64 bits) 472// 2. continue squeezing for the Fisher-Yates index byte 473// 3. for i from N-tau to N-1: 474// repeatedly read 1 byte j from SHAKE stream until j <= i 475// c[i] = c[j] 476// c[j] = (sign_bits & 1) ? -1 : 1; sign_bits >>= 1 477// 478// Caller provides: 479// poly: 1024-byte output (cleared to all-zero first internally) 480// c_tilde_32: 32-byte commitment hash 481// scratch: >= 600 bytes (Keccak state 440 + SHAKE squeeze buf ~136) 482func nx_mldsa_sample_in_ball(poly: *u8, c_tilde_32: *u8, scratch: *u8) -> i64 { 483 // Zero the output poly. 484 var z: i64 = 0 485 while z < MLDSA_N { 486 _i32_store_le(poly, z, 0) 487 z = z + 1 488 } 489 let keccak_state: *u8 = scratch 490 let shake_buf: *u8 = (scratch as i64 + 440) as *u8 491 492 // Stream MUST be incremental: PQClean does one SHAKE256 over c_tilde, 493 // then squeezes 8 bytes for signs, then squeezes individual bytes for j. 494 // We pre-squeeze a generous chunk (1024 bytes) which suffices for tau=49 495 // and a few rejections. If exhausted we re-squeeze with more bytes from 496 // the same seed (deterministic). 497 _shake256(c_tilde_32, 32, keccak_state, shake_buf, 1024) 498 499 // First 8 bytes = sign_bits packed little-endian (only low tau=49 bits used). 500 var sign_bits_lo: i64 = 0 501 var k: i64 = 0 502 while k < 8 { 503 sign_bits_lo = sign_bits_lo | ((shake_buf[k] as i64) << (k * 8)) 504 k = k + 1 505 } 506 var pos: i64 = 8 507 508 var i: i64 = MLDSA_N - MLDSA_TAU 509 while i < MLDSA_N { 510 // Read bytes until j <= i. 511 var j: i64 = i + 1 512 while j > i { 513 if pos >= 1024 { return -1 } // exhausted (very unlikely) 514 j = shake_buf[pos] 515 pos = pos + 1 516 } 517 // c[i] = c[j]; c[j] = sign-from-bits 518 _i32_store_le(poly, i, _i32_load_le(poly, j)) 519 var s: i64 = 1 520 if (sign_bits_lo & 1) != 0 { s = -1 } 521 _i32_store_le(poly, j, s) 522 sign_bits_lo = sign_bits_lo >> 1 523 i = i + 1 524 } 525 return 0 526} 527 528// ============================================================================ 529// SECTION 4.6: Decompose / hint primitives (L133, FIPS 204 §4.5) 530// ============================================================================ 531 532// power2round(a) -> (a1, a0) such that a = a1 * 2^D + a0, |a0| <= 2^(D-1). 533// ML-DSA-65 D = 13. PQClean returns a1, writes a0 to *a0. 534// We return both via caller-provided i64 pointers (out_a1, out_a0). 535// Operates on canonical [0, q) input. 536func _power2round_coef(a: i64, out_a1: *u8, out_a0: *u8) -> i64 { 537 let a1: i64 = (a + (1 << (MLDSA_D - 1)) - 1) >> MLDSA_D 538 let a0: i64 = a - (a1 << MLDSA_D) 539 _i32_store_le(out_a1, 0, a1) 540 _i32_store_le(out_a0, 0, a0) 541 return 0 542} 543 544// Apply power2round to every coef of `in_poly`, writing a1 vec + a0 vec. 545func nx_mldsa_poly_power2round(a1_poly: *u8, a0_poly: *u8, in_poly: *u8) -> i64 { 546 var i: i64 = 0 547 while i < MLDSA_N { 548 let a: i64 = _i32_load_le(in_poly, i) 549 let a1: i64 = (a + (1 << (MLDSA_D - 1)) - 1) >> MLDSA_D 550 let a0: i64 = a - (a1 << MLDSA_D) 551 _i32_store_le(a1_poly, i, a1) 552 _i32_store_le(a0_poly, i, a0) 553 i = i + 1 554 } 555 return 0 556} 557 558// decompose(a) -> (a1, a0) such that a = a1 * alpha + a0, 559// -alpha/2 < a0 <= alpha/2 (with special carry when a1 == 16). 560// ML-DSA-65 alpha = 2 * gamma2 = 523776. 561// Per PQClean Dilithium (LEVEL=3, gamma2=(q-1)/32): 562// a1 = (a + 127) >> 7 563// a1 = (a1 * 11275 + (1 << 23)) >> 24 564// a1 ^= ((43 - a1) >> 31) & a1 // if a1 > 43, a1 = 0 (= 16 wraps to 0) 565// a0 = a - a1 * 2 * gamma2 566// a0 -= (((q-1)/2 - a0) >> 31) & q 567// Caller poly_decompose iterates over 256 coefs. 568func _decompose_coef(a: i64, out_a0: *u8, off: i64) -> i64 { 569 let half_q_minus_1: i64 = (MLDSA_Q - 1) / 2 570 // ML-DSA-65 (GAMMA2 = (q-1)/32) decompose constants per PQClean 571 // Dilithium3 newer ref / FIPS 204: 1025, shift 22, mask 15. 572 // (NOT 11275/24/^=43 -- those are for GAMMA2 = (q-1)/88.) 573 var a1: i64 = (a + 127) >> 7 574 a1 = (a1 * 1025 + (1 << 21)) >> 22 575 a1 = a1 & 15 576 var a0: i64 = a - a1 * 2 * MLDSA_GAMMA2 577 // a0 -= (((q-1)/2 - a0) >> 31) & q 578 let neg_mask: i64 = (half_q_minus_1 - a0) >> 31 579 a0 = a0 - (neg_mask & MLDSA_Q) 580 _i32_store_le(out_a0, off, a0) 581 return a1 582} 583 584func nx_mldsa_poly_decompose(a1_poly: *u8, a0_poly: *u8, in_poly: *u8) -> i64 { 585 var i: i64 = 0 586 while i < MLDSA_N { 587 let a: i64 = _i32_load_le(in_poly, i) 588 let a1: i64 = _decompose_coef(a, a0_poly, i) 589 _i32_store_le(a1_poly, i, a1) 590 i = i + 1 591 } 592 return 0 593} 594 595// make_hint(a0, a1): returns 1 if high bits of a + z would differ from a1. 596// Per PQClean: hint = 1 iff (a0 > gamma2) OR (a0 < -gamma2) OR 597// (a0 == -gamma2 AND a1 != 0). 598func _make_hint_coef(a0: i64, a1: i64) -> i64 { 599 if a0 > MLDSA_GAMMA2 { return 1 } 600 if a0 < -MLDSA_GAMMA2 { return 1 } 601 if a0 == -MLDSA_GAMMA2 { 602 if a1 != 0 { return 1 } 603 } 604 return 0 605} 606 607// make_hint(z, r): given vectors z (a0_poly) and r (a1_poly), produces 608// a hint polynomial (1 bit per coef, stored as i32 0/1). Returns the 609// total weight (number of 1-bits) for the omega bound check. 610func nx_mldsa_poly_make_hint(hint_out: *u8, a0_poly: *u8, a1_poly: *u8) -> i64 { 611 var i: i64 = 0 612 var weight: i64 = 0 613 while i < MLDSA_N { 614 let h: i64 = _make_hint_coef(_i32_load_le(a0_poly, i), 615 _i32_load_le(a1_poly, i)) 616 _i32_store_le(hint_out, i, h) 617 weight = weight + h 618 i = i + 1 619 } 620 return weight 621} 622 623// use_hint(r, h): recover high bits of r + (small) given r and hint. 624// Per PQClean: decompose r -> (a1, a0). If hint==0: return a1. 625// If hint==1 and a0 > 0: return (a1 + 1) & 15 (i.e. mod 16, since a1 in [0, 15]) 626// If hint==1 and a0 <= 0: return (a1 - 1) & 15 627func _use_hint_coef(a: i64, hint: i64) -> i64 { 628 let half_q_minus_1: i64 = (MLDSA_Q - 1) / 2 629 // ML-DSA-65 GAMMA2=(q-1)/32 decompose constants (1025, 22, mask 15) 630 var a1: i64 = (a + 127) >> 7 631 a1 = (a1 * 1025 + (1 << 21)) >> 22 632 a1 = a1 & 15 633 var a0: i64 = a - a1 * 2 * MLDSA_GAMMA2 634 let neg_mask: i64 = (half_q_minus_1 - a0) >> 31 635 a0 = a0 - (neg_mask & MLDSA_Q) 636 if hint == 0 { return a1 } 637 if a0 > 0 { return (a1 + 1) & 15 } 638 return (a1 - 1) & 15 639} 640 641func nx_mldsa_poly_use_hint(out_poly: *u8, in_poly: *u8, hint_poly: *u8) -> i64 { 642 var i: i64 = 0 643 while i < MLDSA_N { 644 let r: i64 = _use_hint_coef(_i32_load_le(in_poly, i), 645 _i32_load_le(hint_poly, i)) 646 _i32_store_le(out_poly, i, r) 647 i = i + 1 648 } 649 return 0 650} 651 652// ============================================================================ 653// SECTION 4.7: Byte encoding for pk + sk (FIPS 204 §7) 654// ============================================================================ 655 656// Pack 256 10-bit coefficients (t1, range [0, 2^10)) into 320 bytes. 657// 4 coefs per 5 bytes (40 bits). 658func _pack_t1_poly(out: *u8, poly: *u8) -> i64 { 659 var i: i64 = 0 660 while i < 64 { // 64 groups of 4 coefs = 256 661 let off: i64 = i * 5 662 let c0: i64 = _i32_load_le(poly, i*4 + 0) & 0x3ff 663 let c1: i64 = _i32_load_le(poly, i*4 + 1) & 0x3ff 664 let c2: i64 = _i32_load_le(poly, i*4 + 2) & 0x3ff 665 let c3: i64 = _i32_load_le(poly, i*4 + 3) & 0x3ff 666 out[off + 0] = c0 & 0xff 667 out[off + 1] = ((c0 >> 8) | (c1 << 2)) & 0xff 668 out[off + 2] = ((c1 >> 6) | (c2 << 4)) & 0xff 669 out[off + 3] = ((c2 >> 4) | (c3 << 6)) & 0xff 670 out[off + 4] = (c3 >> 2) & 0xff 671 i = i + 1 672 } 673 return 0 674} 675 676// Pack 256 [-eta, eta] coefficients (eta=4) into 128 bytes. 677// 2 coefs per byte; each stored as (eta - coef) in 4 bits. 678func _pack_eta4_poly(out: *u8, poly: *u8) -> i64 { 679 var i: i64 = 0 680 while i < 128 { 681 let t0: i64 = MLDSA_ETA - _i32_load_le(poly, i*2) 682 let t1: i64 = MLDSA_ETA - _i32_load_le(poly, i*2 + 1) 683 out[i] = (t0 | (t1 << 4)) & 0xff 684 i = i + 1 685 } 686 return 0 687} 688 689// Pack 256 t0 coefficients (each in (-2^12, 2^12]) into 416 bytes. 690// 8 coefs per 13 bytes (104 bits). Each stored as (2^12 - coef) in 13 bits. 691func _pack_t0_poly(out: *u8, poly: *u8) -> i64 { 692 let bias: i64 = 1 << (MLDSA_D - 1) 693 var i: i64 = 0 694 while i < 32 { // 32 groups of 8 coefs = 256 695 let off: i64 = i * 13 696 let c0: i64 = bias - _i32_load_le(poly, i*8 + 0) 697 let c1: i64 = bias - _i32_load_le(poly, i*8 + 1) 698 let c2: i64 = bias - _i32_load_le(poly, i*8 + 2) 699 let c3: i64 = bias - _i32_load_le(poly, i*8 + 3) 700 let c4: i64 = bias - _i32_load_le(poly, i*8 + 4) 701 let c5: i64 = bias - _i32_load_le(poly, i*8 + 5) 702 let c6: i64 = bias - _i32_load_le(poly, i*8 + 6) 703 let c7: i64 = bias - _i32_load_le(poly, i*8 + 7) 704 out[off + 0] = c0 & 0xff 705 out[off + 1] = ((c0 >> 8) | (c1 << 5)) & 0xff 706 out[off + 2] = (c1 >> 3) & 0xff 707 out[off + 3] = ((c1 >> 11) | (c2 << 2)) & 0xff 708 out[off + 4] = ((c2 >> 6) | (c3 << 7)) & 0xff 709 out[off + 5] = (c3 >> 1) & 0xff 710 out[off + 6] = ((c3 >> 9) | (c4 << 4)) & 0xff 711 out[off + 7] = (c4 >> 4) & 0xff 712 out[off + 8] = ((c4 >> 12) | (c5 << 1)) & 0xff 713 out[off + 9] = ((c5 >> 7) | (c6 << 6)) & 0xff 714 out[off + 10] = (c6 >> 2) & 0xff 715 out[off + 11] = ((c6 >> 10) | (c7 << 3)) & 0xff 716 out[off + 12] = (c7 >> 5) & 0xff 717 i = i + 1 718 } 719 return 0 720} 721 722// ============================================================================ 723// SECTION 4.8: Higher-level keygen composition (FIPS 204 §5.1) 724// ============================================================================ 725 726// Build one matrix-A entry A[i][j] in NTT form via rej_uniform on SHAKE128. 727// seed_bytes buffer is shake_buf+1024 (34 bytes scratch for rho||j||i). 728func _expand_a_entry(out_poly: *u8, rho: *u8, j: i64, i_row: i64, 729 keccak_state: *u8, shake_buf: *u8) -> i64 { 730 let seed_buf: *u8 = (shake_buf as i64 + 1024) as *u8 731 var z: i64 = 0 732 while z < 32 { seed_buf[z] = rho[z]; z = z + 1 } 733 seed_buf[32] = j & 0xff 734 seed_buf[33] = i_row & 0xff 735 // Try increasing lengths until rejection sampler succeeds. 736 var attempt: i64 = 0 737 while attempt < 3 { 738 let try_len: i64 = 840 + attempt * 168 739 _shake128(seed_buf, 34, keccak_state, shake_buf, try_len) 740 let rc: i64 = nx_mldsa_rej_uniform(out_poly, shake_buf, try_len) 741 if rc >= 0 { return 0 } 742 attempt = attempt + 1 743 } 744 return -1 745} 746 747// Sample one s vector entry from rho_prime + nonce via SHAKE256 + rej_eta4. 748func _expand_s_entry(out_poly: *u8, rho_prime: *u8, nonce: i64, 749 keccak_state: *u8, shake_buf: *u8) -> i64 { 750 let seed_buf: *u8 = (shake_buf as i64 + 1024) as *u8 751 var z: i64 = 0 752 while z < 64 { seed_buf[z] = rho_prime[z]; z = z + 1 } 753 seed_buf[64] = nonce & 0xff 754 seed_buf[65] = (nonce >> 8) & 0xff 755 var attempt: i64 = 0 756 while attempt < 3 { 757 let try_len: i64 = 256 + attempt * 136 758 _shake256(seed_buf, 66, keccak_state, shake_buf, try_len) 759 let rc: i64 = nx_mldsa_rej_eta4(out_poly, shake_buf, try_len) 760 if rc >= 0 { return 0 } 761 attempt = attempt + 1 762 } 763 return -1 764} 765 766// ML-DSA-65 keygen. 767// in: seed_32 (caller-provided 32-byte entropy seed) 768// scratch (>= 64 KB working memory) 769// out: pk_out (1952 bytes) 770// sk_out (4032 bytes) 771// Returns 0 on success, -1 on (extremely unlikely) sampling exhaustion. 772// 773// Scratch layout (per F-meta-8 grader annotations): 774func nx_mldsa_keygen(seed_32: *u8, scratch: *u8, pk_out: *u8, sk_out: *u8) -> i64 { 775 let keccak_state: *u8 = scratch // scratch_range: keccak 0..439 776 let shake_buf: *u8 = (scratch as i64 + 512) as *u8 // scratch_range: shake_buf 512..2559 777 let g_out: *u8 = (scratch as i64 + 2560) as *u8 // scratch_range: g_out 2560..2687 (128 B for rho||rho'||K) 778 let rho: *u8 = (scratch as i64 + 2560) as *u8 // rho = g_out[0..32] 779 let rho_prime: *u8 = (scratch as i64 + 2592) as *u8 // rho_prime = g_out[32..96] 780 let k_secret: *u8 = (scratch as i64 + 2656) as *u8 // K = g_out[96..128] 781 let s1_vec: *u8 = (scratch as i64 + 2688) as *u8 // scratch_range: s1_vec 2688..7807 (5 * 1024) 782 let s2_vec: *u8 = (scratch as i64 + 7808) as *u8 // scratch_range: s2_vec 7808..13951 (6 * 1024) 783 let acc: *u8 = (scratch as i64 + 13952) as *u8 // scratch_range: acc 13952..14975 784 let t_vec: *u8 = (scratch as i64 + 14976) as *u8 // scratch_range: t_vec 14976..21119 (6 * 1024) 785 let t1_vec: *u8 = (scratch as i64 + 21120) as *u8 // scratch_range: t1_vec 21120..27263 786 let t0_vec: *u8 = (scratch as i64 + 27264) as *u8 // scratch_range: t0_vec 27264..33407 787 let A_entry: *u8 = (scratch as i64 + 33408) as *u8 // scratch_range: A_entry 33408..34431 (on-demand matrix expansion) 788 let g_input: *u8 = (scratch as i64 + 34432) as *u8 // scratch_range: g_input 34432..34465 (32 + 1 + 1 = 34 bytes for seed||k||l) 789 790 // 1. (rho, rho_prime, K) = SHAKE256(seed || k || l, 128 bytes total). 791 var i: i64 = 0 792 while i < 32 { g_input[i] = seed_32[i]; i = i + 1 } 793 g_input[32] = MLDSA_K & 0xff 794 g_input[33] = MLDSA_L & 0xff 795 _shake256(g_input, 34, keccak_state, g_out, 128) 796 797 // 2. ExpandS: nonce 0..l-1 for s1; nonce l..l+k-1 for s2. 798 var n: i64 = 0 799 var v: i64 = 0 800 while v < MLDSA_L { 801 let rc: i64 = _expand_s_entry((s1_vec as i64 + v * POLY_BUF) as *u8, 802 rho_prime, n, keccak_state, shake_buf) 803 if rc < 0 { return -1 } 804 n = n + 1 805 v = v + 1 806 } 807 v = 0 808 while v < MLDSA_K { 809 let rc: i64 = _expand_s_entry((s2_vec as i64 + v * POLY_BUF) as *u8, 810 rho_prime, n, keccak_state, shake_buf) 811 if rc < 0 { return -1 } 812 n = n + 1 813 v = v + 1 814 } 815 816 // 3. NTT each s1 entry in place. 817 v = 0 818 while v < MLDSA_L { 819 nx_mldsa_ntt((s1_vec as i64 + v * POLY_BUF) as *u8) 820 v = v + 1 821 } 822 823 // 4. For each row r in 0..k: 824 // acc = sum_j NTT(A[r][j]) ∘ NTT(s1[j]) (pointwise, accumulated) 825 // invntt(acc) -> canonical 826 // t[r] = acc + s2[r] 827 var r: i64 = 0 828 while r < MLDSA_K { 829 // Zero acc 830 var z: i64 = 0 831 while z < MLDSA_N { _i32_store_le(acc, z, 0); z = z + 1 } 832 // For each column j: expand A[r][j], basemul into acc. 833 var j: i64 = 0 834 while j < MLDSA_L { 835 let rc: i64 = _expand_a_entry(A_entry, rho, j, r, keccak_state, shake_buf) 836 if rc < 0 { return -1 } 837 // pointwise mul + accumulate 838 var k: i64 = 0 839 while k < MLDSA_N { 840 let p: i64 = _fqmul(_i32_load_le(A_entry, k), 841 _i32_load_le((s1_vec as i64 + j * POLY_BUF) as *u8, k)) 842 let acc_v: i64 = _i32_load_le(acc, k) + p 843 _i32_store_le(acc, k, acc_v) 844 k = k + 1 845 } 846 j = j + 1 847 } 848 // invntt(acc) -> canonical-form polynomial (with one mont factor; that's fine, 849 // it gets absorbed into t and then power2round which uses canon). 850 nx_mldsa_invntt(acc) 851 // t[r] = reduce32(acc + s2[r]) 852 let t_row: *u8 = (t_vec as i64 + r * POLY_BUF) as *u8 853 let s2_row: *u8 = (s2_vec as i64 + r * POLY_BUF) as *u8 854 var k: i64 = 0 855 while k < MLDSA_N { 856 let sum: i64 = _i32_load_le(acc, k) + _i32_load_le(s2_row, k) 857 // Bring to canonical [0, q) for power2round. 858 var canon: i64 = _reduce32(sum) 859 if canon < 0 { canon = canon + MLDSA_Q } 860 _i32_store_le(t_row, k, canon) 861 k = k + 1 862 } 863 r = r + 1 864 } 865 866 // 5. Power2Round each t[r] -> (t1[r], t0[r]). 867 var rr: i64 = 0 868 while rr < MLDSA_K { 869 nx_mldsa_poly_power2round((t1_vec as i64 + rr * POLY_BUF) as *u8, 870 (t0_vec as i64 + rr * POLY_BUF) as *u8, 871 (t_vec as i64 + rr * POLY_BUF) as *u8) 872 rr = rr + 1 873 } 874 875 // 6. pk = rho (32) || pack_t1(t1) (k * 320 = 1920) = 1952 bytes 876 var rho_i: i64 = 0 877 while rho_i < 32 { pk_out[rho_i] = rho[rho_i]; rho_i = rho_i + 1 } 878 var rk: i64 = 0 879 while rk < MLDSA_K { 880 _pack_t1_poly((pk_out as i64 + 32 + rk * 320) as *u8, 881 (t1_vec as i64 + rk * POLY_BUF) as *u8) 882 rk = rk + 1 883 } 884 885 // 7. tr = SHAKE256(pk, 64). Goes into sk at offset 32+32=64. 886 let tr: *u8 = (sk_out as i64 + 64) as *u8 887 _shake256(pk_out, MLDSA_PK_BYTES, keccak_state, tr, 64) 888 889 // 8. sk = rho (32) || K (32) || tr (64) || pack_eta(s1) (l*128=640) 890 // || pack_eta(s2) (k*128=768) || pack_t0(t0) (k*416=2496) = 4032 bytes 891 var sk_i: i64 = 0 892 while sk_i < 32 { sk_out[sk_i] = rho[sk_i]; sk_i = sk_i + 1 } 893 sk_i = 0 894 while sk_i < 32 { sk_out[32 + sk_i] = k_secret[sk_i]; sk_i = sk_i + 1 } 895 // (tr already in place at offset 64) 896 var s1_i: i64 = 0 897 while s1_i < MLDSA_L { 898 _pack_eta4_poly((sk_out as i64 + 128 + s1_i * 128) as *u8, 899 (s1_vec as i64 + s1_i * POLY_BUF) as *u8) 900 s1_i = s1_i + 1 901 } 902 var s2_i: i64 = 0 903 while s2_i < MLDSA_K { 904 _pack_eta4_poly((sk_out as i64 + 768 + s2_i * 128) as *u8, 905 (s2_vec as i64 + s2_i * POLY_BUF) as *u8) 906 s2_i = s2_i + 1 907 } 908 var t0_i: i64 = 0 909 while t0_i < MLDSA_K { 910 _pack_t0_poly((sk_out as i64 + 1536 + t0_i * 416) as *u8, 911 (t0_vec as i64 + t0_i * POLY_BUF) as *u8) 912 t0_i = t0_i + 1 913 } 914 915 return 0 916} 917 918// Self-test: keygen twice with same seed; pk + sk must be byte-identical. 919// Returns 0 on success, non-zero bitmap of failure axes. 920// bit 0: first keygen returned non-zero 921// bit 1: second keygen returned non-zero 922// bit 2: pk mismatch 923// bit 3: sk mismatch 924// Caller provides scratch (>= 64 KB). Internal pk_1, sk_1, pk_2, sk_2 are 925// placed at high addresses inside scratch. 926func nx_mldsa_keygen_deterministic_test(seed_32: *u8, scratch: *u8) -> i64 { 927 let pk_1: *u8 = (scratch as i64 + 50000) as *u8 928 let sk_1: *u8 = (scratch as i64 + 52000) as *u8 929 let pk_2: *u8 = (scratch as i64 + 56500) as *u8 930 let sk_2: *u8 = (scratch as i64 + 58500) as *u8 931 var fail: i64 = 0 932 let rc1: i64 = nx_mldsa_keygen(seed_32, scratch, pk_1, sk_1) 933 if rc1 != 0 { fail = fail | 1 } 934 let rc2: i64 = nx_mldsa_keygen(seed_32, scratch, pk_2, sk_2) 935 if rc2 != 0 { fail = fail | 2 } 936 var pk_diff: i64 = 0 937 var i: i64 = 0 938 while i < MLDSA_PK_BYTES { 939 if pk_1[i] != pk_2[i] { pk_diff = 1 } 940 i = i + 1 941 } 942 if pk_diff != 0 { fail = fail | 4 } 943 var sk_diff: i64 = 0 944 var j: i64 = 0 945 while j < MLDSA_SK_BYTES { 946 if sk_1[j] != sk_2[j] { sk_diff = 1 } 947 j = j + 1 948 } 949 if sk_diff != 0 { fail = fail | 8 } 950 return fail 951} 952 953// ============================================================================ 954// SECTION 4.9: ExpandMask + sig-byte encoders (L135, FIPS 204 §5.3 / §7) 955// ============================================================================ 956 957// Unpack one 20-bit chunk into a coefficient in (-gamma1, gamma1]. 958// Per FIPS 204 §7.1 / PQClean: for gamma1 = 2^19, output = gamma1 - x. 959// 5 bytes -> 2 coefs (PQClean unpacks z 4 coefs at a time across 10 bytes 960// = 80 bits = 4 * 20 bits). Here we do 4-coef chunks. 961func _unpack_z_chunk(out_poly: *u8, base_idx: i64, buf: *u8, buf_off: i64) -> i64 { 962 // 4 coefs from 10 bytes (40 nibbles = 80 bits = 4 * 20) 963 let b0: i64 = buf[buf_off + 0] 964 let b1: i64 = buf[buf_off + 1] 965 let b2: i64 = buf[buf_off + 2] 966 let b3: i64 = buf[buf_off + 3] 967 let b4: i64 = buf[buf_off + 4] 968 let b5: i64 = buf[buf_off + 5] 969 let b6: i64 = buf[buf_off + 6] 970 let b7: i64 = buf[buf_off + 7] 971 let b8: i64 = buf[buf_off + 8] 972 let b9: i64 = buf[buf_off + 9] 973 let v0: i64 = (b0 | (b1 << 8) | ((b2 & 0x0f) << 16)) & 0xfffff 974 let v1: i64 = ((b2 >> 4) | (b3 << 4) | (b4 << 12)) & 0xfffff 975 let v2: i64 = (b5 | (b6 << 8) | ((b7 & 0x0f) << 16)) & 0xfffff 976 let v3: i64 = ((b7 >> 4) | (b8 << 4) | (b9 << 12)) & 0xfffff 977 _i32_store_le(out_poly, base_idx + 0, MLDSA_GAMMA1 - v0) 978 _i32_store_le(out_poly, base_idx + 1, MLDSA_GAMMA1 - v1) 979 _i32_store_le(out_poly, base_idx + 2, MLDSA_GAMMA1 - v2) 980 _i32_store_le(out_poly, base_idx + 3, MLDSA_GAMMA1 - v3) 981 return 0 982} 983 984// Pack 256 coefs in (-gamma1, gamma1] into 640 bytes (20 bits/coef). 985// Per FIPS 204 / PQClean: store (gamma1 - coef) in 20 bits. 986func _pack_z_poly(out: *u8, poly: *u8) -> i64 { 987 var i: i64 = 0 988 while i < 64 { // 64 groups of 4 coefs = 256 989 let off: i64 = i * 10 990 let c0: i64 = MLDSA_GAMMA1 - _i32_load_le(poly, i*4 + 0) 991 let c1: i64 = MLDSA_GAMMA1 - _i32_load_le(poly, i*4 + 1) 992 let c2: i64 = MLDSA_GAMMA1 - _i32_load_le(poly, i*4 + 2) 993 let c3: i64 = MLDSA_GAMMA1 - _i32_load_le(poly, i*4 + 3) 994 out[off + 0] = c0 & 0xff 995 out[off + 1] = (c0 >> 8) & 0xff 996 out[off + 2] = ((c0 >> 16) | (c1 << 4)) & 0xff 997 out[off + 3] = (c1 >> 4) & 0xff 998 out[off + 4] = (c1 >> 12) & 0xff 999 out[off + 5] = c2 & 0xff 1000 out[off + 6] = (c2 >> 8) & 0xff 1001 out[off + 7] = ((c2 >> 16) | (c3 << 4)) & 0xff 1002 out[off + 8] = (c3 >> 4) & 0xff 1003 out[off + 9] = (c3 >> 12) & 0xff 1004 i = i + 1 1005 } 1006 return 0 1007} 1008 1009func _unpack_z_poly(poly: *u8, bytes_in: *u8) -> i64 { 1010 var i: i64 = 0 1011 while i < 64 { 1012 _unpack_z_chunk(poly, i * 4, bytes_in, i * 10) 1013 i = i + 1 1014 } 1015 return 0 1016} 1017 1018// Pack 256 coefs in [0, 16) into 128 bytes (4 bits/coef, 2 per byte). 1019func _pack_w1_poly(out: *u8, poly: *u8) -> i64 { 1020 var i: i64 = 0 1021 while i < 128 { 1022 let c0: i64 = _i32_load_le(poly, i * 2) & 0x0f 1023 let c1: i64 = _i32_load_le(poly, i * 2 + 1) & 0x0f 1024 out[i] = (c0 | (c1 << 4)) & 0xff 1025 i = i + 1 1026 } 1027 return 0 1028} 1029 1030// Pack hint vector: k polynomials of 0/1 -> (omega + k) bytes total. 1031// Format per FIPS 204: for each of k polys, append the indices of its 1032// non-zero positions, then write the cumulative count as a single byte. 1033// Final encoding: omega data bytes + k count bytes = 55 + 6 = 61 bytes. 1034// Caller passes hint_polys (k * POLY_BUF) and writes to out (61 bytes). 1035// Returns -1 if total nonzero weight > omega (invalid signature). 1036func _pack_hint_vec(out: *u8, hint_polys: *u8) -> i64 { 1037 // Zero entire out first. 1038 var z: i64 = 0 1039 while z < MLDSA_OMEGA + MLDSA_K { out[z] = 0; z = z + 1 } 1040 var idx: i64 = 0 1041 var p: i64 = 0 1042 while p < MLDSA_K { 1043 var j: i64 = 0 1044 while j < MLDSA_N { 1045 let h: i64 = _i32_load_le((hint_polys as i64 + p * POLY_BUF) as *u8, j) 1046 if h != 0 { 1047 if idx >= MLDSA_OMEGA { return -1 } 1048 out[idx] = j & 0xff 1049 idx = idx + 1 1050 } 1051 j = j + 1 1052 } 1053 out[MLDSA_OMEGA + p] = idx & 0xff 1054 p = p + 1 1055 } 1056 return idx 1057} 1058 1059// Unpack hint vector: read (omega + k) bytes, recover k polys with 0/1 coefs. 1060// Returns 0 on success, -1 if signature malformed (indices out of order or 1061// cumulative count overflows omega). 1062func _unpack_hint_vec(hint_polys: *u8, bytes_in: *u8) -> i64 { 1063 // Zero output polys. 1064 var p0: i64 = 0 1065 while p0 < MLDSA_K { 1066 var j0: i64 = 0 1067 while j0 < MLDSA_N { 1068 _i32_store_le((hint_polys as i64 + p0 * POLY_BUF) as *u8, j0, 0) 1069 j0 = j0 + 1 1070 } 1071 p0 = p0 + 1 1072 } 1073 var prev_idx: i64 = 0 1074 var p: i64 = 0 1075 while p < MLDSA_K { 1076 let cum: i64 = bytes_in[MLDSA_OMEGA + p] 1077 if cum < prev_idx { return -1 } 1078 if cum > MLDSA_OMEGA { return -1 } 1079 var last_pos: i64 = -1 1080 var i: i64 = prev_idx 1081 while i < cum { 1082 let pos: i64 = bytes_in[i] 1083 if pos <= last_pos { if last_pos >= 0 { return -1 } } 1084 last_pos = pos 1085 _i32_store_le((hint_polys as i64 + p * POLY_BUF) as *u8, pos, 1) 1086 i = i + 1 1087 } 1088 prev_idx = cum 1089 p = p + 1 1090 } 1091 return 0 1092} 1093 1094// ExpandMask: derive y[i] vector entry from rho_prime (64) || kappa+i (2 bytes LE). 1095// y has 256 coefs in (-gamma1, gamma1]. SHAKE256 squeezes 640 bytes 1096// (gamma1=2^19 needs 20 bits/coef * 256 = 5120 bits = 640 bytes). 1097// scratch: shake_buf at >= 640 bytes; seed_buf = shake_buf + 640 (66 bytes). 1098func _expand_mask_entry(out_poly: *u8, rho_prime: *u8, kappa_plus_i: i64, 1099 keccak_state: *u8, shake_buf: *u8) -> i64 { 1100 let seed_buf: *u8 = (shake_buf as i64 + 1024) as *u8 1101 var z: i64 = 0 1102 while z < 64 { seed_buf[z] = rho_prime[z]; z = z + 1 } 1103 seed_buf[64] = kappa_plus_i & 0xff 1104 seed_buf[65] = (kappa_plus_i >> 8) & 0xff 1105 _shake256(seed_buf, 66, keccak_state, shake_buf, 640) 1106 _unpack_z_poly(out_poly, shake_buf) 1107 return 0 1108} 1109 1110// ============================================================================ 1111// SECTION 4.10: L135 KAT export -- encoder round-trips + ExpandMask 1112// ============================================================================ 1113// 1114// Caller passes scratch buffer (>= 32 KB). Returns 0 on success, non-zero 1115// bitmap of failure axes: 1116// bit 0: pack_z/unpack_z round-trip mismatch 1117// bit 1: pack_w1 produces out-of-range output 1118// bit 2: pack_hint/unpack_hint round-trip mismatch 1119// bit 3: ExpandMask produces out-of-range output 1120// bit 4: ExpandMask not deterministic for same (rho_prime, kappa) 1121func nx_mldsa_l135_encoders_test(scratch: *u8) -> i64 { 1122 var fail: i64 = 0 1123 1124 // ---- z pack/unpack round-trip ---- 1125 let z_in: *u8 = scratch 1126 let z_bytes: *u8 = (scratch as i64 + POLY_BUF) as *u8 1127 let z_out: *u8 = (scratch as i64 + POLY_BUF + 640) as *u8 1128 // Fill z_in with values in (-gamma1, gamma1]. Use a deterministic pattern. 1129 var i: i64 = 0 1130 while i < MLDSA_N { 1131 let v: i64 = ((i * 7919) % (2 * MLDSA_GAMMA1)) - MLDSA_GAMMA1 + 1 1132 _i32_store_le(z_in, i, v) 1133 i = i + 1 1134 } 1135 _pack_z_poly(z_bytes, z_in) 1136 _unpack_z_poly(z_out, z_bytes) 1137 var diff_z: i64 = 0 1138 var j: i64 = 0 1139 while j < MLDSA_N { 1140 if _i32_load_le(z_in, j) != _i32_load_le(z_out, j) { diff_z = 1 } 1141 j = j + 1 1142 } 1143 if diff_z != 0 { fail = fail | 1 } 1144 1145 // ---- w1 pack: every output coef must be < 16 (4-bit) ---- 1146 let w1_in: *u8 = (scratch as i64 + 4096) as *u8 1147 let w1_bytes: *u8 = (scratch as i64 + 5120) as *u8 1148 var k: i64 = 0 1149 while k < MLDSA_N { 1150 _i32_store_le(w1_in, k, k % 16) 1151 k = k + 1 1152 } 1153 _pack_w1_poly(w1_bytes, w1_in) 1154 var w1_ok: i64 = 1 1155 var bi: i64 = 0 1156 while bi < 128 { 1157 // each byte should be (c0 | (c1 << 4)) where c0 = 2*bi%16, c1 = (2*bi+1)%16 1158 let expected: i64 = ((2 * bi) % 16) | ((((2 * bi + 1) % 16)) << 4) 1159 if w1_bytes[bi] != (expected & 0xff) { w1_ok = 0 } 1160 bi = bi + 1 1161 } 1162 if w1_ok == 0 { fail = fail | 2 } 1163 1164 // ---- hint pack/unpack round-trip ---- 1165 let hint_in_polys: *u8 = (scratch as i64 + 8192) as *u8 // k * 1024 = 6144 B 1166 let hint_bytes: *u8 = (scratch as i64 + 16384) as *u8 // 61 B 1167 let hint_out_polys: *u8 = (scratch as i64 + 16448) as *u8 // 6144 B 1168 // Fill hint_in with deterministic 0/1 pattern, total weight <= omega. 1169 var p: i64 = 0 1170 var total: i64 = 0 1171 while p < MLDSA_K { 1172 var jp: i64 = 0 1173 while jp < MLDSA_N { 1174 // Pick a few positions per poly; total stays under omega=55. 1175 var v: i64 = 0 1176 if total < MLDSA_OMEGA { 1177 // Sparse pattern: every (p+1)*37'th coef in this poly. 1178 if (jp + p * 7) % ((p + 1) * 37) == 0 { v = 1; total = total + 1 } 1179 } 1180 _i32_store_le((hint_in_polys as i64 + p * POLY_BUF) as *u8, jp, v) 1181 jp = jp + 1 1182 } 1183 p = p + 1 1184 } 1185 let pack_rc: i64 = _pack_hint_vec(hint_bytes, hint_in_polys) 1186 if pack_rc < 0 { fail = fail | 4 } 1187 let unpack_rc: i64 = _unpack_hint_vec(hint_out_polys, hint_bytes) 1188 if unpack_rc < 0 { fail = fail | 4 } 1189 var diff_h: i64 = 0 1190 var pp: i64 = 0 1191 while pp < MLDSA_K { 1192 var jj: i64 = 0 1193 while jj < MLDSA_N { 1194 let a: i64 = _i32_load_le((hint_in_polys as i64 + pp * POLY_BUF) as *u8, jj) 1195 let b: i64 = _i32_load_le((hint_out_polys as i64 + pp * POLY_BUF) as *u8, jj) 1196 if a != b { diff_h = 1 } 1197 jj = jj + 1 1198 } 1199 pp = pp + 1 1200 } 1201 if diff_h != 0 { fail = fail | 4 } 1202 1203 // ---- ExpandMask range check ---- 1204 let rho_prime: *u8 = (scratch as i64 + 24064) as *u8 // 64 B 1205 let mask_state: *u8 = (scratch as i64 + 24128) as *u8 // Keccak 440 B 1206 let mask_buf: *u8 = (scratch as i64 + 24640) as *u8 // shake 1024 + seed 66 1207 let mask_poly_1: *u8 = (scratch as i64 + 26240) as *u8 // 1024 B 1208 let mask_poly_2: *u8 = (scratch as i64 + 27264) as *u8 // 1024 B 1209 var rsi: i64 = 0 1210 while rsi < 64 { rho_prime[rsi] = (rsi * 11 + 5) & 0xff; rsi = rsi + 1 } 1211 _expand_mask_entry(mask_poly_1, rho_prime, 0, mask_state, mask_buf) 1212 // range: every coef in (-gamma1, gamma1] 1213 var range_ok: i64 = 1 1214 var mi: i64 = 0 1215 while mi < MLDSA_N { 1216 let c: i64 = _i32_load_le(mask_poly_1, mi) 1217 if c <= -MLDSA_GAMMA1 { range_ok = 0 } 1218 if c > MLDSA_GAMMA1 { range_ok = 0 } 1219 mi = mi + 1 1220 } 1221 if range_ok == 0 { fail = fail | 8 } 1222 // determinism: same (rho_prime, kappa) -> same poly 1223 _expand_mask_entry(mask_poly_2, rho_prime, 0, mask_state, mask_buf) 1224 var det_diff: i64 = 0 1225 var di: i64 = 0 1226 while di < MLDSA_N { 1227 if _i32_load_le(mask_poly_1, di) != _i32_load_le(mask_poly_2, di) { det_diff = 1 } 1228 di = di + 1 1229 } 1230 if det_diff != 0 { fail = fail | 16 } 1231 return fail 1232} 1233 1234// ============================================================================ 1235// SECTION 4.11: Decoders for sk (L136 prereqs) 1236// ============================================================================ 1237 1238// Unpack 128 bytes -> 256 [-eta, eta] coefs. Inverse of _pack_eta4_poly. 1239func _unpack_eta4_poly(out: *u8, bytes_in: *u8) -> i64 { 1240 var i: i64 = 0 1241 while i < 128 { 1242 let b: i64 = bytes_in[i] 1243 _i32_store_le(out, i*2, MLDSA_ETA - (b & 0x0f)) 1244 _i32_store_le(out, i*2 + 1, MLDSA_ETA - ((b >> 4) & 0x0f)) 1245 i = i + 1 1246 } 1247 return 0 1248} 1249 1250// Unpack 416 bytes -> 256 t0 coefs in (-2^12, 2^12]. Inverse of _pack_t0_poly. 1251func _unpack_t0_poly(out: *u8, bytes_in: *u8) -> i64 { 1252 let bias: i64 = 1 << (MLDSA_D - 1) 1253 var i: i64 = 0 1254 while i < 32 { 1255 let off: i64 = i * 13 1256 let b0: i64 = bytes_in[off + 0]; let b1: i64 = bytes_in[off + 1] 1257 let b2: i64 = bytes_in[off + 2]; let b3: i64 = bytes_in[off + 3] 1258 let b4: i64 = bytes_in[off + 4]; let b5: i64 = bytes_in[off + 5] 1259 let b6: i64 = bytes_in[off + 6]; let b7: i64 = bytes_in[off + 7] 1260 let b8: i64 = bytes_in[off + 8]; let b9: i64 = bytes_in[off + 9] 1261 let b10: i64 = bytes_in[off + 10] 1262 let b11: i64 = bytes_in[off + 11] 1263 let b12: i64 = bytes_in[off + 12] 1264 let v0: i64 = (b0 | (b1 << 8)) & 0x1fff 1265 let v1: i64 = ((b1 >> 5) | (b2 << 3) | (b3 << 11)) & 0x1fff 1266 let v2: i64 = ((b3 >> 2) | (b4 << 6)) & 0x1fff 1267 let v3: i64 = ((b4 >> 7) | (b5 << 1) | (b6 << 9)) & 0x1fff 1268 let v4: i64 = ((b6 >> 4) | (b7 << 4) | (b8 << 12)) & 0x1fff 1269 let v5: i64 = ((b8 >> 1) | (b9 << 7)) & 0x1fff 1270 let v6: i64 = ((b9 >> 6) | (b10 << 2) | (b11 << 10)) & 0x1fff 1271 let v7: i64 = ((b11 >> 3) | (b12 << 5)) & 0x1fff 1272 _i32_store_le(out, i*8 + 0, bias - v0) 1273 _i32_store_le(out, i*8 + 1, bias - v1) 1274 _i32_store_le(out, i*8 + 2, bias - v2) 1275 _i32_store_le(out, i*8 + 3, bias - v3) 1276 _i32_store_le(out, i*8 + 4, bias - v4) 1277 _i32_store_le(out, i*8 + 5, bias - v5) 1278 _i32_store_le(out, i*8 + 6, bias - v6) 1279 _i32_store_le(out, i*8 + 7, bias - v7) 1280 i = i + 1 1281 } 1282 return 0 1283} 1284 1285// Inf-norm of a polynomial: returns max(|coef|) using signed representation. 1286// For checking bound conditions in sign loop (||z||_inf < gamma1 - beta etc.) 1287func _poly_inf_norm(poly: *u8) -> i64 { 1288 var max_n: i64 = 0 1289 var i: i64 = 0 1290 while i < MLDSA_N { 1291 var v: i64 = _i32_load_le(poly, i) 1292 if v < 0 { v = -v } 1293 if v > max_n { max_n = v } 1294 i = i + 1 1295 } 1296 return max_n 1297} 1298 1299// Compute the inf-norm of a vector of k or l polys. 1300func _vec_inf_norm(vec: *u8, n_polys: i64) -> i64 { 1301 var max_n: i64 = 0 1302 var i: i64 = 0 1303 while i < n_polys { 1304 let n: i64 = _poly_inf_norm((vec as i64 + i * POLY_BUF) as *u8) 1305 if n > max_n { max_n = n } 1306 i = i + 1 1307 } 1308 return max_n 1309} 1310 1311// ============================================================================ 1312// SECTION 4.12: Sign function (FIPS 204 §5.2) 1313// ============================================================================ 1314// 1315// Scratch buffer requirement: caller passes >= 128 KB working memory. 1316// Internal layout below uses scratch_range annotations per F-meta-8. 1317 1318func nx_mldsa_sign(sk: *u8, msg: *u8, msg_len: i64, 1319 scratch: *u8, sig_out: *u8) -> i64 { 1320 let keccak_state: *u8 = scratch // scratch_range: keccak 0..439 1321 let shake_buf: *u8 = (scratch as i64 + 512) as *u8 // scratch_range: shake_buf 512..2559 1322 // sk components (decoded once into scratch slots) 1323 let s1_vec: *u8 = (scratch as i64 + 3072) as *u8 // scratch_range: s1_vec 3072..8191 (5 * 1024) 1324 let s2_vec: *u8 = (scratch as i64 + 8192) as *u8 // scratch_range: s2_vec 8192..14335 (6 * 1024) 1325 let t0_vec: *u8 = (scratch as i64 + 14336) as *u8 // scratch_range: t0_vec 14336..20479 (6 * 1024) 1326 // working 1327 let mu: *u8 = (scratch as i64 + 20480) as *u8 // scratch_range: mu 20480..20543 (64) 1328 let rho_prime: *u8 = (scratch as i64 + 20544) as *u8 // scratch_range: rho_prime 20544..20607 (64) 1329 let mu_msg_buf: *u8 = (scratch as i64 + 20608) as *u8 // scratch_range: mu_msg 20608..28799 (8192 max msg) 1330 let k_mu_buf: *u8 = (scratch as i64 + 28800) as *u8 // scratch_range: k_mu 28800..28895 (32+64=96) 1331 let y_vec: *u8 = (scratch as i64 + 28896) as *u8 // scratch_range: y_vec 28896..34015 (5 * 1024) 1332 let w_vec: *u8 = (scratch as i64 + 34016) as *u8 // scratch_range: w_vec 34016..40159 (6 * 1024) 1333 let w1_vec: *u8 = (scratch as i64 + 40160) as *u8 // scratch_range: w1_vec 40160..46303 1334 let w0_vec: *u8 = (scratch as i64 + 46304) as *u8 // scratch_range: w0_vec 46304..52447 1335 let c_poly: *u8 = (scratch as i64 + 52448) as *u8 // scratch_range: c_poly 52448..53471 1336 let c_hat: *u8 = (scratch as i64 + 53472) as *u8 // scratch_range: c_hat 53472..54495 1337 let cs1: *u8 = (scratch as i64 + 54496) as *u8 // scratch_range: cs1 54496..59615 (5 * 1024) 1338 let cs2: *u8 = (scratch as i64 + 59616) as *u8 // scratch_range: cs2 59616..65759 (6 * 1024) 1339 let ct0: *u8 = (scratch as i64 + 65760) as *u8 // scratch_range: ct0 65760..71903 (6 * 1024) 1340 let z_vec: *u8 = (scratch as i64 + 71904) as *u8 // scratch_range: z_vec 71904..77023 (5 * 1024) 1341 let r0_vec: *u8 = (scratch as i64 + 77024) as *u8 // scratch_range: r0_vec 77024..83167 1342 let h_vec: *u8 = (scratch as i64 + 83168) as *u8 // scratch_range: h_vec 83168..89311 1343 let A_entry: *u8 = (scratch as i64 + 89312) as *u8 // scratch_range: A_entry 89312..90335 1344 let acc_poly: *u8 = (scratch as i64 + 90336) as *u8 // scratch_range: acc 90336..91359 1345 let w1_packed: *u8 = (scratch as i64 + 91360) as *u8 // scratch_range: w1_packed 91360..92127 (k*128=768) 1346 let c_tilde: *u8 = (scratch as i64 + 92128) as *u8 // scratch_range: c_tilde 92128..92175 (48 B) 1347 1348 // sk layout: rho(32) || K(32) || tr(64) || s1_packed(640) || s2_packed(768) || t0_packed(2496) 1349 let rho: *u8 = sk 1350 let k_secret: *u8 = (sk as i64 + 32) as *u8 1351 let tr: *u8 = (sk as i64 + 64) as *u8 1352 let s1_packed: *u8 = (sk as i64 + 128) as *u8 1353 let s2_packed: *u8 = (sk as i64 + 768) as *u8 1354 let t0_packed: *u8 = (sk as i64 + 1536) as *u8 1355 1356 // Decode s1, s2, t0; NTT each in place. 1357 var iv: i64 = 0 1358 while iv < MLDSA_L { 1359 _unpack_eta4_poly((s1_vec as i64 + iv * POLY_BUF) as *u8, 1360 (s1_packed as i64 + iv * 128) as *u8) 1361 nx_mldsa_ntt((s1_vec as i64 + iv * POLY_BUF) as *u8) 1362 iv = iv + 1 1363 } 1364 iv = 0 1365 while iv < MLDSA_K { 1366 _unpack_eta4_poly((s2_vec as i64 + iv * POLY_BUF) as *u8, 1367 (s2_packed as i64 + iv * 128) as *u8) 1368 nx_mldsa_ntt((s2_vec as i64 + iv * POLY_BUF) as *u8) 1369 iv = iv + 1 1370 } 1371 iv = 0 1372 while iv < MLDSA_K { 1373 _unpack_t0_poly((t0_vec as i64 + iv * POLY_BUF) as *u8, 1374 (t0_packed as i64 + iv * 416) as *u8) 1375 nx_mldsa_ntt((t0_vec as i64 + iv * POLY_BUF) as *u8) 1376 iv = iv + 1 1377 } 1378 1379 // mu = SHAKE256(tr || msg, 64). Limited to msg_len <= 8128 by scratch. 1380 if msg_len > 8128 { return -2 } 1381 var ti: i64 = 0 1382 while ti < 64 { mu_msg_buf[ti] = tr[ti]; ti = ti + 1 } 1383 var mi: i64 = 0 1384 while mi < msg_len { mu_msg_buf[64 + mi] = msg[mi]; mi = mi + 1 } 1385 _shake256(mu_msg_buf, 64 + msg_len, keccak_state, mu, 64) 1386 1387 // rho_prime = SHAKE256(K || mu, 64). Deterministic variant (no rnd). 1388 var ki: i64 = 0 1389 while ki < 32 { k_mu_buf[ki] = k_secret[ki]; ki = ki + 1 } 1390 var mu_i: i64 = 0 1391 while mu_i < 64 { k_mu_buf[32 + mu_i] = mu[mu_i]; mu_i = mu_i + 1 } 1392 _shake256(k_mu_buf, 96, keccak_state, rho_prime, 64) 1393 1394 // Rejection-sample loop. Bounded by MAX_ATTEMPTS = 200 (real impls 1395 // expect ~3-7 iterations; 200 is safety). 1396 var kappa: i64 = 0 1397 var attempt: i64 = 0 1398 while attempt < 200 { 1399 // 1. y = ExpandMask(rho_prime, kappa+i) for i in 0..l 1400 var yi: i64 = 0 1401 while yi < MLDSA_L { 1402 _expand_mask_entry((y_vec as i64 + yi * POLY_BUF) as *u8, 1403 rho_prime, kappa + yi, keccak_state, shake_buf) 1404 yi = yi + 1 1405 } 1406 // 2. w = INVNTT(A_hat ∘ NTT(y)) per row 1407 // First NTT each y entry (we keep an NTT-form copy in y_vec since we 1408 // need y itself later for z = y + cs1). Use cs1 region as temp NTT 1409 // form of y (cs1 isn't yet computed). 1410 var yi2: i64 = 0 1411 while yi2 < MLDSA_L { 1412 // Copy y -> cs1[yi2]; NTT in place; then matrix-mul reads cs1. 1413 var cj: i64 = 0 1414 while cj < MLDSA_N { 1415 _i32_store_le((cs1 as i64 + yi2 * POLY_BUF) as *u8, cj, 1416 _i32_load_le((y_vec as i64 + yi2 * POLY_BUF) as *u8, cj)) 1417 cj = cj + 1 1418 } 1419 nx_mldsa_ntt((cs1 as i64 + yi2 * POLY_BUF) as *u8) 1420 yi2 = yi2 + 1 1421 } 1422 // For each row r, accumulate A[r,j] ∘ NTT(y[j]) -> w[r] in NTT form, 1423 // then INVNTT -> canonical. 1424 var rr: i64 = 0 1425 while rr < MLDSA_K { 1426 var cz: i64 = 0 1427 while cz < MLDSA_N { _i32_store_le(acc_poly, cz, 0); cz = cz + 1 } 1428 var cj: i64 = 0 1429 while cj < MLDSA_L { 1430 _expand_a_entry(A_entry, rho, cj, rr, keccak_state, shake_buf) 1431 var pk: i64 = 0 1432 while pk < MLDSA_N { 1433 let pv: i64 = _fqmul(_i32_load_le(A_entry, pk), 1434 _i32_load_le((cs1 as i64 + cj * POLY_BUF) as *u8, pk)) 1435 let av: i64 = _i32_load_le(acc_poly, pk) + pv 1436 _i32_store_le(acc_poly, pk, av) 1437 pk = pk + 1 1438 } 1439 cj = cj + 1 1440 } 1441 // PQClean polyveck_reduce(&w1) before polyveck_invntt_tomont(&w1). 1442 // Without this, the 256x amplification inside invntt overflows i32 1443 // (deterministically in WASM, but to DIFFERENT values than verify's 1444 // properly-reduced path -- causing the L137 c_tilde mismatch bug). 1445 var redi: i64 = 0 1446 while redi < MLDSA_N { 1447 _i32_store_le(acc_poly, redi, _reduce32(_i32_load_le(acc_poly, redi))) 1448 redi = redi + 1 1449 } 1450 nx_mldsa_invntt(acc_poly) 1451 // canonicalise -> w[rr] 1452 var wk: i64 = 0 1453 while wk < MLDSA_N { 1454 var cv: i64 = _reduce32(_i32_load_le(acc_poly, wk)) 1455 if cv < 0 { cv = cv + MLDSA_Q } 1456 _i32_store_le((w_vec as i64 + rr * POLY_BUF) as *u8, wk, cv) 1457 wk = wk + 1 1458 } 1459 rr = rr + 1 1460 } 1461 // 3. (w1, w0) = Decompose(w) 1462 var di: i64 = 0 1463 while di < MLDSA_K { 1464 nx_mldsa_poly_decompose((w1_vec as i64 + di * POLY_BUF) as *u8, 1465 (w0_vec as i64 + di * POLY_BUF) as *u8, 1466 (w_vec as i64 + di * POLY_BUF) as *u8) 1467 di = di + 1 1468 } 1469 // 4. c_tilde = SHAKE256(mu || w1Encode(w1), 48) 1470 // w1 encoded as k * 128 bytes (4 bits/coef). 1471 var w1pi: i64 = 0 1472 while w1pi < MLDSA_K { 1473 _pack_w1_poly((w1_packed as i64 + w1pi * 128) as *u8, 1474 (w1_vec as i64 + w1pi * POLY_BUF) as *u8) 1475 w1pi = w1pi + 1 1476 } 1477 // Build mu || w1_packed in mu_msg_buf scratch (reuse since msg done). 1478 var mui: i64 = 0 1479 while mui < 64 { mu_msg_buf[mui] = mu[mui]; mui = mui + 1 } 1480 var wpi: i64 = 0 1481 let w1_total: i64 = MLDSA_K * 128 1482 while wpi < w1_total { mu_msg_buf[64 + wpi] = w1_packed[wpi]; wpi = wpi + 1 } 1483 _shake256(mu_msg_buf, 64 + w1_total, keccak_state, c_tilde, 48) 1484 1485 // 5. c = SampleInBall(c_tilde, 48 bytes); c_hat = NTT(c) 1486 nx_mldsa_sample_in_ball(c_poly, c_tilde, keccak_state) 1487 var ci: i64 = 0 1488 while ci < MLDSA_N { 1489 _i32_store_le(c_hat, ci, _i32_load_le(c_poly, ci)) 1490 ci = ci + 1 1491 } 1492 nx_mldsa_ntt(c_hat) 1493 1494 // 6. cs1 = INVNTT(c_hat ∘ s1_hat); z = y + cs1 1495 var s1i: i64 = 0 1496 while s1i < MLDSA_L { 1497 var pk: i64 = 0 1498 while pk < MLDSA_N { 1499 let pv: i64 = _fqmul(_i32_load_le(c_hat, pk), 1500 _i32_load_le((s1_vec as i64 + s1i * POLY_BUF) as *u8, pk)) 1501 _i32_store_le((cs1 as i64 + s1i * POLY_BUF) as *u8, pk, pv) 1502 pk = pk + 1 1503 } 1504 nx_mldsa_invntt((cs1 as i64 + s1i * POLY_BUF) as *u8) 1505 // z = y + cs1 1506 var zk: i64 = 0 1507 while zk < MLDSA_N { 1508 let s: i64 = _i32_load_le((y_vec as i64 + s1i * POLY_BUF) as *u8, zk) + _i32_load_le((cs1 as i64 + s1i * POLY_BUF) as *u8, zk) 1509 let r: i64 = _reduce32(s) 1510 _i32_store_le((z_vec as i64 + s1i * POLY_BUF) as *u8, zk, r) 1511 zk = zk + 1 1512 } 1513 s1i = s1i + 1 1514 } 1515 // 7. if ||z||_inf >= gamma1 - beta: reject 1516 if _vec_inf_norm(z_vec, MLDSA_L) >= (MLDSA_GAMMA1 - MLDSA_BETA) { 1517 kappa = kappa + MLDSA_L 1518 attempt = attempt + 1 1519 // continue 1520 } else { 1521 // 8. cs2 = INVNTT(c_hat ∘ s2_hat); r0 = w0 - cs2 (per PQClean 1522 // Dilithium reference: subtract cs2 directly from w0=LowBits(w), 1523 // NOT decompose(w - cs2). These differ when (w0 - cs2) overflows 1524 // the gamma2 envelope, and the hint parameterization in step 12 1525 // requires the PQClean form. 1526 var s2i: i64 = 0 1527 while s2i < MLDSA_K { 1528 var pk: i64 = 0 1529 while pk < MLDSA_N { 1530 let pv: i64 = _fqmul(_i32_load_le(c_hat, pk), 1531 _i32_load_le((s2_vec as i64 + s2i * POLY_BUF) as *u8, pk)) 1532 _i32_store_le((cs2 as i64 + s2i * POLY_BUF) as *u8, pk, pv) 1533 pk = pk + 1 1534 } 1535 nx_mldsa_invntt((cs2 as i64 + s2i * POLY_BUF) as *u8) 1536 // r0[s2i] = reduce32(w0[s2i] - cs2[s2i]) 1537 var dk: i64 = 0 1538 while dk < MLDSA_N { 1539 let w0v: i64 = _i32_load_le((w0_vec as i64 + s2i * POLY_BUF) as *u8, dk) 1540 let cs2v: i64 = _i32_load_le((cs2 as i64 + s2i * POLY_BUF) as *u8, dk) 1541 let rv: i64 = _reduce32(w0v - cs2v) 1542 _i32_store_le((r0_vec as i64 + s2i * POLY_BUF) as *u8, dk, rv) 1543 dk = dk + 1 1544 } 1545 s2i = s2i + 1 1546 } 1547 // 9. if ||r0||_inf >= gamma2 - beta: reject 1548 if _vec_inf_norm(r0_vec, MLDSA_K) >= (MLDSA_GAMMA2 - MLDSA_BETA) { 1549 kappa = kappa + MLDSA_L 1550 attempt = attempt + 1 1551 } else { 1552 // 10. ct0 = INVNTT(c_hat ∘ t0_hat) 1553 var t0i: i64 = 0 1554 while t0i < MLDSA_K { 1555 var pk: i64 = 0 1556 while pk < MLDSA_N { 1557 let pv: i64 = _fqmul(_i32_load_le(c_hat, pk), 1558 _i32_load_le((t0_vec as i64 + t0i * POLY_BUF) as *u8, pk)) 1559 _i32_store_le((ct0 as i64 + t0i * POLY_BUF) as *u8, pk, pv) 1560 pk = pk + 1 1561 } 1562 nx_mldsa_invntt((ct0 as i64 + t0i * POLY_BUF) as *u8) 1563 // canonicalise ct0 per PQClean (poly_reduce after invntt_tomont) 1564 var ck: i64 = 0 1565 while ck < MLDSA_N { 1566 let cv: i64 = _reduce32(_i32_load_le((ct0 as i64 + t0i * POLY_BUF) as *u8, ck)) 1567 _i32_store_le((ct0 as i64 + t0i * POLY_BUF) as *u8, ck, cv) 1568 ck = ck + 1 1569 } 1570 t0i = t0i + 1 1571 } 1572 // 11. if ||ct0||_inf >= gamma2: reject 1573 if _vec_inf_norm(ct0, MLDSA_K) >= MLDSA_GAMMA2 { 1574 kappa = kappa + MLDSA_L 1575 attempt = attempt + 1 1576 } else { 1577 // 12. h = MakeHint(r0 + ct0, w1) per PQClean parameterization 1578 // (sign and verify must match; verify uses the same hint 1579 // against UseHint(h, A*z - c*t1*2^d) to recover w1 = HighBits(w)). 1580 var hi: i64 = 0 1581 var total_weight: i64 = 0 1582 while hi < MLDSA_K { 1583 // hint_a0 = r0 + ct0 (reduce32 to keep in canonical range) 1584 let hint_a0: *u8 = (cs1 as i64 + hi * POLY_BUF) as *u8 // reuse cs1 slot 1585 var nk: i64 = 0 1586 while nk < MLDSA_N { 1587 let r0v: i64 = _i32_load_le((r0_vec as i64 + hi * POLY_BUF) as *u8, nk) 1588 let ct0v: i64 = _i32_load_le((ct0 as i64 + hi * POLY_BUF) as *u8, nk) 1589 let sv: i64 = _reduce32(r0v + ct0v) 1590 _i32_store_le(hint_a0, nk, sv) 1591 nk = nk + 1 1592 } 1593 let weight: i64 = nx_mldsa_poly_make_hint( 1594 (h_vec as i64 + hi * POLY_BUF) as *u8, 1595 hint_a0, 1596 (w1_vec as i64 + hi * POLY_BUF) as *u8) 1597 total_weight = total_weight + weight 1598 hi = hi + 1 1599 } 1600 // 13. if total_weight > omega: reject 1601 if total_weight > MLDSA_OMEGA { 1602 kappa = kappa + MLDSA_L 1603 attempt = attempt + 1 1604 } else { 1605 // 14. sigEncode: c_tilde (48) || pack_z(z) (l*640=3200) 1606 // || pack_hint(h) (61) = 3309 bytes 1607 var sii: i64 = 0 1608 while sii < 48 { sig_out[sii] = c_tilde[sii]; sii = sii + 1 } 1609 var zi: i64 = 0 1610 while zi < MLDSA_L { 1611 _pack_z_poly((sig_out as i64 + 48 + zi * 640) as *u8, 1612 (z_vec as i64 + zi * POLY_BUF) as *u8) 1613 zi = zi + 1 1614 } 1615 let pack_rc: i64 = _pack_hint_vec((sig_out as i64 + 48 + MLDSA_L * 640) as *u8, 1616 h_vec) 1617 if pack_rc < 0 { return -3 } 1618 return 0 1619 } 1620 } 1621 } 1622 } 1623 } 1624 return -1 // rejection loop exhausted 1625} 1626 1627// L136 KAT export: sign a fixed message + check basic structural properties. 1628// Returns 0 on success, non-zero bitmap. 1629// bit 0: keygen failed 1630// bit 1: sign failed 1631// bit 2: sig[0..48] (c_tilde) all zero 1632// bit 3: sig[48..3248] (z packed) all zero 1633// bit 4: different messages -> same signature (would mean nondeterministic-bug) 1634func nx_mldsa_sign_test(seed_32: *u8, msg_32: *u8, scratch: *u8) -> i64 { 1635 let pk: *u8 = (scratch as i64 + 110000) as *u8 1636 let sk: *u8 = (scratch as i64 + 112000) as *u8 1637 let sig1: *u8 = (scratch as i64 + 117000) as *u8 1638 let sig2: *u8 = (scratch as i64 + 121000) as *u8 1639 var fail: i64 = 0 1640 let rc_kg: i64 = nx_mldsa_keygen(seed_32, scratch, pk, sk) 1641 if rc_kg != 0 { fail = fail | 1 } 1642 let rc_s1: i64 = nx_mldsa_sign(sk, msg_32, 32, scratch, sig1) 1643 if rc_s1 != 0 { fail = fail | 2 } 1644 // structural: c_tilde must not be all zero 1645 var c_zero: i64 = 1 1646 var i: i64 = 0 1647 while i < 48 { 1648 if sig1[i] != 0 { c_zero = 0 } 1649 i = i + 1 1650 } 1651 if c_zero != 0 { fail = fail | 4 } 1652 // z packed must not be all zero 1653 var z_zero: i64 = 1 1654 var j: i64 = 48 1655 while j < 48 + MLDSA_L * 640 { 1656 if sig1[j] != 0 { z_zero = 0 } 1657 j = j + 1 1658 } 1659 if z_zero != 0 { fail = fail | 8 } 1660 // sign a DIFFERENT msg, check sig differs 1661 let alt_msg: *u8 = (scratch as i64 + 126000) as *u8 1662 var a: i64 = 0 1663 while a < 32 { alt_msg[a] = msg_32[a] ^ 0xff; a = a + 1 } 1664 let rc_s2: i64 = nx_mldsa_sign(sk, alt_msg, 32, scratch, sig2) 1665 if rc_s2 != 0 { fail = fail | 2 } 1666 var same: i64 = 1 1667 var k: i64 = 0 1668 while k < 100 { 1669 if sig1[k] != sig2[k] { same = 0 } 1670 k = k + 1 1671 } 1672 if same != 0 { fail = fail | 16 } 1673 return fail 1674} 1675 1676// ============================================================================ 1677// SECTION 5: KAT export -- NTT round-trip self-test 1678// ============================================================================ 1679// Verifies INVNTT(NTT(poly)) ≡ poly (mod q) for a deterministic poly. 1680// Per PQClean convention: invntt_tomont leaves the result multiplied by 1681// the Montgomery factor; we account by checking equivalence mod q 1682// directly (the caller fills the input and compares post-round-trip 1683// via reduce32+canon mod q). 1684// 1685// scratch needs 1024 bytes (one poly buffer). 1686// Returns 0 on success, non-zero bitmap of failure axes. 1687// === L137 ML-DSA-65 verify === 1688// 1689// Unpack 256 10-bit coefficients (range [0, 2^10)) from 320 bytes. 1690// Inverse of _pack_t1_poly. Outputs as i32 LE in `out_poly` (1024 B). 1691func _unpack_t1_poly(out_poly: *u8, packed: *u8) -> i64 { 1692 var i: i64 = 0 1693 while i < 64 { // 64 groups of 4 coefs = 256 1694 let off: i64 = i * 5 1695 let b0: i64 = packed[off + 0] 1696 let b1: i64 = packed[off + 1] 1697 let b2: i64 = packed[off + 2] 1698 let b3: i64 = packed[off + 3] 1699 let b4: i64 = packed[off + 4] 1700 let c0: i64 = (b0 | (b1 << 8)) & 0x3ff 1701 let c1: i64 = ((b1 >> 2) | (b2 << 6)) & 0x3ff 1702 let c2: i64 = ((b2 >> 4) | (b3 << 4)) & 0x3ff 1703 let c3: i64 = ((b3 >> 6) | (b4 << 2)) & 0x3ff 1704 _i32_store_le(out_poly, i*4 + 0, c0) 1705 _i32_store_le(out_poly, i*4 + 1, c1) 1706 _i32_store_le(out_poly, i*4 + 2, c2) 1707 _i32_store_le(out_poly, i*4 + 3, c3) 1708 i = i + 1 1709 } 1710 return 0 1711} 1712 1713// nx_mldsa_verify -- FIPS 204 §8 Verify(pk, M, sigma) -> {0 (valid), 1 (invalid)} 1714// Returns 0 on accept, positive bitmap on reject (so callers can debug): 1715// bit 0: z norm out of range 1716// bit 1: hint vec malformed (weight > omega or unpack failed) 1717// bit 2: c_tilde mismatch (the cryptographic check) 1718// 1719// pk layout (1952 B): rho(32) || t1_packed(K*320 = 1920) 1720// sig layout (3309 B): c_tilde(48) || z_packed(L*640 = 3200) || hint_packed(61) 1721// 1722// Scratch: requires >= 96 KiB. Layout documented per scratch_range below. 1723func nx_mldsa_verify(pk: *u8, msg: *u8, msg_len: i64, 1724 sig: *u8, scratch: *u8) -> i64 { 1725 let keccak_state: *u8 = scratch // scratch_range: keccak 0..439 1726 let shake_buf: *u8 = (scratch as i64 + 512) as *u8 // scratch_range: shake_buf 512..2559 1727 // Unpacked vectors 1728 let t1_vec: *u8 = (scratch as i64 + 3072) as *u8 // scratch_range: t1_vec 3072..9215 (6 * 1024) 1729 let z_vec: *u8 = (scratch as i64 + 9216) as *u8 // scratch_range: z_vec 9216..14335 (5 * 1024) 1730 let h_vec: *u8 = (scratch as i64 + 14336) as *u8 // scratch_range: h_vec 14336..20479 (6 * 1024) 1731 // Working singletons 1732 let A_entry: *u8 = (scratch as i64 + 20480) as *u8 // scratch_range: A_entry 20480..21503 1733 let acc_poly: *u8 = (scratch as i64 + 21504) as *u8 // scratch_range: acc 21504..22527 1734 let c_poly: *u8 = (scratch as i64 + 22528) as *u8 // scratch_range: c_poly 22528..23551 1735 let c_hat: *u8 = (scratch as i64 + 23552) as *u8 // scratch_range: c_hat 23552..24575 1736 let w1_poly: *u8 = (scratch as i64 + 24576) as *u8 // scratch_range: w1_poly 24576..25599 1737 // Hashing scratch 1738 let tr: *u8 = (scratch as i64 + 25600) as *u8 // scratch_range: tr 25600..25663 (64) 1739 let mu: *u8 = (scratch as i64 + 25664) as *u8 // scratch_range: mu 25664..25727 (64) 1740 let mu_msg_buf: *u8 = (scratch as i64 + 25728) as *u8 // scratch_range: mu_msg 25728..33919 (8192 cap) 1741 let w1_packed: *u8 = (scratch as i64 + 33920) as *u8 // scratch_range: w1_pkd 33920..34687 (k*128 = 768) 1742 let c_tilde_rec: *u8 = (scratch as i64 + 34688) as *u8 // scratch_range: c_til_r 34688..34735 (48) 1743 1744 let rho: *u8 = pk 1745 let t1_packed: *u8 = (pk as i64 + 32) as *u8 1746 let c_tilde: *u8 = sig 1747 let z_packed: *u8 = (sig as i64 + 48) as *u8 1748 let h_packed: *u8 = (sig as i64 + 48 + MLDSA_L * 640) as *u8 1749 1750 var fail_mask: i64 = 0 1751 1752 // 1. Decode z; check ||z||_inf bound. 1753 var zi: i64 = 0 1754 while zi < MLDSA_L { 1755 _unpack_z_poly((z_vec as i64 + zi * POLY_BUF) as *u8, 1756 (z_packed as i64 + zi * 640) as *u8) 1757 zi = zi + 1 1758 } 1759 if _vec_inf_norm(z_vec, MLDSA_L) >= (MLDSA_GAMMA1 - MLDSA_BETA) { 1760 fail_mask = fail_mask | 1 1761 } 1762 1763 // 2. Decode hint; rejects if encoding malformed or weight > omega. 1764 if _unpack_hint_vec(h_vec, h_packed) != 0 { 1765 fail_mask = fail_mask | 2 1766 } 1767 1768 // 3. Decode t1. 1769 var ti: i64 = 0 1770 while ti < MLDSA_K { 1771 _unpack_t1_poly((t1_vec as i64 + ti * POLY_BUF) as *u8, 1772 (t1_packed as i64 + ti * 320) as *u8) 1773 ti = ti + 1 1774 } 1775 1776 // 4. tr = SHAKE256(pk, 64). 1777 _shake256(pk, 1952, keccak_state, tr, 64) 1778 1779 // 5. mu = SHAKE256(tr || msg, 64). msg_len capped at 8128. 1780 if msg_len > 8128 { return -2 } 1781 var tri: i64 = 0 1782 while tri < 64 { mu_msg_buf[tri] = tr[tri]; tri = tri + 1 } 1783 var mi: i64 = 0 1784 while mi < msg_len { mu_msg_buf[64 + mi] = msg[mi]; mi = mi + 1 } 1785 _shake256(mu_msg_buf, 64 + msg_len, keccak_state, mu, 64) 1786 1787 // 6. c = SampleInBall(c_tilde) ; c_hat = NTT(c). 1788 nx_mldsa_sample_in_ball(c_poly, c_tilde, keccak_state) 1789 var ci: i64 = 0 1790 while ci < MLDSA_N { 1791 _i32_store_le(c_hat, ci, _i32_load_le(c_poly, ci)) 1792 ci = ci + 1 1793 } 1794 nx_mldsa_ntt(c_hat) 1795 1796 // 7. NTT every z poly in place. z_vec slots become z_hat slots. 1797 var zh: i64 = 0 1798 while zh < MLDSA_L { 1799 nx_mldsa_ntt((z_vec as i64 + zh * POLY_BUF) as *u8) 1800 zh = zh + 1 1801 } 1802 1803 // 8. NTT every (t1 * 2^d) poly in place. t1_vec slots become t1_hat slots. 1804 var th: i64 = 0 1805 while th < MLDSA_K { 1806 let tp: *u8 = (t1_vec as i64 + th * POLY_BUF) as *u8 1807 var sk: i64 = 0 1808 while sk < MLDSA_N { 1809 _i32_store_le(tp, sk, _i32_load_le(tp, sk) << MLDSA_D) 1810 sk = sk + 1 1811 } 1812 // L137 debug: snapshot t1_vec[0] AFTER shift, BEFORE NTT 1813 if th == 0 { 1814 let dbg_t1_shifted: i64 = scratch as i64 + 205120 1815 var ds: i64 = 0 1816 while ds < MLDSA_N { 1817 _i32_store_le(dbg_t1_shifted as *u8, ds, _i32_load_le(tp, ds)) 1818 ds = ds + 1 1819 } 1820 } 1821 nx_mldsa_ntt(tp) 1822 // L137 debug: snapshot t1_vec[0] AFTER NTT 1823 if th == 0 { 1824 let dbg_t1_ntt: i64 = scratch as i64 + 206144 1825 var dn: i64 = 0 1826 while dn < MLDSA_N { 1827 _i32_store_le(dbg_t1_ntt as *u8, dn, _i32_load_le(tp, dn)) 1828 dn = dn + 1 1829 } 1830 } 1831 th = th + 1 1832 } 1833 1834 // 9. For each row i in K: 1835 // acc = Σ_j A[i][j] * z_hat[j] - c_hat * t1_hat[i] 1836 // INVNTT(acc) -> approx of w 1837 // w1' = UseHint(h[i], approx_w_canonicalised) 1838 // pack w1' into w1_packed[i*128 .. ] 1839 var ri: i64 = 0 1840 while ri < MLDSA_K { 1841 // acc = 0 1842 var z0: i64 = 0 1843 while z0 < MLDSA_N { _i32_store_le(acc_poly, z0, 0); z0 = z0 + 1 } 1844 // Sum Σ_j A[i][j] * z_hat[j] 1845 var jj: i64 = 0 1846 while jj < MLDSA_L { 1847 _expand_a_entry(A_entry, rho, jj, ri, keccak_state, shake_buf) 1848 var pk_i: i64 = 0 1849 while pk_i < MLDSA_N { 1850 let prod: i64 = _fqmul(_i32_load_le(A_entry, pk_i), 1851 _i32_load_le((z_vec as i64 + jj * POLY_BUF) as *u8, pk_i)) 1852 _i32_store_le(acc_poly, pk_i, 1853 _i32_load_le(acc_poly, pk_i) + prod) 1854 pk_i = pk_i + 1 1855 } 1856 jj = jj + 1 1857 } 1858 // L137 debug: save row-0 acc AFTER Σ A*z but BEFORE the c*t1 subtract. 1859 if ri == 0 { 1860 let dbg_az: i64 = scratch as i64 + 203072 1861 var dai: i64 = 0 1862 while dai < MLDSA_N { 1863 _i32_store_le(dbg_az as *u8, dai, _i32_load_le(acc_poly, dai)) 1864 dai = dai + 1 1865 } 1866 } 1867 // Subtract c_hat * t1_hat[i] 1868 var sk2: i64 = 0 1869 while sk2 < MLDSA_N { 1870 let pv: i64 = _fqmul(_i32_load_le(c_hat, sk2), 1871 _i32_load_le((t1_vec as i64 + ri * POLY_BUF) as *u8, sk2)) 1872 // L137 debug: save c_hat * t1_hat[0] (the value being subtracted) for row 0 1873 if ri == 0 { 1874 let dbg_ct1: i64 = scratch as i64 + 204096 1875 _i32_store_le(dbg_ct1 as *u8, sk2, pv) 1876 } 1877 _i32_store_le(acc_poly, sk2, 1878 _i32_load_le(acc_poly, sk2) - pv) 1879 sk2 = sk2 + 1 1880 } 1881 // Reduce32 in NTT domain BEFORE invntt -- PQClean 1882 // polyveck_reduce(&w1) before polyveck_invntt_tomont(&w1). 1883 var redi: i64 = 0 1884 while redi < MLDSA_N { 1885 _i32_store_le(acc_poly, redi, _reduce32(_i32_load_le(acc_poly, redi))) 1886 redi = redi + 1 1887 } 1888 // INVNTT in place -> approx w 1889 nx_mldsa_invntt(acc_poly) 1890 // Canonicalise to [0, q) 1891 var ck: i64 = 0 1892 while ck < MLDSA_N { 1893 var v: i64 = _reduce32(_i32_load_le(acc_poly, ck)) 1894 if v < 0 { v = v + MLDSA_Q } 1895 _i32_store_le(acc_poly, ck, v) 1896 ck = ck + 1 1897 } 1898 // L137 debug: save row-0 acc_poly to scratch+202048 for JS inspect. 1899 if ri == 0 { 1900 let dbg_off: i64 = scratch as i64 + 202048 1901 var di: i64 = 0 1902 while di < MLDSA_N { 1903 _i32_store_le(dbg_off as *u8, di, _i32_load_le(acc_poly, di)) 1904 di = di + 1 1905 } 1906 } 1907 // w1' = UseHint(h[i], acc) 1908 nx_mldsa_poly_use_hint(w1_poly, acc_poly, 1909 (h_vec as i64 + ri * POLY_BUF) as *u8) 1910 // Pack into w1_packed. 1911 _pack_w1_poly((w1_packed as i64 + ri * 128) as *u8, w1_poly) 1912 ri = ri + 1 1913 } 1914 1915 // 10. c_tilde' = SHAKE256(mu || w1_packed, 48). Reuse mu_msg_buf. 1916 var mui: i64 = 0 1917 while mui < 64 { mu_msg_buf[mui] = mu[mui]; mui = mui + 1 } 1918 var wpi: i64 = 0 1919 let w1_total: i64 = MLDSA_K * 128 1920 while wpi < w1_total { mu_msg_buf[64 + wpi] = w1_packed[wpi]; wpi = wpi + 1 } 1921 _shake256(mu_msg_buf, 64 + w1_total, keccak_state, c_tilde_rec, 48) 1922 1923 // 11. Constant-byte compare c_tilde_rec vs c_tilde. 1924 var diff: i64 = 0 1925 var cti: i64 = 0 1926 while cti < 48 { 1927 if c_tilde_rec[cti] != c_tilde[cti] { diff = 1 } 1928 cti = cti + 1 1929 } 1930 if diff != 0 { fail_mask = fail_mask | 4 } 1931 1932 return fail_mask 1933} 1934 1935// L137 KAT export: keygen -> sign -> verify round-trip. Returns 0 on 1936// success, non-zero bitmap on failure. 1937// bit 0: keygen failed 1938// bit 1: sign failed 1939// bit 2: verify rejected the legitimate signature 1940// bit 3: verify accepted a tampered signature (flipped sig byte 0) 1941// bit 4: verify accepted a wrong-message signature 1942func nx_mldsa_verify_test(seed_32: *u8, msg_32: *u8, scratch: *u8) -> i64 { 1943 let pk: *u8 = (scratch as i64 + 100000) as *u8 // scratch_range: pk 100000..101951 (1952) 1944 let sk: *u8 = (scratch as i64 + 102000) as *u8 // scratch_range: sk 102000..106031 (4032) 1945 let sig: *u8 = (scratch as i64 + 106500) as *u8 // scratch_range: sig 106500..109808 (3309) 1946 let inner: *u8 = scratch // first 96K used by keygen/sign/verify ops 1947 1948 var fail: i64 = 0 1949 1950 // 1. keygen 1951 if nx_mldsa_keygen(seed_32, inner, pk, sk) != 0 { fail = fail | 1; return fail } 1952 1953 // 2. sign 1954 if nx_mldsa_sign(sk, msg_32, 32, inner, sig) != 0 { fail = fail | 2; return fail } 1955 1956 // 3. verify legitimate -> expect 0 1957 let v_ok: i64 = nx_mldsa_verify(pk, msg_32, 32, sig, inner) 1958 if v_ok != 0 { fail = fail | 4 } 1959 1960 // 4. tamper: flip a byte of c_tilde, expect non-zero 1961 let orig_byte: i64 = sig[0] 1962 sig[0] = (orig_byte ^ 0xff) & 0xff 1963 let v_tamper: i64 = nx_mldsa_verify(pk, msg_32, 32, sig, inner) 1964 sig[0] = orig_byte 1965 if v_tamper == 0 { fail = fail | 8 } 1966 1967 // 5. wrong message: flip a byte of msg, expect non-zero 1968 let orig_mb: i64 = msg_32[0] 1969 msg_32[0] = (orig_mb ^ 0xff) & 0xff 1970 let v_wrong: i64 = nx_mldsa_verify(pk, msg_32, 32, sig, inner) 1971 msg_32[0] = orig_mb 1972 if v_wrong == 0 { fail = fail | 16 } 1973 1974 return fail 1975} 1976 1977// Debug: L=5 accumulated basemul + invntt with VERIFY-realistic magnitudes. 1978// a values in [0, q) like A entries from rej_uniform. 1979// b values in (-gamma1, gamma1] like z from unpack_z. 1980// Returns # mismatched coefs. 1981func nx_mldsa_acc_basemul_large_test(scratch: *u8) -> i64 { 1982 let L: i64 = 5 1983 let a: *u8 = scratch 1984 let b: *u8 = (scratch as i64 + L * POLY_BUF) as *u8 1985 let a_ntt: *u8 = (scratch as i64 + 2 * L * POLY_BUF) as *u8 1986 let b_ntt: *u8 = (scratch as i64 + 3 * L * POLY_BUF) as *u8 1987 let acc: *u8 = (scratch as i64 + 4 * L * POLY_BUF) as *u8 1988 let direct: *u8 = (scratch as i64 + 4 * L * POLY_BUF + POLY_BUF) as *u8 1989 var p: i64 = 0 1990 while p < L { 1991 var i: i64 = 0 1992 while i < MLDSA_N { 1993 // a in [0, q): pseudo-uniform pattern 1994 let av: i64 = (i * 12345 + p * 7891) % MLDSA_Q 1995 // b signed in (-gamma1, gamma1] 1996 var bv: i64 = ((i * 31 + p * 17) % (2 * MLDSA_GAMMA1)) - MLDSA_GAMMA1 + 1 1997 _i32_store_le((a as i64 + p * POLY_BUF) as *u8, i, av) 1998 _i32_store_le((b as i64 + p * POLY_BUF) as *u8, i, bv) 1999 _i32_store_le((a_ntt as i64 + p * POLY_BUF) as *u8, i, av) 2000 _i32_store_le((b_ntt as i64 + p * POLY_BUF) as *u8, i, bv) 2001 i = i + 1 2002 } 2003 nx_mldsa_ntt((a_ntt as i64 + p * POLY_BUF) as *u8) 2004 nx_mldsa_ntt((b_ntt as i64 + p * POLY_BUF) as *u8) 2005 p = p + 1 2006 } 2007 var z: i64 = 0 2008 while z < MLDSA_N { _i32_store_le(acc, z, 0); z = z + 1 } 2009 var pj: i64 = 0 2010 while pj < L { 2011 var k: i64 = 0 2012 while k < MLDSA_N { 2013 let pv: i64 = _fqmul( 2014 _i32_load_le((a_ntt as i64 + pj * POLY_BUF) as *u8, k), 2015 _i32_load_le((b_ntt as i64 + pj * POLY_BUF) as *u8, k)) 2016 _i32_store_le(acc, k, _i32_load_le(acc, k) + pv) 2017 k = k + 1 2018 } 2019 pj = pj + 1 2020 } 2021 var ri: i64 = 0 2022 while ri < MLDSA_N { 2023 _i32_store_le(acc, ri, _reduce32(_i32_load_le(acc, ri))) 2024 ri = ri + 1 2025 } 2026 nx_mldsa_invntt(acc) 2027 var ci: i64 = 0 2028 while ci < MLDSA_N { 2029 var v: i64 = _reduce32(_i32_load_le(acc, ci)) 2030 if v < 0 { v = v + MLDSA_Q } 2031 _i32_store_le(acc, ci, v) 2032 ci = ci + 1 2033 } 2034 // Direct convolution mod q 2035 var dk: i64 = 0 2036 while dk < MLDSA_N { 2037 var sum: i64 = 0 2038 var dp: i64 = 0 2039 while dp < L { 2040 var di: i64 = 0 2041 while di < MLDSA_N { 2042 let dj: i64 = dk - di 2043 let av: i64 = _i32_load_le((a as i64 + dp * POLY_BUF) as *u8, di) 2044 if dj >= 0 { 2045 let bv: i64 = _i32_load_le((b as i64 + dp * POLY_BUF) as *u8, dj) 2046 // Reduce per-term to avoid sum overflow (av * bv can be q * gamma1 ~ 4e12) 2047 sum = (sum + (av * bv) % MLDSA_Q) % MLDSA_Q 2048 } else { 2049 let bv: i64 = _i32_load_le((b as i64 + dp * POLY_BUF) as *u8, dj + MLDSA_N) 2050 sum = (sum - (av * bv) % MLDSA_Q) % MLDSA_Q 2051 } 2052 di = di + 1 2053 } 2054 dp = dp + 1 2055 } 2056 var canon: i64 = sum % MLDSA_Q 2057 if canon < 0 { canon = canon + MLDSA_Q } 2058 _i32_store_le(direct, dk, canon) 2059 dk = dk + 1 2060 } 2061 var diffs: i64 = 0 2062 var c: i64 = 0 2063 while c < MLDSA_N { 2064 if _i32_load_le(acc, c) != _i32_load_le(direct, c) { 2065 diffs = diffs + 1 2066 } 2067 c = c + 1 2068 } 2069 return diffs 2070} 2071 2072// Debug: L=5-term ACCUMULATED basemul + invntt round-trip. 2073// This mirrors verify's matrix-row computation: Σ_{j=0..L-1} a[j] * b[j]. 2074// Inputs in standard form; output compared to direct N²L convolution sum. 2075// Returns # mismatched coefs. 2076func nx_mldsa_acc_basemul_round_trip_test(scratch: *u8) -> i64 { 2077 let L: i64 = 5 2078 let a: *u8 = scratch 2079 let b: *u8 = (scratch as i64 + L * POLY_BUF) as *u8 2080 let a_ntt: *u8 = (scratch as i64 + 2 * L * POLY_BUF) as *u8 2081 let b_ntt: *u8 = (scratch as i64 + 3 * L * POLY_BUF) as *u8 2082 let acc: *u8 = (scratch as i64 + 4 * L * POLY_BUF) as *u8 2083 let direct: *u8 = (scratch as i64 + 4 * L * POLY_BUF + POLY_BUF) as *u8 2084 // Fill a[0..L], b[0..L] with small signed pattern 2085 var p: i64 = 0 2086 while p < L { 2087 var i: i64 = 0 2088 while i < MLDSA_N { 2089 var av: i64 = (p * 13 + i * 7 + 3) % 11 2090 av = av - 5 2091 var bv: i64 = (p * 17 + i * 11 + 5) % 11 2092 bv = bv - 5 2093 _i32_store_le((a as i64 + p * POLY_BUF) as *u8, i, av) 2094 _i32_store_le((b as i64 + p * POLY_BUF) as *u8, i, bv) 2095 _i32_store_le((a_ntt as i64 + p * POLY_BUF) as *u8, i, av) 2096 _i32_store_le((b_ntt as i64 + p * POLY_BUF) as *u8, i, bv) 2097 i = i + 1 2098 } 2099 nx_mldsa_ntt((a_ntt as i64 + p * POLY_BUF) as *u8) 2100 nx_mldsa_ntt((b_ntt as i64 + p * POLY_BUF) as *u8) 2101 p = p + 1 2102 } 2103 // Accumulate Σ basemul(a_ntt[p], b_ntt[p]) into acc (NTT form) 2104 var z: i64 = 0 2105 while z < MLDSA_N { _i32_store_le(acc, z, 0); z = z + 1 } 2106 var pj: i64 = 0 2107 while pj < L { 2108 var k: i64 = 0 2109 while k < MLDSA_N { 2110 let pv: i64 = _fqmul( 2111 _i32_load_le((a_ntt as i64 + pj * POLY_BUF) as *u8, k), 2112 _i32_load_le((b_ntt as i64 + pj * POLY_BUF) as *u8, k)) 2113 _i32_store_le(acc, k, _i32_load_le(acc, k) + pv) 2114 k = k + 1 2115 } 2116 pj = pj + 1 2117 } 2118 // reduce32, invntt, canonicalize 2119 var ri: i64 = 0 2120 while ri < MLDSA_N { 2121 _i32_store_le(acc, ri, _reduce32(_i32_load_le(acc, ri))) 2122 ri = ri + 1 2123 } 2124 nx_mldsa_invntt(acc) 2125 var ci: i64 = 0 2126 while ci < MLDSA_N { 2127 var v: i64 = _reduce32(_i32_load_le(acc, ci)) 2128 if v < 0 { v = v + MLDSA_Q } 2129 _i32_store_le(acc, ci, v) 2130 ci = ci + 1 2131 } 2132 // Direct Σ_p (a[p] * b[p] cyclic in (X^N+1)) per coef 2133 var dk: i64 = 0 2134 while dk < MLDSA_N { 2135 var sum: i64 = 0 2136 var dp: i64 = 0 2137 while dp < L { 2138 var di: i64 = 0 2139 while di < MLDSA_N { 2140 let dj: i64 = dk - di 2141 let av: i64 = _i32_load_le((a as i64 + dp * POLY_BUF) as *u8, di) 2142 if dj >= 0 { 2143 let bv: i64 = _i32_load_le((b as i64 + dp * POLY_BUF) as *u8, dj) 2144 sum = sum + av * bv 2145 } else { 2146 let bv: i64 = _i32_load_le((b as i64 + dp * POLY_BUF) as *u8, dj + MLDSA_N) 2147 sum = sum - av * bv 2148 } 2149 di = di + 1 2150 } 2151 dp = dp + 1 2152 } 2153 var canon: i64 = sum % MLDSA_Q 2154 if canon < 0 { canon = canon + MLDSA_Q } 2155 _i32_store_le(direct, dk, canon) 2156 dk = dk + 1 2157 } 2158 var diffs: i64 = 0 2159 var c: i64 = 0 2160 while c < MLDSA_N { 2161 if _i32_load_le(acc, c) != _i32_load_le(direct, c) { 2162 diffs = diffs + 1 2163 } 2164 c = c + 1 2165 } 2166 return diffs 2167} 2168 2169// Debug: basemul + invntt round-trip. 2170// Compute a, b polys in standard form; do ntt(a), ntt(b), basemul, invntt, 2171// canonicalize. Compare to (a*b mod (X^N+1)) standard form, computed 2172// directly by O(N^2) convolution in Zq. Returns # mismatched coefs. 2173// 2174// If THIS fails but ntt-only round-trip passes, the bug is in basemul 2175// or in invntt's handling of post-basemul Montgomery-form values. 2176func nx_mldsa_basemul_round_trip_test(scratch: *u8) -> i64 { 2177 let a: *u8 = scratch 2178 let b: *u8 = (scratch as i64 + POLY_BUF) as *u8 2179 let a_ntt: *u8 = (scratch as i64 + 2 * POLY_BUF) as *u8 2180 let b_ntt: *u8 = (scratch as i64 + 3 * POLY_BUF) as *u8 2181 let prod_n: *u8 = (scratch as i64 + 4 * POLY_BUF) as *u8 // NTT(a*b) 2182 let prod_d: *u8 = (scratch as i64 + 5 * POLY_BUF) as *u8 // direct a*b mod (X^N+1) 2183 // Fill a, b with small signed pattern 2184 var i: i64 = 0 2185 while i < MLDSA_N { 2186 var av: i64 = (i * 7 + 3) % 9 2187 av = av - 4 // a_i in [-4, 4] 2188 var bv: i64 = (i * 11 + 5) % 9 2189 bv = bv - 4 // b_i in [-4, 4] 2190 _i32_store_le(a, i, av) 2191 _i32_store_le(b, i, bv) 2192 _i32_store_le(a_ntt, i, av) 2193 _i32_store_le(b_ntt, i, bv) 2194 i = i + 1 2195 } 2196 // NTT both 2197 nx_mldsa_ntt(a_ntt) 2198 nx_mldsa_ntt(b_ntt) 2199 // basemul (per-coef fqmul) 2200 nx_mldsa_basemul(prod_n, a_ntt, b_ntt) 2201 // invntt + canonicalize 2202 nx_mldsa_invntt(prod_n) 2203 var k: i64 = 0 2204 while k < MLDSA_N { 2205 var v: i64 = _reduce32(_i32_load_le(prod_n, k)) 2206 if v < 0 { v = v + MLDSA_Q } 2207 _i32_store_le(prod_n, k, v) 2208 k = k + 1 2209 } 2210 // Direct cyclic convolution: prod_d[k] = Σ_{i+j ≡ k (mod N), with sign flip for wrap} 2211 // For X^N + 1: prod_d[k] = Σ_{i=0..k} a[i]*b[k-i] - Σ_{i=k+1..N-1} a[i]*b[N+k-i] 2212 var dk: i64 = 0 2213 while dk < MLDSA_N { 2214 var sum: i64 = 0 2215 var di: i64 = 0 2216 while di < MLDSA_N { 2217 let dj: i64 = dk - di 2218 let av: i64 = _i32_load_le(a, di) 2219 if dj >= 0 { 2220 let bv: i64 = _i32_load_le(b, dj) 2221 sum = sum + av * bv 2222 } else { 2223 let bv: i64 = _i32_load_le(b, dj + MLDSA_N) 2224 sum = sum - av * bv 2225 } 2226 di = di + 1 2227 } 2228 // Canonicalize sum mod q to [0, q) 2229 var canon: i64 = sum % MLDSA_Q 2230 if canon < 0 { canon = canon + MLDSA_Q } 2231 _i32_store_le(prod_d, dk, canon) 2232 dk = dk + 1 2233 } 2234 // Compare prod_n to prod_d 2235 var diffs: i64 = 0 2236 var c: i64 = 0 2237 while c < MLDSA_N { 2238 if _i32_load_le(prod_n, c) != _i32_load_le(prod_d, c) { 2239 diffs = diffs + 1 2240 } 2241 c = c + 1 2242 } 2243 return diffs 2244} 2245 2246// Debug: NTT round-trip on a poly with MIXED positive/negative coefs. 2247// Existing nx_mldsa_ntt_round_trip_test only covers positive inputs. 2248// z in ML-DSA-65 has range (-gamma1, gamma1] so signed values matter. 2249// Returns 0 if all coefs round-trip to (original * MONT) mod q, else 2250// the number of mismatched coefs. 2251func nx_mldsa_ntt_signed_round_trip_test(scratch: *u8) -> i64 { 2252 let poly: *u8 = scratch 2253 let saved: *u8 = (scratch as i64 + POLY_BUF) as *u8 2254 // Fill with alternating +/- pattern bounded by gamma1 = 524288. 2255 // pattern: ((i * 31 + 17) % gamma1) * (((i & 1) == 0) ? 1 : -1) 2256 var i: i64 = 0 2257 while i < MLDSA_N { 2258 var v: i64 = (i * 31 + 17) % MLDSA_GAMMA1 2259 if (i & 1) != 0 { v = 0 - v } 2260 _i32_store_le(poly, i, v) 2261 _i32_store_le(saved, i, v) 2262 i = i + 1 2263 } 2264 nx_mldsa_ntt(poly) 2265 nx_mldsa_invntt(poly) 2266 let MONT: i64 = 4193792 2267 var diffs: i64 = 0 2268 var j: i64 = 0 2269 while j < MLDSA_N { 2270 var a: i64 = _reduce32(_i32_load_le(poly, j)) 2271 if a < 0 { a = a + MLDSA_Q } 2272 var orig: i64 = _i32_load_le(saved, j) 2273 // (orig * MONT) mod q, canonicalize to [0, q) 2274 var expected: i64 = (orig * MONT) % MLDSA_Q 2275 if expected < 0 { expected = expected + MLDSA_Q } 2276 if a != expected { diffs = diffs + 1 } 2277 j = j + 1 2278 } 2279 return diffs 2280} 2281 2282// Debug: unpack t1 row 0 from a pk (1952 bytes) and write the 256 2283// i32 coefs into out_poly. JS smoke checks that all coefs are in 2284// [0, 1024) -- the range required by spec. 2285func nx_mldsa_unpack_t1_debug(pk: *u8, out_poly: *u8) -> i64 { 2286 let t1_packed: *u8 = (pk as i64 + 32) as *u8 2287 _unpack_t1_poly(out_poly, t1_packed) 2288 return 0 2289} 2290 2291// Debug: take an unpacked t1 row (256 i32 in [0, 1024)), pack it via 2292// the same packer keygen uses, write 320 bytes to out_packed. JS 2293// smoke compares out_packed to pk[32..32+320] to verify round-trip. 2294func nx_mldsa_pack_t1_debug(in_poly: *u8, out_packed: *u8) -> i64 { 2295 _pack_t1_poly(out_packed, in_poly) 2296 return 0 2297} 2298 2299// Debug: keygen + sign + ONE verify, leaving sign's w1_packed at 2300// scratch+91360 and verify's recovered w1_packed at scratch+33920 so 2301// the JS smoke can diff them. Returns the verify fail_mask. 2302func nx_mldsa_verify_debug_one(seed_32: *u8, msg_32: *u8, scratch: *u8) -> i64 { 2303 let pk: *u8 = (scratch as i64 + 100000) as *u8 2304 let sk: *u8 = (scratch as i64 + 102000) as *u8 2305 let sig: *u8 = (scratch as i64 + 106500) as *u8 2306 if nx_mldsa_keygen(seed_32, scratch, pk, sk) != 0 { return -1 } 2307 if nx_mldsa_sign(sk, msg_32, 32, scratch, sig) != 0 { return -2 } 2308 return nx_mldsa_verify(pk, msg_32, 32, sig, scratch) 2309} 2310 2311func nx_mldsa_ntt_round_trip_test(scratch: *u8) -> i64 { 2312 let poly: *u8 = scratch 2313 let saved: *u8 = (scratch as i64 + POLY_BUF) as *u8 2314 // Fill poly[i] = (i * 13 + 7) mod q for a deterministic pattern. 2315 var i: i64 = 0 2316 while i < MLDSA_N { 2317 let v: i64 = (i * 13 + 7) % MLDSA_Q 2318 _i32_store_le(poly, i, v) 2319 _i32_store_le(saved, i, v) 2320 i = i + 1 2321 } 2322 nx_mldsa_ntt(poly) 2323 nx_mldsa_invntt(poly) 2324 // Per PQClean convention: invntt_tomont(ntt(X)) = X * mont (mod q), 2325 // where mont = 2^32 mod q = 4193792. Compare round-tripped coef to 2326 // (saved * mont) mod q after canonicalising both. 2327 let MONT: i64 = 4193792 2328 var diff: i64 = 0 2329 var j: i64 = 0 2330 while j < MLDSA_N { 2331 var a: i64 = _reduce32(_i32_load_le(poly, j)) 2332 if a < 0 { a = a + MLDSA_Q } 2333 var expected: i64 = (_i32_load_le(saved, j) * MONT) % MLDSA_Q 2334 if expected < 0 { expected = expected + MLDSA_Q } 2335 if a != expected { diff = 1 } 2336 j = j + 1 2337 } 2338 if diff != 0 { return 1 } 2339 return 0 2340}