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}