code wiki / (root) / nx_x509_leaf_check.nx

nx_x509_leaf_check.nx source

↩ module page · 113 lines · 5058 B

1// nx_x509_leaf_check.nx -- single-cert "leaf" orchestrator. 2// 3// Composes the three shipped X.509 primitives: 4// - x509_parse (nx_x509.nx) 5// - x509_validity_check (nx_x509_validity.nx) 6// - x509_san_match_hostname (nx_x509_san.nx) 7// 8// into one call that callers (nx_https_client + future chain 9// walker) use to validate a single leaf cert against a hostname 10// + clock. 11// 12// What this DOES verify (today, this commit): 13// - DER parses cleanly per RFC 5280 §4.1 14// - now_epoch_secs is within [notBefore, notAfter] 15// - hostname matches a SAN dNSName entry (RFC 6125 §6.4) 16// 17// What this DOES NOT verify (separate primitives, queued): 18// - signature_algorithm OID is one of the supported set 19// (caller-driven via cert.sig_alg_off/_len + per-alg verifier) 20// - signature value matches the cert's tbs under a trust-anchor 21// pubkey (chain walk + ECDSA-P256 verify queued) 22// - revocation status (OCSP / CRL -- separate primitives) 23// - extension-criticality checks (every extension marked critical 24// must be understood by the validator -- iterator queued) 25// - certificatePolicies, basicConstraints, keyUsage, 26// extendedKeyUsage (queued) 27// 28// The leaf-check verdict therefore answers "is this cert 29// well-formed AND within its validity period AND covering the 30// hostname I asked for" -- it does NOT answer "should I trust 31// this cert". A chain walker that establishes trust calls 32// nx_x509_leaf_check for every cert in the path PLUS its own 33// signature verify. This split keeps single-responsibility 34// (Cardinal 9) clean. 35// 36// license_tier: INDEPENDENT_REDERIVE 37// genealogy_id: international-research-sources/ietf/rfc_5280 38// lineage_id: nishi_x509_leaf_check_q10 39 40// nx_safety_envelope: 41// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 42// sil_target: SIL1 43// evidence: [bulk_applied_2026-05-19, rfc-5280-leaf-orchestrator] 44// verdict: NOT_YET_EVALUATED 45 46import "nx_syscalls.nx" 47import "nx_x509.nx" 48import "nx_x509_validity.nx" 49import "nx_x509_san.nx" 50 51// --- Sealed verdict enum ------------------------------------------- 52 53const NX_X509_LEAF_OK: i64 = 1 54const NX_X509_LEAF_PARSE_FAIL: i64 = 2 // x509_parse returned negative 55const NX_X509_LEAF_NOT_YET_VALID: i64 = 3 // now < notBefore 56const NX_X509_LEAF_EXPIRED: i64 = 4 // now > notAfter 57const NX_X509_LEAF_VALIDITY_BAD: i64 = 5 // bad validity TLV 58const NX_X509_LEAF_NO_SAN: i64 = 6 // cert has no SAN at all 59const NX_X509_LEAF_HOSTNAME_MISMATCH: i64 = 7 // SAN present, no match 60const NX_X509_LEAF_BAD_FORMAT: i64 = 8 // SAN extension malformed 61const NX_X509_LEAF_BAD_PATTERN: i64 = 9 // SAN entry has illegal wildcard 62const NX_X509_LEAF_VERDICT_N: i64 = 10 63 64func nx_x509_leaf_verdict_is_valid(v: i64) -> i64 { 65 if v < NX_X509_LEAF_OK { return 0 } 66 if v >= NX_X509_LEAF_VERDICT_N { return 0 } 67 return 1 68} 69 70// --- Public API ---------------------------------------------------- 71// 72// Parse buf[0..buf_len) as a DER cert, check it covers `host` at 73// epoch `now_epoch_secs`. Returns the highest-priority failure 74// verdict if any individual check fails, NX_X509_LEAF_OK if all 75// pass. 76// 77// Priority (caller can rely on this order in error reporting): 78// 1. parse failure (cert is structurally broken) 79// 2. validity period (notBefore/notAfter out of range) 80// 3. hostname-vs-SAN match (cert doesn't cover this host) 81// 82// This matches the "fail-fast on structural problems first" pattern 83// used by mainstream TLS libs (BoringSSL, NSS). 84func x509_leaf_check(buf: *u8, buf_len: i64, 85 host: *u8, host_len: i64, 86 now_epoch_secs: i64) -> i64 { 87 // ---- Step 1: parse DER ---- 88 let cert_raw: *u8 = sys_mmap(256) 89 let cert: *X509Cert = cert_raw as *X509Cert 90 let p: i64 = x509_parse(buf, buf_len, cert) 91 if p < 0 { return NX_X509_LEAF_PARSE_FAIL } 92 93 // ---- Step 2: validity period ---- 94 let v: i64 = x509_validity_check(buf, cert, now_epoch_secs) 95 if v == NX_X509_VALID_NOT_YET_VALID { return NX_X509_LEAF_NOT_YET_VALID } 96 if v == NX_X509_VALID_EXPIRED { return NX_X509_LEAF_EXPIRED } 97 if v != NX_X509_VALID_OK { return NX_X509_LEAF_VALIDITY_BAD } 98 99 // ---- Step 3: SAN hostname match ---- 100 let s: i64 = x509_san_match_hostname(buf, cert, host, host_len) 101 if s == NX_X509_SAN_OK { return NX_X509_LEAF_OK } 102 if s == NX_X509_SAN_NO_MATCH { return NX_X509_LEAF_HOSTNAME_MISMATCH } 103 if s == NX_X509_SAN_NO_EXTENSIONS { return NX_X509_LEAF_NO_SAN } 104 if s == NX_X509_SAN_NO_SAN { return NX_X509_LEAF_NO_SAN } 105 if s == NX_X509_SAN_BAD_FORMAT { return NX_X509_LEAF_BAD_FORMAT } 106 if s == NX_X509_SAN_BAD_PATTERN { return NX_X509_LEAF_BAD_PATTERN } 107 return NX_X509_LEAF_BAD_FORMAT 108} 109 110// Compile-only smoke. Real KAT in nx_x509_leaf_check_test.nx. 111func main() -> i64 { 112 return 0 113}