code wiki / (root) / nx_https_cert_pipeline.nx

nx_https_cert_pipeline.nx source

↩ module page · 297 lines · 13582 B

1// nx_https_cert_pipeline.nx -- top-level HTTPS cert verification 2// pipeline. 3// 4// Phase 0b §I.4 piece 8 of the chain-walker arc (the user-facing 5// API). Bundles the shipped substrate primitives into ONE call 6// that takes raw TLS 1.3 Certificate message bytes + hostname + 7// trust anchor + now-epoch and returns one composite verdict. 8// 9// Composes 4 substrate primitives: 10// nx_tls13_auth.tls13_parse_certificate_chain_all (9f35188f) 11// -- extract cert offset array from Certificate message 12// nx_x509.x509_parse (extended for issuer/subject DN) 13// -- parse each cert's DER into an X509Cert struct 14// nx_x509_leaf_check.x509_leaf_check (50d861a8) 15// -- leaf-cert validity period + SAN hostname match 16// nx_x509_chain_verify.nx_x509_chain_verify (afe895e6) 17// -- chain walker (DN linkage + per-cert sig verify) 18// 19// Flow: 20// 1. parse_certificate_chain_all -> cert offset array 21// 2. for each offset, x509_parse into a parsed-cert slab 22// 3. leaf_check(cert[0]) -- validity period + SAN hostname match 23// (returns LEAF_* on first failure) 24// 4. Append the trust anchor cert at the end of the parsed array 25// and call chain_verify on the (leaf..root) ordering 26// 5. Return composite verdict 27// 28// Public API: 29// nx_https_cert_pipeline_verify( 30// cert_msg_buf, cert_msg_len, 31// hostname, hostname_len, 32// now_epoch_secs, 33// trust_anchor_buf, trust_anchor_cert 34// ) -> verdict 35// 36// nx_https_cert_pipeline_verdict_is_valid(v) -> 0|1 37// 38// Sealed verdict enum: 39// NX_HTTPS_PIPELINE_OK full validation passed 40// NX_HTTPS_PIPELINE_CERT_MSG_BAD Certificate-message parse fail 41// NX_HTTPS_PIPELINE_CERT_PARSE_FAIL x509_parse fail on some cert 42// NX_HTTPS_PIPELINE_LEAF_NOT_YET_VALID leaf cert not yet valid (clock) 43// NX_HTTPS_PIPELINE_LEAF_EXPIRED leaf cert expired 44// NX_HTTPS_PIPELINE_LEAF_NO_SAN leaf has no SAN extension 45// NX_HTTPS_PIPELINE_HOSTNAME_MISMATCH hostname doesn't match SAN 46// NX_HTTPS_PIPELINE_LEAF_OTHER other leaf-check failure 47// NX_HTTPS_PIPELINE_CHAIN_DN_MISMATCH DN linkage broken 48// NX_HTTPS_PIPELINE_CHAIN_SIG_FAIL cert sig under issuer failed 49// NX_HTTPS_PIPELINE_CHAIN_OTHER other chain-walk failure 50// 51// What this primitive does NOT do (caller responsibility): 52// - Pick the trust_anchor_cert from a root CA store. Caller 53// does that lookup (typically: get leaf's issuer DN -> look up 54// in Mozilla bundle subject-DN index -> pass matching root in). 55// - Revocation (OCSP / CRL). 56// - Extension policy (basicConstraints, keyUsage, 57// extendedKeyUsage, name constraints). 58// 59// Per Cardinals 9 (single-responsibility -- this is the 60// composition point, no new logic), 12 (defensive at boundaries -- 61// cap on cert array size), 22 (composition -- 4 substrate 62// primitives into 1 user-facing call), 23 (preamble names every 63// caller responsibility). 64// 65// license_tier: INDEPENDENT_REDERIVE 66// genealogy_id: international-research-sources/ietf/rfc_5280 + rfc_8446 67// lineage_id: nishi_https_cert_pipeline_q10 68 69// nx_safety_envelope: 70// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 71// sil_target: SIL1 72// evidence: [bulk_applied_2026-05-19, https-cert-pipeline-orchestrator] 73// verdict: NOT_YET_EVALUATED 74 75import "nx_syscalls.nx" 76import "nx_x509.nx" 77import "nx_x509_leaf_check.nx" 78import "nx_x509_chain_verify.nx" 79import "nx_x509_trust_store.nx" 80import "nx_tls13_auth.nx" 81 82const NX_HTTPS_PIPELINE_OK: i64 = 1 83const NX_HTTPS_PIPELINE_CERT_MSG_BAD: i64 = 2 84const NX_HTTPS_PIPELINE_CERT_PARSE_FAIL: i64 = 3 85const NX_HTTPS_PIPELINE_LEAF_NOT_YET_VALID: i64 = 4 86const NX_HTTPS_PIPELINE_LEAF_EXPIRED: i64 = 5 87const NX_HTTPS_PIPELINE_LEAF_NO_SAN: i64 = 6 88const NX_HTTPS_PIPELINE_HOSTNAME_MISMATCH: i64 = 7 89const NX_HTTPS_PIPELINE_LEAF_OTHER: i64 = 8 90const NX_HTTPS_PIPELINE_CHAIN_DN_MISMATCH: i64 = 9 91const NX_HTTPS_PIPELINE_CHAIN_SIG_FAIL: i64 = 10 92const NX_HTTPS_PIPELINE_CHAIN_OTHER: i64 = 11 93const NX_HTTPS_PIPELINE_NO_TRUST_ANCHOR: i64 = 12 94const NX_HTTPS_PIPELINE_VERDICT_N: i64 = 13 95 96const NX_HTTPS_PIPELINE_MAX_CERTS: i64 = 8 97 98func nx_https_cert_pipeline_verdict_is_valid(v: i64) -> i64 { 99 if v < NX_HTTPS_PIPELINE_OK { return 0 } 100 if v >= NX_HTTPS_PIPELINE_VERDICT_N { return 0 } 101 return 1 102} 103 104// Parametrized core: verify the first `n_use_in` PRESENTED certs (leaf-first) with 105// `trust_anchor_*` appended as the terminal trust anchor. n_use_in <= 0 means "use ALL 106// presented certs" (the classic server-omitted-root case). n_use_in = n-1 lets the 107// resolver DROP a server-included (or cross-signed) root and re-terminate on OUR trusted 108// store copy of that root -- so the chain ALWAYS ends on a cert whose key we independently 109// trust, never on an attacker-presentable terminal. Single-responsibility core shared by 110// the public explicit-anchor API and the trust-store resolver. 111func _pipeline_verify_n( 112 cert_msg_buf: *u8, cert_msg_len: i64, 113 n_use_in: i64, 114 hostname: *u8, hostname_len: i64, 115 now_epoch_secs: i64, 116 trust_anchor_buf: *u8, trust_anchor_cert: *X509Cert 117) -> i64 { 118 // ---- Step 1: parse the Certificate message into offset arrays ---- 119 let cert_offs_raw: *u8 = sys_mmap(NX_HTTPS_PIPELINE_MAX_CERTS * 8) 120 let cert_lens_raw: *u8 = sys_mmap(NX_HTTPS_PIPELINE_MAX_CERTS * 8) 121 let cert_offs: *i64 = cert_offs_raw as *i64 122 let cert_lens: *i64 = cert_lens_raw as *i64 123 let n_certs_p: *i64 = sys_mmap(16) as *i64 124 125 let parse_msg: i64 = tls13_parse_certificate_chain_all( 126 cert_msg_buf, cert_msg_len, 127 cert_offs, cert_lens, n_certs_p, 128 NX_HTTPS_PIPELINE_MAX_CERTS 129 ) 130 if parse_msg != NX_TLS13_AUTH_VERDICT_OK { return NX_HTTPS_PIPELINE_CERT_MSG_BAD } 131 let n_present: i64 = *n_certs_p 132 if n_present < 1 { return NX_HTTPS_PIPELINE_CERT_MSG_BAD } 133 134 // How many presented certs to include below the appended anchor. 135 var n_use: i64 = n_use_in 136 if n_use < 1 { n_use = n_present } // sentinel: use all presented 137 if n_use > n_present { n_use = n_present } // clamp defensively 138 139 // ---- Step 2: x509_parse each used cert; append the trust anchor at index n_use ---- 140 let total_certs: i64 = n_use + 1 141 let bufs_raw: *u8 = sys_mmap(total_certs * 8) 142 let certs_raw: *u8 = sys_mmap(total_certs * 8) 143 let bufs: **u8 = bufs_raw as **u8 144 let certs: **X509Cert = certs_raw as **X509Cert 145 146 var i: i64 = 0 147 while i < n_use { 148 let c_raw: *u8 = sys_mmap(256) 149 let c: *X509Cert = c_raw as *X509Cert 150 let cert_der_ptr: *u8 = cert_msg_buf + cert_offs[i] 151 let rc: i64 = x509_parse(cert_der_ptr, cert_lens[i], c) 152 if rc < 0 { return NX_HTTPS_PIPELINE_CERT_PARSE_FAIL } 153 bufs[i] = cert_der_ptr 154 certs[i] = c 155 i = i + 1 156 } 157 // Append trust anchor (our trusted store copy = the terminal we trust) 158 bufs[n_use] = trust_anchor_buf 159 certs[n_use] = trust_anchor_cert 160 161 // ---- Step 3: leaf check (validity period + SAN hostname) -- always the real leaf ---- 162 let leaf_v: i64 = x509_leaf_check( 163 bufs[0], cert_lens[0], 164 hostname, hostname_len, now_epoch_secs 165 ) 166 if leaf_v == NX_X509_LEAF_OK { 167 // Continue to chain check. 168 } else { 169 if leaf_v == NX_X509_LEAF_NOT_YET_VALID { return NX_HTTPS_PIPELINE_LEAF_NOT_YET_VALID } 170 if leaf_v == NX_X509_LEAF_EXPIRED { return NX_HTTPS_PIPELINE_LEAF_EXPIRED } 171 if leaf_v == NX_X509_LEAF_NO_SAN { return NX_HTTPS_PIPELINE_LEAF_NO_SAN } 172 if leaf_v == NX_X509_LEAF_HOSTNAME_MISMATCH { return NX_HTTPS_PIPELINE_HOSTNAME_MISMATCH } 173 return NX_HTTPS_PIPELINE_LEAF_OTHER 174 } 175 176 // ---- Step 4: chain verify (every adjacent (child, parent) link) ---- 177 let chain_v: i64 = nx_x509_chain_verify(bufs, certs, total_certs) 178 if chain_v == NX_X509_CHAIN_OK { return NX_HTTPS_PIPELINE_OK } 179 if chain_v == NX_X509_CHAIN_DN_MISMATCH { return NX_HTTPS_PIPELINE_CHAIN_DN_MISMATCH } 180 if chain_v == NX_X509_CHAIN_SIG_FAIL { return NX_HTTPS_PIPELINE_CHAIN_SIG_FAIL } 181 return NX_HTTPS_PIPELINE_CHAIN_OTHER 182} 183 184// Public explicit-anchor API (UNCHANGED contract): verify ALL presented certs against the 185// caller-supplied anchor appended as terminal. Thin wrapper over the parametrized core. 186func nx_https_cert_pipeline_verify( 187 cert_msg_buf: *u8, cert_msg_len: i64, 188 hostname: *u8, hostname_len: i64, 189 now_epoch_secs: i64, 190 trust_anchor_buf: *u8, trust_anchor_cert: *X509Cert 191) -> i64 { 192 return _pipeline_verify_n( 193 cert_msg_buf, cert_msg_len, 194 0, // 0 => use all presented certs 195 hostname, hostname_len, 196 now_epoch_secs, 197 trust_anchor_buf, trust_anchor_cert 198 ) 199} 200 201// Trust-store-aware variant. Like nx_https_cert_pipeline_verify 202// but resolves the trust anchor automatically by looking up the 203// LAST presented cert's issuer DN in the supplied store. 204// 205// Use case: caller has a Mozilla root CA bundle loaded into a 206// TrustStore (one anchor per root), and wants the substrate to 207// figure out which root the peer's chain terminates at. No need 208// to pre-pick the trust anchor based on hostname. 209// 210// Returns NX_HTTPS_PIPELINE_NO_TRUST_ANCHOR if the last cert's 211// issuer DN matches no entry in the store. Other verdicts pass 212// through from the inner pipeline. 213// 214// Algorithm: 215// 1. parse_certificate_chain_all -> offsets 216// 2. x509_parse the LAST cert (needed for issuer DN); rest are 217// parsed again inside the inner verify call (cheap) 218// 3. trust_store_lookup_by_subject(last.issuer) 219// 4. If found -> delegate to nx_https_cert_pipeline_verify with 220// that anchor. 221func nx_https_cert_pipeline_verify_with_store( 222 cert_msg_buf: *u8, cert_msg_len: i64, 223 hostname: *u8, hostname_len: i64, 224 now_epoch_secs: i64, 225 store: *TrustStore 226) -> i64 { 227 // ---- Step 1: parse cert offsets ---- 228 let offs_raw: *u8 = sys_mmap(NX_HTTPS_PIPELINE_MAX_CERTS * 8) 229 let lens_raw: *u8 = sys_mmap(NX_HTTPS_PIPELINE_MAX_CERTS * 8) 230 let offs: *i64 = offs_raw as *i64 231 let lens: *i64 = lens_raw as *i64 232 let n_p: *i64 = sys_mmap(16) as *i64 233 let parse_v: i64 = tls13_parse_certificate_chain_all( 234 cert_msg_buf, cert_msg_len, offs, lens, n_p, 235 NX_HTTPS_PIPELINE_MAX_CERTS 236 ) 237 if parse_v != NX_TLS13_AUTH_VERDICT_OK { return NX_HTTPS_PIPELINE_CERT_MSG_BAD } 238 let n: i64 = *n_p 239 if n < 1 { return NX_HTTPS_PIPELINE_CERT_MSG_BAD } 240 241 // ---- Step 2: parse the LAST cert to read its issuer DN ---- 242 let last_buf: *u8 = cert_msg_buf + offs[n - 1] 243 let last_len: i64 = lens[n - 1] 244 let last_cert_raw: *u8 = sys_mmap(256) 245 let last_cert: *X509Cert = last_cert_raw as *X509Cert 246 let rc: i64 = x509_parse(last_buf, last_len, last_cert) 247 if rc < 0 { return NX_HTTPS_PIPELINE_CERT_PARSE_FAIL } 248 249 // ---- Step 3: trust-anchor resolution -- handle BOTH chain shapes ---- 250 // Case A (root-INCLUDED / cross-signed root): the last presented cert IS a trusted root 251 // (its SUBJECT matches a store anchor). Modern servers send these -- e.g. Google's 252 // chain ends in GTS Root R4 cross-signed by GTS Root R1 (R1 is the *issuer*, not in 253 // our store; R4 is the trusted root, IS in our store). Re-terminate on OUR trusted 254 // store copy of that root and verify the presented chain MINUS its (redundant, and 255 // not-independently-trusted) terminal. This is the gap that blocked every Cloudflare/ 256 // Google-fronted site while servers that OMIT the root (Case B) worked. 257 // Case B (root-OMITTED): the last presented cert's ISSUER is a trusted root. Append it. 258 // SECURITY: in BOTH cases the chain terminates on a cert whose public key we independently 259 // trust (the store copy), and every link is signature-verified up to it -- a forged 260 // "GTS Root R4" with a different key cannot validate, because we verify the intermediate 261 // under OUR copy's key, never the presented terminal's. 262 let anchor_by_issuer: *TrustAnchor = trust_store_lookup_by_subject( 263 store, last_buf, last_cert.issuer_off, last_cert.issuer_len 264 ) 265 let anchor_by_subject: *TrustAnchor = trust_store_lookup_by_subject( 266 store, last_buf, last_cert.subject_off, last_cert.subject_len 267 ) 268 269 // Case A takes precedence: a presented terminal that IS a trusted root is authoritative. 270 if (anchor_by_subject as i64) != 0 { 271 if n >= 2 { 272 return _pipeline_verify_n( 273 cert_msg_buf, cert_msg_len, 274 n - 1, // drop the presented (untrusted) root copy 275 hostname, hostname_len, 276 now_epoch_secs, 277 anchor_by_subject.buf, anchor_by_subject.cert 278 ) 279 } 280 } 281 // Case B: server omitted the root; append the one whose subject == last cert's issuer. 282 if (anchor_by_issuer as i64) != 0 { 283 return _pipeline_verify_n( 284 cert_msg_buf, cert_msg_len, 285 n, // use all presented, append the root 286 hostname, hostname_len, 287 now_epoch_secs, 288 anchor_by_issuer.buf, anchor_by_issuer.cert 289 ) 290 } 291 return NX_HTTPS_PIPELINE_NO_TRUST_ANCHOR 292} 293 294// Compile-only smoke. Real KAT in nx_https_cert_pipeline_test.nx. 295func main() -> i64 { 296 return 0 297}