code wiki / _hdl_build / nx_edge_probe.nx

nx_edge_probe.nx source

↩ module page · 251 lines · 13523 B

1// nx_edge_probe.nx -- SOVEREIGN edge-chain probe: connect to an EXPLICIT ip:port, present a chosen 2// SNI, and GET a path with that Host -- so a capability SUBDOMAIN's full public edge chain 3// (iptables:443->sni_router:7443 -> SNI route -> sites.elf cert-select -> proxy_routes -> daemon) can be 4// verified from the LAN by targeting the edge IP directly, WITHOUT depending on public DNS or NAT 5// hairpin (the reason admin.andelinwest.com looked dead from inside the network). This is the honest 6// test the earlier `curl --resolve` cheated at: it drives the REAL sni_router + real cert + real proxy. 7// usage: nx_edge_probe <ip> <port> <sni_host> <path> [expect-substring] 8// e.g.: nx_edge_probe 192.168.8.240 443 admin.andelinwest.com / "Nishi document portal" 9// Prints the served leaf cert acceptance + HTTP status + whether expect is present. license_tier: ORIGINAL 10import "nx_syscalls.nx" 11import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host 12import "nx_csprng.nx" 13import "nx_x509_trust_store.nx" 14import "nx_trust_store_load_from_certdata.nx" 15import "nx_tls13_client_validate_certificate.nx" 16import "nx_tls13_client_session.nx" 17import "nx_tls13_client_session_run.nx" 18import "nx_tls13_chrome_session.nx" 19import "nx_x509_san.nx" 20import "nx_x509_validity.nx" 21import "nx_https_get_complete.nx" 22import "nx_https_post_complete.nx" 23const K_MAGIC_1000000000: i64 = 1000000000 24const K_MAGIC_1024: i64 = 1024 25const K_MAGIC_86400: i64 = 86400 26const K_MAGIC_4194304: i64 = 4194304 27const K_MAGIC_32768: i64 = 32768 28const K_MAGIC_262144: i64 = 262144 29 30func ep_puts(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 31func ep_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 32func ep_putn(v: i64) -> i64 { let b: *u8=sys_mmap(24); var m: i64=v; if m<0{m=0-m;sys_write(1,"-" as *u8,1)} let t: *u8=sys_mmap(24); var k: i64=0; if m==0{t[0]=48 as u8;k=1} while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} var i: i64=0; while i<k{b[i]=t[k-1-i];i=i+1} sys_write(1,b,k); return 0 } 33func ep_atoi(s: *u8) -> i64 { var v: i64=0; var i: i64=0; while s[i]!=(0 as u8){ let c: i64=s[i] as i64; if c>=48 { if c<=57 { v=v*10+(c-48) } } i=i+1 } return v } 34func ep_contains(hay: *u8, n: i64, needle: *u8) -> i64 { 35 let nl: i64 = ep_slen(needle); if nl==0 { return 1 } 36 var i: i64=0 37 while i+nl<=n { var j: i64=0; var ok: i64=1; while j<nl { if hay[i+j]!=needle[j]{ok=0;j=nl} else {j=j+1} } if ok==1{return 1} i=i+1 } 38 return 0 39} 40// parse dotted-ipv4 into packed BE (a<<24|b<<16|c<<8|d) 41func ep_ip(s: *u8) -> i64 { 42 var parts: i64=0; var cur: i64=0; var packed: i64=0; var i: i64=0 43 while 1==1 { 44 let c: i64 = s[i] as i64 45 if c == 0 { packed = (packed << 8) | cur; parts = parts + 1; i = K_MAGIC_1000000000 } 46 else { if c == 46 { packed = (packed << 8) | cur; cur = 0; parts = parts + 1 } 47 else { if c >= 48 { if c <= 57 { cur = cur*10 + (c-48) } } } } 48 if i >= K_MAGIC_1000000000 { i = i } else { i = i + 1 } 49 if i >= K_MAGIC_1000000000 { break } 50 } 51 return packed 52} 53 54// Count the certificates the server actually SENT in its TLS 1.3 Certificate message. A BROWSER needs 55// the full chain (leaf + intermediate); if the server sends only the leaf (count==1), the browser shows 56// "insecure" even though a client that already holds the intermediate (like our validator) accepts it. 57// TLS1.3 Certificate = [1B ctx_len][ctx][3B list_len]{[3B cert_len][cert][2B ext_len][ext]}... 58func ep_count_chain(cm: *u8, cmlen: i64) -> i64 { 59 if cmlen < 4 { return -1 } 60 var o: i64 = 0 61 let ctxlen: i64 = cm[o] as i64 62 o = o + 1 + ctxlen 63 if o + 3 > cmlen { return -1 } 64 o = o + 3 // skip the 3-byte certificate_list length 65 var count: i64 = 0 66 while o + 3 <= cmlen { 67 let clen: i64 = (((cm[o] as i64)&0xff)<<16) | (((cm[o+1] as i64)&0xff)<<8) | ((cm[o+2] as i64)&0xff) 68 o = o + 3 69 if clen <= 0 { break } 70 if o + clen > cmlen { break } 71 o = o + clen 72 count = count + 1 73 if o + 2 > cmlen { break } 74 let extlen: i64 = (((cm[o] as i64)&0xff)<<8) | ((cm[o+1] as i64)&0xff) 75 o = o + 2 + extlen 76 } 77 return count 78} 79 80// Parse the served LEAF cert DER and print its SubjectAltName dNSNames -- the ground truth of WHAT the 81// edge serves for this SNI. If admin.andelinwest.com gets a leaf whose SAN is only andelinwest.com/www 82// (no *.andelinwest.com and no admin.andelinwest.com), the wildcard/cert-select is wrong = the bug. 83func ep_dump_san(der: *u8, derlen: i64) -> i64 { 84 if derlen <= 0 { ep_puts(" served leaf: (not captured)\n" as *u8); return 0 } 85 let cert_raw: *u8 = sys_mmap(256) 86 let cert: *X509Cert = cert_raw as *X509Cert 87 if x509_parse(der, derlen, cert) < 0 { ep_puts(" served leaf: (DER parse failed)\n" as *u8); return 0 } 88 let so: *i64 = sys_mmap(16) as *i64 89 let sl: *i64 = sys_mmap(16) as *i64 90 let rc: i64 = x509_san_locate(der, cert, so, sl) 91 if rc < 0 { ep_puts(" served leaf SAN: (none found rc=" as *u8); ep_putn(rc); ep_puts(")\n" as *u8); return 0 } 92 ep_puts(" served leaf SAN dNSNames = [ " as *u8) 93 var o: i64 = so[0] 94 let end: i64 = so[0] + sl[0] 95 var cnt: i64 = 0 96 while o + 2 <= end { 97 let tag: i64 = (der[o] as i64) & 0xff 98 let ln: i64 = (der[o+1] as i64) & 0xff 99 o = o + 2 100 if o + ln > end { break } 101 if tag == 0x82 { if cnt > 0 { ep_puts(", " as *u8) } sys_write(1, der + o, ln); cnt = cnt + 1 } 102 o = o + ln 103 } 104 if cnt == 0 { ep_puts("(NO dNSNames!)" as *u8) } 105 ep_puts(" ] (" as *u8); ep_putn(cnt); ep_puts(" names)\n" as *u8) 106 return 0 107} 108 109// FAILED-handshake autopsy: extract the LEAF from the captured TLS1.3 Certificate message and print 110// its SAN dNSNames + validity vs now -- turns "rc=-7" into "EXPIRED n days ago" or "SAN=[...] no match". 111func ep_autopsy(cm: *u8, cmlen: i64, now: i64) -> i64 { 112 if cmlen < 8 { ep_puts(" autopsy: (no Certificate message captured before the failure)\n" as *u8); return 0 } 113 // leaf DER: [1B ctx_len][ctx][3B list_len][3B cert_len][DER...] 114 var o: i64 = 0 115 let ctxlen: i64 = cm[o] as i64 116 o = o + 1 + ctxlen 117 if o + 6 > cmlen { ep_puts(" autopsy: (short cert message)\n" as *u8); return 0 } 118 o = o + 3 119 let clen: i64 = (((cm[o] as i64)&0xff)<<16) | (((cm[o+1] as i64)&0xff)<<8) | ((cm[o+2] as i64)&0xff) 120 o = o + 3 121 if clen <= 0 { ep_puts(" autopsy: (empty cert list)\n" as *u8); return 0 } 122 var avail: i64 = cmlen - o 123 var dlen: i64 = clen 124 if dlen > avail { dlen = avail } 125 let der: *u8 = cm + o 126 ep_puts(" autopsy of the SERVED leaf:\n" as *u8) 127 ep_dump_san(der, dlen) 128 let cert_raw: *u8 = sys_mmap(K_MAGIC_1024) 129 let cert: *X509Cert = cert_raw as *X509Cert 130 if x509_parse(der, dlen, cert) != 0 { ep_puts(" validity: (leaf DER parse failed)\n" as *u8); return 0 } 131 let nb: *i64 = sys_mmap(16) as *i64 132 let na: *i64 = sys_mmap(16) as *i64 133 if x509_validity_get(der, cert, nb, na) != NX_X509_VALID_OK { ep_puts(" validity: (unparseable)\n" as *u8); return 0 } 134 let days: i64 = (na[0] - now) / K_MAGIC_86400 135 ep_puts(" validity: notAfter_epoch=" as *u8) 136 ep_putn(na[0]) 137 ep_puts(" days_until_expiry=" as *u8) 138 ep_putn(days) 139 if days < 0 { ep_puts(" <== EXPIRED. This is why browsers say insecure." as *u8) } 140 ep_puts("\n" as *u8) 141 return 0 142} 143 144func main(argc: i64, argv: *i64) -> i64 { 145 if argc < 5 { ep_puts("usage: nx_edge_probe <ip> <port> <sni_host> <path> [expect] [POST-body]\n" as *u8); return 2 } 146 let ipstr: *u8 = argv[1] as *u8 147 let port: i64 = ep_atoi(argv[2] as *u8) 148 let sni: *u8 = argv[3] as *u8 149 let path: *u8 = argv[4] as *u8 150 var expect: *u8 = "" as *u8 151 if argc >= 6 { expect = argv[5] as *u8 } 152 // if a 6th arg (POST body) is given, this is a POST (application/x-www-form-urlencoded) 153 var post_body: *u8 = "" as *u8 154 var is_post: i64 = 0 155 if argc >= 7 { post_body = argv[6] as *u8; is_post = 1 } 156 let sni_n: i64 = ep_slen(sni) 157 let path_n: i64 = ep_slen(path) 158 159 ep_puts("=== nx_edge_probe: " as *u8); ep_puts(ipstr); ep_puts(":" as *u8); ep_putn(port) 160 ep_puts(" SNI=" as *u8); ep_puts(sni); ep_puts(" GET " as *u8); ep_puts(path); ep_puts(" ===\n" as *u8) 161 162 let lr: i64 = nx_trust_store_load_from_certdata("data/mozilla_certdata.txt\x00" as *u8, 300, K_MAGIC_4194304) 163 if lr <= 0 { ep_puts("trust load FAIL\n" as *u8); return 1 } 164 let store: *TrustStore = lr as *TrustStore 165 let now: i64 = sys_now_realtime_sec() 166 167 // ---- TCP connect to the explicit edge IP:port ---- 168 let fd: i64 = sys_socket(2, 1, 0) 169 if fd < 0 { ep_puts("socket FAIL\n" as *u8); return 1 } 170 sys_set_socket_timeout(fd, 45) // OPAQUE register/login runs a memory-hard Argon2id KDF (~20s on the NAS CPU) 171 let packed: i64 = ep_ip(ipstr) 172 let sa: *u8 = sys_mmap(16) 173 sa[0] = 2 as u8; sa[1] = 0 as u8 174 sa[2] = ((port >> 8) & 0xff) as u8; sa[3] = (port & 0xff) as u8 175 sa[4] = ((packed >> 24) & 0xff) as u8; sa[5] = ((packed >> 16) & 0xff) as u8 176 sa[6] = ((packed >> 8) & 0xff) as u8; sa[7] = (packed & 0xff) as u8 177 var z: i64 = 8; while z < 16 { sa[z] = 0 as u8; z = z + 1 } 178 if nx_connect_bounded(fd, sa, 16, NX_CONN_DEFAULT_MS) < 0 { ep_puts("RESULT: TCP-CONNECT-FAIL (nothing listening / blocked)\n" as *u8); sys_close(fd); return 3 } 179 ep_puts(" tcp-connect ok\n" as *u8) 180 181 // ---- TLS 1.3 with the CHOSEN SNI (validates the served cert against the Mozilla store) ---- 182 let cr: *u8 = sys_mmap(32); nx_csprng_fill(cr, 32) 183 let xp: *u8 = sys_mmap(32); nx_csprng_fill(xp, 32) 184 let vc_raw: *u8 = sys_mmap(128) 185 let vc: *TlsValidationContext = vc_raw as *TlsValidationContext 186 vc.store = store 187 vc.sni_host = sni 188 vc.sni_host_len = sni_n 189 vc.now_epoch = now 190 vc.cached_cert = 0 as *u8 191 vc.cached_cert_len = 0 192 let certmsg: *u8 = sys_mmap(K_MAGIC_32768) // capture the served Certificate message to count the chain 193 vc.cert_out = certmsg 194 vc.cert_out_cap = K_MAGIC_32768 195 vc.cert_out_len = 0 196 // Chrome-JA3 ClientHello mode (any arg == "chrome") reproduces a real browser's handshake; default = minimal hello. 197 var use_chrome: i64 = 0 198 var ai: i64 = 1 199 while ai < argc { if ep_contains(argv[ai] as *u8, ep_slen(argv[ai] as *u8), "chrome" as *u8) == 1 { if ep_slen(argv[ai] as *u8) == 6 { use_chrome = 1 } } ai = ai + 1 } 200 var sr: i64 = 0 201 if use_chrome == 1 { ep_puts(" [Chrome-JA3 ClientHello mode]\n" as *u8); sr = nx_tls13_client_session_run_chrome(fd, sni, sni_n, cr, xp, vc) } 202 else { sr = nx_tls13_client_session_run(fd, sni, sni_n, cr, xp, vc) } 203 if sr < 0 { 204 ep_puts("RESULT: TLS-HANDSHAKE-FAIL rc=" as *u8) 205 ep_putn(sr) 206 ep_puts(" (edge served an invalid/mismatched cert for this SNI, or no route)\n" as *u8) 207 // AUTOPSY: the Certificate message is captured BEFORE validation -- so even on a failed 208 // handshake we can show WHAT the edge served (SAN + validity) instead of guessing why. 209 ep_autopsy(certmsg, vc.cert_out_len, now) 210 sys_close(fd) 211 return 4 212 } 213 ep_puts(" tls handshake ok (served cert VALIDATED for SNI " as *u8); ep_puts(sni); ep_puts(")\n" as *u8) 214 // BROWSER-TRUTH: how many certs did the server actually SEND? A browser needs leaf+intermediate. 215 let chain_n: i64 = ep_count_chain(certmsg, vc.cert_out_len) 216 ep_puts(" served chain length = " as *u8); ep_putn(chain_n) 217 if chain_n == 1 { ep_puts(" <== LEAF-ONLY: incomplete chain -> BROWSERS SHOW 'insecure' (my validator has the intermediate, a browser does not). THIS is the bug.\n" as *u8) } 218 else { if chain_n >= 2 { ep_puts(" (leaf + intermediate present -> chain is browser-complete)\n" as *u8) } 219 else { ep_puts(" (could not parse chain length)\n" as *u8) } } 220 let s: *Tls13ClientSession = sr as *Tls13ClientSession 221 ep_dump_san(s.leaf_cert, s.leaf_cert_len) // <== print the SERVED leaf's SAN dNSNames (wildcard truth) 222 223 // ---- HTTP GET or POST with Host = the SNI host ---- 224 let out: *u8 = sys_mmap(K_MAGIC_262144) 225 var n: i64 = 0 226 if is_post == 1 { 227 ep_puts(" POST body: " as *u8); ep_puts(post_body); ep_puts("\n" as *u8) 228 n = nx_https_post_complete(s, fd, path, path_n, sni, sni_n, "application/x-www-form-urlencoded" as *u8, 33, post_body, ep_slen(post_body), out, K_MAGIC_262144) 229 } else { 230 n = nx_https_get_complete(s, fd, path, path_n, sni, sni_n, out, K_MAGIC_262144) 231 } 232 sys_close(fd) 233 if n <= 0 { ep_puts("RESULT: HTTP-FETCH-FAIL rc=" as *u8); ep_putn(n); ep_puts("\n" as *u8); return 5 } 234 // print the status line 235 ep_puts(" http: " as *u8) 236 var i: i64 = 0 237 while i < n { if (out[i] as i64) == 13 { i = n } else { sys_write(1, ((out as i64)+i) as *u8, 1); i = i + 1 } } 238 ep_puts("\n" as *u8) 239 var okexpect: i64 = 1 240 if ep_slen(expect) > 0 { 241 okexpect = ep_contains(out, n, expect) 242 ep_puts(" expect '" as *u8); ep_puts(expect); ep_puts("': " as *u8) 243 if okexpect == 1 { ep_puts("PRESENT\n" as *u8) } else { ep_puts("ABSENT\n" as *u8) } 244 } 245 if ep_contains(out, n, "200 OK" as *u8) == 1 { if okexpect == 1 { 246 ep_puts("RESULT: EDGE-CHAIN-GREEN (sni_router -> cert-select -> proxy -> daemon all serve this subdomain)\n" as *u8) 247 return 0 248 } } 249 ep_puts("RESULT: reached the daemon but not a clean 200+expect (see status above)\n" as *u8) 250 return 6 251}