code wiki / (root) / nx_p256_solinas_difftest.nx

nx_p256_solinas_difftest.nx source

↩ module page · 456 lines · 14053 B

1// nx_p256_solinas_difftest.nx -- DIFFERENTIAL correctness gate for the 2// NIST P-256 Solinas fast reduction used by TLS ECDSA certificate verify. 3// 4// It proves the FAST Solinas reduction (_p256_solinas_reduce, reached via 5// the public p256_field_reduce_solinas) is BIT-IDENTICAL, in every one of 6// the 8 output limbs, to a trivially-correct bit-serial long-division 7// reduction mod p -- over the FULL 512-bit product domain: 8// * >= 2000 deterministic LCG-random 512-bit products (seeded from the 9// loop index; NO Date.now/random -- the .nx runtime forbids them), and 10// * the exact reduction boundary values: 0, p-1, p, p+1, 2p-1, 2p, 2p+1, 11// 3p, 2^256-1, 2^256, (p-1)^2, p^2, p<<256, 2^512-1, 2^512-2, 2^512-1-p. 12// Plus an end-to-end cross-check: p256_field_mul (fast) == p256_field_mul_slow 13// (the retained bit-serial oracle) over random a,b. 14// 15// WHY THIS MATTERS: this reduction is on the cert-verify hot path 16// (p256_field_mul -> point add/double -> scalar mul -> ECDSA verify). A 17// single wrong reduction could let a FORGED signature verify. The test 18// exits 0 ONLY if EVERY input is bit-identical; any mismatch exits 1. 19// 20// ORACLE INDEPENDENCE: the bit-serial oracle here (_dt_oracle_reduce) shares 21// ZERO code with _p256_solinas_reduce except the prime-constant loader 22// p256_field_load_p (both must reduce mod the same p). It is the SAME 23// algorithm p256_field_mul_slow uses for its reduction step; the end-to-end 24// section cross-checks it against p256_field_mul_slow so a faithful 25// transcription is proven, not assumed. 26// 27// NOTE ON PREMISE: the Solinas reduction ALREADY existed and was ALREADY 28// wired into p256_field_mul before this test (commit "ECDSA-P256 ยง1c"). 29// This organ ADDS a far stronger correctness proof (>= 2000 random + full 30// boundary set) than the pre-existing 606-case oracle test; it modifies no 31// production logic. 32// 33// expect_exit: 0 34// license_tier: ORIGINAL 35 36import "nx_syscalls.nx" 37import "nx_u256.nx" 38import "nx_u256_mul.nx" 39import "nx_p256_field.nx" 40import "nx_p256_field_mul.nx" 41const K_MAGIC_2500: i64 = 2500 42const K_MAGIC_2654435761: i64 = 2654435761 43const K_MAGIC_1013904223: i64 = 1013904223 44const K_MAGIC_1103515245: i64 = 1103515245 45const K_MAGIC_12345: i64 = 12345 46const K_MAGIC_40503: i64 = 40503 47 48// ---- bounded decimal printer (self-contained; no runtime import) ---- 49func _dt_print_dec(n: i64) -> i64 { 50 let out: *u8 = sys_mmap(32) 51 if n == 0 { 52 out[0] = 48 as u8 53 sys_write(1, out, 1) 54 return 0 55 } 56 let tmp: *u8 = sys_mmap(32) 57 var v: i64 = n 58 var i: i64 = 0 59 while v > 0 { 60 let d: i64 = v - (v / 10) * 10 61 tmp[i] = (48 + d) as u8 62 v = v / 10 63 i = i + 1 64 } 65 var j: i64 = 0 66 while i > 0 { 67 i = i - 1 68 out[j] = tmp[i] 69 j = j + 1 70 } 71 sys_write(1, out, j) 72 return 0 73} 74 75// ---- independent bit-serial oracle: out8 = c16 mod p ---- 76// Repeated conditional subtract of (p << k) for k = 256 down to 0 -- plain 77// schoolbook long division. Uses ONLY compare/subtract/shift on 16 limbs 78// (u256_wide_cmp / u256_wide_sub / u256_wide_shr_1), none of which is part 79// of the Solinas path under test. 80func _dt_oracle_reduce(out8: *i64, c16: *i64) -> i64 { 81 let _m: i64 = nx_scratch_save() 82 let work: *i64 = u256_wide_alloc() 83 let shp: *i64 = u256_wide_alloc() 84 let p: *i64 = u256_alloc() 85 p256_field_load_p(p) 86 var i: i64 = 0 87 while i < NX_U256_WIDE_LIMBS { 88 work[i] = c16[i] & NX_U256_LIMB_MASK 89 i = i + 1 90 } 91 i = 0 92 while i < NX_U256_WIDE_LIMBS { 93 shp[i] = 0 94 i = i + 1 95 } 96 i = 0 97 while i < NX_U256_LIMBS { 98 shp[i + 8] = p[i] & NX_U256_LIMB_MASK 99 i = i + 1 100 } 101 var k: i64 = 0 102 while k < 257 { 103 if u256_wide_cmp(work, shp) >= 0 { 104 u256_wide_sub(work, work, shp) 105 } 106 u256_wide_shr_1(shp) 107 k = k + 1 108 } 109 i = 0 110 while i < NX_U256_LIMBS { 111 out8[i] = work[i] & NX_U256_LIMB_MASK 112 i = i + 1 113 } 114 nx_scratch_restore(_m) 115 return 0 116} 117 118// 8-limb equality (1 == equal, 0 == differ). 119func _dt_eq8(a: *i64, b: *i64) -> i64 { 120 var i: i64 = 0 121 while i < NX_U256_LIMBS { 122 if (a[i] & NX_U256_LIMB_MASK) != (b[i] & NX_U256_LIMB_MASK) { 123 return 0 124 } 125 i = i + 1 126 } 127 return 1 128} 129 130// Reduce c16 with BOTH paths; return 0 if bit-identical, 1 if any limb differs. 131func _dt_check_c(c16: *i64, fast: *i64, oracle: *i64) -> i64 { 132 p256_field_reduce_solinas(fast, c16) 133 _dt_oracle_reduce(oracle, c16) 134 if _dt_eq8(fast, oracle) == 1 { 135 return 0 136 } 137 return 1 138} 139 140// ---- 16-limb helpers for building boundary inputs ---- 141func _dt_zero16(c: *i64) -> i64 { 142 var i: i64 = 0 143 while i < NX_U256_WIDE_LIMBS { 144 c[i] = 0 145 i = i + 1 146 } 147 return 0 148} 149 150func _dt_copy16(dst: *i64, src: *i64) -> i64 { 151 var i: i64 = 0 152 while i < NX_U256_WIDE_LIMBS { 153 dst[i] = src[i] & NX_U256_LIMB_MASK 154 i = i + 1 155 } 156 return 0 157} 158 159// dst (16-limb) += src (16-limb), in place. High-order carry-out dropped 160// (all callers keep the sum < 2^512). 161func _dt_add16(dst: *i64, src: *i64) -> i64 { 162 var carry: i64 = 0 163 var i: i64 = 0 164 while i < NX_U256_WIDE_LIMBS { 165 let s: i64 = (dst[i] & NX_U256_LIMB_MASK) + (src[i] & NX_U256_LIMB_MASK) + carry 166 dst[i] = s & NX_U256_LIMB_MASK 167 carry = (s >> NX_U256_LIMB_BITS) & 1 168 i = i + 1 169 } 170 return 0 171} 172 173// c (16-limb) += small scalar in [0, 2^32), in place. 174func _dt_add_small(c: *i64, val: i64) -> i64 { 175 var carry: i64 = val & NX_U256_LIMB_MASK 176 var i: i64 = 0 177 while i < NX_U256_WIDE_LIMBS { 178 if carry == 0 { 179 i = NX_U256_WIDE_LIMBS 180 } else { 181 let s: i64 = (c[i] & NX_U256_LIMB_MASK) + carry 182 c[i] = s & NX_U256_LIMB_MASK 183 carry = s >> NX_U256_LIMB_BITS 184 i = i + 1 185 } 186 } 187 return 0 188} 189 190// c (16-limb) -= small scalar in [0, 2^32), in place. Caller guarantees 191// no underflow below zero. 192func _dt_sub_small(c: *i64, val: i64) -> i64 { 193 var borrow: i64 = val & NX_U256_LIMB_MASK 194 var i: i64 = 0 195 while i < NX_U256_WIDE_LIMBS { 196 if borrow == 0 { 197 i = NX_U256_WIDE_LIMBS 198 } else { 199 let d: i64 = (c[i] & NX_U256_LIMB_MASK) - borrow 200 if d < 0 { 201 c[i] = (d + (1 << NX_U256_LIMB_BITS)) & NX_U256_LIMB_MASK 202 borrow = 1 203 } else { 204 c[i] = d & NX_U256_LIMB_MASK 205 borrow = 0 206 } 207 i = i + 1 208 } 209 } 210 return 0 211} 212 213// Run one boundary case: check c16, bump counters. Returns updated fails. 214// (Helper kept out; counters live in main for clarity.) 215 216func main() -> i64 { 217 let _s0: i64 = nx_scratch_save() 218 219 let p: *i64 = u256_alloc() 220 p256_field_load_p(p) 221 222 // Persistent working buffers (allocated ONCE; both reducers frame and 223 // reclaim their own temporaries internally, so the arena never grows 224 // across the loops below). 225 let c16: *i64 = u256_wide_alloc() 226 let fast: *i64 = u256_alloc() 227 let oracle: *i64 = u256_alloc() 228 229 var total: i64 = 0 230 var fails: i64 = 0 231 232 // ================================================================ 233 // 1) >= 2000 LCG-random 512-bit products, deterministically seeded 234 // from the loop index. Each of 16 limbs gets a fresh 32-bit LCG 235 // output, covering the full 512-bit domain. 236 // ================================================================ 237 let NRAND: i64 = K_MAGIC_2500 238 var idx: i64 = 0 239 while idx < NRAND { 240 var st: i64 = (idx * K_MAGIC_2654435761 + K_MAGIC_1013904223) & NX_U256_LIMB_MASK 241 var k: i64 = 0 242 while k < NX_U256_WIDE_LIMBS { 243 st = (st * K_MAGIC_1103515245 + K_MAGIC_12345) & NX_U256_LIMB_MASK 244 c16[k] = st 245 k = k + 1 246 } 247 fails = fails + _dt_check_c(c16, fast, oracle) 248 total = total + 1 249 idx = idx + 1 250 } 251 252 // ================================================================ 253 // 2) Explicit boundary inputs. 254 // ================================================================ 255 // p as a 16-limb value (low 8 limbs = p, high 8 = 0). 256 let pw: *i64 = u256_wide_alloc() 257 _dt_zero16(pw) 258 var bi: i64 = 0 259 while bi < NX_U256_LIMBS { 260 pw[bi] = p[bi] & NX_U256_LIMB_MASK 261 bi = bi + 1 262 } 263 264 // 0 265 _dt_zero16(c16) 266 fails = fails + _dt_check_c(c16, fast, oracle) 267 total = total + 1 268 269 // p 270 _dt_copy16(c16, pw) 271 fails = fails + _dt_check_c(c16, fast, oracle) 272 total = total + 1 273 274 // p - 1 275 _dt_copy16(c16, pw) 276 _dt_sub_small(c16, 1) 277 fails = fails + _dt_check_c(c16, fast, oracle) 278 total = total + 1 279 280 // p + 1 281 _dt_copy16(c16, pw) 282 _dt_add_small(c16, 1) 283 fails = fails + _dt_check_c(c16, fast, oracle) 284 total = total + 1 285 286 // 2p 287 _dt_copy16(c16, pw) 288 _dt_add16(c16, pw) 289 fails = fails + _dt_check_c(c16, fast, oracle) 290 total = total + 1 291 292 // 2p - 1 293 _dt_copy16(c16, pw) 294 _dt_add16(c16, pw) 295 _dt_sub_small(c16, 1) 296 fails = fails + _dt_check_c(c16, fast, oracle) 297 total = total + 1 298 299 // 2p + 1 300 _dt_copy16(c16, pw) 301 _dt_add16(c16, pw) 302 _dt_add_small(c16, 1) 303 fails = fails + _dt_check_c(c16, fast, oracle) 304 total = total + 1 305 306 // 3p (stresses the reduce9 multi-subtract loop) 307 _dt_copy16(c16, pw) 308 _dt_add16(c16, pw) 309 _dt_add16(c16, pw) 310 fails = fails + _dt_check_c(c16, fast, oracle) 311 total = total + 1 312 313 // 2^256 - 1 (low 8 limbs all ones) 314 _dt_zero16(c16) 315 var i2: i64 = 0 316 while i2 < NX_U256_LIMBS { 317 c16[i2] = NX_U256_LIMB_MASK 318 i2 = i2 + 1 319 } 320 fails = fails + _dt_check_c(c16, fast, oracle) 321 total = total + 1 322 323 // 2^256 (limb 8 = 1) 324 _dt_zero16(c16) 325 c16[8] = 1 326 fails = fails + _dt_check_c(c16, fast, oracle) 327 total = total + 1 328 329 // 2^512 - 1 (all 16 limbs all ones) 330 var i3: i64 = 0 331 while i3 < NX_U256_WIDE_LIMBS { 332 c16[i3] = NX_U256_LIMB_MASK 333 i3 = i3 + 1 334 } 335 fails = fails + _dt_check_c(c16, fast, oracle) 336 total = total + 1 337 338 // 2^512 - 2 339 var i4: i64 = 0 340 while i4 < NX_U256_WIDE_LIMBS { 341 c16[i4] = NX_U256_LIMB_MASK 342 i4 = i4 + 1 343 } 344 c16[0] = 0xFFFFFFFE 345 fails = fails + _dt_check_c(c16, fast, oracle) 346 total = total + 1 347 348 // 2^512 - 1 - p (all ones minus p) 349 var i5: i64 = 0 350 while i5 < NX_U256_WIDE_LIMBS { 351 c16[i5] = NX_U256_LIMB_MASK 352 i5 = i5 + 1 353 } 354 u256_wide_sub(c16, c16, pw) 355 fails = fails + _dt_check_c(c16, fast, oracle) 356 total = total + 1 357 358 // (p - 1)^2 -- the maximum real field product (via the production wide mul) 359 let pm1: *i64 = u256_alloc() 360 u256_copy(pm1, p) 361 var bpm: i64 = 1 362 var jpm: i64 = 0 363 while jpm < NX_U256_LIMBS { 364 let d: i64 = (pm1[jpm] & NX_U256_LIMB_MASK) - bpm 365 if d < 0 { 366 pm1[jpm] = (d + (1 << NX_U256_LIMB_BITS)) & NX_U256_LIMB_MASK 367 bpm = 1 368 } else { 369 pm1[jpm] = d & NX_U256_LIMB_MASK 370 bpm = 0 371 } 372 jpm = jpm + 1 373 } 374 u256_mul_wide(c16, pm1, pm1) 375 fails = fails + _dt_check_c(c16, fast, oracle) 376 total = total + 1 377 378 // p^2 (reduces to 0) 379 u256_mul_wide(c16, p, p) 380 fails = fails + _dt_check_c(c16, fast, oracle) 381 total = total + 1 382 383 // p << 256 (high 8 limbs = p, low 8 = 0; reduces to 0) 384 _dt_zero16(c16) 385 var i6: i64 = 0 386 while i6 < NX_U256_LIMBS { 387 c16[i6 + 8] = p[i6] & NX_U256_LIMB_MASK 388 i6 = i6 + 1 389 } 390 fails = fails + _dt_check_c(c16, fast, oracle) 391 total = total + 1 392 393 // ================================================================ 394 // 3) End-to-end cross-check on the ACTUAL production functions: 395 // p256_field_mul (fast) == p256_field_mul_slow (bit-serial oracle) 396 // over random a,b. Also confirms the standalone _dt_oracle_reduce 397 // matches the retained slow path (faithful-transcription check). 398 // ================================================================ 399 let a: *i64 = u256_alloc() 400 let b: *i64 = u256_alloc() 401 let e_fast: *i64 = u256_alloc() 402 let e_slow: *i64 = u256_alloc() 403 let e_orac: *i64 = u256_alloc() 404 let cab: *i64 = u256_wide_alloc() 405 var e: i64 = 0 406 while e < 600 { 407 var sa: i64 = (e * K_MAGIC_40503 + K_MAGIC_12345) & NX_U256_LIMB_MASK 408 var ka: i64 = 0 409 while ka < NX_U256_LIMBS { 410 sa = (sa * K_MAGIC_1103515245 + K_MAGIC_12345) & NX_U256_LIMB_MASK 411 a[ka] = sa 412 ka = ka + 1 413 } 414 var sb: i64 = (e * K_MAGIC_2654435761 + 7) & NX_U256_LIMB_MASK 415 var kb: i64 = 0 416 while kb < NX_U256_LIMBS { 417 sb = (sb * K_MAGIC_1103515245 + K_MAGIC_12345) & NX_U256_LIMB_MASK 418 b[kb] = sb 419 kb = kb + 1 420 } 421 p256_field_mul(e_fast, a, b) 422 p256_field_mul_slow(e_slow, a, b) 423 if _dt_eq8(e_fast, e_slow) == 0 { 424 fails = fails + 1 425 } 426 total = total + 1 427 // faithful-transcription check: my oracle on the same product 428 u256_mul_wide(cab, a, b) 429 _dt_oracle_reduce(e_orac, cab) 430 if _dt_eq8(e_orac, e_slow) == 0 { 431 fails = fails + 1 432 } 433 total = total + 1 434 e = e + 1 435 } 436 437 // ================================================================ 438 // Verdict. 439 // ================================================================ 440 if fails == 0 { 441 sys_write(1, "=== difftest PASS " as *u8, 18) 442 _dt_print_dec(total) 443 sys_write(1, "/" as *u8, 1) 444 _dt_print_dec(total) 445 sys_write(1, "\n" as *u8, 1) 446 nx_scratch_restore(_s0) 447 return 0 448 } 449 sys_write(1, "=== difftest FAIL " as *u8, 18) 450 _dt_print_dec(fails) 451 sys_write(1, " of " as *u8, 4) 452 _dt_print_dec(total) 453 sys_write(1, " mismatched\n" as *u8, 12) 454 nx_scratch_restore(_s0) 455 return 1 456}