code wiki / (root) / nx_tls12_req.nx

nx_tls12_req.nx source

↩ module page · 454 lines · 27438 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 { t[0]=48 as u8; k=1 } 54 while m>0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 } 55 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 56} 57func tr_u64be(d: *u8, o: i64, v: i64) -> i64 { 58 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 59 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 60 return o+8 61} 62func tr_write_all(fd: i64, buf: *u8, n: i64) -> i64 { 63 var off: i64=0 64 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 } 65 return 0 66} 67// TLS 1.2 PRF = P_SHA256(secret, label||seed) 68func tr_prf(secret: *u8, secret_len: i64, label: *u8, label_len: i64, seed: *u8, seed_len: i64, out: *u8, out_len: i64) -> i64 { 69 let ls_len: i64 = label_len + seed_len 70 let ls: *u8 = sys_mmap(ls_len + 8) 71 var i: i64 = 0 72 while i < label_len { ls[i] = label[i]; i = i + 1 } 73 i = 0 74 while i < seed_len { ls[label_len + i] = seed[i]; i = i + 1 } 75 let a: *u8 = sys_mmap(32); let a_next: *u8 = sys_mmap(32) 76 hmac_sha256(secret, secret_len, ls, ls_len, a) 77 let tmp: *u8 = sys_mmap(32 + ls_len + 8); let block: *u8 = sys_mmap(32) 78 var off: i64 = 0 79 while off < out_len { 80 var j: i64 = 0 81 while j < 32 { tmp[j] = a[j]; j = j + 1 } 82 j = 0 83 while j < ls_len { tmp[32 + j] = ls[j]; j = j + 1 } 84 hmac_sha256(secret, secret_len, tmp, 32 + ls_len, block) 85 var c: i64 = 32 86 if out_len - off < 32 { c = out_len - off } 87 j = 0 88 while j < c { out[off + j] = block[j]; j = j + 1 } 89 off = off + c 90 hmac_sha256(secret, secret_len, a, 32, a_next) 91 j = 0 92 while j < 32 { a[j] = a_next[j]; j = j + 1 } 93 } 94 return 0 95} 96func tr_client_hello(host: *u8, hlen: i64, cr_out: *u8, out: *u8) -> i64 { 97 let body: *u8 = sys_mmap(TR_MAGIC_1024); var o: i64 = 0 98 o = tr_b2(body, o, 0x0303) 99 nx_csprng_fill(cr_out, 32) 100 o = tr_bytes(body, o, cr_out, 32) 101 o = tr_b1(body, o, 0) 102 o = tr_b2(body, o, 2); o = tr_b2(body, o, 0xC02F) 103 o = tr_b1(body, o, 1); o = tr_b1(body, o, 0) 104 let ext: *u8 = sys_mmap(512); var e: i64 = 0 105 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) 106 e = tr_b2(ext, e, 0x000A); e = tr_b2(ext, e, 4); e = tr_b2(ext, e, 2); e = tr_b2(ext, e, 0x0017) 107 e = tr_b2(ext, e, 0x000B); e = tr_b2(ext, e, 2); e = tr_b1(ext, e, 1); e = tr_b1(ext, e, 0) 108 e = tr_b2(ext, e, 0x000D); e = tr_b2(ext, e, 8); e = tr_b2(ext, e, 6); e = tr_b2(ext, e, 0x0401); e = tr_b2(ext, e, 0x0501); e = tr_b2(ext, e, 0x0601) 109 o = tr_b2(body, o, e); o = tr_bytes(body, o, ext, e) 110 let body_len: i64 = o 111 var r: i64 = 0 112 r = tr_b1(out, r, TR_HS); r = tr_b2(out, r, 0x0303); r = tr_b2(out, r, body_len + 4) 113 r = tr_b1(out, r, 1); r = tr_b3(out, r, body_len); r = tr_bytes(out, r, body, body_len) 114 return r 115} 116func tr_hdr_end(b: *u8, n: i64) -> i64 { 117 var i: i64 = 0 118 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 } 119 return 0 - 1 120} 121func tr_lc(c: i64) -> i64 { if c >= 65 { if c <= 90 { return c + 32 } } return c } 122func tr_match_ci(b: *u8, i: i64, n: i64, targ: *u8, tlen: i64) -> i64 { 123 if i + tlen > n { return 0 } 124 var j: i64 = 0 125 while j < tlen { if tr_lc(b[i+j]&0xff) != (targ[j] as i64) { return 0 } j = j + 1 } 126 return 1 127} 128func tr_content_length(b: *u8, n: i64) -> i64 { 129 var i: i64 = 0 130 while i + 15 < n { 131 if tr_match_ci(b, i, n, "content-length:" as *u8, 15) == 1 { 132 var p: i64 = i + 15; var sk: i64 = 1 133 while sk == 1 { if p < n { if b[p]==(32 as u8) { p = p + 1 } else { sk = 0 } } else { sk = 0 } } 134 var val: i64 = 0; var any: i64 = 0; var go: i64 = 1 135 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 } } 136 if any == 1 { return val } 137 return 0 - 1 138 } 139 i = i + 1 140 } 141 return 0 - 1 142} 143// Does the header block declare the chunked transfer coding? Bounded to the headers so a body that merely 144// contains the word cannot flip it. 145func tr_is_chunked(b: *u8, hdr_end: i64) -> i64 { 146 var i: i64 = 0 147 while i + 7 <= hdr_end { 148 if tr_match_ci(b, i, hdr_end, "chunked" as *u8, 7) == 1 { return 1 } 149 i = i + 1 150 } 151 return 0 152} 153// Has the TERMINAL zero-length chunk arrived? Looks for CRLF "0" CRLF -- that byte run can only be the 154// last-chunk marker, because every other chunk-size line is hex with a non-CR byte following the digit 155// (a size of 0x0a reads "0a\r\n", so b[i+3] is 'a', not CR). 156func tr_chunk_done(b: *u8, from: i64, n: i64) -> i64 { 157 var i: i64 = from 158 while i + 5 <= n { 159 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 } } } } } 160 i = i + 1 161 } 162 return 0 163} 164// build request-line + headers (+ body) into out; returns length 165func 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 { 166 var o: i64 = 0 167 o = tr_bytes(out, o, method, mlen); out[o]=0x20 as u8; o=o+1 168 o = tr_bytes(out, o, target, tlen) 169 o = tr_cat(out, o, " HTTP/1.1\r\nHost: " as *u8) 170 o = tr_bytes(out, o, host, hlen) 171 if tn > 0 { o = tr_cat(out, o, "\r\nAuthorization: Bearer " as *u8); o = tr_bytes(out, o, token, tn) } 172 if blen > 0 { o = tr_cat(out, o, "\r\nContent-Type: application/json\r\nContent-Length: " as *u8); o = tr_catn(out, o, blen) } 173 o = tr_cat(out, o, "\r\nUser-Agent: nishi-tls12\r\nConnection: close\r\n\r\n" as *u8) 174 if blen > 0 { o = tr_bytes(out, o, body, blen) } 175 return o 176} 177 178// Reformat a TLS-1.2 Certificate message body ([3B list_len][ {3B certlen}{DER} ...]) into a TLS-1.3 Certificate 179// message ([1B ctx=0][3B list_len][ {3B certlen}{DER}{2B ext_len=0} ...]) so the shipped 1.3 chain+hostname+validity 180// pipeline (nx_https_cert_pipeline_verify_with_store) validates it unchanged. Returns the 1.3 message length. 181func tr_certs12_to_13(body12: *u8, len12: i64, out13: *u8) -> i64 { 182 var o: i64 = 0 183 out13[o]=11 as u8; o=o+1 // HT_CERTIFICATE handshake type (the parser checks this) 184 let hdrpos: i64 = o 185 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 186 out13[o]=0 as u8; o=o+1 // certificate_request_context length = 0 187 let listpos: i64 = o 188 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 189 var i: i64 = 3 // skip the 1.2 cert-list's own 3-byte length 190 while i + 3 <= len12 { 191 let cl: i64 = ((body12[i]&0xff)<<16)|((body12[i+1]&0xff)<<8)|(body12[i+2]&0xff) 192 if cl <= 0 { i = len12 } else { 193 out13[o]=body12[i]; out13[o+1]=body12[i+1]; out13[o+2]=body12[i+2]; o=o+3 194 var z: i64=0; while z<cl { out13[o+z]=body12[i+3+z]; z=z+1 } o=o+cl 195 out13[o]=0 as u8; out13[o+1]=0 as u8; o=o+2 // per-cert extensions length = 0 196 i = i + 3 + cl 197 } 198 } 199 let ll: i64 = o - (listpos+3) // certificate_list content length 200 out13[listpos]=((ll>>16)&0xff) as u8; out13[listpos+1]=((ll>>8)&0xff) as u8; out13[listpos+2]=(ll&0xff) as u8 201 let total: i64 = o - (hdrpos+3) // handshake-message length = everything after the 4B header 202 out13[hdrpos]=((total>>16)&0xff) as u8; out13[hdrpos+1]=((total>>8)&0xff) as u8; out13[hdrpos+2]=(total&0xff) as u8 203 return o 204} 205 206// Verify the ServerKeyExchange signature: the server signs SHA256(client_random || server_random || ecdhe_params) 207// with its certificate's private key; we verify with the leaf cert's PUBLIC key. This BINDS the ECDHE key we're 208// about to use to the (already chain-validated) cert -- closing the active-MITM gap (an attacker replaying a valid 209// cert with its own ECDHE key can't produce this signature). We only offer ECDHE_RSA (0xC02F) so the signature is 210// always RSA-PKCS1. Returns 1 on valid; 2=cert-parse 3=pubkey-extract 4=SIG-INVALID 5=malformed; 1000+alg=unsupported. 211func tr_verify_ske_sig(cr: *u8, sr: *u8, ske: *u8, ske_len: i64, leaf_der: *u8, leaf_len: i64) -> i64 { 212 if ske_len < 8 { return 5 } 213 let pubkey_len: i64 = ske[3] & 0xff 214 let params_len: i64 = 4 + pubkey_len // curve_type(1)+named_curve(2)+pklen(1)+pubkey 215 if params_len + 4 > ske_len { return 5 } 216 let sig_alg: i64 = ((ske[params_len]&0xff)<<8)|(ske[params_len+1]&0xff) 217 let sig_len: i64 = ((ske[params_len+2]&0xff)<<8)|(ske[params_len+3]&0xff) 218 let sig_ptr: *u8 = ((ske as i64) + params_len + 4) as *u8 219 if params_len + 4 + sig_len > ske_len { return 5 } 220 if sig_alg != 0x0401 { return 1000 + sig_alg } // only rsa_pkcs1_sha256 wired (fail-closed for others) 221 // tbs = client_random(32) || server_random(32) || ecdhe_params(params_len) 222 let tbs: *u8 = sys_mmap(512); var o: i64 = 0 223 var z: i64 = 0; while z < 32 { tbs[o]=cr[z]; o=o+1; z=z+1 } 224 z = 0; while z < 32 { tbs[o]=sr[z]; o=o+1; z=z+1 } 225 z = 0; while z < params_len { tbs[o]=ske[z]; o=o+1; z=z+1 } 226 let leaf_raw: *u8 = sys_mmap(256); let leaf: *X509Cert = leaf_raw as *X509Cert 227 if x509_parse(leaf_der, leaf_len, leaf) < 0 { return 2 } 228 if sig_len == 256 { 229 let n: *i64 = u2048_alloc(); let e_p: *i64 = sys_mmap(16) as *i64 230 if nx_x509_pubkey_extract_rsa(leaf_der, leaf, n, e_p) != NX_X509_PUBKEY_RSA_OK { return 3 } 231 if nx_x509_verify_rsa_pkcs1_sha256(tbs, o, sig_ptr, sig_len, n, *e_p) == NX_X509_RSA_PKCS1_SHA256_OK { return 1 } 232 return 4 233 } 234 if sig_len == 512 { 235 let n4: *i64 = u4096_alloc(); let e4: *i64 = sys_mmap(16) as *i64 236 if nx_x509_pubkey_extract_rsa_4096(leaf_der, leaf, n4, e4) != NX_X509_PUBKEY_RSA_4096_OK { return 3 } 237 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 } 238 return 4 239 } 240 return 5 241} 242 243// THE reusable request. full_url = "https://host/path?query". method/token/body optional (token/body=0 to omit). 244// Fills `out` with the plaintext HTTP response (status line + headers + body); returns its length, or <0 on error: 245// -1 bad url -2 connect -3 handshake(no server random/pubkey) -4 ECDHE -5 request write 246const TR_READ_TIMEOUT_SECS: i64 = 20 // declared: per-read ceiling, so a stalling peer fails fast 247func t12_request(full_url: *u8, method: *u8, mlen: i64, token: *u8, tn: i64, body: *u8, blen: i64, out: *u8, outcap: i64) -> i64 { 248 let target_raw: *u8 = sys_mmap(64); let target: *NxHttpsTarget = target_raw as *NxHttpsTarget 249 target.url = nx_url_new(); target.port = 0 250 if nx_https_url_for_fetch(full_url, target) != NX_HTTPS_URL_OK { return 0 - 1 } 251 let host: *u8 = ((full_url as i64) + target.url.host_off) as *u8 252 let hlen: i64 = target.url.host_len 253 // request-target = everything in the URL AFTER the host (path + ?query) -- NOT the parsed path (drops query) 254 let reqtgt: *u8 = ((full_url as i64) + target.url.host_off + hlen) as *u8 255 let reqtlen: i64 = tr_slen(reqtgt) 256 let now: i64 = sys_now_realtime_sec() 257 let fd_p: *i64 = sys_mmap(16) as *i64 258 if nx_https_url_connect(target, full_url, now, fd_p) != NX_HTTPS_CONNECT_OK { return 0 - 2 } 259 let fd: i64 = fd_p[0] 260 // Bound every read. Required now that a close-delimited response legitimately reads until EOF: without 261 // this, a keep-alive peer that sends neither Content-Length nor a terminal chunk would block forever. 262 sys_set_socket_timeout(fd, TR_READ_TIMEOUT_SECS) 263 264 let tr: *u8 = sys_mmap(TR_MAGIC_16384); var trlen: i64 = 0 265 let cr: *u8 = sys_mmap(32); let ch: *u8 = sys_mmap(TR_MAGIC_2048) 266 let chlen: i64 = tr_client_hello(host, hlen, cr, ch) 267 if tr_write_all(fd, ch, chlen) < 0 { sys_close(fd); return 0 - 5 } 268 trlen = tr_bytes(tr, trlen, ((ch as i64)+5) as *u8, chlen-5) 269 270 let buf: *u8 = sys_mmap(TR_MAGIC_65536); var total: i64 = 0 271 let sr: *u8 = sys_mmap(32); let spub: *u8 = sys_mmap(65) 272 let cert12: *u8 = sys_mmap(TR_MAGIC_32768); var cert12_len: i64 = 0; var got_cert: i64 = 0 273 let ske_body: *u8 = sys_mmap(TR_MAGIC_4096); var ske_len: i64 = 0 274 var got_sr: i64 = 0; var got_spub: i64 = 0; var got_shd: i64 = 0; var reads: i64 = 0 275 while got_shd == 0 { 276 if reads > 40 { got_shd = 2 } 277 let n: i64 = sys_read(fd, ((buf as i64)+total) as *u8, TR_MAGIC_65536-total) 278 if n <= 0 { got_shd = 2 } else { 279 total = total + n; reads = reads + 1 280 var p: i64 = 0 281 while p + 5 <= total { 282 let rtype: i64 = buf[p]&0xff; let rlen: i64 = ((buf[p+3]&0xff)<<8)|(buf[p+4]&0xff) 283 if p+5+rlen > total { p = total } else { 284 if rtype == TR_HS { 285 var hp: i64 = p+5; let rend: i64 = p+5+rlen 286 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 } 287 } 288 p = p + 5 + rlen 289 } 290 } 291 } 292 } 293 var p: i64 = 0 294 while p + 5 <= total { 295 let rtype: i64 = buf[p]&0xff; let rlen: i64 = ((buf[p+3]&0xff)<<8)|(buf[p+4]&0xff) 296 if rtype == TR_HS { 297 trlen = tr_bytes(tr, trlen, ((buf as i64)+p+5) as *u8, rlen) 298 var hp: i64 = p+5; let rend: i64 = p+5+rlen 299 while hp + 4 <= rend { 300 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 301 if ht == 2 { var z: i64=0; while z<32 { sr[z]=buf[bp+2+z]; z=z+1 } got_sr=1 } 302 if ht == 11 { var z: i64=0; while z<hl { cert12[z]=buf[bp+z]; z=z+1 } cert12_len=hl; got_cert=1 } 303 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 } 304 hp = hp + 4 + hl 305 } 306 } 307 p = p + 5 + rlen 308 } 309 if got_sr==0 { sys_close(fd); return 0 - 3 } 310 if got_spub==0 { sys_close(fd); return 0 - 3 } 311 if got_cert==0 { sys_close(fd); return 0 - 6 } 312 // ---- CERT VALIDATION: chain (to Mozilla roots) + hostname + validity, via the shipped 1.3 pipeline after a 313 // 1.2->1.3 reformat. FAIL-CLOSED: a cert that doesn't chain to a trusted root / doesn't cover the host / is 314 // expired ABORTS the connection (return -8) before we send the Bearer token. NOTE(residual): the 315 // ServerKeyExchange SIGNATURE (which binds the ECDHE key to this cert) is not yet verified -- so this stops 316 // self-signed / wrong-host / expired certs but not a sophisticated active MITM that replays a valid cert with 317 // its own ECDHE key. That SKE-sig check (RSA/ECDSA over client_random||server_random||params) is the final step. 318 let store_r: i64 = nx_trust_store_load_from_certdata("data/mozilla_certdata.txt" as *u8, 512, TR_MAGIC_4194304) 319 if store_r <= 0 { sys_close(fd); return 0 - 7 } 320 let store: *TrustStore = store_r as *TrustStore 321 let c13: *u8 = sys_mmap(TR_MAGIC_40960) 322 let c13len: i64 = tr_certs12_to_13(cert12, cert12_len, c13) 323 let cv: i64 = nx_https_cert_pipeline_verify_with_store(c13, c13len, host, hlen, now, store) 324 if cv != NX_HTTPS_PIPELINE_OK { sys_close(fd); return 0 - 200 - cv } // encode pipeline verdict for diagnosis 325 // ---- SKE-SIGNATURE verify: bind the ECDHE key to the validated leaf cert (closes the active-MITM gap) ---- 326 let leaf_len: i64 = ((cert12[3]&0xff)<<16)|((cert12[4]&0xff)<<8)|(cert12[5]&0xff) 327 let leaf_der: *u8 = ((cert12 as i64)+6) as *u8 328 let sv: i64 = tr_verify_ske_sig(cr, sr, ske_body, ske_len, leaf_der, leaf_len) 329 if sv != 1 { sys_close(fd); return 0 - 400 - sv } // encode SKE-verify verdict for diagnosis 330 331 let seed: *u8 = sys_mmap(32); nx_csprng_fill(seed, 32) 332 let mypriv: *u8 = sys_mmap(32) 333 if p256_ecdh_derive_priv(seed, mypriv) != NX_P256_ECDH_OK { sys_close(fd); return 0 - 4 } 334 let mypub: *u8 = sys_mmap(65) 335 if p256_ecdh_pub(mypriv, mypub) != NX_P256_ECDH_OK { sys_close(fd); return 0 - 4 } 336 let premaster: *u8 = sys_mmap(32) 337 if p256_ecdh_shared(mypriv, spub, 65, premaster) != NX_P256_ECDH_OK { sys_close(fd); return 0 - 4 } 338 339 let crsr: *u8 = sys_mmap(64); tr_bytes(crsr, 0, cr, 32); tr_bytes(crsr, 32, sr, 32) 340 let srcr: *u8 = sys_mmap(64); tr_bytes(srcr, 0, sr, 32); tr_bytes(srcr, 32, cr, 32) 341 let master: *u8 = sys_mmap(48); tr_prf(premaster, 32, "master secret" as *u8, 13, crsr, 64, master, 48) 342 let kb: *u8 = sys_mmap(40); tr_prf(master, 48, "key expansion" as *u8, 13, srcr, 64, kb, 40) 343 let cwk: *u8 = ((kb as i64)+0) as *u8 344 let swk: *u8 = ((kb as i64)+16) as *u8 345 let civ: *u8 = ((kb as i64)+32) as *u8 346 let siv: *u8 = ((kb as i64)+36) as *u8 347 348 let cke: *u8 = sys_mmap(128); var ck: i64 = 0 349 ck = tr_b1(cke, ck, TR_HS); ck = tr_b2(cke, ck, 0x0303); ck = tr_b2(cke, ck, 4 + 66) 350 let cke_hs: i64 = ck 351 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) 352 if tr_write_all(fd, cke, ck) < 0 { sys_close(fd); return 0 - 5 } 353 trlen = tr_bytes(tr, trlen, ((cke as i64)+cke_hs) as *u8, ck - cke_hs) 354 355 let ccs: *u8 = sys_mmap(8); var cc: i64=0 356 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) 357 if tr_write_all(fd, ccs, cc) < 0 { sys_close(fd); return 0 - 5 } 358 359 let thash: *u8 = sys_mmap(32); sha256_digest(tr, trlen, thash) 360 let cvd: *u8 = sys_mmap(12); tr_prf(master, 48, "client finished" as *u8, 15, thash, 32, cvd, 12) 361 let fin: *u8 = sys_mmap(16); var fo: i64=0 362 fo = tr_b1(fin, fo, 20); fo = tr_b3(fin, fo, 12); fo = tr_bytes(fin, fo, cvd, 12) 363 trlen = tr_bytes(tr, trlen, fin, 16) 364 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 } 365 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 366 let fct: *u8 = sys_mmap(16); let ftag: *u8 = sys_mmap(16) 367 nx_aes128_gcm_seal(cwk, nonce, aad, 13, fin, 16, fct, ftag) 368 let frec: *u8 = sys_mmap(64); var fr: i64=0 369 fr = tr_b1(frec, fr, TR_HS); fr = tr_b2(frec, fr, 0x0303); fr = tr_b2(frec, fr, 8+16+16) 370 q=0; while q<8 { frec[fr]=0 as u8; fr=fr+1; q=q+1 } 371 fr = tr_bytes(frec, fr, fct, 16); fr = tr_bytes(frec, fr, ftag, 16) 372 if tr_write_all(fd, frec, fr) < 0 { sys_close(fd); return 0 - 5 } 373 374 // drain server CCS + Finished (don't strictly verify here; the request+response GCM auth already proves keys) 375 let rbuf: *u8 = sys_mmap(TR_MAGIC_16384); var rtot: i64 = 0; var rr: i64 = 0; var server_done: i64 = 0 376 while server_done == 0 { 377 if rr > 30 { server_done = 2 } 378 let n: i64 = sys_read(fd, ((rbuf as i64)+rtot) as *u8, TR_MAGIC_16384-rtot) 379 if n <= 0 { server_done = 2 } else { 380 rtot = rtot + n; rr = rr + 1 381 var pp: i64 = 0 382 while pp + 5 <= rtot { 383 let rtype: i64 = rbuf[pp]&0xff; let rlen: i64 = ((rbuf[pp+3]&0xff)<<8)|(rbuf[pp+4]&0xff) 384 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 } 385 } 386 } 387 } 388 389 // send the application request (client app seq = 1: Finished was seq 0) 390 let reqb: *u8 = sys_mmap(TR_MAGIC_8192) 391 let rq: i64 = tr_http_req(method, mlen, reqtgt, reqtlen, host, hlen, token, tn, body, blen, reqb) 392 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) 393 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 394 let act: *u8 = sys_mmap(TR_MAGIC_8192); let atag: *u8 = sys_mmap(16) 395 nx_aes128_gcm_seal(cwk, anonce, aaad, 13, reqb, rq, act, atag) 396 let arec: *u8 = sys_mmap(TR_MAGIC_8300); var ar: i64=0 397 ar = tr_b1(arec, ar, 23); ar = tr_b2(arec, ar, 0x0303); ar = tr_b2(arec, ar, 8+rq+16) 398 ar = tr_u64be(arec, ar, 1); ar = tr_bytes(arec, ar, act, rq); ar = tr_bytes(arec, ar, atag, 16) 399 if tr_write_all(fd, arec, ar) < 0 { sys_close(fd); return 0 - 5 } 400 401 // read + decrypt response app-data (server app seq = 1); stop on complete HTTP response (Content-Length) or close 402 let respbuf: *u8 = sys_mmap(TR_MAGIC_262144); var resptot: i64 = 0 403 var rp: i64 = 0; var rseq: i64 = 1; var plain_total: i64 = 0 404 var reading: i64 = 1; var rreads: i64 = 0 405 while reading == 1 { 406 var more: i64 = 1 407 while more == 1 { 408 if rp + 5 > resptot { more = 0 } else { 409 let rt: i64 = respbuf[rp]&0xff; let rl: i64 = ((respbuf[rp+3]&0xff)<<8)|(respbuf[rp+4]&0xff) 410 if rp+5+rl > resptot { more = 0 } else { 411 if rt == 21 { reading = 0 } 412 if rt == 23 { 413 let pl: i64 = rl - 8 - 16 414 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 } 415 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 416 let dtag: *u8 = sys_mmap(16); dz=0; while dz<16 { dtag[dz]=respbuf[rp+5+8+pl+dz]; dz=dz+1 } 417 let dpt: *u8 = sys_mmap(TR_MAGIC_65536) 418 if nx_aes128_gcm_open(swk, dn, dad, 13, ((respbuf as i64)+rp+5+8) as *u8, pl, dtag, dpt) == 0 { 419 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 420 } 421 rseq = rseq + 1 422 } 423 rp = rp + 5 + rl 424 } 425 } 426 } 427 // TERMINATION. v1 read "no Content-Length" as "the response is complete" and stopped the instant 428 // the headers landed -- so every CHUNKED reply (Apache 2.2's default for dynamic content, which is 429 // exactly what graphis.ne.jp serves) came back as headers plus a ZERO-length body. Downstream 430 // nx_http_dechunk then dechunked 0 bytes into 0, and the fetch ladder reported that as a handshake 431 // failure, sending two sessions hunting a TLS bug that was never there. debt 1785971025. 432 // ★A 200 ALONGSIDE AN EMPTY BODY IS A PARSE BUG, NOT A TRANSPORT ONE. 433 let he: i64 = tr_hdr_end(out, plain_total) 434 if he >= 0 { 435 let cl: i64 = tr_content_length(out, plain_total) 436 if cl >= 0 { if plain_total - he >= cl { reading = 0 } } 437 else { 438 var cfrom: i64 = he - 2 439 if cfrom < 0 { cfrom = 0 } 440 if tr_is_chunked(out, he) == 1 { if tr_chunk_done(out, cfrom, plain_total) == 1 { reading = 0 } } 441 // Neither Content-Length nor chunked = close-delimited (HTTP/1.0 style): keep reading until 442 // the peer closes. The socket timeout and the rreads cap bound it in both directions. 443 } 444 } 445 if reading == 1 { 446 rreads = rreads + 1 447 if rreads > 60 { reading = 0 } 448 let n: i64 = sys_read(fd, ((respbuf as i64)+resptot) as *u8, TR_MAGIC_262144-resptot) 449 if n <= 0 { reading = 0 } else { resptot = resptot + n } 450 } 451 } 452 sys_close(fd) 453 return plain_total 454}