code wiki / (root) / nx_tls_cert_cache.nx

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 }