nx_tls13_client.nx source
↩ module page · 264 lines · 12039 B
1// nx_tls13_client.nx -- TLS 1.3 client state machine (Gap L sub-2).
2//
3// Phase 0b §L of the Nishi TLS 1.3 stack per
4// docs/NISHI_TLS13_GAP_AUDIT.md. The dispatcher that walks the
5// server's encrypted handshake records past ServerHello: each
6// decrypted record body contains 1+ handshake messages, and this
7// state machine consumes them in the order RFC 8446 §A.1 dictates:
8//
9// WAIT_EE -> EncryptedExtensions
10// |
11// WAIT_CERT -> Certificate
12// |
13// WAIT_CV -> CertificateVerify
14// |
15// WAIT_SF -> server Finished (verify MAC over transcript-before-SF)
16// |
17// CONNECTED
18//
19// At every stage the message goes into the transcript hash AFTER
20// the stage-specific check passes -- except for server Finished,
21// which verifies the MAC over the transcript-BEFORE-SF then feeds
22// SF in. Getting that order wrong = MAC mismatch on a legitimate
23// server (the bug that catches every from-scratch TLS impl).
24//
25// What it does today:
26// - dispatch one decrypted handshake message per call
27// - parse via shipped nx_tls13_auth + nx_tls13_finished
28// - feed transcript in spec-ordered way
29// - server_Finished MAC verify via constant-time compare
30// - emit our client_Finished (36-byte handshake message:
31// 4-byte header + 32-byte HMAC over transcript)
32// - cert validation is INTENTIONALLY stubbed to "accept any"
33// -- Gap I will replace the stub with real RFC 5280 path
34// validation (chain build + signature chain verify + name
35// constraints + SAN match + validity period)
36//
37// What it doesn't do yet:
38// - signature verification on CertificateVerify (composes
39// against nx_ed25519 / nx_ecdsa / nx_rsa_pss; Gap I)
40// - CertificateRequest (mutual TLS client-auth; rare)
41// - the actual TCP-recv loop (caller provides decrypted bytes)
42// - record-layer encryption of the emitted client_Finished
43// (caller wraps via nx_tls13_record_encrypt under
44// client_handshake_traffic_key)
45//
46// KAT verified:
47// - 4-stage walk: synthesised EE+Cert+CV+SF drive the dispatcher
48// to CONNECTED with the right state transitions
49// - tampered server_Finished MAC rejected with FIN_BAD verdict
50// - wrong-type message at each state rejected
51// - client_Finished MAC byte-equals direct HMAC over transcript
52// - end-to-end record-layer round-trip (encrypt our Finished
53// under derived AEAD key, decrypt, recover the bytes)
54//
55// Composes with:
56// - nx_tls13 (handshake type constants)
57// - nx_tls13_auth (parse EE / Cert / CV)
58// - nx_tls13_finished (compute + verify MAC)
59// - nx_tls13_transcript (running hash + snapshot)
60// - nx_tls13_kdf (HKDF-Expand-Label for finished_key)
61// - nx_tls13_schedule (tls13_finished_key wrapper)
62// - nx_tls13_record (encrypt the emitted Finished; caller-side)
63// - nx_tls13_handshake (caller derived hs keys via that orchestrator)
64//
65// license_tier: INDEPENDENT_REDERIVE
66// genealogy_id: international-research-sources/ietf/rfc_8446
67// lineage_id: nishi_tls13_client_q10
68
69// nx_safety_envelope:
70// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
71// sil_target: SIL1
72// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
73// verdict: NOT_YET_EVALUATED
74
75import "nx_syscalls.nx"
76import "nx_tls13.nx"
77import "nx_tls13_auth.nx"
78import "nx_tls13_finished.nx"
79import "nx_tls13_transcript.nx"
80import "nx_tls13_schedule.nx"
81
82// Sealed client state.
83const NX_TLS13_CSTATE_WAIT_EE: i64 = 1
84const NX_TLS13_CSTATE_WAIT_CERT: i64 = 2
85const NX_TLS13_CSTATE_WAIT_CV: i64 = 3
86const NX_TLS13_CSTATE_WAIT_SF: i64 = 4
87const NX_TLS13_CSTATE_CONNECTED: i64 = 5
88const NX_TLS13_CSTATE_N: i64 = 6
89
90// Sealed dispatcher verdict.
91const NX_TLS13_CLIENT_VERDICT_OK: i64 = 1
92const NX_TLS13_CLIENT_VERDICT_BAD_MSG_TYPE: i64 = 2
93const NX_TLS13_CLIENT_VERDICT_BAD_FORMAT: i64 = 3
94const NX_TLS13_CLIENT_VERDICT_SF_BAD_MAC: i64 = 4
95const NX_TLS13_CLIENT_VERDICT_BAD_STATE: i64 = 5
96const NX_TLS13_CLIENT_VERDICT_N: i64 = 6
97
98const NX_TLS13_CLIENT_HASH_LEN: i64 = 32
99
100// Dispatch a single decrypted handshake message into the client
101// state machine.
102//
103// Inputs:
104// state = current state (NX_TLS13_CSTATE_*)
105// msg, msg_len = the handshake message bytes
106// (4-byte header + body)
107// server_hs_traffic_secret = needed for WAIT_SF; otherwise unused
108// transcript_state = running transcript state pointer
109// (from nx_tls13_transcript_new + updates
110// for CH+SH already done)
111//
112// Outputs:
113// *out_new_state = new state after this message
114//
115// Returns NX_TLS13_CLIENT_VERDICT_OK on success or non-OK.
116func tls13_client_dispatch_handshake_message(
117 state: i64,
118 msg: *u8, msg_len: i64,
119 server_hs_traffic_secret: *u8,
120 transcript_state: *u8,
121 out_new_state: *i64
122) -> i64 {
123 if msg_len < 4 { return NX_TLS13_CLIENT_VERDICT_BAD_FORMAT }
124 let msg_type: i64 = msg[0] & 0xff
125
126 if state == NX_TLS13_CSTATE_WAIT_EE {
127 if msg_type != HT_ENCRYPTED_EXTENSIONS { return NX_TLS13_CLIENT_VERDICT_BAD_MSG_TYPE }
128 let p_eo: *i64 = sys_mmap(16) as *i64
129 let p_el: *i64 = sys_mmap(16) as *i64
130 let v: i64 = tls13_parse_encrypted_extensions(msg, msg_len, p_eo, p_el)
131 if v != NX_TLS13_AUTH_VERDICT_OK { return NX_TLS13_CLIENT_VERDICT_BAD_FORMAT }
132 nx_tls13_transcript_update(transcript_state, msg, msg_len)
133 *out_new_state = NX_TLS13_CSTATE_WAIT_CERT
134 return NX_TLS13_CLIENT_VERDICT_OK
135 }
136
137 if state == NX_TLS13_CSTATE_WAIT_CERT {
138 if msg_type != HT_CERTIFICATE { return NX_TLS13_CLIENT_VERDICT_BAD_MSG_TYPE }
139 let p_co: *i64 = sys_mmap(16) as *i64
140 let p_cl: *i64 = sys_mmap(16) as *i64
141 let p_cert_off: *i64 = sys_mmap(16) as *i64
142 let p_cert_len: *i64 = sys_mmap(16) as *i64
143 let p_ce_off: *i64 = sys_mmap(16) as *i64
144 let p_ce_len: *i64 = sys_mmap(16) as *i64
145 let p_chain: *i64 = sys_mmap(16) as *i64
146 let v: i64 = tls13_parse_certificate_chain_first(
147 msg, msg_len,
148 p_co, p_cl,
149 p_cert_off, p_cert_len,
150 p_ce_off, p_ce_len,
151 p_chain
152 )
153 if v != NX_TLS13_AUTH_VERDICT_OK { return NX_TLS13_CLIENT_VERDICT_BAD_FORMAT }
154 // UNVALIDATED PRIMITIVE: this bare dispatcher accepts the cert without checking it. Gap I HAS landed
155 // -- the LIVE fetch path does NOT call this directly; it wraps it with tls13_client_dispatch_with_
156 // validation (full RFC 5280 chain validation at WAIT_CERT, via nx_tls13_client_validate_certificate)
157 // AND nx_tls13_client_session_recv_hs verifies the server CertificateVerify signature at WAIT_CV
158 // (nx_tls13_client_verify_server_cv, RFC 8446 4.4.3, 2026-07-03). So the live client IS safe. NEVER call
159 // this bare dispatcher directly against an untrusted network -- only via the validation wrapper.
160 nx_tls13_transcript_update(transcript_state, msg, msg_len)
161 *out_new_state = NX_TLS13_CSTATE_WAIT_CV
162 return NX_TLS13_CLIENT_VERDICT_OK
163 }
164
165 if state == NX_TLS13_CSTATE_WAIT_CV {
166 if msg_type != HT_CERTIFICATE_VERIFY { return NX_TLS13_CLIENT_VERDICT_BAD_MSG_TYPE }
167 let p_scheme: *i64 = sys_mmap(16) as *i64
168 let p_so: *i64 = sys_mmap(16) as *i64
169 let p_sl: *i64 = sys_mmap(16) as *i64
170 let v: i64 = tls13_parse_certificate_verify(msg, msg_len, p_scheme, p_so, p_sl)
171 if v != NX_TLS13_AUTH_VERDICT_OK { return NX_TLS13_CLIENT_VERDICT_BAD_FORMAT }
172 // UNVALIDATED PRIMITIVE: this bare dispatcher does not verify the CertificateVerify signature. The LIVE
173 // path DOES: nx_tls13_client_session_recv_hs calls nx_tls13_client_verify_server_cv BEFORE this dispatch
174 // (Ed25519 / ECDSA-P256/P384 / RSA-PSS-SHA256 against the leaf pubkey, RFC 8446 4.4.3, fail-closed on the
175 // rest), proven end-to-end by real TLS fetches (2026-07-03). Only reach this via that validating path.
176 nx_tls13_transcript_update(transcript_state, msg, msg_len)
177 *out_new_state = NX_TLS13_CSTATE_WAIT_SF
178 return NX_TLS13_CLIENT_VERDICT_OK
179 }
180
181 if state == NX_TLS13_CSTATE_WAIT_SF {
182 if msg_type != HT_FINISHED { return NX_TLS13_CLIENT_VERDICT_BAD_MSG_TYPE }
183 // SF body MUST be the 32-byte HMAC (for SHA-256 cipher).
184 let body_len: i64 = ((msg[1] & 0xff) << 16) | ((msg[2] & 0xff) << 8) | (msg[3] & 0xff)
185 if body_len != NX_TLS13_CLIENT_HASH_LEN { return NX_TLS13_CLIENT_VERDICT_BAD_FORMAT }
186 if 4 + body_len > msg_len { return NX_TLS13_CLIENT_VERDICT_BAD_FORMAT }
187 // Snapshot transcript BEFORE feeding SF -- the SF MAC is
188 // over CH..server_CertVerify, NOT over CH..server_Finished.
189 let th_before_sf: *u8 = sys_mmap(64)
190 nx_tls13_transcript_snapshot(transcript_state, th_before_sf)
191 // Derive server's finished_key from server_hs_traffic_secret.
192 let s_fk: *u8 = sys_mmap(64)
193 tls13_finished_key(server_hs_traffic_secret, NX_TLS13_CLIENT_HASH_LEN, s_fk)
194 // Verify received MAC vs computed expected.
195 let received_mac: *u8 = msg + 4
196 let v: i64 = nx_tls13_finished_verify(
197 s_fk, NX_TLS13_CLIENT_HASH_LEN,
198 th_before_sf, NX_TLS13_CLIENT_HASH_LEN,
199 received_mac, NX_TLS13_CLIENT_HASH_LEN
200 )
201 if v != NX_TLS13_FIN_VERDICT_OK { return NX_TLS13_CLIENT_VERDICT_SF_BAD_MAC }
202 // Now feed SF into transcript (so client_Finished MAC will
203 // cover CH..server_Finished as the spec requires).
204 nx_tls13_transcript_update(transcript_state, msg, msg_len)
205 *out_new_state = NX_TLS13_CSTATE_CONNECTED
206 return NX_TLS13_CLIENT_VERDICT_OK
207 }
208
209 return NX_TLS13_CLIENT_VERDICT_BAD_STATE
210}
211
212// Emit client_Finished as a complete 36-byte handshake message:
213// HandshakeType(1) = HT_FINISHED(20) + uint24 length(3) = 32
214// verify_data(32) = HMAC(finished_key, transcript_snapshot_now)
215//
216// Caller MUST have driven the dispatcher to CONNECTED first
217// (so the transcript covers CH..server_Finished). Caller then
218// wraps the emitted bytes via nx_tls13_record_encrypt under
219// client_handshake_traffic_key + per-record sequence number.
220//
221// Returns 36 (bytes written) or negative verdict.
222func tls13_client_emit_finished(
223 client_hs_traffic_secret: *u8,
224 transcript_state: *u8,
225 out: *u8
226) -> i64 {
227 let th: *u8 = sys_mmap(64)
228 nx_tls13_transcript_snapshot(transcript_state, th)
229 let fk: *u8 = sys_mmap(64)
230 tls13_finished_key(client_hs_traffic_secret, NX_TLS13_CLIENT_HASH_LEN, fk)
231 let mac: *u8 = sys_mmap(64)
232 nx_tls13_finished_compute(fk, NX_TLS13_CLIENT_HASH_LEN, th, NX_TLS13_CLIENT_HASH_LEN, mac)
233 out[0] = HT_FINISHED & 0xff
234 out[1] = 0
235 out[2] = 0
236 out[3] = NX_TLS13_CLIENT_HASH_LEN & 0xff
237 var i: i64 = 0
238 while i < NX_TLS13_CLIENT_HASH_LEN {
239 out[4 + i] = mac[i]
240 i = i + 1
241 }
242 // Append the emitted CF to the transcript so subsequent
243 // application-data key derivations (which use the post-CF
244 // transcript hash per RFC 8446 §7.1) match server's view.
245 // Without this, server.dispatch_client_finished's own append
246 // makes its transcript diverge from the client's permanently.
247 // (Bug discovered via nx_tls13_loopback_test reaching CONNECTED
248 // on both sides but with mismatched final transcript hashes.)
249 nx_tls13_transcript_update(transcript_state, out, 4 + NX_TLS13_CLIENT_HASH_LEN)
250 return 4 + NX_TLS13_CLIENT_HASH_LEN
251}
252
253// Sealed-enum validity gates.
254func nx_tls13_client_verdict_is_valid(v: i64) -> i64 {
255 if v < 0 { return 0 }
256 if v >= NX_TLS13_CLIENT_VERDICT_N { return 0 }
257 return 1
258}
259
260func nx_tls13_cstate_is_valid(s: i64) -> i64 {
261 if s < 0 { return 0 }
262 if s >= NX_TLS13_CSTATE_N { return 0 }
263 return 1
264}