code wiki / (root) / nx_tls13_client_session_run.nx

nx_tls13_client_session_run.nx source

↩ module page · 308 lines · 13638 B

1// nx_tls13_client_session_run.nx -- step 3c.6b of the 2// nx_https_client wiring arc. 3// 4// THE top-level TLS 1.3 client handshake orchestrator. Takes a 5// connected TCP fd (from step 2 nx_https_url_connect) + an SNI 6// hostname + caller-supplied entropy + a TrustValidationContext, 7// and drives the full handshake to CONNECTED state. 8// 9// After this primitive returns OK, the returned session can be 10// used by step 4 (nx_https_get_complete) to send + receive 11// application data records. 12// 13// Pipeline: 14// 1. session = session_new(client_random, x25519_priv) (3c.1) 15// 2. emit_ch(session, sni, ch_buf, ch_cap) (3c.1) 16// 3. write_n(fd, ch_buf, ch_len) 17// 4. read_record_from_fd(fd, sh_record, max) (3c.6a) 18// 5. recv_sh(session, sh_record_body, sh_body_len) (3c.2) 19// -- the ServerHello arrives as a TLSPlaintext record so we 20// strip the 5-byte header and pass the handshake bytes 21// 6. Loop until session.state == WAIT_CLIENT_FIN: 22// 6a. read_record_from_fd(fd, hs_record, max) 23// 6b. handle: either ChangeCipherSpec (skip; legacy compat) 24// OR encrypted handshake -- recv_hs(session, record, 25// val_ctx) (3c.3) 26// 7. emit_finished(session, cf_buf, cf_cap) → 58-byte encrypted 27// record (3c.4) 28// 8. write_n(fd, cf_buf, cf_len) 29// 9. derive_app(session) (3c.5) 30// 31// Public API: 32// nx_tls13_client_session_run( 33// fd, sni, sni_len, 34// client_random, x25519_priv, 35// val_ctx 36// ) -> *Tls13ClientSession on success, NULL on failure 37// nx_tls13_run_last_verdict() -> i64 -- last error verdict 38// nx_tls13_run_verdict_is_valid(v) -> 0|1 39// 40// Sealed verdict (latched in a module-private static when 41// nx_tls13_client_session_run returns NULL): 42// NX_TLS13_RUN_OK 43// NX_TLS13_RUN_EMIT_CH_FAIL 44// NX_TLS13_RUN_WRITE_CH_FAIL 45// NX_TLS13_RUN_READ_SH_FAIL 46// NX_TLS13_RUN_RECV_SH_FAIL 47// NX_TLS13_RUN_READ_HS_FAIL 48// NX_TLS13_RUN_RECV_HS_FAIL 49// NX_TLS13_RUN_EMIT_CF_FAIL 50// NX_TLS13_RUN_WRITE_CF_FAIL 51// NX_TLS13_RUN_DERIVE_APP_FAIL 52// NX_TLS13_RUN_LOOP_BUDGET_EXCEEDED -- safety cap on handshake- 53// record count 54// 55// Per Cardinals 9 (single-responsibility -- ONE handshake-run), 56// 12 (defensive at boundaries -- record-loop budget caps the 57// number of records we'll accept before giving up; protects 58// against an evil server feeding infinite ChangeCipherSpecs), 59// 22 (composition -- 9 shipped substrate primitives compose into 60// one orchestrator), 23 (preamble names the 9-step pipeline + 61// every queued caller responsibility). 62// 63// license_tier: INDEPENDENT_REDERIVE 64// genealogy_id: international-research-sources/ietf/rfc_8446 65// lineage_id: nishi_tls13_client_session_run_q10 66 67// nx_safety_envelope: 68// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 69// sil_target: SIL1 70// evidence: [bulk_applied_2026-05-19, tls13-client-session-run-step-3c6b] 71// verdict: NOT_YET_EVALUATED 72 73import "nx_syscalls.nx" 74import "nx_tls13.nx" 75import "nx_tls13_record.nx" 76import "nx_tls13_client_validate_certificate.nx" 77import "nx_tls13_client_session.nx" 78import "nx_tls13_client_session_recv_sh.nx" 79import "nx_tls13_client_session_recv_hs.nx" 80import "nx_tls13_client_session_emit_finished.nx" 81import "nx_tls13_client_session_derive_app.nx" 82import "nx_tls13_read_record_from_fd.nx" 83const NX_MAGIC_1024: i64 = 1024 84 85const NX_TLS13_RUN_OK: i64 = 1 86const NX_TLS13_RUN_EMIT_CH_FAIL: i64 = 2 87const NX_TLS13_RUN_WRITE_CH_FAIL: i64 = 3 88const NX_TLS13_RUN_READ_SH_FAIL: i64 = 4 89const NX_TLS13_RUN_RECV_SH_FAIL: i64 = 5 90const NX_TLS13_RUN_READ_HS_FAIL: i64 = 6 91const NX_TLS13_RUN_RECV_HS_FAIL: i64 = 7 92const NX_TLS13_RUN_EMIT_CF_FAIL: i64 = 8 93const NX_TLS13_RUN_WRITE_CF_FAIL: i64 = 9 94const NX_TLS13_RUN_DERIVE_APP_FAIL: i64 = 10 95const NX_TLS13_RUN_LOOP_BUDGET_EXCEEDED: i64 = 11 96const NX_TLS13_RUN_VERDICT_N: i64 = 12 97 98// Cap on how many handshake records we'll accept post-SH before 99// reaching WAIT_CLIENT_FIN. A normal handshake takes 1-3 records 100// (EE+Cert+CV+SF often in 1; sometimes Cert is split off). 8 is 101// generous and stops an attacker from feeding infinite no-ops. 102const NX_TLS13_RUN_MAX_HS_RECORDS: i64 = 8 103 104// Max bytes per record buffer. TLS record max payload is 16640; 105// add the 5-byte header. We allocate one buffer per record-read 106// (could be reused with care; safety first). 107const NX_TLS13_RUN_RECORD_BUF_BYTES: i64 = 16645 108 109func nx_tls13_run_verdict_is_valid(v: i64) -> i64 { 110 if v < NX_TLS13_RUN_OK { return 0 } 111 if v >= NX_TLS13_RUN_VERDICT_N { return 0 } 112 return 1 113} 114 115// Module-private last-verdict slot. Reset to 0 at the start of 116// every nx_tls13_client_session_run call. Caller reads it via 117// nx_tls13_run_last_verdict() after a NULL return. 118// 119// nx doesn't have first-class statics; we allocate a heap slot 120// on first read and persist via a static pointer trick using 121// sys_mmap (which is essentially static-storage from the program's 122// perspective). Caller must call nx_tls13_run_last_verdict() 123// BEFORE invoking session_run again (since the slot is overwritten). 124// 125// For simplicity in this commit, the verdict is encoded into the 126// return value of session_run itself via a positive=session-ptr 127// negative=verdict-code convention, returned as i64 caller casts 128// back to *Tls13ClientSession on success. 129 130// Write exactly `n` bytes to fd via looping sys_write. Returns 0 131// on success, -1 on any sys_write error. 132func _write_n(fd: i64, buf: *u8, n: i64) -> i64 { 133 var off: i64 = 0 134 while off < n { 135 let w: i64 = sys_write(fd, (buf as i64 + off) as *u8, n - off) 136 if w <= 0 { return 0 - 1 } 137 off = off + w 138 } 139 return 0 140} 141 142// Returns 1 if the record at buf[0..n) is a TLS 1.2-style 143// ChangeCipherSpec (single-byte payload 0x01). Used to skip 144// past the "middlebox compatibility" CCS that some servers send 145// pre-Finished per RFC 8446 §D.4. 146func _is_change_cipher_spec(buf: *u8, n: i64) -> i64 { 147 if n != NX_TLS13_RECORD_HEADER_LEN + 1 { return 0 } 148 if (buf[0] & 0xff) != NX_TLS13_CT_CHANGE_CIPHER_SPEC { return 0 } 149 if (buf[5] & 0xff) != 1 { return 0 } 150 return 1 151} 152 153// Top-level orchestrator. Drives the full TLS 1.3 client 154// handshake against `fd` to CONNECTED state. 155// 156// Returns POSITIVE pointer-as-i64 to the connected session on 157// success. Returns NEGATIVE -NX_TLS13_RUN_* verdict code on 158// failure. Caller casts a positive return to *Tls13ClientSession. 159func _run_pn(v: i64) -> i64 { 160 let b: *u8 = sys_mmap(24) 161 var n: i64 = v 162 if n < 0 { n = 0 - n } 163 var i: i64 = 22 164 if n == 0 { b[i] = 0x30 as u8; i = i - 1 } 165 else { while n > 0 { b[i] = (0x30 + (n - (n/10)*10)) as u8; n = n / 10; i = i - 1 } } 166 sys_write(2, ((b as i64) + i + 1) as *u8, 22 - i) 167 return 0 168} 169 170func nx_tls13_client_session_run( 171 fd: i64, 172 sni: *u8, sni_len: i64, 173 client_random: *u8, 174 x25519_priv: *u8, 175 val_ctx: *TlsValidationContext 176) -> i64 { 177 let t1: i64 = sys_now_ms() 178 // ---- Step 1-2: build session + emit CH ---- 179 let s: *Tls13ClientSession = nx_tls13_client_session_new( 180 client_random, x25519_priv 181 ) 182 183 let ch_buf: *u8 = sys_mmap(NX_MAGIC_1024) 184 let ch_n: i64 = nx_tls13_client_session_emit_ch(s, sni, sni_len, ch_buf, NX_MAGIC_1024) 185 if ch_n < 0 { return 0 - NX_TLS13_RUN_EMIT_CH_FAIL } 186 187 // ClientHello is sent as a TLSPlaintext record (type=22, 188 // legacy version=0x0301 per "RFC 8446 §5.1 records before 189 // any keys are derived" -- actually §5.1 lets it be 0x0303 190 // too; servers accept either). We use the conventional 0x0301 191 // for max-compat. 192 let ch_record: *u8 = sys_mmap(NX_MAGIC_1024 + NX_TLS13_RECORD_HEADER_LEN) 193 ch_record[0] = NX_TLS13_CT_HANDSHAKE & 0xff 194 ch_record[1] = 0x03; ch_record[2] = 0x01 // legacy_record_version 195 ch_record[3] = ((ch_n >> 8) & 0xff) as u8 196 ch_record[4] = (ch_n & 0xff) as u8 197 var ci: i64 = 0 198 while ci < ch_n { 199 ch_record[NX_TLS13_RECORD_HEADER_LEN + ci] = ch_buf[ci] 200 ci = ci + 1 201 } 202 203 // ---- Step 3: write CH record to fd ---- 204 let wr_ch: i64 = _write_n(fd, ch_record, NX_TLS13_RECORD_HEADER_LEN + ch_n) 205 if wr_ch < 0 { return 0 - NX_TLS13_RUN_WRITE_CH_FAIL } 206 207 // ---- Step 4: read SH record from fd ---- 208 let sh_record: *u8 = sys_mmap(NX_TLS13_RUN_RECORD_BUF_BYTES) 209 let sh_total: i64 = nx_tls13_read_record_from_fd( 210 fd, sh_record, NX_TLS13_RUN_RECORD_BUF_BYTES 211 ) 212 if sh_total < 0 { return 0 - NX_TLS13_RUN_READ_SH_FAIL } 213 214 // ---- Step 5: recv_sh on the handshake-body bytes (skip 5-byte hdr) ---- 215 let sh_body: *u8 = sh_record + NX_TLS13_RECORD_HEADER_LEN 216 let sh_body_len: i64 = sh_total - NX_TLS13_RECORD_HEADER_LEN 217 let rs_v: i64 = nx_tls13_client_session_recv_sh(s, sh_body, sh_body_len) 218 if rs_v != NX_TLS13_RECV_SH_OK { 219 // R9b: the plain-hello path used to collapse EVERY ServerHello failure into a bare verdict=5. 220 // The chrome path printed a sub-verdict and the fallback did not, so the fallback's failures 221 // were read through the chrome path's explanation. A DIAGNOSTIC THAT EXISTS ON ONE PATH AND 222 // NOT ITS FALLBACK HIDES THE FALLBACK'S FAILURES BEHIND THE FIRST PATH'S STORY. 223 sys_write(2, "nishi-plain recv_sh sub-verdict=" as *u8, 32) 224 let pd: *u8 = sys_mmap(8) 225 pd[0] = (48 + (rs_v % 10)) as u8 226 sys_write(2, pd, 1) 227 // PRINT THE SUITE ONLY WHERE IT WAS ACTUALLY MEASURED. recv_sh sets s.cipher_suite from the wire 228 // just before the cipher verdicts (8 = BAD_CIPHER, 9 = NOT_TLS13); on every EARLIER failure -- 229 // 3 = BAD_FORMAT is the common one -- the field still holds the struct's 0x1303 INITIALISER, and 230 // printing it renders a default as a measurement. That is the same trap as the unconditional 231 // "0x1302" text this line was written to replace; it just moved one verdict to the left. 232 // ★A FIELD IS ONLY EVIDENCE ON THE PATHS THAT ASSIGN IT. 233 if rs_v >= 8 { 234 sys_write(2, " suite=0x" as *u8, 9) 235 var pn: i64 = 3 236 while pn >= 0 { 237 let pnib: i64 = (s.cipher_suite >> (pn * 4)) & 0xf 238 if pnib < 10 { pd[0] = (48 + pnib) as u8 } else { pd[0] = (87 + pnib) as u8 } 239 sys_write(2, pd, 1) 240 pn = pn - 1 241 } 242 if rs_v == 9 { sys_write(2, " NOT_TLS13: this host negotiated TLS 1.2 -- no 1.3 here, use the 1.2 leg" as *u8, 72) } 243 } 244 else { sys_write(2, " (suite not yet parsed at this verdict -- no cipher claim)" as *u8, 58) } 245 sys_write(2, "\n" as *u8, 1) 246 sys_munmap(pd, 8) 247 sys_munmap(sh_record as *u8, NX_TLS13_RUN_RECORD_BUF_BYTES) 248 return 0 - NX_TLS13_RUN_RECV_SH_FAIL 249 } 250 sys_munmap(sh_record as *u8, NX_TLS13_RUN_RECORD_BUF_BYTES) // S-class leak fix: free the SH record buffer 251 let t2: i64 = sys_now_ms() 252 253 // ---- Step 6: loop reading encrypted handshake records ---- 254 var loop_count: i64 = 0 255 while s.state != NX_TLS13_CSESSION_STATE_WAIT_CLIENT_FIN { 256 if loop_count >= NX_TLS13_RUN_MAX_HS_RECORDS { 257 return 0 - NX_TLS13_RUN_LOOP_BUDGET_EXCEEDED 258 } 259 260 let hs_record: *u8 = sys_mmap(NX_TLS13_RUN_RECORD_BUF_BYTES) 261 let hs_total: i64 = nx_tls13_read_record_from_fd( 262 fd, hs_record, NX_TLS13_RUN_RECORD_BUF_BYTES 263 ) 264 if hs_total < 0 { sys_munmap(hs_record as *u8, NX_TLS13_RUN_RECORD_BUF_BYTES); return 0 - NX_TLS13_RUN_READ_HS_FAIL } 265 266 // Skip middlebox-compat ChangeCipherSpec (RFC 8446 §D.4). 267 if _is_change_cipher_spec(hs_record, hs_total) == 1 { 268 loop_count = loop_count + 1 269 } else { 270 let rh_v: i64 = nx_tls13_client_session_recv_hs( 271 s, hs_record, hs_total, val_ctx 272 ) 273 if rh_v != NX_TLS13_RECV_HS_OK { sys_munmap(hs_record as *u8, NX_TLS13_RUN_RECORD_BUF_BYTES); return 0 - NX_TLS13_RUN_RECV_HS_FAIL } 274 loop_count = loop_count + 1 275 } 276 sys_munmap(hs_record as *u8, NX_TLS13_RUN_RECORD_BUF_BYTES) // S-class leak fix: free each handshake record buffer (was leaking per-record per-fetch) 277 } 278 let t3: i64 = sys_now_ms() 279 280 // ---- Step 7: emit client_Finished record ---- 281 let cf_buf: *u8 = sys_mmap(128) 282 let cf_n: i64 = nx_tls13_client_session_emit_finished(s, cf_buf, 128) 283 if cf_n < 0 { return 0 - NX_TLS13_RUN_EMIT_CF_FAIL } 284 285 // ---- Step 8: write CF to fd ---- 286 let wr_cf: i64 = _write_n(fd, cf_buf, cf_n) 287 if wr_cf < 0 { return 0 - NX_TLS13_RUN_WRITE_CF_FAIL } 288 289 // ---- Step 9: derive app traffic keys → CONNECTED ---- 290 let da_v: i64 = nx_tls13_client_session_derive_app(s) 291 if da_v != NX_TLS13_DERIVE_APP_OK { return 0 - NX_TLS13_RUN_DERIVE_APP_FAIL } 292 let t4: i64 = sys_now_ms() 293 294 // native sovereign handshake breakdown (stderr): SH+x25519 kex vs cert-validation vs finish. 295 sys_write(2, "nishi-hs sh+kex=" as *u8, 16); _run_pn(t2 - t1) 296 sys_write(2, "ms certloop=" as *u8, 12); _run_pn(t3 - t2) 297 sys_write(2, "ms fin=" as *u8, 7); _run_pn(t4 - t3) 298 sys_write(2, "ms\n" as *u8, 3) 299 300 // Return session as positive i64 (caller casts to *Tls13ClientSession). 301 return s as i64 302} 303 304// Compile-only smoke. Real KAT in 305// nx_tls13_client_session_run_test.nx. 306func main() -> i64 { 307 return 0 308}