nx_acme_directory_live_real_ca_test.nx source
↩ module page · 244 lines · 9215 B
1// nx_acme_directory_live_real_ca_test.nx -- L11 step 2d.
2//
3// Live HTTPS GET against Let's Encrypt PRODUCTION /directory using the
4// already-shipped modular pipeline (url_for_fetch -> url_connect ->
5// tls13_client_session_run -> https_get_complete -> response_parse +
6// dechunk) and the real Mozilla trust store.
7//
8// Production rather than staging: LE STAGING certs chain to the
9// "(STAGING) Pretend Pear X1" root which is NOT in Mozilla's public
10// trust store -- by design. Hitting production /directory is safe
11// (GET, no order placed, no rate-limit consumption) AND validates the
12// real ISRG Root X1 chain that Mozilla anchors.
13//
14// What this proves:
15// - DNS resolves acme-v02.api.letsencrypt.org
16// - TCP connects to LE production
17// - TLS 1.3 handshake completes
18// - LE's cert chain validates against our shipped Mozilla store
19// (ISRG Root X1 is one of the 167 anchors loaded earlier)
20// - HTTP/1.1 GET succeeds, response parses, body dechunks
21// - JSON body contains the expected ACME endpoint keys
22//
23// What this does NOT prove:
24// - any POST against LE (that's step 2e -- needs JOSE-signed body
25// via nx_https_post_complete shipped in step 2c)
26// - staging cert chain support (separate task: add the staging
27// trust anchor or implement a staging trust mode)
28//
29// expect_exit: 0
30// license_tier: ORIGINAL
31
32import "nx_syscalls.nx"
33import "nx_x509_trust_store.nx"
34import "nx_trust_store_load_from_certdata.nx"
35import "nx_tls13_client_validate_certificate.nx"
36import "nx_tls13_client_session_run.nx"
37import "nx_https_url_for_fetch.nx"
38import "nx_https_url_connect.nx"
39import "nx_https_get.nx"
40import "nx_https_get_complete.nx"
41import "nx_http_response_parse.nx"
42
43func dump_dec(label0: i64, label1: i64, v: i64) -> i64 {
44 let lab: *u8 = sys_mmap(8)
45 lab[0] = label0 as u8; lab[1] = label1 as u8; lab[2] = 0x3D
46 sys_write(2, lab, 3)
47 var av: i64 = v
48 if av < 0 {
49 let neg: *u8 = sys_mmap(8); neg[0]=0x2D; sys_write(2, neg, 1)
50 av = 0 - av
51 }
52 if av == 0 {
53 let z: *u8 = sys_mmap(8); z[0]=0x30; sys_write(2, z, 1)
54 } else {
55 let buf: *u8 = sys_mmap(16)
56 var pos: i64 = 0
57 var x: i64 = av
58 while x > 0 {
59 buf[pos] = (0x30 + (x % 10)) as u8
60 x = x / 10
61 pos = pos + 1
62 }
63 let out: *u8 = sys_mmap(16)
64 var oi: i64 = 0
65 while oi < pos {
66 out[oi] = buf[pos - 1 - oi]
67 oi = oi + 1
68 }
69 sys_write(2, out, pos)
70 }
71 let nl: *u8 = sys_mmap(8); nl[0]=0x0A; sys_write(2, nl, 1)
72 return 0
73}
74
75// Substring search: returns 1 if needle (n bytes) is found inside
76// haystack (h bytes), 0 otherwise. Linear scan -- ACME directory
77// body is <2 KB so O(h*n) is fine.
78func _contains(haystack: *u8, h: i64, needle: *u8, n: i64) -> i64 {
79 if n <= 0 { return 1 }
80 if n > h { return 0 }
81 var i: i64 = 0
82 while i <= h - n {
83 var k: i64 = 0
84 var hit: i64 = 1
85 while k < n {
86 if haystack[i + k] != needle[k] { hit = 0; k = n }
87 else { k = k + 1 }
88 }
89 if hit == 1 { return 1 }
90 i = i + 1
91 }
92 return 0
93}
94
95func main() -> i64 {
96 // ---- Load Mozilla trust store ----
97 let path: *u8 = sys_mmap(64)
98 path[0]=0x2F; path[1]=0x74; path[2]=0x6D; path[3]=0x70 // /tmp
99 path[4]=0x2F // /
100 path[5]=0x6D; path[6]=0x6F; path[7]=0x7A; path[8]=0x69 // mozi
101 path[9]=0x6C; path[10]=0x6C; path[11]=0x61 // lla
102 path[12]=0x5F // _
103 path[13]=0x63; path[14]=0x65; path[15]=0x72; path[16]=0x74
104 path[17]=0x64; path[18]=0x61; path[19]=0x74; path[20]=0x61 // certdata
105 path[21]=0x2E; path[22]=0x74; path[23]=0x78; path[24]=0x74 // .txt
106 path[25]=0
107
108 let r: i64 = nx_trust_store_load_from_certdata(path, 300, 4194304)
109 dump_dec(0x4C, 0x4F, r) // "LO="
110 if r <= 0 { return 1 }
111 let store: *TrustStore = r as *TrustStore
112 let n: i64 = trust_store_count(store)
113 dump_dec(0x43, 0x41, n) // "CA="
114 if n < 100 { return 3 }
115
116 // ---- URL: https://acme-v02.api.letsencrypt.org/directory ----
117 // 46 bytes total + NUL at offset 46.
118 let url: *u8 = sys_mmap(80)
119 // "https://" 8
120 url[0]=0x68; url[1]=0x74; url[2]=0x74; url[3]=0x70; url[4]=0x73
121 url[5]=0x3A; url[6]=0x2F; url[7]=0x2F
122 // "acme-v02.api.letsencrypt.org" 28
123 url[8]=0x61; url[9]=0x63; url[10]=0x6D; url[11]=0x65 // acme
124 url[12]=0x2D; url[13]=0x76; url[14]=0x30; url[15]=0x32 // -v02
125 url[16]=0x2E; url[17]=0x61; url[18]=0x70; url[19]=0x69 // .api
126 url[20]=0x2E // .
127 url[21]=0x6C; url[22]=0x65; url[23]=0x74; url[24]=0x73 // lets
128 url[25]=0x65; url[26]=0x6E; url[27]=0x63; url[28]=0x72 // encr
129 url[29]=0x79; url[30]=0x70; url[31]=0x74 // ypt
130 url[32]=0x2E; url[33]=0x6F; url[34]=0x72; url[35]=0x67 // .org
131 // "/directory" 10
132 url[36]=0x2F; url[37]=0x64; url[38]=0x69; url[39]=0x72 // /dir
133 url[40]=0x65; url[41]=0x63; url[42]=0x74; url[43]=0x6F // ecto
134 url[44]=0x72; url[45]=0x79 // ry
135 url[46]=0
136
137 // ---- Client random + private (caller-supplied; pseudo) ----
138 let cr: *u8 = sys_mmap(32)
139 var i: i64 = 0
140 while i < 32 { cr[i] = (0xC0 + i) as u8; i = i + 1 }
141 let priv: *u8 = sys_mmap(32)
142 i = 0
143 while i < 32 { priv[i] = (0xA0 + i) as u8; i = i + 1 }
144
145 // ---- URL parse + connect ----
146 let url_p: *NxUrl = nx_url_new()
147 let target_raw: *u8 = sys_mmap(32)
148 let target: *NxHttpsTarget = target_raw as *NxHttpsTarget
149 target.url = url_p
150 target.port = 0
151 if nx_https_url_for_fetch(url, target) != NX_HTTPS_URL_OK { return 41 }
152
153 let fd_p: *i64 = sys_mmap(16) as *i64
154 if nx_https_url_connect(target, url, 1779284141, fd_p) != NX_HTTPS_CONNECT_OK { return 42 }
155 let fd: i64 = *fd_p
156
157 // ---- TLS validation ctx ----
158 let val_ctx_raw: *u8 = sys_mmap(64)
159 let val_ctx: *TlsValidationContext = val_ctx_raw as *TlsValidationContext
160 val_ctx.store = store
161 val_ctx.sni_host = url + target.url.host_off
162 val_ctx.sni_host_len = target.url.host_len
163 val_ctx.now_epoch = sys_now_realtime_sec() // REAL clock: hardcoded epochs rot when live sites renew certs (B1 root cause)
164
165 let sr: i64 = nx_tls13_client_session_run(
166 fd, url + target.url.host_off, target.url.host_len,
167 cr, priv, val_ctx
168 )
169 dump_dec(0x53, 0x52, sr)
170 if sr <= 0 {
171 sys_close(fd)
172 return 200 + (0 - sr)
173 }
174
175 let session: *Tls13ClientSession = sr as *Tls13ClientSession
176
177 // ---- GET /directory ----
178 let buf: *u8 = sys_mmap(65536)
179 let req_path: *u8 = sys_mmap(16)
180 req_path[0]=0x2F; req_path[1]=0x64; req_path[2]=0x69; req_path[3]=0x72 // /dir
181 req_path[4]=0x65; req_path[5]=0x63; req_path[6]=0x74; req_path[7]=0x6F // ecto
182 req_path[8]=0x72; req_path[9]=0x79 // ry
183
184 let gc: i64 = nx_https_get_complete(
185 session, fd,
186 req_path, 10,
187 url + target.url.host_off, target.url.host_len,
188 buf, 65536
189 )
190 sys_close(fd)
191 dump_dec(0x47, 0x43, gc)
192 if gc < 0 { return 100 + (0 - gc) }
193
194 // ---- Parse response ----
195 let rr: *i64 = sys_mmap(128) as *i64
196 let pv: i64 = nx_http_response_parse(buf, gc, rr)
197 dump_dec(0x50, 0x56, pv)
198 if pv != 0 { return 50 }
199 let status: i64 = rr[1]
200 dump_dec(0x53, 0x54, status)
201 if status != 200 { return 51 }
202 let body_off: i64 = rr[6]
203 let body_kind: i64 = rr[8]
204 dump_dec(0x42, 0x4F, body_off)
205 dump_dec(0x42, 0x4B, body_kind)
206
207 // ---- Dechunk if necessary ----
208 let body: *u8 = sys_mmap(16384)
209 var body_len: i64 = 0
210 if body_kind == 2 {
211 body_len = nx_http_dechunk(buf + body_off, gc - body_off, body, 16384)
212 if body_len < 0 { return 60 }
213 } else {
214 body_len = gc - body_off
215 var k: i64 = 0
216 while k < body_len { body[k] = buf[body_off + k]; k = k + 1 }
217 }
218 dump_dec(0x42, 0x4C, body_len) // "BL=" body len
219
220 // ---- Assert the JSON contains expected ACME directory keys ----
221 let key_na: *u8 = "newAccount" as *u8
222 let key_no: *u8 = "newOrder" as *u8
223 let key_nn: *u8 = "newNonce" as *u8
224 let key_rv: *u8 = "revokeCert" as *u8
225
226 if _contains(body, body_len, key_na, 10) != 1 { return 70 }
227 if _contains(body, body_len, key_no, 8) != 1 { return 71 }
228 if _contains(body, body_len, key_nn, 8) != 1 { return 72 }
229 if _contains(body, body_len, key_rv, 10) != 1 { return 73 }
230
231 // ---- Save body for archaeology ----
232 let outpath: *u8 = "/tmp/le_directory.json\x00"
233 let ofd: i64 = sys_openat_wr(outpath, 0x1A4)
234 if ofd <= 0 { return 80 }
235 sys_write(ofd, body, body_len)
236 sys_close(ofd)
237
238 let banner: *u8 = sys_mmap(64)
239 banner[0]=0x4C; banner[1]=0x45 // "LE"
240 banner[2]=0x5F; banner[3]=0x4F; banner[4]=0x4B // _OK
241 banner[5]=0x0A
242 sys_write(1, banner, 6)
243 return 0
244}