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}