code wiki / (root) / nx_tls13_client_validate_certificate.nx

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}