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}