code wiki / (root) / _ed25519_ct_gate.nx

_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}