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}