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}