code wiki / (root) / nx_x509_chain_verify.nx

nx_x509_chain_verify.nx source

↩ module page · 236 lines · 10716 B

1// nx_x509_chain_verify.nx -- walk an X.509 cert chain leaf-to-root. 2// 3// Phase 0b §I.4 piece 7 of the chain-walker arc. THE primitive 4// that turns the substrate's single-cert verify (verify_under_issuer, 5// commit 927d9d47) into a real Web PKI chain validator. 6// 7// Composes: 8// nx_x509_verify_under_issuer.nx -- per-cert sig verify 9// nx_x509_dn_match.nx -- DN byte-equality 10// nx_x509_leaf_check.nx (caller) -- separate; caller invokes 11// first for leaf cert's 12// validity + SAN. 13// 14// Chain model: caller prepares an array of parsed X509Cert 15// pointers + their backing buffers, in order from leaf to root: 16// certs[0] = leaf cert (server identity) 17// certs[1] = first intermediate (signed leaf) 18// certs[..] = more intermediates 19// certs[n-1] = trust anchor (root CA, treated as self-signed for 20// the chain-walker purpose -- caller 21// has separately confirmed it lives in 22// the trusted root set) 23// 24// For each adjacent (child, parent) pair (i, i+1): 25// 1. DN linkage: child.issuer must byte-equal parent.subject 26// 2. Sig verify: child's outer sig verifies under parent's pubkey 27// 28// The final cert (trust anchor) is NOT verified against anything 29// further -- the caller's responsibility to ensure it's actually 30// in the trusted root set. RFC 5280 §6.1 calls this the "trust 31// anchor input" to path validation. 32// 33// Public API: 34// nx_x509_chain_verify(bufs, certs, n) -> verdict 35// bufs[i] = *u8 to cert[i]'s DER buffer 36// certs[i] = *X509Cert parsed from bufs[i] 37// n = number of certs in the chain (>=1) 38// nx_x509_chain_verdict_is_valid(v) -> 0|1 39// 40// Sealed verdict enum: 41// NX_X509_CHAIN_OK every link verified 42// NX_X509_CHAIN_EMPTY n < 1 43// NX_X509_CHAIN_TOO_LONG n > NX_X509_CHAIN_MAX_LEN 44// NX_X509_CHAIN_DN_MISMATCH child.issuer != parent.subject 45// at some link 46// NX_X509_CHAIN_SIG_FAIL verify_under_issuer returned 47// non-OK at some link 48// 49// What this primitive does NOT do (caller responsibility): 50// - Leaf cert's validity period + SAN-hostname match 51// (caller runs nx_x509_leaf_check FIRST) 52// - Trust-anchor lookup against a root CA store (caller 53// prepares the ordered chain ending in a confirmed root) 54// - Path-length constraint checks (basicConstraints, 55// pathLenConstraint extension) 56// - keyUsage / extendedKeyUsage / certificatePolicies enforcement 57// - Revocation (OCSP / CRL) 58// - Name constraints 59// These are separate primitives queued for the path-validation 60// completion arc. 61// 62// Per Cardinals 9 (single-responsibility -- this primitive does 63// JUST link-by-link chain walk; trust-anchor lookup + leaf-check 64// + revocation are separate), 12 (defensive at boundaries -- max 65// chain length cap), 22 (composition -- 2 substrate primitives 66// compose into one walker), 23 (preamble names every caller- 67// responsibility item). 68// 69// license_tier: INDEPENDENT_REDERIVE 70// genealogy_id: international-research-sources/ietf/rfc_5280 71// lineage_id: nishi_x509_chain_verify_q10 72 73// nx_safety_envelope: 74// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 75// sil_target: SIL1 76// evidence: [bulk_applied_2026-05-19, x509-chain-walker] 77// verdict: NOT_YET_EVALUATED 78 79import "nx_syscalls.nx" 80import "nx_x509.nx" 81import "nx_x509_dn_match.nx" 82import "nx_x509_verify_under_issuer.nx" 83import "nx_x509_sig_alg.nx" 84import "nx_u384.nx" 85import "nx_sha256.nx" 86const NX_MAGIC_8192: i64 = 8192 87 88static _CV_PN_BUF: i64 89func _cv_pn(v: i64) -> i64 { 90 if _CV_PN_BUF == 0 { _CV_PN_BUF = sys_mmap(24) as i64 } // certloop leak fix: alloc the print buf ONCE 91 let b: *u8 = _CV_PN_BUF as *u8 92 var x: i64 = v 93 if x < 0 { x = 0 - x } 94 var i: i64 = 22 95 if x == 0 { b[i] = 0x30 as u8; i = i - 1 } 96 else { while x > 0 { b[i] = (0x30 + (x - (x/10)*10)) as u8; x = x / 10; i = i - 1 } } 97 sys_write(2, ((b as i64) + i + 1) as *u8, 22 - i) 98 return 0 99} 100 101const NX_X509_CHAIN_OK: i64 = 1 102const NX_X509_CHAIN_EMPTY: i64 = 2 103const NX_X509_CHAIN_TOO_LONG: i64 = 3 104const NX_X509_CHAIN_DN_MISMATCH: i64 = 4 105const NX_X509_CHAIN_SIG_FAIL: i64 = 5 106const NX_X509_CHAIN_VERDICT_N: i64 = 6 107 108const NX_X509_CHAIN_MAX_LEN: i64 = 16 109 110func nx_x509_chain_verdict_is_valid(v: i64) -> i64 { 111 if v < NX_X509_CHAIN_OK { return 0 } 112 if v >= NX_X509_CHAIN_VERDICT_N { return 0 } 113 return 1 114} 115 116// ---- MEMOIZATION CACHE (SOTA TLS speedup, F102c/F108f) -------------------------------------------------------- 117// nx_x509_verify_under_issuer is a PURE FUNCTION of the child+parent DER bytes (no time/state -- validity/expiry 118// are checked separately by the caller's leaf_check, NOT here). So memoizing it on sha256(child_der || parent_der) 119// is PROVABLY SAFE: a different/forged cert = different bytes = different hash = cache MISS = full verify. A hit can 120// only occur for a (child,parent) pair whose EXACT bytes were verified before. THE WIN: Let's Encrypt's ECDSA-P-384 121// intermediate+root (E5/E6 + ISRG Root X2) are byte-identical across every LE-issued host, so their ~273ms P-384 122// verify is computed ONCE per process then served from cache (~0ms). In-process static only -- no cross-process or 123// persisted state, no poisoning surface. Gated: nx_x509_chain_cache_gate (hit==recompute, tamper==miss==reject, 124// distinct chains never cross-hit). 125const CV_CACHE_N: i64 = 128 126static _CV_KEYS: i64 // mmap CV_CACHE_N*32 : sha256 keys 127static _CV_RES: i64 // mmap CV_CACHE_N*8 : i64 memoized results 128static _CV_VAL: i64 // mmap CV_CACHE_N : 1=slot occupied 129static _CV_NEXT: i64 // round-robin insert cursor 130static _CV_HITS: i64 // diagnostics 131// DER total length (header+content) of a SEQUENCE; 0 if not parseable -> caller falls through to full verify (safe). 132func _cv_der_len(buf: *u8) -> i64 { 133 if (buf[0] & 0xff) != 48 { return 0 } // 0x30 = SEQUENCE 134 let l0: i64 = buf[1] & 0xff 135 if l0 < 128 { return 2 + l0 } // short-form length 136 let nlen: i64 = l0 - 128 // long-form: nlen length octets 137 if nlen <= 0 { return 0 } 138 if nlen > 4 { return 0 } 139 var len: i64 = 0 140 var k: i64 = 0 141 while k < nlen { len = (len << 8) | (buf[2 + k] & 0xff); k = k + 1 } 142 return 2 + nlen + len 143} 144func _cv_key_eq(keys: *u8, off: i64, key: *u8) -> i64 { 145 var i: i64 = 0 146 while i < 32 { if (keys[off + i] & 0xff) != (key[i] & 0xff) { return 0 } i = i + 1 } 147 return 1 148} 149// memoized verify: same result as nx_x509_verify_under_issuer, served from cache when the exact bytes recur. 150func _cv_verify_memo(child_buf: *u8, child: *X509Cert, parent_buf: *u8, parent: *X509Cert) -> i64 { 151 let clen: i64 = _cv_der_len(child_buf) 152 let plen: i64 = _cv_der_len(parent_buf) 153 if clen <= 0 { return nx_x509_verify_under_issuer(child_buf, child, parent_buf, parent) } 154 if plen <= 0 { return nx_x509_verify_under_issuer(child_buf, child, parent_buf, parent) } 155 if clen + plen > NX_MAGIC_8192 { return nx_x509_verify_under_issuer(child_buf, child, parent_buf, parent) } 156 let tmp: *u8 = sys_mmap(NX_MAGIC_8192) 157 var o: i64 = 0 158 var a: i64 = 0 159 while a < clen { tmp[o] = child_buf[a]; o = o + 1; a = a + 1 } 160 var b: i64 = 0 161 while b < plen { tmp[o] = parent_buf[b]; o = o + 1; b = b + 1 } 162 let key: *u8 = sys_mmap(32) 163 sha256_digest(tmp, o, key) 164 if _CV_KEYS == 0 { 165 _CV_KEYS = sys_mmap(CV_CACHE_N * 32) as i64 166 _CV_RES = sys_mmap(CV_CACHE_N * 8) as i64 167 _CV_VAL = sys_mmap(CV_CACHE_N) as i64 168 _CV_NEXT = 0 169 } 170 let keys: *u8 = _CV_KEYS as *u8 171 let res: *i64 = _CV_RES as *i64 172 let val: *u8 = _CV_VAL as *u8 173 var e: i64 = 0 174 while e < CV_CACHE_N { 175 if (val[e] & 0xff) == 1 { if _cv_key_eq(keys, e * 32, key) == 1 { _CV_HITS = _CV_HITS + 1; return res[e] } } 176 e = e + 1 177 } 178 let v: i64 = nx_x509_verify_under_issuer(child_buf, child, parent_buf, parent) 179 let slot: i64 = _CV_NEXT 180 var c: i64 = 0 181 while c < 32 { keys[slot * 32 + c] = key[c]; c = c + 1 } 182 res[slot] = v 183 val[slot] = 1 as u8 184 _CV_NEXT = slot + 1 185 if _CV_NEXT >= CV_CACHE_N { _CV_NEXT = 0 } 186 return v 187} 188 189// Walk a parsed cert chain from leaf (index 0) to trust anchor 190// (index n-1). For each adjacent (child, parent) pair, verify 191// DN linkage + signature. Single-cert chains are accepted (n=1, 192// implies the only cert IS the trust anchor; caller's 193// responsibility to confirm). 194// 195// bufs: array of *u8 (DER buffers, one per cert) 196// certs: array of *X509Cert (already parsed against the matching buf) 197func nx_x509_chain_verify(bufs: **u8, certs: **X509Cert, n: i64) -> i64 { 198 if n < 1 { return NX_X509_CHAIN_EMPTY } 199 if n > NX_X509_CHAIN_MAX_LEN { return NX_X509_CHAIN_TOO_LONG } 200 201 // Walk adjacent pairs. For n=1 this loop body runs 0 times 202 // and we return OK -- single-cert "chain" is trivially valid. 203 var i: i64 = 0 204 while i < n - 1 { 205 let _sav: i64 = nx_u384_scratch_save() // certloop leak fix: reclaim this link's verify scratch each iter 206 let child_buf: *u8 = bufs[i] 207 let child: *X509Cert = certs[i] 208 let parent_buf: *u8 = bufs[i + 1] 209 let parent: *X509Cert = certs[i + 1] 210 211 // ---- DN linkage: child.issuer == parent.subject ---- 212 let dn_ok: i64 = nx_x509_dn_match(child_buf, 213 child.issuer_off, child.issuer_len, 214 parent_buf, 215 parent.subject_off, parent.subject_len) 216 if dn_ok != 1 { nx_u384_scratch_restore(_sav); return NX_X509_CHAIN_DN_MISMATCH } 217 218 // ---- Sig verify: child's outer sig under parent's pubkey ---- 219 let _alg: i64 = nx_x509_sig_alg_identify(child_buf, child.sig_alg_off, child.sig_alg_len) 220 let _t0: i64 = sys_now_ms() 221 let v: i64 = _cv_verify_memo(child_buf, child, parent_buf, parent) 222 let _t1: i64 = sys_now_ms() 223 sys_write(2, "nishi-cert link alg=" as *u8, 20); _cv_pn(_alg) 224 sys_write(2, " ms=" as *u8, 4); _cv_pn(_t1 - _t0); sys_write(2, "\n" as *u8, 1) 225 if v != NX_X509_VUI_OK { nx_u384_scratch_restore(_sav); return NX_X509_CHAIN_SIG_FAIL } 226 227 nx_u384_scratch_restore(_sav) 228 i = i + 1 229 } 230 return NX_X509_CHAIN_OK 231} 232 233// Compile-only smoke. Real KAT in nx_x509_chain_verify_test.nx. 234func main() -> i64 { 235 return 0 236}