nx_tls13_client_validate_certificate.nx source
↩ module page · 166 lines · 7614 B
1// nx_tls13_client_validate_certificate.nx -- the FORMAL bridge
2// between the TLS 1.3 client state machine's WAIT_CERT state and
3// the X.509 cert pipeline shipped earlier today.
4//
5// Phase 0b §L sub-3 of the TLS client arc, AND Arc A step 3
6// of the nx_https_client wiring roadmap.
7//
8// The existing nx_tls13_client dispatcher (commit pre-dating this
9// session) has an explicit STUB at line 154-157 of
10// nx_tls13_client.nx:
11//
12// // STUB: cert validation deferred to Gap I. We currently
13// // accept ANY presented certificate, which means this client
14// // is NOT safe to use against an untrusted network until
15// // Gap I lands.
16//
17// We BUILT Gap I today: nx_https_cert_pipeline_verify_with_store
18// (commit f294961e) does full RFC 5280 path validation against a
19// TrustStore. This primitive is the SHIM that lets the TLS
20// dispatcher close that stub.
21//
22// API:
23// struct TlsValidationContext {
24// store: *TrustStore,
25// sni_host: *u8,
26// sni_host_len: i64,
27// now_epoch: i64,
28// }
29// nx_tls13_client_validate_certificate(
30// cert_msg_bytes, cert_msg_len,
31// val_ctx
32// ) -> verdict
33// nx_tls13_client_cv_verdict_is_valid(v) -> 0|1
34//
35// Sealed verdict enum:
36// NX_TLS13_CLIENT_CV_OK chain valid + SAN matches SNI
37// NX_TLS13_CLIENT_CV_CERT_MSG_BAD Cert message format error
38// NX_TLS13_CLIENT_CV_NOT_YET_VALID leaf cert not yet valid
39// NX_TLS13_CLIENT_CV_EXPIRED leaf cert expired
40// NX_TLS13_CLIENT_CV_HOSTNAME_FAIL no SAN entry matches SNI
41// NX_TLS13_CLIENT_CV_CHAIN_FAIL chain sig verify failed
42// NX_TLS13_CLIENT_CV_NO_TRUST trust anchor lookup failed
43//
44// The next-step composition (queued separately) is a NEW dispatcher
45// variant `tls13_client_dispatch_with_validation` that takes a
46// TlsValidationContext and calls THIS primitive at the WAIT_CERT
47// step instead of accepting any cert. We ship the bridge first
48// in isolation so its KAT exercises the bridge logic cleanly
49// without dragging in the full handshake state machine.
50//
51// Per Cardinals 9 (single-responsibility -- bridge, not full
52// dispatcher rewrite), 19 (API contract stability -- existing
53// dispatcher signature unchanged; this is additive), 22
54// (composition -- two shipped primitives bolted into one
55// security-critical decision), 23 (preamble names the queued
56// dispatcher variant + WHY the bridge ships independently).
57//
58// license_tier: INDEPENDENT_REDERIVE
59// genealogy_id: international-research-sources/ietf/rfc_8446 + rfc_5280
60// lineage_id: nishi_tls13_client_validate_certificate_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, tls13-x509-bridge]
66// verdict: NOT_YET_EVALUATED
67
68import "nx_syscalls.nx"
69import "nx_x509_trust_store.nx"
70import "nx_https_cert_pipeline.nx"
71
72const NX_TLS13_CLIENT_CV_OK: i64 = 1
73const NX_TLS13_CLIENT_CV_CERT_MSG_BAD: i64 = 2
74const NX_TLS13_CLIENT_CV_NOT_YET_VALID: i64 = 3
75const NX_TLS13_CLIENT_CV_EXPIRED: i64 = 4
76const NX_TLS13_CLIENT_CV_HOSTNAME_FAIL: i64 = 5
77const NX_TLS13_CLIENT_CV_CHAIN_FAIL: i64 = 6
78const NX_TLS13_CLIENT_CV_NO_TRUST: i64 = 7
79const NX_TLS13_CLIENT_CV_OTHER: i64 = 8
80const NX_TLS13_CLIENT_CV_VERDICT_N: i64 = 9
81
82struct TlsValidationContext {
83 store: *TrustStore,
84 sni_host: *u8,
85 sni_host_len: i64,
86 now_epoch: i64,
87 // --- session cert-validation CACHE (additive; ALL-ZERO => classic full validation, byte-for-byte unchanged).
88 // Skips ONLY the ~350ms ECDSA chain/SAN validation when the presented Certificate message is byte-identical to
89 // one the caller already fully validated this session. SAFE: the handshake's CertificateVerify possession proof
90 // is separate + ALWAYS runs, so a MITM replaying our cert bytes still cannot finish the handshake. The caller
91 // MUST only set cached_cert for the SAME host it validated it on (the mgmt client caches per single-host upload).
92 cached_cert: *u8, // a previously-validated Certificate message; 0 = none
93 cached_cert_len: i64, // its length (0 = none => no fast path)
94 cert_out: *u8, // OUT: the presented Certificate message is copied here for the caller to cache; 0 = skip
95 cert_out_cap: i64, // capacity of cert_out
96 cert_out_len: i64, // OUT: bytes written (0 if not captured or it wouldn't fit)
97}
98
99func nx_tls13_client_cv_verdict_is_valid(v: i64) -> i64 {
100 if v < NX_TLS13_CLIENT_CV_OK { return 0 }
101 if v >= NX_TLS13_CLIENT_CV_VERDICT_N { return 0 }
102 return 1
103}
104
105// Validate a TLS 1.3 Certificate handshake message against the
106// caller's trust store + SNI hostname + current time.
107//
108// Returns NX_TLS13_CLIENT_CV_OK on full validation success
109// (chain rooted at a tracked anchor, sig chain verifies, leaf
110// covers the SNI hostname, all within validity period). Otherwise
111// a sealed non-OK verdict mapping pipeline errors to TLS-client
112// vocabulary.
113// byte-equality of two slices -- used ONLY to gate the validation fast-path (never a security decision on its own;
114// see the safety note on TlsValidationContext.cached_cert).
115func nx_tls13_cv_memeq(a: *u8, an: i64, b: *u8, bn: i64) -> i64 {
116 if an != bn { return 0 }
117 var i: i64 = 0
118 while i < an { if a[i] != b[i] { return 0 } i = i + 1 }
119 return 1
120}
121
122func nx_tls13_client_validate_certificate(
123 cert_msg_bytes: *u8, cert_msg_len: i64,
124 ctx: *TlsValidationContext
125) -> i64 {
126 // Capture the presented Certificate message so the caller can cache it after this connection succeeds.
127 ctx.cert_out_len = 0
128 if ctx.cert_out != (0 as *u8) {
129 if cert_msg_len <= ctx.cert_out_cap {
130 var oc: i64 = 0
131 while oc < cert_msg_len { ctx.cert_out[oc] = cert_msg_bytes[oc]; oc = oc + 1 }
132 ctx.cert_out_len = cert_msg_len
133 }
134 }
135 // CACHE HIT: byte-identical to a Certificate message the caller already fully validated this session -> the same
136 // chain/SAN/validity result -> skip the ECDSA chain crypto. ANY difference falls through to full validation.
137 if ctx.cached_cert_len > 0 {
138 if nx_tls13_cv_memeq(cert_msg_bytes, cert_msg_len, ctx.cached_cert, ctx.cached_cert_len) == 1 {
139 return NX_TLS13_CLIENT_CV_OK
140 }
141 }
142 let v: i64 = nx_https_cert_pipeline_verify_with_store(
143 cert_msg_bytes, cert_msg_len,
144 ctx.sni_host, ctx.sni_host_len,
145 ctx.now_epoch,
146 ctx.store
147 )
148
149 if v == NX_HTTPS_PIPELINE_OK { return NX_TLS13_CLIENT_CV_OK }
150 if v == NX_HTTPS_PIPELINE_CERT_MSG_BAD { return NX_TLS13_CLIENT_CV_CERT_MSG_BAD }
151 if v == NX_HTTPS_PIPELINE_CERT_PARSE_FAIL { return NX_TLS13_CLIENT_CV_CERT_MSG_BAD }
152 if v == NX_HTTPS_PIPELINE_LEAF_NOT_YET_VALID { return NX_TLS13_CLIENT_CV_NOT_YET_VALID }
153 if v == NX_HTTPS_PIPELINE_LEAF_EXPIRED { return NX_TLS13_CLIENT_CV_EXPIRED }
154 if v == NX_HTTPS_PIPELINE_LEAF_NO_SAN { return NX_TLS13_CLIENT_CV_HOSTNAME_FAIL }
155 if v == NX_HTTPS_PIPELINE_HOSTNAME_MISMATCH { return NX_TLS13_CLIENT_CV_HOSTNAME_FAIL }
156 if v == NX_HTTPS_PIPELINE_CHAIN_DN_MISMATCH { return NX_TLS13_CLIENT_CV_CHAIN_FAIL }
157 if v == NX_HTTPS_PIPELINE_CHAIN_SIG_FAIL { return NX_TLS13_CLIENT_CV_CHAIN_FAIL }
158 if v == NX_HTTPS_PIPELINE_NO_TRUST_ANCHOR { return NX_TLS13_CLIENT_CV_NO_TRUST }
159 return NX_TLS13_CLIENT_CV_OTHER
160}
161
162// Compile-only smoke. Real KAT in
163// nx_tls13_client_validate_certificate_test.nx.
164func main() -> i64 {
165 return 0
166}