code wiki / (root) / nx_ecdsa_p256_sign.nx

nx_ecdsa_p256_sign.nx source

↩ module page · 239 lines · 9403 B

1// nx_ecdsa_p256_sign.nx -- ECDSA P-256 signature generation. 2// 3// Companion to nx_ecdsa_p256 (verify, shipped 2026-05-19 with RFC 6979 4// KAT green). Implements signature GENERATION via RFC 6979 deterministic 5// nonce so the same {priv_key, msg_hash} pair always produces the same 6// (r, s) -- per [[feedback-bits-up-exceed-never-match]] determinism axis. 7// 8// Pure NishiLang composition. No new substrate primitives invented; 9// every step uses an already-shipped op: 10// 11// nx_drbg_hmac -- RFC 6979 HMAC-DRBG for deterministic k 12// nx_p256_scalar_mul -- k * G (the generator) 13// nx_p256_point.to_affine -- normalize R from Jacobian to (x, y) 14// nx_p256_modn -- mod-n arithmetic (add, mul, inv) 15// 16// Algorithm (FIPS 186-5 §6.4.1): 17// 18// Input: private key d (256-bit scalar, 1 <= d < n) 19// message hash e (256-bit, already SHA-256'd by caller) 20// 21// 1. Generate deterministic k via RFC 6979: 22// seed = d_bytes || e_bytes 23// HMAC-DRBG(seed) -> k_bytes; reject if k == 0 or k >= n 24// (in practice for P-256 + SHA-256 + a sane priv key, 25// the first DRBG output is always acceptable) 26// 2. R = k * G (R is a point on the curve) 27// 3. Convert R to affine: get R.x 28// 4. r = R.x mod n (reduce x coordinate into Z/nZ) 29// 5. Reject if r == 0 (probability ~2^-256; if hit, reseed DRBG 30// with extra byte and retry -- caller-visible verdict) 31// 6. s = k^-1 * (e + r * d) mod n 32// 7. Reject if s == 0 (same probability) 33// 8. Return (r, s) as the signature 34// 35// Public API: 36// nx_ecdsa_p256_sign(priv_key: *i64, -- 256-bit scalar d 37// hash_e: *i64, -- 256-bit msg digest 38// out_r: *i64, out_s: *i64) -> verdict 39// 40// Unlocks: 41// - nx_edge TLS termination (CertificateVerify signs the transcript) 42// - nx_acme own-cert issuance (CSR signing) 43// - Future nx_x509 self-signed cert generation (sovereign CA path) 44// - Any RFC 6979 deterministic-nonce ECDSA producer 45 46// nx_safety_envelope: 47// intended_use: ECDSA P-256 deterministic signature generation 48// sil_target: SIL2 49// evidence: bench/nx_ecdsa_p256_sign_smoke.nx round-trips 50// sign + verify with deterministic KAT 51// verdict: NOT_YET_EVALUATED -- pending smoke 52 53import "nx_syscalls.nx" 54import "nx_u256.nx" 55import "nx_p256_modn.nx" 56import "nx_p256_point.nx" 57import "nx_p256_scalar_mul.nx" 58import "nx_p256_comb.nx" 59import "nx_drbg_hmac.nx" 60import "nx_ecdsa_p256.nx" 61 62// ---- Sealed verdicts ---- 63 64const NX_ECDSA_SIGN_OK: i64 = 1 65const NX_ECDSA_SIGN_BAD_PRIVKEY: i64 = 2 // d == 0 or d >= n 66const NX_ECDSA_SIGN_R_ZERO: i64 = 3 // r == 0; reseed + retry 67const NX_ECDSA_SIGN_S_ZERO: i64 = 4 // s == 0; reseed + retry 68const NX_ECDSA_SIGN_K_ZERO: i64 = 5 // DRBG produced k == 0 69const NX_ECDSA_SIGN_VERDICT_N: i64 = 6 70 71// ---- Pack/unpack 256-bit scalar as bytes (big-endian, 32 bytes) ---- 72// 73// Per FIPS 186-5: scalar is 256 bits big-endian for wire/cert formats. 74// Internal representation is 4 i64 limbs little-endian (u256 form). 75// These helpers convert between the two. 76 77// Use u256_store_be / u256_load_be from runtime/nx_u256.nx for 78// big-endian byte conversions; hand-rolled limb-swap helpers were 79// removed in favor of the canonical substrate primitives. 80 81// ---- Generate deterministic k via RFC 6979 ---- 82// 83// Standard RFC 6979 §3.2 procedure with HMAC-SHA256. Seed material 84// is the privkey || msg_hash (both 32 bytes big-endian). Output is 85// 32 bytes -> 256-bit scalar; reject if k == 0 or k >= n and pull 86// another 32 bytes. 87// 88// For P-256 + SHA-256 with sane priv key + non-degenerate hash, the 89// first DRBG output is essentially always acceptable; the retry path 90// exists to be FIPS-compliant. 91 92func _ecdsa_sign_derive_k(priv_be: *u8, hash_be: *u8, out_k_limbs: *i64) -> i64 { 93 // All temporaries (incl. the per-attempt cmp_buf) live in the u256 94 // scratch arena and are reclaimed on return; out_k_limbs is the 95 // caller's buffer (below this frame) and survives. 96 let _fm: i64 = nx_scratch_save() 97 // HMAC-DRBG state: 32+32+1 = 65 bytes (K + V + reseed counter). 98 let drbg_state: *u8 = nx_scratch(128) 99 // Seed = priv (32) || hash (32) = 64 bytes. 100 let seed: *u8 = nx_scratch(64) 101 var i: i64 = 0 102 while i < 32 { seed[i] = priv_be[i]; i = i + 1 } 103 var j: i64 = 0 104 while j < 32 { seed[32 + j] = hash_be[j]; j = j + 1 } 105 drbg_hmac_init(drbg_state, seed, 64) 106 107 let k_bytes: *u8 = nx_scratch(64) 108 let n_limbs: *i64 = u256_alloc() 109 p256_modn_load_n(n_limbs) 110 111 var got: i64 = 0 112 var attempts: i64 = 0 113 while got == 0 { 114 if attempts >= 16 { 115 nx_scratch_restore(_fm) 116 return NX_ECDSA_SIGN_K_ZERO 117 } 118 drbg_hmac_generate(drbg_state, k_bytes, 32) 119 u256_load_be(out_k_limbs, k_bytes) 120 // Validate 1 <= k < n. 121 if u256_is_zero(out_k_limbs) == 0 { 122 let cmp_buf: *i64 = u256_alloc() 123 u256_copy(cmp_buf, out_k_limbs) 124 p256_modn_reduce(cmp_buf, cmp_buf) 125 if u256_eq(cmp_buf, out_k_limbs) == 1 { 126 got = 1 // k < n 127 } 128 } 129 attempts = attempts + 1 130 } 131 nx_scratch_restore(_fm) 132 return NX_ECDSA_SIGN_OK 133} 134 135// ---- Main sign function ---- 136 137func _nx_ecdsa_p256_sign_impl(priv_key: *i64, 138 hash_e: *i64, 139 out_r: *i64, out_s: *i64) -> i64 { 140 // 1. Validate priv key: 1 <= d < n. 141 if u256_is_zero(priv_key) == 1 { return NX_ECDSA_SIGN_BAD_PRIVKEY } 142 let n_limbs: *i64 = u256_alloc() 143 p256_modn_load_n(n_limbs) 144 let priv_reduced: *i64 = u256_alloc() 145 u256_copy(priv_reduced, priv_key) 146 p256_modn_reduce(priv_reduced, priv_reduced) 147 if u256_eq(priv_reduced, priv_key) == 0 { return NX_ECDSA_SIGN_BAD_PRIVKEY } 148 149 // 2. Convert priv + hash to big-endian bytes for RFC 6979 seeding. 150 let priv_be: *u8 = nx_scratch(32) 151 let hash_be: *u8 = nx_scratch(32) 152 u256_store_be(priv_be, priv_key) 153 u256_store_be(hash_be, hash_e) 154 155 // 3. Generate deterministic k. 156 let k_limbs: *i64 = u256_alloc() 157 let kv: i64 = _ecdsa_sign_derive_k(priv_be, hash_be, k_limbs) 158 if kv != NX_ECDSA_SIGN_OK { return kv } 159 160 // 4. R = k * G. Per nx_p256_scalar_mul_test.nx, scalar is a 161 // 4-limb u256_alloc'd buffer (the `_8` suffix in the param name 162 // is historical; functionally takes 4 limbs). 163 let g_point: *P256Point = p256_point_alloc() 164 p256_point_load_g(g_point) 165 let r_point: *P256Point = p256_point_alloc() 166 // Fixed-base comb fast path: if the precomputed k*G table file exists 167 // (the sites daemon builds it at deploy), use it (~4x fewer point ops, 168 // no doublings); otherwise fall back to the generic double-and-add 169 // (ACME/JOSE callers + tests that never built the table). 170 let comb_len_box: *i64 = (nx_scratch(8)) as *i64 171 comb_len_box[0] = 0 172 let comb_tbl: *u8 = sys_read_file(NX_P256_COMB_PATH, comb_len_box) 173 if (comb_tbl as i64) != 0 { 174 if comb_len_box[0] >= NX_P256_COMB_BYTES { 175 p256_scalar_mul_base(r_point, k_limbs, comb_tbl as *i64) 176 } else { 177 p256_scalar_mul(r_point, k_limbs, g_point) 178 } 179 } else { 180 p256_scalar_mul(r_point, k_limbs, g_point) 181 } 182 183 // 5. Normalize R to affine to get R.x. 184 p256_point_to_affine(r_point) 185 let r_x_field: *i64 = r_point.x 186 187 // 6. r = R.x mod n. r_x_field is mod p (field element); 188 // we need it mod n. Since p > n and the difference is small, 189 // p256_modn_reduce handles it. 190 p256_modn_reduce(out_r, r_x_field) 191 if u256_is_zero(out_r) == 1 { return NX_ECDSA_SIGN_R_ZERO } 192 193 // 7. s = k^-1 * (e + r * d) mod n. 194 // a) r_d = (r * d) mod n 195 // b) e_plus_rd = (e + r_d) mod n -- e first reduced mod n 196 // c) k_inv = k^-1 mod n 197 // d) s = k_inv * e_plus_rd mod n 198 199 let e_reduced: *i64 = u256_alloc() 200 u256_copy(e_reduced, hash_e) 201 p256_modn_reduce(e_reduced, e_reduced) 202 203 // p256_modn_mul writes 4-limb (mod n) result into a 4-limb buffer. 204 // Compare with how nx_ecdsa_p256_verify uses it (lines 135/139): 205 // let u1: *i64 = u256_alloc(); p256_modn_mul(u1, e_n, s_inv); 206 let r_d: *i64 = u256_alloc() 207 p256_modn_mul(r_d, out_r, priv_key) 208 209 let e_plus_rd: *i64 = u256_alloc() 210 p256_modn_add(e_plus_rd, e_reduced, r_d) 211 212 let k_inv: *i64 = u256_alloc() 213 p256_modn_inv(k_inv, k_limbs) 214 215 p256_modn_mul(out_s, k_inv, e_plus_rd) 216 217 if u256_is_zero(out_s) == 1 { return NX_ECDSA_SIGN_S_ZERO } 218 219 return NX_ECDSA_SIGN_OK 220} 221 222// Public entry: frame the whole sign so every scratch temporary (incl. 223// the many early-return paths in the impl) is reclaimed in O(1) on 224// return. The verdict is an i64 value, so it survives the restore. 225// out_r / out_s are caller buffers (below this frame) and persist. 226func nx_ecdsa_p256_sign(priv_key: *i64, 227 hash_e: *i64, 228 out_r: *i64, out_s: *i64) -> i64 { 229 let _fm: i64 = nx_scratch_save() 230 let v: i64 = _nx_ecdsa_p256_sign_impl(priv_key, hash_e, out_r, out_s) 231 nx_scratch_restore(_fm) 232 return v 233} 234 235func nx_ecdsa_p256_sign_verdict_is_valid(v: i64) -> i64 { 236 if v < NX_ECDSA_SIGN_OK { return 0 } 237 if v >= NX_ECDSA_SIGN_VERDICT_N { return 0 } 238 return 1 239}