nx_tls12_req.nx source
↩ module page · 537 lines · 33784 B
1// nx_tls12_req.nx -- REUSABLE sovereign TLS-1.2 HTTPS request (ECDHE-RSA-AES128-GCM-SHA256). LIBRARY (no main):
2// one call t12_request(full_url, method, token, body, ...) does connect -> ECDHE-RSA handshake -> encrypted request
3// -> incremental-decrypt response, returning the plaintext HTTP response (headers+body). This is what gets nx_gpu_ctl
4// (and Porkbun) off curl: console.vast.ai + Porkbun are TLS-1.2-only while the shipped stack is 1.3-only. Composes
5// the shipped crypto primitives (p256_ecdh_*, hmac_sha256 PRF, nx_aes128_gcm_seal/open, sha256_digest). SILENT: no
6// stdout chatter, so a caller gets clean response bytes. Proven end-to-end (authenticated account GET) 2026-07-14.
7// NOTE(security): cert-chain + ServerKeyExchange-signature validation NOT yet wired -- confidential but not yet
8// MITM-authenticated; harden before trusting for adversarial networks. license_tier: ORIGINAL
9import "nx_syscalls.nx"
10import "nx_csprng.nx"
11import "nx_https_url_for_fetch.nx"
12import "nx_https_url_connect.nx"
13import "nx_sha256.nx"
14import "nx_p256_ecdh.nx"
15import "nx_hmac.nx" // was hmac.nx: SAME hmac_sha256 signature but the LEGACY family (syscalls.nx + sha256.nx).
16// Importing it alongside the nx_ family gave DUPLICATE hmac_sha256 the moment nx_https_fetch_follow
17// started importing this file, breaking every organ whose closure held both. This organ already
18// imports nx_sha256.nx, so the legacy pull-in was redundant as well as harmful.
19import "nx_aes128_gcm.nx"
20import "nx_trust_store_load_from_certdata.nx" // trust store loader
21import "nx_https_cert_pipeline.nx" // chain + hostname + validity verify (+ NX_HTTPS_PIPELINE_OK, x509_parse, X509Cert)
22import "nx_x509_pubkey_rsa.nx" // leaf RSA pubkey extract (2048)
23import "nx_x509_pubkey_rsa_4096.nx" // leaf RSA pubkey extract (4096/3072)
24import "nx_x509_verify_rsa_pkcs1_sha256.nx" // generic RSA-PKCS1-SHA256 verify -- used on the SKE signature
25import "nx_x509_verify_rsa_pkcs1_sha256_4096.nx"
26import "nx_u2048.nx"
27import "nx_u4096.nx"
28const TR_MAGIC_2048: i64 = 2048
29const TR_MAGIC_4096: i64 = 4096
30const TR_MAGIC_3072: i64 = 3072
31const TR_MAGIC_1024: i64 = 1024
32const TR_MAGIC_16384: i64 = 16384
33const TR_MAGIC_65536: i64 = 65536
34const TR_MAGIC_32768: i64 = 32768
35const TR_MAGIC_4194304: i64 = 4194304
36const TR_MAGIC_40960: i64 = 40960
37const TR_MAGIC_8192: i64 = 8192
38const TR_MAGIC_8300: i64 = 8300
39const TR_MAGIC_262144: i64 = 262144
40
41const TR_HS: i64 = 22
42const TR_CCS: i64 = 20
43const TR_ALERT: i64 = 21
44
45func tr_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
46func tr_b1(d: *u8, o: i64, v: i64) -> i64 { d[o]=(v&0xff) as u8; return o+1 }
47func tr_b2(d: *u8, o: i64, v: i64) -> i64 { d[o]=((v>>8)&0xff) as u8; d[o+1]=(v&0xff) as u8; return o+2 }
48func tr_b3(d: *u8, o: i64, v: i64) -> i64 { d[o]=((v>>16)&0xff) as u8; d[o+1]=((v>>8)&0xff) as u8; d[o+2]=(v&0xff) as u8; return o+3 }
49func tr_bytes(d: *u8, o: i64, s: *u8, n: i64) -> i64 { var i: i64=0; while i<n { d[o+i]=s[i]; i=i+1 } return o+n }
50func tr_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){ d[o+i]=s[i]; i=i+1 } return o+i }
51func tr_catn(d: *u8, o: i64, v: i64) -> i64 {
52 let t: *u8=sys_mmap(24); var m: i64=v; var k: i64=0
53 if m < 0 { d[o] = 45 as u8; o = o + 1; m = 0 - m }
54 if m==0 { t[0]=48 as u8; k=1 }
55 while m>0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 }
56 var w: i64=o; var i: i64=0; while i<k { d[w]=t[k-1-i]; w=w+1; i=i+1 } return w
57}
58func tr_u64be(d: *u8, o: i64, v: i64) -> i64 {
59 d[o]=((v>>56)&0xff) as u8; d[o+1]=((v>>48)&0xff) as u8; d[o+2]=((v>>40)&0xff) as u8; d[o+3]=((v>>32)&0xff) as u8
60 d[o+4]=((v>>24)&0xff) as u8; d[o+5]=((v>>16)&0xff) as u8; d[o+6]=((v>>8)&0xff) as u8; d[o+7]=(v&0xff) as u8
61 return o+8
62}
63func tr_write_all(fd: i64, buf: *u8, n: i64) -> i64 {
64 var off: i64=0
65 while off<n { let w: i64=sys_write(fd, ((buf as i64)+off) as *u8, n-off); if w<=0 { return 0-1 } off=off+w }
66 return 0
67}
68// TLS 1.2 PRF = P_SHA256(secret, label||seed)
69func tr_prf(secret: *u8, secret_len: i64, label: *u8, label_len: i64, seed: *u8, seed_len: i64, out: *u8, out_len: i64) -> i64 {
70 let ls_len: i64 = label_len + seed_len
71 let ls: *u8 = sys_mmap(ls_len + 8)
72 var i: i64 = 0
73 while i < label_len { ls[i] = label[i]; i = i + 1 }
74 i = 0
75 while i < seed_len { ls[label_len + i] = seed[i]; i = i + 1 }
76 let a: *u8 = sys_mmap(32); let a_next: *u8 = sys_mmap(32)
77 hmac_sha256(secret, secret_len, ls, ls_len, a)
78 let tmp: *u8 = sys_mmap(32 + ls_len + 8); let block: *u8 = sys_mmap(32)
79 var off: i64 = 0
80 while off < out_len {
81 var j: i64 = 0
82 while j < 32 { tmp[j] = a[j]; j = j + 1 }
83 j = 0
84 while j < ls_len { tmp[32 + j] = ls[j]; j = j + 1 }
85 hmac_sha256(secret, secret_len, tmp, 32 + ls_len, block)
86 var c: i64 = 32
87 if out_len - off < 32 { c = out_len - off }
88 j = 0
89 while j < c { out[off + j] = block[j]; j = j + 1 }
90 off = off + c
91 hmac_sha256(secret, secret_len, a, 32, a_next)
92 j = 0
93 while j < 32 { a[j] = a_next[j]; j = j + 1 }
94 }
95 return 0
96}
97func tr_client_hello(host: *u8, hlen: i64, cr_out: *u8, out: *u8) -> i64 {
98 let body: *u8 = sys_mmap(TR_MAGIC_1024); var o: i64 = 0
99 o = tr_b2(body, o, 0x0303)
100 nx_csprng_fill(cr_out, 32)
101 o = tr_bytes(body, o, cr_out, 32)
102 o = tr_b1(body, o, 0)
103 o = tr_b2(body, o, 2); o = tr_b2(body, o, 0xC02F)
104 o = tr_b1(body, o, 1); o = tr_b1(body, o, 0)
105 let ext: *u8 = sys_mmap(512); var e: i64 = 0
106 e = tr_b2(ext, e, 0x0000); e = tr_b2(ext, e, hlen + 5); e = tr_b2(ext, e, hlen + 3); e = tr_b1(ext, e, 0); e = tr_b2(ext, e, hlen); e = tr_bytes(ext, e, host, hlen)
107 e = tr_b2(ext, e, 0x000A); e = tr_b2(ext, e, 4); e = tr_b2(ext, e, 2); e = tr_b2(ext, e, 0x0017)
108 e = tr_b2(ext, e, 0x000B); e = tr_b2(ext, e, 2); e = tr_b1(ext, e, 1); e = tr_b1(ext, e, 0)
109 // signature_algorithms: advertise ONLY what tr_verify_ske_sig can verify (rsa_pkcs1_sha256, 0x0401).
110 // MEASURED 2026-08-18 (debt 1787075987): this hello offered sha384/sha512 too, the verifier is
111 // fail-closed to sha256, so every server that PREFERRED a stronger hash (whatwg-class) signed the
112 // SKE with 0x0601 and the leg died at t12-ske-signature rc=-2937 -- A PRODUCER AND A CONSUMER EACH
113 // CORRECT IN ISOLATION STILL DISAGREED ON THE WIRE. Offering only 0x0401 makes the server sign
114 // sha256 or abort the handshake, which is never worse than the guaranteed failure it replaces.
115 e = tr_b2(ext, e, 0x000D); e = tr_b2(ext, e, 4); e = tr_b2(ext, e, 2); e = tr_b2(ext, e, 0x0401)
116 o = tr_b2(body, o, e); o = tr_bytes(body, o, ext, e)
117 let body_len: i64 = o
118 var r: i64 = 0
119 r = tr_b1(out, r, TR_HS); r = tr_b2(out, r, 0x0303); r = tr_b2(out, r, body_len + 4)
120 r = tr_b1(out, r, 1); r = tr_b3(out, r, body_len); r = tr_bytes(out, r, body, body_len)
121 return r
122}
123func tr_hdr_end(b: *u8, n: i64) -> i64 {
124 var i: i64 = 0
125 while i + 3 < n { if b[i]==(13 as u8) { if b[i+1]==(10 as u8) { if b[i+2]==(13 as u8) { if b[i+3]==(10 as u8) { return i+4 } } } } i = i + 1 }
126 return 0 - 1
127}
128func tr_lc(c: i64) -> i64 { if c >= 65 { if c <= 90 { return c + 32 } } return c }
129func tr_match_ci(b: *u8, i: i64, n: i64, targ: *u8, tlen: i64) -> i64 {
130 if i + tlen > n { return 0 }
131 var j: i64 = 0
132 while j < tlen { if tr_lc(b[i+j]&0xff) != (targ[j] as i64) { return 0 } j = j + 1 }
133 return 1
134}
135func tr_content_length(b: *u8, n: i64) -> i64 {
136 var i: i64 = 0
137 while i + 15 < n {
138 if tr_match_ci(b, i, n, "content-length:" as *u8, 15) == 1 {
139 var p: i64 = i + 15; var sk: i64 = 1
140 while sk == 1 { if p < n { if b[p]==(32 as u8) { p = p + 1 } else { sk = 0 } } else { sk = 0 } }
141 var val: i64 = 0; var any: i64 = 0; var go: i64 = 1
142 while go == 1 { if p < n { let c: i64 = b[p]&0xff; if c >= 48 { if c <= 57 { val = val*10 + (c-48); any = 1; p = p + 1 } else { go = 0 } } else { go = 0 } } else { go = 0 } }
143 if any == 1 { return val }
144 return 0 - 1
145 }
146 i = i + 1
147 }
148 return 0 - 1
149}
150// Does the header block declare the chunked transfer coding? Bounded to the headers so a body that merely
151// contains the word cannot flip it.
152func tr_is_chunked(b: *u8, hdr_end: i64) -> i64 {
153 var i: i64 = 0
154 while i + 7 <= hdr_end {
155 if tr_match_ci(b, i, hdr_end, "chunked" as *u8, 7) == 1 { return 1 }
156 i = i + 1
157 }
158 return 0
159}
160// Has the TERMINAL zero-length chunk arrived? Looks for CRLF "0" CRLF -- that byte run can only be the
161// last-chunk marker, because every other chunk-size line is hex with a non-CR byte following the digit
162// (a size of 0x0a reads "0a\r\n", so b[i+3] is 'a', not CR).
163func tr_chunk_done(b: *u8, from: i64, n: i64) -> i64 {
164 var i: i64 = from
165 while i + 5 <= n {
166 if b[i]==(13 as u8) { if b[i+1]==(10 as u8) { if b[i+2]==(48 as u8) { if b[i+3]==(13 as u8) { if b[i+4]==(10 as u8) { return 1 } } } } }
167 i = i + 1
168 }
169 return 0
170}
171// build request-line + headers (+ body) into out; returns length
172func tr_http_req(method: *u8, mlen: i64, target: *u8, tlen: i64, host: *u8, hlen: i64, token: *u8, tn: i64, body: *u8, blen: i64, out: *u8) -> i64 {
173 var o: i64 = 0
174 o = tr_bytes(out, o, method, mlen); out[o]=0x20 as u8; o=o+1
175 o = tr_bytes(out, o, target, tlen)
176 o = tr_cat(out, o, " HTTP/1.1\r\nHost: " as *u8)
177 o = tr_bytes(out, o, host, hlen)
178 if tn > 0 { o = tr_cat(out, o, "\r\nAuthorization: Bearer " as *u8); o = tr_bytes(out, o, token, tn) }
179 if blen > 0 { o = tr_cat(out, o, "\r\nContent-Type: application/json\r\nContent-Length: " as *u8); o = tr_catn(out, o, blen) }
180 o = tr_cat(out, o, "\r\nUser-Agent: nishi-tls12\r\nConnection: close\r\n\r\n" as *u8)
181 if blen > 0 { o = tr_bytes(out, o, body, blen) }
182 return o
183}
184
185// Reformat a TLS-1.2 Certificate message body ([3B list_len][ {3B certlen}{DER} ...]) into a TLS-1.3 Certificate
186// message ([1B ctx=0][3B list_len][ {3B certlen}{DER}{2B ext_len=0} ...]) so the shipped 1.3 chain+hostname+validity
187// pipeline (nx_https_cert_pipeline_verify_with_store) validates it unchanged. Returns the 1.3 message length.
188func tr_certs12_to_13(body12: *u8, len12: i64, out13: *u8) -> i64 {
189 var o: i64 = 0
190 out13[o]=11 as u8; o=o+1 // HT_CERTIFICATE handshake type (the parser checks this)
191 let hdrpos: i64 = o
192 out13[o]=0 as u8; out13[o+1]=0 as u8; out13[o+2]=0 as u8; o=o+3 // reserve 3-byte handshake-message length
193 out13[o]=0 as u8; o=o+1 // certificate_request_context length = 0
194 let listpos: i64 = o
195 out13[o]=0 as u8; out13[o+1]=0 as u8; out13[o+2]=0 as u8; o=o+3 // reserve 3-byte certificate_list length
196 var i: i64 = 3 // skip the 1.2 cert-list's own 3-byte length
197 while i + 3 <= len12 {
198 let cl: i64 = ((body12[i]&0xff)<<16)|((body12[i+1]&0xff)<<8)|(body12[i+2]&0xff)
199 if cl <= 0 { i = len12 } else {
200 out13[o]=body12[i]; out13[o+1]=body12[i+1]; out13[o+2]=body12[i+2]; o=o+3
201 var z: i64=0; while z<cl { out13[o+z]=body12[i+3+z]; z=z+1 } o=o+cl
202 out13[o]=0 as u8; out13[o+1]=0 as u8; o=o+2 // per-cert extensions length = 0
203 i = i + 3 + cl
204 }
205 }
206 let ll: i64 = o - (listpos+3) // certificate_list content length
207 out13[listpos]=((ll>>16)&0xff) as u8; out13[listpos+1]=((ll>>8)&0xff) as u8; out13[listpos+2]=(ll&0xff) as u8
208 let total: i64 = o - (hdrpos+3) // handshake-message length = everything after the 4B header
209 out13[hdrpos]=((total>>16)&0xff) as u8; out13[hdrpos+1]=((total>>8)&0xff) as u8; out13[hdrpos+2]=(total&0xff) as u8
210 return o
211}
212
213// Verify the ServerKeyExchange signature: the server signs SHA256(client_random || server_random || ecdhe_params)
214// with its certificate's private key; we verify with the leaf cert's PUBLIC key. This BINDS the ECDHE key we're
215// about to use to the (already chain-validated) cert -- closing the active-MITM gap (an attacker replaying a valid
216// cert with its own ECDHE key can't produce this signature). We only offer ECDHE_RSA (0xC02F) so the signature is
217// always RSA-PKCS1. Returns 1 on valid; 2=cert-parse 3=pubkey-extract 4=SIG-INVALID 5=malformed; 1000+alg=unsupported.
218func tr_verify_ske_sig(cr: *u8, sr: *u8, ske: *u8, ske_len: i64, leaf_der: *u8, leaf_len: i64) -> i64 {
219 if ske_len < 8 { return 5 }
220 let pubkey_len: i64 = ske[3] & 0xff
221 let params_len: i64 = 4 + pubkey_len // curve_type(1)+named_curve(2)+pklen(1)+pubkey
222 if params_len + 4 > ske_len { return 5 }
223 let sig_alg: i64 = ((ske[params_len]&0xff)<<8)|(ske[params_len+1]&0xff)
224 let sig_len: i64 = ((ske[params_len+2]&0xff)<<8)|(ske[params_len+3]&0xff)
225 let sig_ptr: *u8 = ((ske as i64) + params_len + 4) as *u8
226 if params_len + 4 + sig_len > ske_len { return 5 }
227 if sig_alg != 0x0401 { return 1000 + sig_alg } // only rsa_pkcs1_sha256 wired (fail-closed for others)
228 // tbs = client_random(32) || server_random(32) || ecdhe_params(params_len)
229 let tbs: *u8 = sys_mmap(512); var o: i64 = 0
230 var z: i64 = 0; while z < 32 { tbs[o]=cr[z]; o=o+1; z=z+1 }
231 z = 0; while z < 32 { tbs[o]=sr[z]; o=o+1; z=z+1 }
232 z = 0; while z < params_len { tbs[o]=ske[z]; o=o+1; z=z+1 }
233 let leaf_raw: *u8 = sys_mmap(256); let leaf: *X509Cert = leaf_raw as *X509Cert
234 if x509_parse(leaf_der, leaf_len, leaf) < 0 { return 2 }
235 if sig_len == 256 {
236 let n: *i64 = u2048_alloc(); let e_p: *i64 = sys_mmap(16) as *i64
237 if nx_x509_pubkey_extract_rsa(leaf_der, leaf, n, e_p) != NX_X509_PUBKEY_RSA_OK { return 3 }
238 if nx_x509_verify_rsa_pkcs1_sha256(tbs, o, sig_ptr, sig_len, n, *e_p) == NX_X509_RSA_PKCS1_SHA256_OK { return 1 }
239 return 4
240 }
241 if sig_len == 512 {
242 let n4: *i64 = u4096_alloc(); let e4: *i64 = sys_mmap(16) as *i64
243 if nx_x509_pubkey_extract_rsa_4096(leaf_der, leaf, n4, e4) != NX_X509_PUBKEY_RSA_4096_OK { return 3 }
244 if nx_x509_verify_rsa_pkcs1_sha256_4096(tbs, o, sig_ptr, sig_len, n4, *e4) == NX_X509_RSA_PKCS1_SHA256_4096_OK { return 1 }
245 return 4
246 }
247 return 5
248}
249
250// THE reusable request. full_url = "https://host/path?query". method/token/body optional (token/body=0 to omit).
251// Fills `out` with the plaintext HTTP response (status line + headers + body); returns its length, or <0 on error:
252// -1 bad url -2 connect -3 handshake(no server random/pubkey) -4 ECDHE -5 request write
253const TR_READ_TIMEOUT_SECS: i64 = 20 // declared: per-read ceiling, so a stalling peer fails fast
254// A PASS THAT DECRYPTS NO NEW PLAINTEXT MADE NO PROGRESS. This bounds a STALLING peer, and deliberately
255// NOT the size of a transfer: the old `rreads > 60` counted EVERY pass, so a large body was cut off after
256// 60 reads no matter how well it was going -- a transfer ceiling wearing a timeout's name. Paired with the
257// 20s per-read socket ceiling above, 60 consecutive no-progress passes is the outer bound on a dribbling peer.
258const TR_MAX_STALL_PASSES: i64 = 60
259const TR_TRUNCATED: i64 = 9 // returned negated: the peer declared a Content-Length we did not reach
260const TR_RECORD_OVERSIZE: i64 = 10 // returned negated: one record exceeds the whole receive window (protocol violation)
261const TR_ALERTED: i64 = 11 // returned negated: the peer answered the ClientHello with a TLS alert record (a 1.3-only host refusing 1.2) -- an ANSWER, never a slow flight
262func tr_puts(s: *u8) -> i64 { sys_write(2, s, tr_slen(s)); return 0 }
263func tr_putn(v: i64) -> i64 {
264 if v == 0 { sys_write(2, "0" as *u8, 1); return 0 }
265 var m: i64 = v
266 if m < 0 { sys_write(2, "-" as *u8, 1); m = 0 - m }
267 let d: *u8 = sys_mmap(24); var k: i64 = 0
268 while m > 0 { d[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
269 let o: *u8 = sys_mmap(24); var wi: i64 = 0
270 while wi < k { o[wi] = d[k - 1 - wi]; wi = wi + 1 }
271 sys_write(2, o, k)
272 return 0
273}
274func t12_request(full_url: *u8, method: *u8, mlen: i64, token: *u8, tn: i64, body: *u8, blen: i64, out: *u8, outcap: i64) -> i64 {
275 let target_raw: *u8 = sys_mmap(64); let target: *NxHttpsTarget = target_raw as *NxHttpsTarget
276 target.url = nx_url_new(); target.port = 0
277 if nx_https_url_for_fetch(full_url, target) != NX_HTTPS_URL_OK { return 0 - 1 }
278 let host: *u8 = ((full_url as i64) + target.url.host_off) as *u8
279 let hlen: i64 = target.url.host_len
280 // request-target = everything in the URL AFTER the host (path + ?query) -- NOT the parsed path (drops query)
281 let reqtgt: *u8 = ((full_url as i64) + target.url.host_off + hlen) as *u8
282 let reqtlen: i64 = tr_slen(reqtgt)
283 let now: i64 = sys_now_realtime_sec()
284 let fd_p: *i64 = sys_mmap(16) as *i64
285 if nx_https_url_connect(target, full_url, now, fd_p) != NX_HTTPS_CONNECT_OK { return 0 - 2 }
286 let fd: i64 = fd_p[0]
287 // Bound every read. Required now that a close-delimited response legitimately reads until EOF: without
288 // this, a keep-alive peer that sends neither Content-Length nor a terminal chunk would block forever.
289 sys_set_socket_timeout(fd, TR_READ_TIMEOUT_SECS)
290
291 let tr: *u8 = sys_mmap(TR_MAGIC_16384); var trlen: i64 = 0
292 let cr: *u8 = sys_mmap(32); let ch: *u8 = sys_mmap(TR_MAGIC_2048)
293 let chlen: i64 = tr_client_hello(host, hlen, cr, ch)
294 if tr_write_all(fd, ch, chlen) < 0 { sys_close(fd); return 0 - 5 }
295 trlen = tr_bytes(tr, trlen, ((ch as i64)+5) as *u8, chlen-5)
296
297 let buf: *u8 = sys_mmap(TR_MAGIC_65536); var total: i64 = 0
298 let sr: *u8 = sys_mmap(32); let spub: *u8 = sys_mmap(65)
299 let cert12: *u8 = sys_mmap(TR_MAGIC_32768); var cert12_len: i64 = 0; var got_cert: i64 = 0
300 let ske_body: *u8 = sys_mmap(TR_MAGIC_4096); var ske_len: i64 = 0
301 var got_sr: i64 = 0; var got_spub: i64 = 0; var got_shd: i64 = 0; var reads: i64 = 0
302 var alert_desc: i64 = 0 - 1 // the alert description byte when the first flight is an alert record
303 while got_shd == 0 {
304 if reads > 40 { got_shd = 2 }
305 let n: i64 = sys_read(fd, ((buf as i64)+total) as *u8, TR_MAGIC_65536-total)
306 if n <= 0 { got_shd = 2 } else {
307 total = total + n; reads = reads + 1
308 var p: i64 = 0
309 while p + 5 <= total {
310 let rtype: i64 = buf[p]&0xff; let rlen: i64 = ((buf[p+3]&0xff)<<8)|(buf[p+4]&0xff)
311 if p+5+rlen > total { p = total } else {
312 if rtype == TR_HS {
313 var hp: i64 = p+5; let rend: i64 = p+5+rlen
314 while hp + 4 <= rend { let ht: i64 = buf[hp]&0xff; let hl: i64 = ((buf[hp+1]&0xff)<<16)|((buf[hp+2]&0xff)<<8)|(buf[hp+3]&0xff); if ht==14 { got_shd=1 } hp=hp+4+hl }
315 }
316 if rtype == TR_ALERT { if rlen >= 2 { alert_desc = buf[p+6]&0xff } got_shd = 3 }
317 p = p + 5 + rlen
318 }
319 }
320 }
321 }
322 // A TLS ALERT IN THE FIRST FLIGHT IS AN ANSWER, NOT A SLOW FLIGHT (2026-09-17): a 1.3-only host answers the
323 // 1.2 ClientHello with protocol_version (desc 70) or handshake_failure (40) and closes. This used to fall
324 // out of the loop on the close and report -3 "no SH/SKE inside the read budget", sending every caller to
325 // the read budget when the peer had already said no. Named here, with its description byte.
326 if got_shd == 3 { tr_puts("T12-ALERT desc="); tr_putn(alert_desc); tr_puts("\n"); sys_close(fd); return 0 - TR_ALERTED }
327 var p: i64 = 0
328 while p + 5 <= total {
329 let rtype: i64 = buf[p]&0xff; let rlen: i64 = ((buf[p+3]&0xff)<<8)|(buf[p+4]&0xff)
330 if rtype == TR_HS {
331 trlen = tr_bytes(tr, trlen, ((buf as i64)+p+5) as *u8, rlen)
332 var hp: i64 = p+5; let rend: i64 = p+5+rlen
333 while hp + 4 <= rend {
334 let ht: i64 = buf[hp]&0xff; let hl: i64 = ((buf[hp+1]&0xff)<<16)|((buf[hp+2]&0xff)<<8)|(buf[hp+3]&0xff); let bp: i64 = hp+4
335 if ht == 2 { var z: i64=0; while z<32 { sr[z]=buf[bp+2+z]; z=z+1 } got_sr=1 }
336 if ht == 11 { var z: i64=0; while z<hl { cert12[z]=buf[bp+z]; z=z+1 } cert12_len=hl; got_cert=1 }
337 if ht == 12 { var z: i64=0; while z<65 { spub[z]=buf[bp+4+z]; z=z+1 } var w: i64=0; while w<hl { ske_body[w]=buf[bp+w]; w=w+1 } ske_len=hl; got_spub=1 }
338 hp = hp + 4 + hl
339 }
340 }
341 p = p + 5 + rlen
342 }
343 if got_sr==0 { sys_close(fd); return 0 - 3 }
344 if got_spub==0 { sys_close(fd); return 0 - 3 }
345 if got_cert==0 { sys_close(fd); return 0 - 6 }
346 // ---- CERT VALIDATION: chain (to Mozilla roots) + hostname + validity, via the shipped 1.3 pipeline after a
347 // 1.2->1.3 reformat. FAIL-CLOSED: a cert that doesn't chain to a trusted root / doesn't cover the host / is
348 // expired ABORTS the connection (return -8) before we send the Bearer token. NOTE(residual): the
349 // ServerKeyExchange SIGNATURE (which binds the ECDHE key to this cert) is not yet verified -- so this stops
350 // self-signed / wrong-host / expired certs but not a sophisticated active MITM that replays a valid cert with
351 // its own ECDHE key. That SKE-sig check (RSA/ECDSA over client_random||server_random||params) is the final step.
352 let store_r: i64 = nx_trust_store_load_from_certdata("data/mozilla_certdata.txt" as *u8, 512, TR_MAGIC_4194304)
353 if store_r <= 0 { sys_close(fd); return 0 - 7 }
354 let store: *TrustStore = store_r as *TrustStore
355 let c13: *u8 = sys_mmap(TR_MAGIC_40960)
356 let c13len: i64 = tr_certs12_to_13(cert12, cert12_len, c13)
357 let cv: i64 = nx_https_cert_pipeline_verify_with_store(c13, c13len, host, hlen, now, store)
358 if cv != NX_HTTPS_PIPELINE_OK { sys_close(fd); return 0 - 200 - cv } // encode pipeline verdict for diagnosis
359 // ---- SKE-SIGNATURE verify: bind the ECDHE key to the validated leaf cert (closes the active-MITM gap) ----
360 let leaf_len: i64 = ((cert12[3]&0xff)<<16)|((cert12[4]&0xff)<<8)|(cert12[5]&0xff)
361 let leaf_der: *u8 = ((cert12 as i64)+6) as *u8
362 let sv: i64 = tr_verify_ske_sig(cr, sr, ske_body, ske_len, leaf_der, leaf_len)
363 if sv != 1 { sys_close(fd); return 0 - 400 - sv } // encode SKE-verify verdict for diagnosis
364
365 let seed: *u8 = sys_mmap(32); nx_csprng_fill(seed, 32)
366 let mypriv: *u8 = sys_mmap(32)
367 if p256_ecdh_derive_priv(seed, mypriv) != NX_P256_ECDH_OK { sys_close(fd); return 0 - 4 }
368 let mypub: *u8 = sys_mmap(65)
369 if p256_ecdh_pub(mypriv, mypub) != NX_P256_ECDH_OK { sys_close(fd); return 0 - 4 }
370 let premaster: *u8 = sys_mmap(32)
371 if p256_ecdh_shared(mypriv, spub, 65, premaster) != NX_P256_ECDH_OK { sys_close(fd); return 0 - 4 }
372
373 let crsr: *u8 = sys_mmap(64); tr_bytes(crsr, 0, cr, 32); tr_bytes(crsr, 32, sr, 32)
374 let srcr: *u8 = sys_mmap(64); tr_bytes(srcr, 0, sr, 32); tr_bytes(srcr, 32, cr, 32)
375 let master: *u8 = sys_mmap(48); tr_prf(premaster, 32, "master secret" as *u8, 13, crsr, 64, master, 48)
376 let kb: *u8 = sys_mmap(40); tr_prf(master, 48, "key expansion" as *u8, 13, srcr, 64, kb, 40)
377 let cwk: *u8 = ((kb as i64)+0) as *u8
378 let swk: *u8 = ((kb as i64)+16) as *u8
379 let civ: *u8 = ((kb as i64)+32) as *u8
380 let siv: *u8 = ((kb as i64)+36) as *u8
381
382 let cke: *u8 = sys_mmap(128); var ck: i64 = 0
383 ck = tr_b1(cke, ck, TR_HS); ck = tr_b2(cke, ck, 0x0303); ck = tr_b2(cke, ck, 4 + 66)
384 let cke_hs: i64 = ck
385 ck = tr_b1(cke, ck, 16); ck = tr_b3(cke, ck, 66); ck = tr_b1(cke, ck, 65); ck = tr_bytes(cke, ck, mypub, 65)
386 if tr_write_all(fd, cke, ck) < 0 { sys_close(fd); return 0 - 5 }
387 trlen = tr_bytes(tr, trlen, ((cke as i64)+cke_hs) as *u8, ck - cke_hs)
388
389 let ccs: *u8 = sys_mmap(8); var cc: i64=0
390 cc = tr_b1(ccs, cc, TR_CCS); cc = tr_b2(ccs, cc, 0x0303); cc = tr_b2(ccs, cc, 1); cc = tr_b1(ccs, cc, 1)
391 if tr_write_all(fd, ccs, cc) < 0 { sys_close(fd); return 0 - 5 }
392
393 let thash: *u8 = sys_mmap(32); sha256_digest(tr, trlen, thash)
394 let cvd: *u8 = sys_mmap(12); tr_prf(master, 48, "client finished" as *u8, 15, thash, 32, cvd, 12)
395 let fin: *u8 = sys_mmap(16); var fo: i64=0
396 fo = tr_b1(fin, fo, 20); fo = tr_b3(fin, fo, 12); fo = tr_bytes(fin, fo, cvd, 12)
397 trlen = tr_bytes(tr, trlen, fin, 16)
398 let nonce: *u8 = sys_mmap(12); var q: i64=0; while q<4 { nonce[q]=civ[q]; q=q+1 } while q<12 { nonce[q]=0; q=q+1 }
399 let aad: *u8 = sys_mmap(13); q=0; while q<8 { aad[q]=0; q=q+1 } aad[8]=22 as u8; aad[9]=3 as u8; aad[10]=3 as u8; aad[11]=0 as u8; aad[12]=16 as u8
400 let fct: *u8 = sys_mmap(16); let ftag: *u8 = sys_mmap(16)
401 nx_aes128_gcm_seal(cwk, nonce, aad, 13, fin, 16, fct, ftag)
402 let frec: *u8 = sys_mmap(64); var fr: i64=0
403 fr = tr_b1(frec, fr, TR_HS); fr = tr_b2(frec, fr, 0x0303); fr = tr_b2(frec, fr, 8+16+16)
404 q=0; while q<8 { frec[fr]=0 as u8; fr=fr+1; q=q+1 }
405 fr = tr_bytes(frec, fr, fct, 16); fr = tr_bytes(frec, fr, ftag, 16)
406 if tr_write_all(fd, frec, fr) < 0 { sys_close(fd); return 0 - 5 }
407
408 // drain server CCS + Finished (don't strictly verify here; the request+response GCM auth already proves keys)
409 let rbuf: *u8 = sys_mmap(TR_MAGIC_16384); var rtot: i64 = 0; var rr: i64 = 0; var server_done: i64 = 0
410 while server_done == 0 {
411 if rr > 30 { server_done = 2 }
412 let n: i64 = sys_read(fd, ((rbuf as i64)+rtot) as *u8, TR_MAGIC_16384-rtot)
413 if n <= 0 { server_done = 2 } else {
414 rtot = rtot + n; rr = rr + 1
415 var pp: i64 = 0
416 while pp + 5 <= rtot {
417 let rtype: i64 = rbuf[pp]&0xff; let rlen: i64 = ((rbuf[pp+3]&0xff)<<8)|(rbuf[pp+4]&0xff)
418 if pp+5+rlen > rtot { pp = rtot } else { if rtype==TR_ALERT { server_done=3 } if rtype==TR_HS { server_done=1 } pp = pp + 5 + rlen }
419 }
420 }
421 }
422
423 // send the application request (client app seq = 1: Finished was seq 0)
424 let reqb: *u8 = sys_mmap(TR_MAGIC_8192)
425 let rq: i64 = tr_http_req(method, mlen, reqtgt, reqtlen, host, hlen, token, tn, body, blen, reqb)
426 let anonce: *u8 = sys_mmap(12); var az: i64=0; while az<4 { anonce[az]=civ[az]; az=az+1 } tr_u64be(anonce, 4, 1)
427 let aaad: *u8 = sys_mmap(13); tr_u64be(aaad, 0, 1); aaad[8]=23 as u8; aaad[9]=3 as u8; aaad[10]=3 as u8; aaad[11]=((rq>>8)&0xff) as u8; aaad[12]=(rq&0xff) as u8
428 let act: *u8 = sys_mmap(TR_MAGIC_8192); let atag: *u8 = sys_mmap(16)
429 nx_aes128_gcm_seal(cwk, anonce, aaad, 13, reqb, rq, act, atag)
430 let arec: *u8 = sys_mmap(TR_MAGIC_8300); var ar: i64=0
431 ar = tr_b1(arec, ar, 23); ar = tr_b2(arec, ar, 0x0303); ar = tr_b2(arec, ar, 8+rq+16)
432 ar = tr_u64be(arec, ar, 1); ar = tr_bytes(arec, ar, act, rq); ar = tr_bytes(arec, ar, atag, 16)
433 if tr_write_all(fd, arec, ar) < 0 { sys_close(fd); return 0 - 5 }
434
435 // read + decrypt response app-data (server app seq = 1); stop on complete HTTP response (Content-Length) or close
436 // 🔴 THE 256 KiB SILENT TRUNCATION LIVED HERE (found 2026-08-20). respbuf is a CIPHERTEXT window and
437 // v1 NEVER COMPACTED IT: rp walked forward over consumed records while resptot only ever grew, so once
438 // resptot reached TR_MAGIC_262144 the read below asked for `262144 - 262144` = ZERO bytes, sys_read
439 // returned 0, and `if n <= 0 { reading = 0 }` ended the loop AS IF THE PEER HAD CLOSED. The plaintext
440 // decrypted so far was then returned as a COMPLETE body, status 200, and saved as a clean mirror.
441 // MEASURED by a sibling lane: an 814,052-byte PDF saved 255,935 bytes TWICE, byte-identical -- and it is
442 // deterministic precisely because TLS record boundaries are.
443 // ★★★★★★A FIXED WINDOW THAT IS NEVER COMPACTED IS NOT A BUFFER, IT IS A TRANSFER CEILING -- AND ITS
444 // EXHAUSTION IS INDISTINGUISHABLE FROM END-OF-FILE, SO IT REPORTS SUCCESS.
445 // THE FIX IS NOT A BIGGER NUMBER. The window is compacted after every pass, so it only ever has to hold
446 // ONE TLS record plus a partial (max 16,384 + 2,048 + 5 bytes) -- the constant stops being a limit at all
447 // and cannot be reached by any legal stream. A raise would only have moved the cliff.
448 let respbuf: *u8 = sys_mmap(TR_MAGIC_262144); var resptot: i64 = 0
449 var rp: i64 = 0; var rseq: i64 = 1; var plain_total: i64 = 0
450 var reading: i64 = 1; var stalls: i64 = 0; var last_plain: i64 = 0 - 1; var oversize: i64 = 0
451 while reading == 1 {
452 var more: i64 = 1
453 while more == 1 {
454 if rp + 5 > resptot { more = 0 } else {
455 let rt: i64 = respbuf[rp]&0xff; let rl: i64 = ((respbuf[rp+3]&0xff)<<8)|(respbuf[rp+4]&0xff)
456 if rp+5+rl > resptot { more = 0 } else {
457 if rt == 21 { reading = 0 }
458 if rt == 23 {
459 let pl: i64 = rl - 8 - 16
460 let dn: *u8 = sys_mmap(12); var dz: i64=0; while dz<4 { dn[dz]=siv[dz]; dz=dz+1 } while dz<12 { dn[dz]=respbuf[rp+5+(dz-4)]; dz=dz+1 }
461 let dad: *u8 = sys_mmap(13); tr_u64be(dad, 0, rseq); dad[8]=23 as u8; dad[9]=3 as u8; dad[10]=3 as u8; dad[11]=((pl>>8)&0xff) as u8; dad[12]=(pl&0xff) as u8
462 let dtag: *u8 = sys_mmap(16); dz=0; while dz<16 { dtag[dz]=respbuf[rp+5+8+pl+dz]; dz=dz+1 }
463 let dpt: *u8 = sys_mmap(TR_MAGIC_65536)
464 if nx_aes128_gcm_open(swk, dn, dad, 13, ((respbuf as i64)+rp+5+8) as *u8, pl, dtag, dpt) == 0 {
465 var cz: i64=0; while cz<pl { if plain_total+cz < outcap { out[plain_total+cz]=dpt[cz] } cz=cz+1 } plain_total=plain_total+pl
466 }
467 rseq = rseq + 1
468 }
469 rp = rp + 5 + rl
470 }
471 }
472 }
473 // ---- COMPACT THE CIPHERTEXT WINDOW. This one loop is the whole truncation fix: everything before
474 // rp has already been decrypted into `out`, so it is dead weight holding the window shut.
475 if rp > 0 {
476 var mv: i64 = 0
477 while rp + mv < resptot { respbuf[mv] = respbuf[rp+mv]; mv = mv + 1 }
478 resptot = resptot - rp
479 rp = 0
480 }
481 // PROGRESS, NOT PASSES. A pass that produced new plaintext resets the stall budget, so an arbitrarily
482 // large body never runs out of reads; only a peer that keeps the socket open while delivering nothing
483 // decodable does. The old counter bounded the TRANSFER and called itself a timeout.
484 if plain_total > last_plain { stalls = 0; last_plain = plain_total } else { stalls = stalls + 1 }
485 if stalls > TR_MAX_STALL_PASSES { reading = 0 }
486 // TERMINATION. v1 read "no Content-Length" as "the response is complete" and stopped the instant
487 // the headers landed -- so every CHUNKED reply (Apache 2.2's default for dynamic content, which is
488 // exactly what graphis.ne.jp serves) came back as headers plus a ZERO-length body. Downstream
489 // nx_http_dechunk then dechunked 0 bytes into 0, and the fetch ladder reported that as a handshake
490 // failure, sending two sessions hunting a TLS bug that was never there. debt 1785971025.
491 // ★A 200 ALONGSIDE AN EMPTY BODY IS A PARSE BUG, NOT A TRANSPORT ONE.
492 let he: i64 = tr_hdr_end(out, plain_total)
493 if he >= 0 {
494 let cl: i64 = tr_content_length(out, plain_total)
495 if cl >= 0 { if plain_total - he >= cl { reading = 0 } }
496 else {
497 var cfrom: i64 = he - 2
498 if cfrom < 0 { cfrom = 0 }
499 if tr_is_chunked(out, he) == 1 { if tr_chunk_done(out, cfrom, plain_total) == 1 { reading = 0 } }
500 // Neither Content-Length nor chunked = close-delimited (HTTP/1.0 style): keep reading until
501 // the peer closes. The socket timeout and the rreads cap bound it in both directions.
502 }
503 }
504 if reading == 1 {
505 let room: i64 = TR_MAGIC_262144 - resptot
506 // AFTER COMPACTION THIS CAN ONLY MEAN ONE RECORD IS BIGGER THAN THE WHOLE WINDOW, which no
507 // legal TLS stream produces. It is a distinct, LOUD state -- never a quiet short read.
508 if room <= 0 { oversize = 1; reading = 0 } else {
509 let n: i64 = sys_read(fd, ((respbuf as i64)+resptot) as *u8, room)
510 if n <= 0 { reading = 0 } else { resptot = resptot + n }
511 }
512 }
513 }
514 sys_close(fd)
515 if oversize == 1 {
516 tr_puts("T12-RECORD-OVERSIZE: a single TLS record exceeds the "); tr_putn(TR_MAGIC_262144)
517 tr_puts("-byte receive window -- refusing to return a partial body\n" as *u8)
518 return 0 - TR_RECORD_OVERSIZE
519 }
520 // ---- TRUNCATION WITNESS. THE PEER ALREADY TOLD US HOW LONG THE BODY IS; NOT CHECKING IT IS WHAT MADE
521 // A CUT DOCUMENT INDISTINGUISHABLE FROM A WHOLE ONE. A short body now REFUSES with both numbers, so no
522 // caller can pin it as evidence. Only fires when a Content-Length was DECLARED: a close-delimited or
523 // chunked response is not judged here, because for those the peer never stated a length to fall short of.
524 let he_f: i64 = tr_hdr_end(out, plain_total)
525 if he_f >= 0 {
526 let cl_f: i64 = tr_content_length(out, plain_total)
527 if cl_f >= 0 {
528 if plain_total - he_f < cl_f {
529 tr_puts("T12-TRUNCATED expected="); tr_putn(cl_f)
530 tr_puts(" got="); tr_putn(plain_total - he_f)
531 tr_puts(" -- the peer declared a Content-Length this read did not reach; REFUSING to return a partial body\n" as *u8)
532 return 0 - TR_TRUNCATED
533 }
534 }
535 }
536 return plain_total
537}