nx_tls_cert_cache.nx source
↩ module page · 145 lines · 6970 B
1// nx_tls_cert_cache.nx -- PERSIST the (already gate-proven) TLS cert-validation cache ACROSS PROCESSES.
2// F799 certloop cure, wiring half: nx_tls13_client_validate_certificate already supports ctx.cached_cert
3// (byte-identical presented Certificate message -> skip the ~3x450ms ECDSA chain walk; the handshake's
4// CertificateVerify possession proof ALWAYS still runs, so a MITM replaying cached bytes cannot finish)
5// and ctx.cert_out capture. What was missing: every tools/call fork-exec is a FRESH process, so the
6// in-process cache never survived -- each nx_https_get paid the full certloop. This lib persists the
7// validated Certificate message per host:
8// knowledge/tlscache/<host>.cert = "NXTCC1\n" [pad to 8] + epoch(8B LE) + len(8B LE) + cert bytes
9// SAFETY CONTRACT (mirrors the validator's own doc):
10// - per-host files; the caller passes the SAME host it will SNI-validate against.
11// - tcc_save ONLY after a session whose handshake completed (full validation ran OK on those bytes,
12// or the identical bytes were previously fully validated within TTL).
13// - freshness TTL (TCC_TTL_S = 7 days): a cache older than TTL is IGNORED -> full validation re-runs,
14// bounding the expired-but-unchanged-cert window far below any real cert lifetime. Rotation (new
15// bytes) is a natural miss -> full validation.
16// - fail-open everywhere: missing dir/file, bad magic, oversize, hostile hostname chars -> no cache,
17// classic full validation (byte-for-byte unchanged behavior).
18// Envelope: cert message <= TCC_CERT_CAP (16 KiB); hostname <= 200 chars of [A-Za-z0-9.-] else uncached.
19// license_tier: ORIGINAL expect_exit: 0 (lib; compile smoke main)
20import "nx_syscalls.nx"
21import "nx_tls13_client_validate_certificate.nx"
22
23const TCC_TTL_S: i64 = 604800
24const TCC_CERT_CAP: i64 = 16384
25const TCC_HDR: i64 = 24
26
27func tcc_err(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(2, s, n); return 0 }
28// build "knowledge/tlscache/<host>.cert" into dst; 1 ok / 0 refused (hostile or too long)
29func tcc_path(host: *u8, host_len: i64, dst: *u8) -> i64 {
30 if host_len < 1 { return 0 }
31 if host_len > 200 { return 0 }
32 var o: i64 = 0
33 let pfx: *u8 = "knowledge/tlscache/" as *u8
34 var i: i64 = 0
35 while pfx[i] != (0 as u8) { dst[o] = pfx[i]; o = o + 1; i = i + 1 }
36 var k: i64 = 0
37 while k < host_len {
38 let c: i64 = host[k] as i64
39 var ok: i64 = 0
40 if c >= 97 { if c <= 122 { ok = 1 } }
41 if c >= 65 { if c <= 90 { ok = 1 } }
42 if c >= 48 { if c <= 57 { ok = 1 } }
43 if c == 46 { ok = 1 }
44 if c == 45 { ok = 1 }
45 if ok == 0 { return 0 }
46 dst[o] = c as u8
47 o = o + 1
48 k = k + 1
49 }
50 let sfx: *u8 = ".cert" as *u8
51 var j: i64 = 0
52 while sfx[j] != (0 as u8) { dst[o] = sfx[j]; o = o + 1; j = j + 1 }
53 dst[o] = 0 as u8
54 return 1
55}
56func tcc_r64(b: *u8, off: i64) -> i64 { var v: i64 = 0; var i: i64 = 7; while i >= 0 { v = (v << 8) | (b[off + i] & 0xff); i = i - 1 } return v }
57func tcc_w64(b: *u8, off: i64, v: i64) -> i64 { var m: i64 = v; var i: i64 = 0; while i < 8 { b[off + i] = (m & 0xff) as u8; m = m >> 8; i = i + 1 } return 0 }
58// load a fresh cached cert for host into ctx.cached_cert/_len. returns 1 loaded / 0 none (fail-open).
59func tcc_load(host: *u8, host_len: i64, now: i64, ctx: *TlsValidationContext) -> i64 {
60 let p: *u8 = sys_mmap(256)
61 if tcc_path(host, host_len, p) != 1 { return 0 }
62 let fd: i64 = sys_openat_rd(p)
63 if fd < 0 { return 0 }
64 let buf: *u8 = sys_mmap(TCC_HDR + TCC_CERT_CAP)
65 var n: i64 = 0
66 var go: i64 = 1
67 while go == 1 { let dst: *u8 = ((buf as i64) + n) as *u8; let r: i64 = sys_read(fd, dst, TCC_HDR + TCC_CERT_CAP - n); if r <= 0 { go = 0 } else { n = n + r } if n >= TCC_HDR + TCC_CERT_CAP { go = 0 } }
68 sys_close(fd)
69 if n < TCC_HDR + 1 { return 0 }
70 if (buf[0] & 0xff) != 78 { return 0 }
71 if (buf[1] & 0xff) != 88 { return 0 }
72 if (buf[2] & 0xff) != 84 { return 0 }
73 if (buf[3] & 0xff) != 67 { return 0 }
74 if (buf[4] & 0xff) != 67 { return 0 }
75 if (buf[5] & 0xff) != 49 { return 0 }
76 let ep: i64 = tcc_r64(buf, 8)
77 let cl: i64 = tcc_r64(buf, 16)
78 if ep <= 0 { return 0 }
79 if now - ep > TCC_TTL_S { tcc_err("nishi-tcc stale (ttl) -> full validate\n" as *u8); return 0 }
80 if cl < 1 { return 0 }
81 if cl > TCC_CERT_CAP { return 0 }
82 if TCC_HDR + cl != n { return 0 }
83 ctx.cached_cert = ((buf as i64) + TCC_HDR) as *u8
84 ctx.cached_cert_len = cl
85 return 1
86}
87// arm ctx.cert_out so the validator captures the presented Certificate message for us to persist.
88func tcc_arm(ctx: *TlsValidationContext) -> i64 {
89 let cb: *u8 = sys_mmap(TCC_CERT_CAP)
90 ctx.cert_out = cb
91 ctx.cert_out_cap = TCC_CERT_CAP
92 ctx.cert_out_len = 0
93 return 0
94}
95// after a SUCCESSFUL session: persist the captured cert iff it differs from what we loaded (i.e. full
96// validation just ran on new bytes). Cache-hit sessions keep the original validation epoch (TTL honest).
97func tcc_save(host: *u8, host_len: i64, now: i64, ctx: *TlsValidationContext) -> i64 {
98 if ctx.cert_out_len < 1 { return 0 }
99 if ctx.cert_out_len > TCC_CERT_CAP { return 0 }
100 if ctx.cached_cert_len == ctx.cert_out_len {
101 let a: *u8 = ctx.cached_cert
102 let b: *u8 = ctx.cert_out
103 var i: i64 = 0
104 var same: i64 = 1
105 while i < ctx.cert_out_len { if (a[i] & 0xff) != (b[i] & 0xff) { same = 0; i = ctx.cert_out_len } else { i = i + 1 } }
106 if same == 1 { return 0 }
107 }
108 let p: *u8 = sys_mmap(256)
109 if tcc_path(host, host_len, p) != 1 { return 0 }
110 sys_mkdir("knowledge" as *u8, 0x1ed)
111 sys_mkdir("knowledge/tlscache" as *u8, 0x1ed)
112 let hb: *u8 = sys_mmap(TCC_HDR)
113 hb[0] = 78 as u8
114 hb[1] = 88 as u8
115 hb[2] = 84 as u8
116 hb[3] = 67 as u8
117 hb[4] = 67 as u8
118 hb[5] = 49 as u8
119 hb[6] = 10 as u8
120 hb[7] = 0 as u8
121 tcc_w64(hb, 8, now)
122 tcc_w64(hb, 16, ctx.cert_out_len)
123 // atomic: .nxw tmp + rename so a concurrent reader never sees a torn cache (it would fail-open anyway)
124 let tp: *u8 = sys_mmap(272)
125 var o: i64 = 0
126 while p[o] != (0 as u8) { tp[o] = p[o]; o = o + 1 }
127 tp[o] = 46 as u8
128 tp[o+1] = 110 as u8
129 tp[o+2] = 120 as u8
130 tp[o+3] = 119 as u8
131 tp[o+4] = 0 as u8
132 let fd: i64 = sys_openat_wr(tp, 420)
133 if fd < 0 { return 0 }
134 var w: i64 = 0
135 while w < TCC_HDR { let sr: i64 = sys_write(fd, ((hb as i64) + w) as *u8, TCC_HDR - w); if sr <= 0 { w = TCC_HDR } else { w = w + sr } }
136 var w2: i64 = 0
137 while w2 < ctx.cert_out_len { let sr2: i64 = sys_write(fd, ((ctx.cert_out as i64) + w2) as *u8, ctx.cert_out_len - w2); if sr2 <= 0 { w2 = ctx.cert_out_len } else { w2 = w2 + sr2 } }
138 sys_fsync(fd)
139 sys_close(fd)
140 sys_renameat(tp, p)
141 tcc_err("nishi-tcc saved (full validation ran; next fetch skips certloop)\n" as *u8)
142 return 1
143}
144
145func main() -> i64 { return 0 }