nx_ecdsa_sig_der.nx source
↩ module page · 236 lines · 9049 B
1// nx_ecdsa_sig_der.nx -- parse DER-encoded ECDSA signatures into
2// (r, s) scalars.
3//
4// Phase 0b §I.4 piece 1 of the chain-walker arc. Composes the
5// shipped nx_ecdsa_p256_verify (commit 6bd3fdf3) with X.509
6// cert.sig_off/_len bytes (which carry the ECDSA signature in
7// DER form per RFC 5480 §2.2).
8//
9// ECDSA-Sig-Value ::= SEQUENCE {
10// r INTEGER,
11// s INTEGER
12// }
13//
14// Per RFC 3279 §2.2.3 and X.690 §8.3, the INTEGER encoding is
15// two's-complement big-endian. A 256-bit unsigned ECDSA scalar
16// in [0, n) thus encodes as either:
17// - 32 bytes when its leading bit is 0 (positive small)
18// - 33 bytes when its leading bit is 1 (must prefix 0x00 to
19// stay positive in two's complement)
20// - fewer than 32 bytes when leading zero bytes get stripped
21//
22// We accept all of these widths and zero-extend / strip-prefix as
23// needed to produce a canonical 8-limb LE U256 in [0, n). The
24// caller (ecdsa_p256_verify) range-checks r and s against n
25// itself.
26//
27// Public API:
28// nx_ecdsa_sig_parse_der(buf, len, out_r, out_s) -> verdict
29// nx_ecdsa_sig_der_verdict_is_valid(v) -> 0|1
30//
31// Sealed verdict enum:
32// NX_ECDSA_DER_OK sig parsed cleanly
33// NX_ECDSA_DER_BAD_OUTER outer SEQUENCE missing / wrong tag
34// NX_ECDSA_DER_BAD_R r INTEGER malformed
35// NX_ECDSA_DER_BAD_S s INTEGER malformed
36// NX_ECDSA_DER_LEN_TOO_BIG r or s longer than 33 bytes (won't
37// fit a 256-bit scalar; not a valid
38// ECDSA-P256 / ECDSA-P384 sig either)
39// NX_ECDSA_DER_TRAILING bytes left after parsing both
40// INTEGERs (malformed sig)
41//
42// Per Cardinals 9 (single-responsibility -- parse, not verify),
43// 12 (defensive at boundaries -- bounded reads against `len`),
44// and 23 (preamble explains why 33-byte INTEGER is normal).
45//
46// license_tier: INDEPENDENT_REDERIVE
47// genealogy_id: international-research-sources/ietf/rfc_3279 + rfc_5480 + itu_t/x_690
48// lineage_id: nishi_ecdsa_sig_der_q10
49
50// nx_safety_envelope:
51// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
52// sil_target: SIL1
53// evidence: [bulk_applied_2026-05-19, ecdsa-sig-der-parse]
54// verdict: NOT_YET_EVALUATED
55
56import "nx_syscalls.nx"
57import "nx_u256.nx"
58import "nx_asn1.nx"
59
60const NX_ECDSA_DER_OK: i64 = 1
61const NX_ECDSA_DER_BAD_OUTER: i64 = 2
62const NX_ECDSA_DER_BAD_R: i64 = 3
63const NX_ECDSA_DER_BAD_S: i64 = 4
64const NX_ECDSA_DER_LEN_TOO_BIG: i64 = 5
65const NX_ECDSA_DER_TRAILING: i64 = 6
66const NX_ECDSA_DER_VERDICT_N: i64 = 7
67
68func nx_ecdsa_sig_der_verdict_is_valid(v: i64) -> i64 {
69 if v < NX_ECDSA_DER_OK { return 0 }
70 if v >= NX_ECDSA_DER_VERDICT_N { return 0 }
71 return 1
72}
73
74// Helper: load a 1-33 byte big-endian unsigned integer into an
75// 8-limb LE U256 buffer, zero-padding the high limbs as needed.
76// Strips a single leading 0x00 byte (DER requires it for any
77// INTEGER whose high bit is set, to remain positive in two's
78// complement -- the leading zero is NOT part of the magnitude).
79//
80// Returns 0 on success, NX_ECDSA_DER_LEN_TOO_BIG if len > 33,
81// or NX_ECDSA_DER_BAD_R if len <= 0.
82func ecdsa_int_load(out: *i64, bytes: *u8, off: i64, len: i64) -> i64 {
83 if len <= 0 { return NX_ECDSA_DER_BAD_R }
84 if len > 33 { return NX_ECDSA_DER_LEN_TOO_BIG }
85
86 var skip: i64 = 0
87 var elen: i64 = len
88 // Strip one leading zero byte (the DER 0x00 prefix for high-bit
89 // positive INTEGERs). Only valid if it brings us back to <= 32
90 // bytes of actual magnitude.
91 if len == 33 {
92 if (bytes[off] & 0xff) != 0 { return NX_ECDSA_DER_LEN_TOO_BIG }
93 skip = 1
94 elen = 32
95 }
96
97 u256_zero(out)
98 // Walk MSB-first. bytes[off+skip] is the most-significant byte;
99 // bytes[off+skip+elen-1] is the LSB.
100 //
101 // Inverse of u256_load_be: that one takes exactly 32 bytes and
102 // maps bytes[i*4..i*4+3] to limb[7-i]. Here we have a variable
103 // length elen in [1, 32]; pad with zeros on the left to reach 32,
104 // then do the same mapping.
105 let padded: *u8 = sys_mmap(32)
106 var k: i64 = 0
107 while k < 32 { padded[k] = 0; k = k + 1 }
108 let pad_off: i64 = 32 - elen
109 k = 0
110 while k < elen {
111 padded[pad_off + k] = bytes[off + skip + k]
112 k = k + 1
113 }
114 u256_load_be(out, padded)
115 return 0
116}
117
118// Parse a DER-encoded ECDSA signature (SEQUENCE { INTEGER r,
119// INTEGER s }) into two 8-limb LE U256 buffers.
120//
121// Returns NX_ECDSA_DER_OK on success, or a non-OK verdict.
122func nx_ecdsa_sig_parse_der(buf: *u8, len: i64,
123 out_r: *i64, out_s: *i64) -> i64 {
124 let c_raw: *u8 = sys_mmap(32)
125 let c: *Asn1Cursor = c_raw as *Asn1Cursor
126 asn1_cursor_init(c, len)
127
128 // ---- Outer SEQUENCE ----
129 let outer_len_p: *i64 = sys_mmap(16) as *i64
130 let rc1: i64 = asn1_expect_tag(buf, c, ASN1_SEQUENCE, outer_len_p)
131 if rc1 < 0 { return NX_ECDSA_DER_BAD_OUTER }
132 let outer_end: i64 = c.pos + *outer_len_p
133 if outer_end > len { return NX_ECDSA_DER_BAD_OUTER }
134
135 // ---- r INTEGER ----
136 let r_len_p: *i64 = sys_mmap(16) as *i64
137 let rc2: i64 = asn1_expect_tag(buf, c, ASN1_INTEGER, r_len_p)
138 if rc2 < 0 { return NX_ECDSA_DER_BAD_R }
139 let r_off: i64 = c.pos
140 let r_len: i64 = *r_len_p
141 c.pos = c.pos + r_len
142 if c.pos > outer_end { return NX_ECDSA_DER_BAD_R }
143
144 let load_r: i64 = ecdsa_int_load(out_r, buf, r_off, r_len)
145 if load_r != 0 { return load_r }
146
147 // ---- s INTEGER ----
148 let s_len_p: *i64 = sys_mmap(16) as *i64
149 let rc3: i64 = asn1_expect_tag(buf, c, ASN1_INTEGER, s_len_p)
150 if rc3 < 0 { return NX_ECDSA_DER_BAD_S }
151 let s_off: i64 = c.pos
152 let s_len: i64 = *s_len_p
153 c.pos = c.pos + s_len
154 if c.pos > outer_end { return NX_ECDSA_DER_BAD_S }
155
156 let load_s: i64 = ecdsa_int_load(out_s, buf, s_off, s_len)
157 if load_s != 0 {
158 if load_s == NX_ECDSA_DER_BAD_R { return NX_ECDSA_DER_BAD_S }
159 return load_s
160 }
161
162 // ---- No trailing bytes inside the outer SEQUENCE ----
163 if c.pos != outer_end { return NX_ECDSA_DER_TRAILING }
164
165 return NX_ECDSA_DER_OK
166}
167
168// ---- DER ENCODE: (r,s limbs) -> SEQUENCE { INTEGER r, INTEGER s } ----
169// Needed by the ACME CSR signature + any ECDSA-in-X.509/TLS encoder.
170// Each INTEGER is minimal big-endian with a 0x00 prepended when the
171// high bit is set (DER positive-integer rule).
172
173// Emit one INTEGER (from u256 limbs) at out[off]; return new offset.
174func ecdsa_int_emit(limbs: *i64, out: *u8, off: i64) -> i64 {
175 let be: *u8 = sys_mmap(32)
176 u256_store_be(be, limbs)
177 var start: i64 = 0
178 while start < 31 {
179 if be[start] != (0 as u8) { break }
180 start = start + 1
181 }
182 let vlen: i64 = 32 - start
183 var prepend: i64 = 0
184 if ((be[start] as i64) & 0x80) != 0 { prepend = 1 }
185 var o: i64 = off
186 out[o] = 0x02 as u8; o = o + 1 // INTEGER tag
187 out[o] = (vlen + prepend) as u8; o = o + 1 // length (always < 128 here)
188 if prepend == 1 { out[o] = 0 as u8; o = o + 1 }
189 var i: i64 = start
190 while i < 32 { out[o] = be[i]; o = o + 1; i = i + 1 }
191 return o
192}
193
194func nx_ecdsa_sig_emit_der(r_limbs: *i64, s_limbs: *i64,
195 out: *u8, cap: i64, out_n: *i64) -> i64 {
196 let body: *u8 = sys_mmap(80)
197 var bo: i64 = 0
198 bo = ecdsa_int_emit(r_limbs, body, bo)
199 bo = ecdsa_int_emit(s_limbs, body, bo)
200 if bo + 2 > cap { return NX_ECDSA_DER_BAD_OUTER }
201 var o: i64 = 0
202 out[o] = 0x30 as u8; o = o + 1 // SEQUENCE
203 out[o] = bo as u8; o = o + 1 // length (two ~33B ints < 128)
204 var i: i64 = 0
205 while i < bo { out[o] = body[i]; o = o + 1; i = i + 1 }
206 out_n[0] = o
207 return NX_ECDSA_DER_OK
208}
209
210// KAT: encode (r,s) -> parse back -> limbs must match. Covers the
211// high-bit-prepend path (r MSB set) and the no-prepend path (s small).
212func main() -> i64 {
213 let rbe: *u8 = sys_mmap(32)
214 let sbe: *u8 = sys_mmap(32)
215 var i: i64 = 0
216 while i < 32 { rbe[i] = (0x80 + i) as u8; sbe[i] = (i + 1) as u8; i = i + 1 }
217 rbe[0] = 0xC3 as u8 // high bit set -> prepend 0x00
218 sbe[0] = 0x00 as u8 // leading zero -> stripped
219 let r: *i64 = u256_alloc(); u256_load_be(r, rbe)
220 let s: *i64 = u256_alloc(); u256_load_be(s, sbe)
221
222 let der: *u8 = sys_mmap(128)
223 let der_n: *i64 = sys_mmap(8) as *i64
224 if nx_ecdsa_sig_emit_der(r, s, der, 128, der_n) != NX_ECDSA_DER_OK {
225 sys_write(2, "emit FAIL\n" as *u8, 10); return 1
226 }
227 let r2: *i64 = u256_alloc()
228 let s2: *i64 = u256_alloc()
229 if nx_ecdsa_sig_parse_der(der, der_n[0], r2, s2) != NX_ECDSA_DER_OK {
230 sys_write(2, "parse-back FAIL\n" as *u8, 16); return 2
231 }
232 if u256_eq(r, r2) != 1 { sys_write(2, "r mismatch FAIL\n" as *u8, 16); return 3 }
233 if u256_eq(s, s2) != 1 { sys_write(2, "s mismatch FAIL\n" as *u8, 16); return 4 }
234 sys_write(1, "ECDSA SIG DER encode<->parse KAT PASS\n" as *u8, 38)
235 return 0
236}