code wiki / (root) / nx_ecdsa_sig_der.nx

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}