nx_x509_trust_store.nx source
↩ module page · 158 lines · 5892 B
1// nx_x509_trust_store.nx -- trust anchor store + subject-DN lookup.
2//
3// Phase 0b §I.4 piece 9 of the chain-walker arc. Provides the
4// data structure + lookup helper for cert chain validation's
5// trust-anchor input.
6//
7// Model: A trust store holds an array of TrustAnchor entries.
8// Each entry pairs a subject DN (full DER-encoded Name SEQUENCE
9// bytes, byte-equality matchable) with a parsed X509Cert struct
10// + its backing DER buffer that the chain walker uses as the
11// "root" cert.
12//
13// Lookup: given the LAST cert in a peer's presented chain (the
14// one signed by a CA), find its issuer DN, then search the store
15// for a TrustAnchor whose subject DN byte-equals that issuer DN.
16// If found -> caller has its trust anchor. If not -> chain
17// terminates outside the trust store (UNTRUSTED).
18//
19// Why subject-DN-keyed lookup:
20// - RFC 5280 §6.1.1 says the trust anchor is identified by
21// its issuer DN matching the next cert's subject DN.
22// - Indexing by full DER bytes (vs. parsing into a canonical
23// form first) keeps the primitive sovereign + simple. Real
24// CAs emit byte-equal DN encodings in matched pairs.
25// - For multi-megabyte CA bundles a hash-table or binary tree
26// would be needed; current linear scan is adequate for the
27// Mozilla NSS bundle (~150 roots) at qemu speed.
28//
29// Public API:
30// struct TrustAnchor {
31// buf: *u8, DER cert buffer
32// cert: *X509Cert, already-parsed cert
33// }
34// trust_store_alloc(max_n) -> *TrustStore
35// trust_store_add(store, buf, cert) -> verdict
36// trust_store_lookup_by_subject(store, dn_buf, dn_off, dn_len)
37// -> *TrustAnchor (or NULL if not found)
38// trust_store_count(store) -> i64
39//
40// Sealed verdict enum for the add path:
41// NX_TRUST_STORE_OK anchor added
42// NX_TRUST_STORE_FULL store at capacity
43// NX_TRUST_STORE_BAD cert has invalid subject DN
44//
45// What this primitive does NOT do (caller responsibility):
46// - Verify trust anchors are self-signed (they should be, but
47// this primitive trusts the caller's selection)
48// - Constrain by name constraints / policy mappings
49// - Multi-platform CA store loading (Mozilla NSS vs. Apple
50// keychain vs. Windows root store) -- separate primitive
51// that calls trust_store_add for each entry
52//
53// Per Cardinals 9 (single-responsibility -- just store + lookup),
54// 12 (defensive at boundaries -- capacity cap + DN validity check),
55// 22 (composition -- caller's CA-bundle parser populates this
56// store, chain walker reads from it).
57//
58// license_tier: INDEPENDENT_REDERIVE
59// genealogy_id: international-research-sources/ietf/rfc_5280 + mozilla/nss
60// lineage_id: nishi_x509_trust_store_q10
61
62// nx_safety_envelope:
63// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
64// sil_target: SIL1
65// evidence: [bulk_applied_2026-05-19, x509-trust-store-foundation]
66// verdict: NOT_YET_EVALUATED
67
68import "nx_syscalls.nx"
69import "nx_x509.nx"
70import "nx_x509_dn_match.nx"
71
72const NX_TRUST_STORE_OK: i64 = 1
73const NX_TRUST_STORE_FULL: i64 = 2
74const NX_TRUST_STORE_BAD: i64 = 3
75const NX_TRUST_STORE_VERDICT_N: i64 = 4
76
77// One anchor entry. The cert MUST have been parsed against the
78// matching buf -- the store doesn't re-parse, it relies on the
79// caller's prior x509_parse + the fact that x509_parse populates
80// subject_off/_len.
81struct TrustAnchor {
82 buf: *u8,
83 cert: *X509Cert,
84}
85
86// The store: a fixed-cap pool + count. Capacity is set at
87// alloc time; nx_https_pipeline tests use 4 entries, a real
88// Mozilla bundle would use ~150.
89struct TrustStore {
90 anchors: *TrustAnchor, // pointer to anchors[0..cap)
91 cap: i64,
92 n: i64,
93}
94
95func nx_trust_store_verdict_is_valid(v: i64) -> i64 {
96 if v < NX_TRUST_STORE_OK { return 0 }
97 if v >= NX_TRUST_STORE_VERDICT_N { return 0 }
98 return 1
99}
100
101// Allocate a new store with capacity for `max_n` anchors.
102func trust_store_alloc(max_n: i64) -> *TrustStore {
103 let st_raw: *u8 = sys_mmap(32)
104 let store: *TrustStore = st_raw as *TrustStore
105 // TrustAnchor is 2 pointers = 16 bytes per entry.
106 let anchors_raw: *u8 = sys_mmap(max_n * 16)
107 store.anchors = anchors_raw as *TrustAnchor
108 store.cap = max_n
109 store.n = 0
110 return store
111}
112
113// Number of anchors currently in the store.
114func trust_store_count(store: *TrustStore) -> i64 {
115 return store.n
116}
117
118// Append an anchor (buf, cert). Caller has already x509_parse'd
119// `cert` against `buf`; the store does not re-parse.
120func trust_store_add(store: *TrustStore, buf: *u8, cert: *X509Cert) -> i64 {
121 if store.n >= store.cap { return NX_TRUST_STORE_FULL }
122 if cert.subject_len <= 0 { return NX_TRUST_STORE_BAD }
123 let slot: *TrustAnchor = (store.anchors as *u8 + store.n * 16) as *TrustAnchor
124 slot.buf = buf
125 slot.cert = cert
126 store.n = store.n + 1
127 return NX_TRUST_STORE_OK
128}
129
130// Look up an anchor whose subject DN byte-equals the caller's
131// (dn_buf + dn_off, dn_len). Returns the matching *TrustAnchor,
132// or NULL if not found.
133//
134// Linear scan -- adequate for ~150-entry Mozilla bundle at qemu
135// speed. An indexed lookup (hash of leading bytes of subject DN)
136// is a separate optimization queued for when a real bundle is
137// embedded.
138func trust_store_lookup_by_subject(
139 store: *TrustStore,
140 dn_buf: *u8, dn_off: i64, dn_len: i64
141) -> *TrustAnchor {
142 var i: i64 = 0
143 while i < store.n {
144 let slot: *TrustAnchor = (store.anchors as *u8 + i * 16) as *TrustAnchor
145 let c: *X509Cert = slot.cert
146 if nx_x509_dn_match(slot.buf, c.subject_off, c.subject_len,
147 dn_buf, dn_off, dn_len) == 1 {
148 return slot
149 }
150 i = i + 1
151 }
152 return 0 as *TrustAnchor
153}
154
155// Compile-only smoke. Real KAT in nx_x509_trust_store_test.nx.
156func main() -> i64 {
157 return 0
158}