code wiki / (root) / nx_ecdsa_p256.nx

nx_ecdsa_p256.nx source

↩ module page · 198 lines · 7918 B

1// nx_ecdsa_p256.nx -- ECDSA signature verification over NIST P-256. 2// 3// Phase 0b §I.3 FINAL piece of the ECDSA-P256 arc: 4// ✓ 0. u256 big-int (6ff89dd8) 5// ✓ 1. p256_field add/sub/neg (133f16a1) 6// ✓ 1b. u256 wide multiplication (ddb0994a) 7// ✓ 1c. p256_field mul + sq (d295c0a3) 8// ✓ 1d. p256_field inverse (48e8e69d) 9// ✓ 2. p256_point double + on_curve + to_affine (ecc7ad5f) 10// ✓ 2b. p256_point_add (190acf1f) 11// ✓ 2c. p256_scalar_mul (also shipped earlier) 12// ✓ 3. p256_modn (Z/nZ arithmetic) (7e836066) 13// ✓ 4. ecdsa_p256_verify (THIS commit) 14// 15// This is the primitive that x509_validate's signature dispatch 16// will call for ECDSA-signed certs -- including every modern 17// Web PKI cert chain (google.com, Cloudflare, ~80% of the Web). 18// 19// Algorithm (FIPS 186-5 §6.4.2 / SEC 1 v2.0 §4.1.4): 20// 21// Input: pubkey Q = (Qx, Qy) (affine point on curve, != O) 22// msg hash e (256-bit, already truncated/computed) 23// signature (r, s) -- both 256-bit 24// 25// 1. Verify 1 <= r < n and 1 <= s < n. Reject otherwise. 26// 2. Verify Q is on the curve. Reject otherwise. 27// 3. e_n = e mod n (truncate-and-reduce; SHA-256 output 28// has the same bit-length as n for 29// P-256 + SHA-256, so this is just a 30// conditional subtract.) 31// 4. s_inv = s^(-1) mod n 32// 5. u1 = (e_n * s_inv) mod n 33// 6. u2 = (r * s_inv) mod n 34// 7. R = u1 * G + u2 * Q 35// 8. Reject if R = O (point at infinity). 36// 9. Convert R to affine; v = x_R mod n 37// 10. Accept iff v == r. 38// 39// Public API: 40// nx_ecdsa_p256_verify(pub_x, pub_y, hash_e, sig_r, sig_s) -> verdict 41// nx_ecdsa_p256_verdict_is_valid(v) -> 0|1 42// 43// All input arrays are 8-limb LE U256 buffers. Caller responsibility: 44// - hash_e is already the message hash (caller does SHA-256) 45// - hash_e bytes are interpreted as a 256-bit BE integer per 46// FIPS 186-5 §6.4.2 step 1 (use u256_load_be). 47// 48// Sealed verdict enum: 49// 50// NX_ECDSA_OK (1) signature valid 51// NX_ECDSA_BAD_R_RANGE (2) r outside [1, n-1] 52// NX_ECDSA_BAD_S_RANGE (3) s outside [1, n-1] 53// NX_ECDSA_PUBKEY_NOT_ON_CURVE (4) Q not on the curve 54// NX_ECDSA_PUBKEY_INFINITY (5) Q is infinity (degenerate) 55// NX_ECDSA_INFINITY_RESULT (6) R = u1*G + u2*Q is infinity 56// NX_ECDSA_BAD_SIG (7) v != r (sig doesn't verify) 57// 58// Per Cardinals 9 (single-responsibility -- this primitive is JUST 59// orchestration; each step has its own primitive), 12 (defensive 60// at boundaries -- range-check r, s and on-curve-check Q first), 61// 22 (composition -- 10 substrate primitives compose into one 62// verify), and 23 (preamble explains the algorithm step-by-step). 63// 64// license_tier: INDEPENDENT_REDERIVE 65// genealogy_id: international-research-sources/nist/fips_186_5 + sec_g/sec1_v2 66// lineage_id: nishi_ecdsa_p256_verify_q10 67 68// nx_safety_envelope: 69// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 70// sil_target: SIL1 71// evidence: [bulk_applied_2026-05-19, ecdsa-p256-verify-orchestrator] 72// verdict: NOT_YET_EVALUATED 73 74import "nx_syscalls.nx" 75import "nx_u256.nx" 76import "nx_p256_field.nx" 77import "nx_p256_point.nx" 78import "nx_p256_point_add.nx" 79import "nx_p256_scalar_mul.nx" 80import "nx_p256_modn.nx" 81import "nx_p256_comb.nx" // fixed-base comb k*G (4.9x faster than generic) for the u1*G half of verify 82import "nx_p256_scalar_mul_wnaf.nx" // width-4 wNAF variable-base k*Q (1.33x faster) for the u2*Q half 83 84const NX_ECDSA_OK: i64 = 1 85const NX_ECDSA_BAD_R_RANGE: i64 = 2 86const NX_ECDSA_BAD_S_RANGE: i64 = 3 87const NX_ECDSA_PUBKEY_NOT_ON_CURVE: i64 = 4 88const NX_ECDSA_PUBKEY_INFINITY: i64 = 5 89const NX_ECDSA_INFINITY_RESULT: i64 = 6 90const NX_ECDSA_BAD_SIG: i64 = 7 91const NX_ECDSA_VERDICT_N: i64 = 8 92 93func nx_ecdsa_p256_verdict_is_valid(v: i64) -> i64 { 94 if v < NX_ECDSA_OK { return 0 } 95 if v >= NX_ECDSA_VERDICT_N { return 0 } 96 return 1 97} 98 99// Cached fixed-base comb table for G (2026-07-02). Built ONCE (lazily, first verify) into 100// PERSISTENT mmap memory (NOT scratch -> survives every nx_scratch_restore), reused across all 101// verifies. p256_scalar_mul_base(...,table) is proven == generic p256_scalar_mul(k,G) 102// (nx_p256_comb_test + nx_p256_comb_bench 64 scalars) and ~4.9x faster -> the u1*G half of verify. 103// Fork-per-connection server: each process builds its own once (a benign one-time cost + no race). 104static _g_p256_comb: i64 105func _p256_comb_table() -> *i64 { 106 if _g_p256_comb == 0 { 107 let t: *i64 = (sys_mmap(NX_P256_COMB_BYTES)) as *i64 108 p256_comb_build(t) 109 _g_p256_comb = t as i64 110 } 111 return _g_p256_comb as *i64 112} 113 114// Verify an ECDSA-P256 signature. 115// 116// Returns NX_ECDSA_OK on valid signature, or one of the 117// NX_ECDSA_* non-OK verdicts on any failure. 118func _nx_ecdsa_p256_verify_impl(pub_x: *i64, pub_y: *i64, 119 hash_e: *i64, 120 sig_r: *i64, sig_s: *i64) -> i64 { 121 let n: *i64 = u256_alloc() 122 p256_modn_load_n(n) 123 let one: *i64 = u256_alloc() 124 p256_modn_one(one) 125 126 // ---- Step 1: range-check r, s ---- 127 // r >= 1 128 if u256_is_zero(sig_r) == 1 { return NX_ECDSA_BAD_R_RANGE } 129 // r < n 130 if u256_cmp(sig_r, n) >= 0 { return NX_ECDSA_BAD_R_RANGE } 131 // s >= 1 132 if u256_is_zero(sig_s) == 1 { return NX_ECDSA_BAD_S_RANGE } 133 // s < n 134 if u256_cmp(sig_s, n) >= 0 { return NX_ECDSA_BAD_S_RANGE } 135 136 // ---- Step 2: pubkey on curve + not infinity ---- 137 let Q: *P256Point = p256_point_alloc() 138 p256_point_set_affine(Q, pub_x, pub_y) 139 if p256_point_is_infinity(Q) == 1 { return NX_ECDSA_PUBKEY_INFINITY } 140 if p256_point_on_curve(Q) != 1 { return NX_ECDSA_PUBKEY_NOT_ON_CURVE } 141 142 // ---- Step 3: e_n = e mod n ---- 143 let e_n: *i64 = u256_alloc() 144 p256_modn_reduce(e_n, hash_e) 145 146 // ---- Step 4: s_inv = s^(-1) mod n ---- 147 let s_inv: *i64 = u256_alloc() 148 p256_modn_inv(s_inv, sig_s) 149 150 // ---- Step 5: u1 = (e_n * s_inv) mod n ---- 151 let u1: *i64 = u256_alloc() 152 p256_modn_mul(u1, e_n, s_inv) 153 154 // ---- Step 6: u2 = (r * s_inv) mod n ---- 155 let u2: *i64 = u256_alloc() 156 p256_modn_mul(u2, sig_r, s_inv) 157 158 // ---- Step 7: R = u1*G + u2*Q ---- 159 let G: *P256Point = p256_point_alloc() 160 p256_point_load_g(G) 161 162 let u1G: *P256Point = p256_point_alloc() 163 let u2Q: *P256Point = p256_point_alloc() 164 p256_scalar_mul_base(u1G, u1, _p256_comb_table()) // fixed-base comb (~4.9x faster than generic) 165 p256_scalar_mul_wnaf(u2Q, u2, Q) // width-4 wNAF (~1.33x faster than generic) 166 167 let R: *P256Point = p256_point_alloc() 168 p256_point_add(R, u1G, u2Q) 169 170 // ---- Step 8: reject if R is infinity ---- 171 if p256_point_is_infinity(R) == 1 { return NX_ECDSA_INFINITY_RESULT } 172 173 // ---- Step 9: v = x_R mod n ---- 174 p256_point_to_affine(R) 175 let v: *i64 = u256_alloc() 176 p256_modn_reduce(v, R.x) 177 178 // ---- Step 10: accept iff v == r ---- 179 if p256_modn_eq(v, sig_r) == 1 { return NX_ECDSA_OK } 180 return NX_ECDSA_BAD_SIG 181} 182 183// Public entry: frame the whole verify so every scratch temporary 184// (across all ~9 early-return reject paths in the impl) is reclaimed 185// in O(1) on return. The verdict is an i64 value, surviving restore. 186func nx_ecdsa_p256_verify(pub_x: *i64, pub_y: *i64, 187 hash_e: *i64, 188 sig_r: *i64, sig_s: *i64) -> i64 { 189 let _fm: i64 = nx_scratch_save() 190 let v: i64 = _nx_ecdsa_p256_verify_impl(pub_x, pub_y, hash_e, sig_r, sig_s) 191 nx_scratch_restore(_fm) 192 return v 193} 194 195// Compile-only smoke. Real KAT in nx_ecdsa_p256_test.nx. 196func main() -> i64 { 197 return 0 198}