nx_tls13_client_session_recv_hs.nx source
↩ module page · 266 lines · 11992 B
1// nx_tls13_client_session_recv_hs.nx -- step 3c.3 of the
2// nx_https_client wiring arc.
3//
4// Consumes ONE encrypted TLS record (the caller has already read
5// it from fd into a buffer, including the 5-byte TLSCiphertext
6// header) and walks the dispatch loop:
7//
8// 1. Decrypt record with session.server_hs_traffic_key + IV +
9// session.server_seq via shipped nx_tls13_record_decrypt
10// -> produces inner plaintext + real ContentType
11// 2. Increment session.server_seq for next record
12// 3. Reject record if ContentType != 22 (HANDSHAKE)
13// 4. Walk the inner plaintext extracting handshake messages
14// (each is [type:1][length:3][body:length])
15// 5. For each handshake message:
16// - Call shipped tls13_client_dispatch_with_validation
17// (which enforces cert validation at WAIT_CERT via the
18// bridge shipped in c5ddbf81)
19// - Update session.state from out_new_state
20// - If dispatcher returned non-OK -> bail with mapped verdict
21// 6. Return OK after processing all messages in the record
22//
23// Caller invokes this once per record received from fd. After
24// multiple calls the session reaches CONNECTED state (server
25// Finished verified), at which point step 3c.4 emits the client
26// Finished.
27//
28// Public API:
29// nx_tls13_client_session_recv_hs(
30// session, record_bytes, record_len, val_ctx
31// ) -> verdict
32// nx_tls13_recv_hs_verdict_is_valid(v) -> 0|1
33//
34// Sealed verdict enum:
35// NX_TLS13_RECV_HS_OK record processed (may be partial
36// handshake progress; check
37// session.state)
38// NX_TLS13_RECV_HS_BAD_STATE session not in WAIT_EE..WAIT_SF
39// NX_TLS13_RECV_HS_RECORD_FAIL record decrypt verdict non-OK
40// NX_TLS13_RECV_HS_WRONG_CT decrypted ContentType != 22
41// NX_TLS13_RECV_HS_MSG_TOO_SHORT handshake message header invalid
42// NX_TLS13_RECV_HS_DISPATCH_FAIL dispatcher rejected (cert verify
43// failed / bad MAC / bad format etc)
44// NX_TLS13_RECV_HS_INTERNAL out_of_band parse failure
45//
46// Per Cardinals 9 (single-responsibility -- ONE record per call),
47// 12 (defensive at boundaries -- 6 distinct verdict mappings;
48// server_seq incremented BEFORE any error return so replay
49// counters track delivered records), 19 (composes shipped
50// dispatcher unchanged), 22 (composition -- 3 shipped primitives
51// compose into one orchestrator step), 23 (preamble names what
52// the caller does between calls + the multi-call contract).
53//
54// license_tier: INDEPENDENT_REDERIVE
55// genealogy_id: international-research-sources/ietf/rfc_8446
56// lineage_id: nishi_tls13_client_session_recv_hs_q10
57
58// nx_safety_envelope:
59// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
60// sil_target: SIL1
61// evidence: [bulk_applied_2026-05-19, tls13-client-recv-hs-step-3c3]
62// verdict: NOT_YET_EVALUATED
63
64import "nx_syscalls.nx"
65import "nx_tls13_record.nx"
66import "nx_tls13_client.nx"
67import "nx_tls13_client_validate_certificate.nx"
68import "nx_tls13_client_dispatch_with_validation.nx"
69import "nx_tls13_client_session.nx"
70import "nx_tls13_client_verify_cv.nx" // CertificateVerify signature check (closes the client auth-bypass stub)
71const NX_MAGIC_8192: i64 = 8192
72
73const NX_TLS13_RECV_HS_OK: i64 = 1
74const NX_TLS13_RECV_HS_BAD_STATE: i64 = 2
75const NX_TLS13_RECV_HS_RECORD_FAIL: i64 = 3
76const NX_TLS13_RECV_HS_WRONG_CT: i64 = 4
77const NX_TLS13_RECV_HS_MSG_TOO_SHORT: i64 = 5
78const NX_TLS13_RECV_HS_DISPATCH_FAIL: i64 = 6
79const NX_TLS13_RECV_HS_INTERNAL: i64 = 7
80const NX_TLS13_RECV_HS_VERDICT_N: i64 = 8
81
82func nx_tls13_recv_hs_verdict_is_valid(v: i64) -> i64 {
83 if v < NX_TLS13_RECV_HS_OK { return 0 }
84 if v >= NX_TLS13_RECV_HS_VERDICT_N { return 0 }
85 return 1
86}
87
88// Map the underlying dispatch-with-validation enum into the
89// recv-hs vocabulary. OK passes through; all others collapse to
90// DISPATCH_FAIL since the recv-hs caller doesn't need to
91// distinguish (it just knows the handshake stalled).
92//
93// Internally the dispatcher's exact verdict is observable via
94// session.state (unchanged on failure) + the underlying KAT.
95func _map_dwv_verdict(v: i64) -> i64 {
96 if v == NX_TLS13_DWV_OK { return NX_TLS13_RECV_HS_OK }
97 return NX_TLS13_RECV_HS_DISPATCH_FAIL
98}
99
100// Process ONE encrypted record. Caller has read the record off
101// fd into record_bytes; record_len is the total byte count (5-byte
102// header + ciphertext + 16-byte AEAD tag).
103func nx_tls13_client_session_recv_hs(
104 s: *Tls13ClientSession,
105 record_bytes: *u8, record_len: i64,
106 val_ctx: *TlsValidationContext
107) -> i64 {
108 // Session must be in one of the four "expecting handshake
109 // messages" states.
110 if s.state < NX_TLS13_CSESSION_STATE_WAIT_EE { return NX_TLS13_RECV_HS_BAD_STATE }
111 if s.state > NX_TLS13_CSESSION_STATE_WAIT_SF { return NX_TLS13_RECV_HS_BAD_STATE }
112
113 if record_len < NX_TLS13_RECORD_HEADER_LEN + 1 + NX_TLS13_RECORD_TAG_LEN {
114 return NX_TLS13_RECV_HS_RECORD_FAIL
115 }
116
117 // Split record_bytes into (header[0..5], ciphertext[5..n-16], tag[n-16..n]).
118 let header: *u8 = record_bytes
119 let ct: *u8 = record_bytes + NX_TLS13_RECORD_HEADER_LEN
120 let ct_len: i64 = record_len - NX_TLS13_RECORD_HEADER_LEN - NX_TLS13_RECORD_TAG_LEN
121 let tag: *u8 = record_bytes + record_len - NX_TLS13_RECORD_TAG_LEN
122
123 let inner: *u8 = sys_mmap(ct_len + 16)
124 let real_ct_p: *i64 = sys_mmap(16) as *i64
125 let real_len_p: *i64 = sys_mmap(16) as *i64
126
127 let dv: i64 = nx_tls13_record_decrypt_v2(
128 s.cipher_suite,
129 s.server_hs_traffic_key,
130 s.server_hs_iv,
131 s.server_seq,
132 header,
133 ct, ct_len,
134 tag,
135 inner,
136 real_ct_p,
137 real_len_p
138 )
139
140 // Increment server_seq AFTER use (the next record uses seq+1).
141 s.server_seq = s.server_seq + 1
142
143 if dv != NX_TLS13_REC_VERDICT_OK { return NX_TLS13_RECV_HS_RECORD_FAIL }
144
145 if *real_ct_p != NX_TLS13_CT_HANDSHAKE { return NX_TLS13_RECV_HS_WRONG_CT }
146
147 // ---- REASSEMBLY (RFC 8446 ยง5.1): append this record's decrypted plaintext to the cross-record
148 // accumulator, then parse only COMPLETE handshake messages; carry any partial-message remainder to
149 // the next record. Meta/CDN servers fragment the Certificate across records; without this we bailed. ----
150 let plaintext_len: i64 = *real_len_p
151 let hb: *u8 = s.hs_reasm
152 if s.hs_reasm_len + plaintext_len > NX_TLS13_HS_REASM_BYTES {
153 return NX_TLS13_RECV_HS_MSG_TOO_SHORT // flight exceeds our 64KB bound -> reject (DoS guard)
154 }
155 var ai: i64 = 0
156 while ai < plaintext_len { hb[s.hs_reasm_len + ai] = inner[ai]; ai = ai + 1 }
157 let total: i64 = s.hs_reasm_len + plaintext_len
158 var off: i64 = 0
159 let new_state_p: *i64 = sys_mmap(16) as *i64
160
161 while off < total {
162 // Fewer than 4 header bytes buffered -> carry the remainder, wait for the next record.
163 if off + 4 > total {
164 var c1: i64 = 0
165 while c1 < total - off { hb[c1] = hb[off + c1]; c1 = c1 + 1 }
166 s.hs_reasm_len = total - off
167 return NX_TLS13_RECV_HS_OK
168 }
169 let body_len: i64 = ((hb[off + 1] & 0xff) << 16)
170 | ((hb[off + 2] & 0xff) << 8)
171 | (hb[off + 3] & 0xff)
172 let msg_len: i64 = 4 + body_len
173 // Message fragmented across records -> carry [off..total] to the front, wait for the next record.
174 if off + msg_len > total {
175 var c2: i64 = 0
176 while c2 < total - off { hb[c2] = hb[off + c2]; c2 = c2 + 1 }
177 s.hs_reasm_len = total - off
178 return NX_TLS13_RECV_HS_OK
179 }
180
181 let cur_state: i64 = s.state
182
183 // WAIT_CV: verify the server's CertificateVerify signature over Transcript-Hash(CH..Certificate)
184 // BEFORE the dispatcher folds CV into the transcript. This is the possession-proof the old dispatcher
185 // stubbed out; without it a MITM presenting the (public) real chain + a garbage CV would be accepted.
186 if cur_state == NX_TLS13_CSESSION_STATE_WAIT_CV {
187 // R9: the CertificateVerify transcript hash is the SUITE'S hash (48B for 0x1302).
188 let th_cv: *u8 = sys_mmap(64)
189 if s.cipher_suite == NX_TLS13_CS_AES_256_GCM_SHA384 {
190 if nx_tls13_transcript_snapshot384(s.transcript, th_cv) != NX_TLS13_TX_VERDICT_OK { return NX_TLS13_RECV_HS_INTERNAL }
191 } else {
192 if nx_tls13_transcript_snapshot(s.transcript, th_cv) != NX_TLS13_TX_VERDICT_OK { return NX_TLS13_RECV_HS_INTERNAL }
193 }
194 let cvv: i64 = nx_tls13_client_verify_server_cv(hb + off, msg_len, s.leaf_cert, s.leaf_cert_len, th_cv)
195 if cvv != NX_CVV_OK { return NX_TLS13_RECV_HS_DISPATCH_FAIL }
196 }
197
198 // Dispatch via the validation-enforcing wrapper. Map the
199 // CSESSION state space to the CSTATE space the dispatcher
200 // expects (they happen to be identical for WAIT_* states).
201 let cstate: i64 = s.state - NX_TLS13_CSESSION_STATE_WAIT_EE + NX_TLS13_CSTATE_WAIT_EE
202
203 let dv2: i64 = tls13_client_dispatch_with_validation(
204 cstate,
205 hb + off, msg_len,
206 s.server_hs_traffic_secret,
207 s.transcript,
208 new_state_p,
209 val_ctx
210 )
211 if dv2 != NX_TLS13_DWV_OK {
212 return _map_dwv_verdict(dv2)
213 }
214
215 // Convert dispatcher's new CSTATE back into CSESSION state.
216 if *new_state_p == NX_TLS13_CSTATE_WAIT_EE {
217 s.state = NX_TLS13_CSESSION_STATE_WAIT_EE
218 } else { if *new_state_p == NX_TLS13_CSTATE_WAIT_CERT {
219 s.state = NX_TLS13_CSESSION_STATE_WAIT_CERT
220 } else { if *new_state_p == NX_TLS13_CSTATE_WAIT_CV {
221 s.state = NX_TLS13_CSESSION_STATE_WAIT_CV
222 } else { if *new_state_p == NX_TLS13_CSTATE_WAIT_SF {
223 s.state = NX_TLS13_CSESSION_STATE_WAIT_SF
224 } else { if *new_state_p == NX_TLS13_CSTATE_CONNECTED {
225 // RFC 8446: server Finished verified; client must
226 // still emit its own Finished (step 3c.4) before the
227 // session is fully usable for app data. We mark a
228 // distinct WAIT_CLIENT_FIN state so 3c.4 knows.
229 s.state = NX_TLS13_CSESSION_STATE_WAIT_CLIENT_FIN
230 } else { return NX_TLS13_RECV_HS_INTERNAL } } } } }
231
232 // WAIT_CERT just processed (dispatch validated the chain): capture the server leaf cert DER so the
233 // next message (CertificateVerify) can be verified against the leaf public key.
234 if cur_state == NX_TLS13_CSESSION_STATE_WAIT_CERT {
235 let p_co: *i64 = sys_mmap(16) as *i64
236 let p_cl: *i64 = sys_mmap(16) as *i64
237 let p_cert_off: *i64 = sys_mmap(16) as *i64
238 let p_cert_len: *i64 = sys_mmap(16) as *i64
239 let p_ce_off: *i64 = sys_mmap(16) as *i64
240 let p_ce_len: *i64 = sys_mmap(16) as *i64
241 let p_chain: *i64 = sys_mmap(16) as *i64
242 if tls13_parse_certificate_chain_first(hb + off, msg_len,
243 p_co, p_cl, p_cert_off, p_cert_len, p_ce_off, p_ce_len, p_chain) == NX_TLS13_AUTH_VERDICT_OK {
244 let clen: i64 = p_cert_len[0]
245 if clen > 0 { if clen <= NX_MAGIC_8192 {
246 let src: *u8 = (hb as i64 + off + p_cert_off[0]) as *u8
247 var ci: i64 = 0
248 while ci < clen { s.leaf_cert[ci] = src[ci]; ci = ci + 1 }
249 s.leaf_cert_len = clen
250 } }
251 }
252 }
253
254 off = off + msg_len
255 }
256
257 // All buffered messages consumed exactly on a record boundary -> accumulator drained.
258 s.hs_reasm_len = 0
259 return NX_TLS13_RECV_HS_OK
260}
261
262// Compile-only smoke. Real KAT in
263// nx_tls13_client_session_recv_hs_test.nx.
264func main() -> i64 {
265 return 0
266}