nx_tls13_client_session.nx source
↩ module page · 273 lines · 12336 B
1// nx_tls13_client_session.nx -- TLS 1.3 client session state +
2// emit-ClientHello primitive (step 3c.1 of the nx_https_client
3// wiring arc).
4//
5// The Tls13ClientSession struct is the container that the
6// remaining handshake-orchestrator steps (3c.2 recv ServerHello +
7// derive handshake keys; 3c.3 recv encrypted handshake messages
8// via dispatcher_with_validation; 3c.4 emit client Finished;
9// 3c.5 derive application traffic keys) populate field by field
10// as the handshake progresses.
11//
12// At session creation:
13// - client_random[0..32] filled with caller-supplied entropy
14// - x25519 ephemeral keypair generated from caller-supplied seed
15// - transcript hash state initialised
16// - state = CSTATE_INIT (before any message)
17//
18// After emit-CH (this commit):
19// - ClientHello bytes written to caller buffer + transcript
20// updated
21// - state = CSTATE_CH_SENT
22//
23// After 3c.2 (queued): state = WAIT_EE (handshake keys derived)
24// After 3c.3 (queued): state cycles WAIT_EE -> WAIT_CERT ->
25// WAIT_CV -> WAIT_SF -> WAIT_CLIENT_FIN
26// After 3c.4 (queued): state = WAIT_APP_KEYS
27// After 3c.5 (queued): state = CONNECTED (application data flow OK)
28//
29// Public API (this commit):
30// struct Tls13ClientSession { /* see below */ }
31// nx_tls13_client_session_new(client_random, x25519_priv) -> *Tls13ClientSession
32// nx_tls13_client_session_emit_ch(session, sni, sni_len, out_buf, out_cap)
33// -> bytes-written or negative-verdict
34// nx_tls13_client_session_verdict_is_valid(v) -> 0|1
35//
36// Sealed verdict enum:
37// NX_TLS13_CSESSION_OK positive return = bytes written
38// NX_TLS13_CSESSION_BAD_STATE called emit_ch outside INIT state
39// NX_TLS13_CSESSION_BUF_OVERFLOW output buffer too small
40// NX_TLS13_CSESSION_INTERNAL hello-emit returned negative
41//
42// Negative return values from emit_ch carry the verdict code in
43// their absolute value: rc = -NX_TLS13_CSESSION_*. Positive rc =
44// bytes written to out_buf.
45//
46// Per Cardinals 9 (single-responsibility -- session struct + ONE
47// orchestrator step), 12 (defensive at boundaries -- state check +
48// out_cap check), 19 (API contract stability -- composes shipped
49// tls13_client_hello_emit without modifying it), 23 (preamble names
50// every queued sub-step + the state-transition contract).
51//
52// license_tier: INDEPENDENT_REDERIVE
53// genealogy_id: international-research-sources/ietf/rfc_8446
54// lineage_id: nishi_tls13_client_session_q10
55
56// nx_safety_envelope:
57// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
58// sil_target: SIL1
59// evidence: [bulk_applied_2026-05-19, tls13-client-session-step-3c1]
60// verdict: NOT_YET_EVALUATED
61
62import "nx_syscalls.nx"
63import "nx_x25519.nx"
64import "nx_x25519_ephemeral.nx"
65import "nx_tls13.nx"
66import "nx_tls13_hello.nx"
67import "nx_tls13_transcript.nx"
68import "nx_p256_ecdh.nx"
69const NX_MAGIC_8192: i64 = 8192
70
71// Internal session states -- the field tracks where the handshake
72// orchestrator is in the RFC 8446 §A.1 client state machine.
73const NX_TLS13_CSESSION_STATE_INIT: i64 = 0
74const NX_TLS13_CSESSION_STATE_CH_SENT: i64 = 1
75const NX_TLS13_CSESSION_STATE_WAIT_EE: i64 = 2
76const NX_TLS13_CSESSION_STATE_WAIT_CERT: i64 = 3
77const NX_TLS13_CSESSION_STATE_WAIT_CV: i64 = 4
78const NX_TLS13_CSESSION_STATE_WAIT_SF: i64 = 5
79const NX_TLS13_CSESSION_STATE_WAIT_CLIENT_FIN: i64 = 6
80const NX_TLS13_CSESSION_STATE_WAIT_APP_KEYS: i64 = 7
81const NX_TLS13_CSESSION_STATE_CONNECTED: i64 = 8
82
83const NX_TLS13_CSESSION_OK: i64 = 1
84const NX_TLS13_CSESSION_BAD_STATE: i64 = 2
85const NX_TLS13_CSESSION_BUF_OVERFLOW: i64 = 3
86const NX_TLS13_CSESSION_INTERNAL: i64 = 4
87const NX_TLS13_CSESSION_VERDICT_N: i64 = 5
88
89struct Tls13ClientSession {
90 // Filled at session-new:
91 client_random: *u8, // 32 bytes
92 x25519_priv: *u8, // 32 bytes
93 x25519_pub: *u8, // 32 bytes (derived)
94 transcript: *u8, // running transcript-hash state
95 // Updated by orchestrator steps as handshake progresses:
96 state: i64,
97 server_x25519_pub: *u8, // populated by 3c.2 (recv SH)
98 handshake_secret: *u8, // populated by 3c.2 (key schedule)
99 client_hs_traffic_secret: *u8, // populated by 3c.2 (needed for Finished + key updates)
100 server_hs_traffic_secret: *u8, // populated by 3c.2 (needed for SF MAC verify)
101 client_hs_traffic_key: *u8, // populated by 3c.2
102 server_hs_traffic_key: *u8, // populated by 3c.2
103 client_hs_iv: *u8, // populated by 3c.2
104 server_hs_iv: *u8, // populated by 3c.2
105 client_seq: i64, // record-layer sequence (per direction)
106 server_seq: i64,
107 // Negotiated cipher suite (TLS 1.3 wire value): 0x1303 ChaCha20-
108 // Poly1305-SHA256 or 0x1301 AES-128-GCM-SHA256. Populated by
109 // recv_sh after parsing the ServerHello's chosen suite. Picks
110 // both the key derivation width AND the record encrypt/decrypt
111 // AEAD primitive.
112 cipher_suite: i64,
113 // Transcript-hash snapshot taken IMMEDIATELY AFTER server Finished
114 // is consumed, BEFORE client Finished is appended. RFC 8446 §7.1
115 // mandates this exact prefix for application-traffic-secret
116 // derivation (both client and server use Hash(ClientHello...server
117 // Finished), NOT the post-CF transcript). Populated in
118 // emit_finished step 3c.4 before the CF transcript update.
119 h_post_sf: *u8, // 32 bytes (SHA-256)
120 // App-traffic state derived by 3c.5 after CONNECTED:
121 master_secret: *u8, // derived from handshake_secret
122 client_app_traffic_secret: *u8, // needed for key updates
123 server_app_traffic_secret: *u8,
124 client_app_traffic_key: *u8,
125 server_app_traffic_key: *u8,
126 client_app_iv: *u8,
127 server_app_iv: *u8,
128 client_app_seq: i64,
129 server_app_seq: i64,
130 // P-256 ephemeral keypair (rung B4-P256-KEYSHARE). Derived in
131 // session-new from the same caller-supplied 32 seed bytes via
132 // domain-separated SHA-256 (p256_ecdh_derive_priv) so the
133 // 2-arg session-new contract is unchanged. The ClientHello
134 // carries BOTH shares (x25519 + secp256r1); recv_sh completes
135 // whichever group the server picked.
136 p256_priv: *u8, // 32 bytes
137 p256_pub: *u8, // 65 bytes, 0x04 || X || Y
138 // Server leaf-cert DER, captured at WAIT_CERT so WAIT_CV can verify the CertificateVerify
139 // signature against the leaf public key (closes the client auth-bypass stub, 2026-07-03).
140 leaf_cert: *u8, // buffer; filled with the server leaf cert DER at WAIT_CERT
141 leaf_cert_len: i64, // 0 until the server Certificate message is processed
142 // Handshake-message REASSEMBLY across TLS records (RFC 8446 §5.1: a handshake message MAY be
143 // fragmented across several records -- Meta/CDN servers do this for the Certificate). recv_hs
144 // appends each decrypted record's plaintext here and parses only COMPLETE messages, carrying any
145 // partial-message remainder to the next record. Without this, fragmented certs fail (verdict=7).
146 hs_reasm: *u8, // accumulator buffer (NX_TLS13_HS_REASM_BYTES)
147 hs_reasm_len: i64, // bytes currently buffered (unparsed partial-message remainder)
148}
149
150const NX_TLS13_CSESSION_BYTES: i64 = 512 // struct + leaf-cert capture fields; page-backed, ample headroom
151const NX_TLS13_HS_REASM_BYTES: i64 = 65536 // handshake-reassembly accumulator: holds a Certificate flight fragmented across records (RFC 8446 §5.1) + also the DoS bound on a claimed message length
152
153func nx_tls13_client_session_verdict_is_valid(v: i64) -> i64 {
154 if v < NX_TLS13_CSESSION_OK { return 0 }
155 if v >= NX_TLS13_CSESSION_VERDICT_N { return 0 }
156 return 1
157}
158
159// Allocate a new session. Caller supplies 32 random bytes for
160// client_random and 32 random bytes for x25519_priv. We derive
161// x25519_pub via the shipped scalar-mult. Transcript hash state
162// is freshly initialised.
163func nx_tls13_client_session_new(
164 client_random_in: *u8,
165 x25519_priv_in: *u8
166) -> *Tls13ClientSession {
167 let raw: *u8 = sys_mmap(NX_TLS13_CSESSION_BYTES)
168 let s: *Tls13ClientSession = raw as *Tls13ClientSession
169
170 s.client_random = sys_mmap(32)
171 let r: *u8 = s.client_random
172 var i: i64 = 0
173 while i < 32 { r[i] = client_random_in[i]; i = i + 1 }
174
175 s.x25519_priv = sys_mmap(32)
176 let pr: *u8 = s.x25519_priv
177 i = 0
178 while i < 32 { pr[i] = x25519_priv_in[i]; i = i + 1 }
179
180 s.x25519_pub = sys_mmap(32)
181 // x25519 keypair-public derives pub from priv (scalar-mult on
182 // the standard basepoint).
183 x25519_keypair_public(s.x25519_priv, s.x25519_pub)
184
185 s.transcript = nx_tls13_transcript_new()
186
187 s.state = NX_TLS13_CSESSION_STATE_INIT
188 s.server_x25519_pub = sys_mmap(32)
189 // R9 (2026-08-05): SECRET buffers are 64, not 32. Cipher suite 0x1302 runs the whole key
190 // schedule on SHA-384, so every *_secret is 48 bytes. A 48-byte write into a 32-byte
191 // allocation is silent heap corruption that would surface as a handshake failure three
192 // layers away. KEY buffers stay 32 (AES-256 key = 32, ChaCha20 = 32) and IVs stay 12/16.
193 s.handshake_secret = sys_mmap(64)
194 s.client_hs_traffic_secret = sys_mmap(64)
195 s.server_hs_traffic_secret = sys_mmap(64)
196 s.client_hs_traffic_key = sys_mmap(32)
197 s.server_hs_traffic_key = sys_mmap(32)
198 s.client_hs_iv = sys_mmap(16)
199 s.server_hs_iv = sys_mmap(16)
200 s.client_seq = 0
201 s.server_seq = 0
202
203 s.cipher_suite = 0x1303 // default; recv_sh overwrites
204 // R9: 64 for the same reason -- these are SECRETS (48 bytes under SHA-384), and h_post_sf
205 // holds a transcript HASH which is 48 bytes for 0x1302.
206 s.h_post_sf = sys_mmap(64)
207 s.master_secret = sys_mmap(64)
208 s.client_app_traffic_secret = sys_mmap(64)
209 s.server_app_traffic_secret = sys_mmap(64)
210 s.client_app_traffic_key = sys_mmap(32)
211 s.server_app_traffic_key = sys_mmap(32)
212 s.client_app_iv = sys_mmap(16)
213 s.server_app_iv = sys_mmap(16)
214 s.client_app_seq = 0
215 s.server_app_seq = 0
216
217 // P-256 ephemeral (B4): derived, not separately supplied, so
218 // every existing session-new caller gets the dual-share CH.
219 s.p256_priv = sys_mmap(32)
220 s.p256_pub = sys_mmap(72)
221 p256_ecdh_derive_priv(x25519_priv_in, s.p256_priv)
222 p256_ecdh_pub(s.p256_priv, s.p256_pub)
223
224 // Leaf-cert capture buffer for the CertificateVerify signature check (WAIT_CERT -> WAIT_CV).
225 s.leaf_cert = sys_mmap(NX_MAGIC_8192)
226 s.leaf_cert_len = 0
227
228 // Handshake-message reassembly accumulator (RFC 8446 §5.1: messages may fragment across records).
229 s.hs_reasm = sys_mmap(NX_TLS13_HS_REASM_BYTES)
230 s.hs_reasm_len = 0
231
232 return s
233}
234
235// Emit ClientHello bytes into the caller's buffer + feed them
236// into the running transcript hash + advance state to CH_SENT.
237//
238// Returns positive bytes-written on success. Returns negative
239// verdict code on failure: rc = -NX_TLS13_CSESSION_*.
240//
241// Caller responsibility: send the returned bytes over fd. This
242// primitive does NOT do network IO -- that's the next step
243// (queued 3c.2).
244func nx_tls13_client_session_emit_ch(
245 s: *Tls13ClientSession,
246 sni: *u8, sni_len: i64,
247 out_buf: *u8, out_cap: i64
248) -> i64 {
249 if s.state != NX_TLS13_CSESSION_STATE_INIT {
250 return 0 - NX_TLS13_CSESSION_BAD_STATE
251 }
252 if out_cap < 160 { // CH minimum ~150 bytes (incl ALPN-wired)
253 return 0 - NX_TLS13_CSESSION_BUF_OVERFLOW
254 }
255
256 let n: i64 = tls13_client_hello_emit2(
257 s.client_random, sni, sni_len, s.x25519_pub, s.p256_pub,
258 out_buf, out_cap
259 )
260 if n < 0 { return 0 - NX_TLS13_CSESSION_INTERNAL }
261
262 // Feed CH into transcript hash.
263 nx_tls13_transcript_update(s.transcript, out_buf, n)
264
265 s.state = NX_TLS13_CSESSION_STATE_CH_SENT
266 return n
267}
268
269// Compile-only smoke. Real KAT in
270// nx_tls13_client_session_test.nx.
271func main() -> i64 {
272 return 0
273}