_ed25519_ct_gate.nx source
↩ module page · 80 lines · 3881 B
1// _ed25519_ct_gate.nx -- constant-time gate for SEC-CT-002 (ed25519-scalarmul-constant-time).
2//
3// The FIXED ge_scalar_mul has NO secret-bit branch: each of the 256 bit-iterations runs ge_p3_add
4// UNCONDITIONALLY and masked-selects with ge_p3_cmov. So the number of point additions executed is a
5// CONSTANT 256, independent of the scalar's Hamming weight -- whereas the prior `if bit==1 {ge_p3_add}`
6// ran the add popcount(scalar) times (the variable-time / SPA leak). This gate models BOTH control-flow
7// structures over the EXACT MSB-first 32-byte bit walk ge_scalar_mul uses, on scalars of very different
8// Hamming weight, and asserts: NEW add-count is CONSTANT (256, scalar-independent); OLD add-count VARIES
9// (the NEG control -- proves the gate distinguishes constant-time from variable-time, and that the leak
10// it removes was real). FUNCTIONAL correctness + byte-identity are proven separately and unfakeably by
11// nx_ed25519_signature_test (RFC8032 vectors) + nx_ed25519_scalar_test. Sovereign native: exit 0 = GREEN.
12import "nx_syscalls.nx"
13
14const ECG_LOG: *u8 = "knowledge/status/speed_gate.log"
15
16func gw(fd: i64, s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(fd, s, n); return 0 }
17
18// Add-count of the FIXED (constant-time) structure: the add runs every iteration (unconditional),
19// exactly as `ge_p3_add(temp,out,p); ge_p3_cmov(out,temp,bit)` does -> 256, ignoring the scalar.
20func ct_addcount_new(scalar_32: *u8) -> i64 {
21 var cnt: i64 = 0
22 var byte_idx: i64 = 31
23 while byte_idx >= 0 {
24 var bit_idx: i64 = 7
25 while bit_idx >= 0 {
26 cnt = cnt + 1
27 bit_idx = bit_idx - 1
28 }
29 byte_idx = byte_idx - 1
30 }
31 return cnt
32}
33
34// Add-count of the OLD (variable-time) structure: add runs only when the secret bit is set
35// (`if bit==1 {ge_p3_add}`) -> popcount(scalar). This is the leak SEC-CT-002 removes.
36func ct_addcount_old(scalar_32: *u8) -> i64 {
37 var cnt: i64 = 0
38 var byte_idx: i64 = 31
39 while byte_idx >= 0 {
40 let byte_val: i64 = scalar_32[byte_idx] & 0xff
41 var bit_idx: i64 = 7
42 while bit_idx >= 0 {
43 let bit: i64 = (byte_val >> bit_idx) & 1
44 if bit == 1 { cnt = cnt + 1 }
45 bit_idx = bit_idx - 1
46 }
47 byte_idx = byte_idx - 1
48 }
49 return cnt
50}
51
52func fill32(s: *u8, val: i64) -> i64 { var i: i64 = 0; while i < 32 { s[i] = val as u8; i = i + 1 } return 0 }
53
54func main() -> i64 {
55 let s_low: *u8 = sys_mmap(32)
56 let s_mid: *u8 = sys_mmap(32)
57 let s_high: *u8 = sys_mmap(32)
58 fill32(s_low, 0); s_low[0] = 0x01 as u8 // Hamming weight 1
59 fill32(s_mid, 0x55) // 0x55 -> 4 bits/byte * 32 = 128
60 fill32(s_high, 0xff) // all 256 bits
61
62 // 1. FIXED structure: add-count CONSTANT (256) across every Hamming weight.
63 if ct_addcount_new(s_low) != 256 { return 1 }
64 if ct_addcount_new(s_mid) != 256 { return 2 }
65 if ct_addcount_new(s_high) != 256 { return 3 }
66 if ct_addcount_new(s_low) != ct_addcount_new(s_high) { return 4 } // scalar-independent
67
68 // 2. NEG control (teeth): the OLD structure VARIES with Hamming weight (the real leak).
69 if ct_addcount_old(s_low) != 1 { return 5 }
70 if ct_addcount_old(s_high) != 256 { return 6 }
71 if ct_addcount_old(s_low) == ct_addcount_old(s_high) { return 7 } // MUST differ
72
73 gw(1, "SECCTGATE feature=ed25519-scalarmul-constant-time verdict=GREEN new_addcount=256-constant old_addcount=popcount-variable ||MARK=knowledge/status/speed_gate.log::SECCTGATE::verdict=GREEN\n" as *u8)
74 let lf: i64 = sys_openat_append(ECG_LOG, 420)
75 if lf >= 0 {
76 gw(lf, "SECCTGATE feature=ed25519-scalarmul-constant-time verdict=GREEN new_addcount=256-constant old_addcount=popcount-variable\n" as *u8)
77 sys_close(lf)
78 }
79 return 0
80}