nx_tls13_server_session_recv_ch.nx source
↩ module page · 295 lines · 13339 B
1// nx_tls13_server_session_recv_ch.nx -- server-side ClientHello
2// receive step (transitions Tls13ServerSession INIT -> CH_RECEIVED).
3//
4// Composes:
5// tls13_client_hello_parse (parse handshake message into offsets)
6// tls13_ext_find (locate key_share extension)
7// nx_tls13_transcript_update (feed CH bytes into transcript hash)
8//
9// Side effects on session:
10// - client_x25519_pub buffer allocated + filled with 32 bytes
11// from the X25519 KeyShareEntry inside the key_share extension
12// - transcript hash state advanced by the ClientHello message
13// - state advanced to NX_TLS13_SSTATE_CH_RECEIVED
14//
15// Public API:
16// nx_tls13_server_session_recv_ch(session, buf, n)
17// buf, n -- bytes starting at the Handshake header (msg_type = 1)
18// Returns NX_TLS13_SSESSION_OK or a non-OK verdict.
19//
20// Per Cardinals 9 + 12 + 19: single-responsibility step; defensive
21// state + null checks at boundaries; composes shipped primitives
22// without modifying any.
23//
24// license_tier: ORIGINAL
25
26import "nx_syscalls.nx"
27import "nx_tls13.nx"
28import "nx_tls13_hello.nx"
29import "nx_tls13_transcript.nx"
30import "nx_tls13_server_session.nx"
31
32const NX_TLS13_RECV_CH_MIN_CH_BYTES: i64 = 64
33
34// Extract the 32-byte X25519 client public key from a ClientHello's
35// key_share extension data. ext_data layout:
36// client_shares_len(2) + [group(2) + ke_len(2) + ke(ke_len)]*
37//
38// Returns NX_TLS13_SSESSION_OK with 32 bytes copied into out_pub32,
39// or NX_TLS13_SSESSION_PROTOCOL_ERR if no X25519 share was found.
40
41func nx_tls13_recv_ch_extract_x25519_pub(
42 ext_data: *u8, ext_data_len: i64,
43 out_pub32: *u8
44) -> i64 {
45 if ext_data_len < 4 { return NX_TLS13_SSESSION_PROTOCOL_ERR }
46 let shares_len: i64 = tls_read_u16_be(ext_data, 0)
47 if 2 + shares_len > ext_data_len { return NX_TLS13_SSESSION_PROTOCOL_ERR }
48
49 var off: i64 = 2
50 let end: i64 = 2 + shares_len
51 while off + 4 <= end {
52 let group: i64 = tls_read_u16_be(ext_data, off)
53 let ke_len: i64 = tls_read_u16_be(ext_data, off + 2)
54 if off + 4 + ke_len > end { return NX_TLS13_SSESSION_PROTOCOL_ERR }
55 if group == NG_X25519 {
56 if ke_len != 32 { return NX_TLS13_SSESSION_PROTOCOL_ERR }
57 var i: i64 = 0
58 while i < 32 {
59 out_pub32[i] = ext_data[off + 4 + i]
60 i = i + 1
61 }
62 return NX_TLS13_SSESSION_OK
63 }
64 off = off + 4 + ke_len
65 }
66 return NX_TLS13_SSESSION_PROTOCOL_ERR
67}
68
69// Same walk for a secp256r1 (0x0017) KeyShareEntry: 65-byte
70// uncompressed point 0x04 || X || Y into out_pub65. Server side of
71// rung B4-P256-KEYSHARE (2026-06-10).
72func nx_tls13_recv_ch_extract_p256_pub(
73 ext_data: *u8, ext_data_len: i64,
74 out_pub65: *u8
75) -> i64 {
76 if ext_data_len < 4 { return NX_TLS13_SSESSION_PROTOCOL_ERR }
77 let shares_len: i64 = tls_read_u16_be(ext_data, 0)
78 if 2 + shares_len > ext_data_len { return NX_TLS13_SSESSION_PROTOCOL_ERR }
79
80 var off: i64 = 2
81 let end: i64 = 2 + shares_len
82 while off + 4 <= end {
83 let group: i64 = tls_read_u16_be(ext_data, off)
84 let ke_len: i64 = tls_read_u16_be(ext_data, off + 2)
85 if off + 4 + ke_len > end { return NX_TLS13_SSESSION_PROTOCOL_ERR }
86 if group == NG_SECP256R1 {
87 if ke_len != 65 { return NX_TLS13_SSESSION_PROTOCOL_ERR }
88 var i: i64 = 0
89 while i < 65 {
90 out_pub65[i] = ext_data[off + 4 + i]
91 i = i + 1
92 }
93 return NX_TLS13_SSESSION_OK
94 }
95 off = off + 4 + ke_len
96 }
97 return NX_TLS13_SSESSION_PROTOCOL_ERR
98}
99
100// Returns 1 if the supported_groups extension data lists NG_X25519.
101// ext_data layout (RFC 8446 §4.2.7): list_len(2) + [group(2)]*.
102func nx_tls13_recv_ch_sg_has_x25519(ext_data: *u8, ext_data_len: i64) -> i64 {
103 if ext_data_len < 2 { return 0 }
104 let list_len: i64 = tls_read_u16_be(ext_data, 0)
105 if 2 + list_len > ext_data_len { return 0 }
106 var off: i64 = 2
107 let end: i64 = 2 + list_len
108 while off + 2 <= end {
109 if tls_read_u16_be(ext_data, off) == NG_X25519 { return 1 }
110 off = off + 2
111 }
112 return 0
113}
114
115// Pick the AEAD cipher from the client's cipher_suites blob (each entry
116// 2 bytes, at buf[off..off+len)). PREFER ChaCha20-Poly1305 (0x1303) --
117// the proven path that browsers use -- so their behavior is unchanged;
118// fall to AES-128-GCM (0x1301) only for AES-only clients (e.g. Windows
119// schannel, which doesn't offer ChaCha20). Default 0x1303.
120func nx_tls13_recv_ch_pick_cipher(buf: *u8, off: i64, len: i64) -> i64 {
121 var have_aes128: i64 = 0
122 var i: i64 = 0
123 while i + 2 <= len {
124 let cs: i64 = tls_read_u16_be(buf, off + i)
125 if cs == 0x1303 { return 0x1303 }
126 if cs == 0x1301 { have_aes128 = 1 }
127 i = i + 2
128 }
129 if have_aes128 == 1 { return 0x1301 }
130 return 0x1303
131}
132
133func nx_tls13_server_session_recv_ch(
134 session: *Tls13ServerSession,
135 buf: *u8, n: i64
136) -> i64 {
137 if (session as i64) == 0 { return NX_TLS13_SSESSION_BAD_STATE }
138 if (buf as i64) == 0 { return NX_TLS13_SSESSION_BAD_STATE }
139 if session.state != NX_TLS13_SSTATE_INIT { return NX_TLS13_SSESSION_BAD_STATE }
140 if n < NX_TLS13_RECV_CH_MIN_CH_BYTES { return NX_TLS13_SSESSION_PROTOCOL_ERR }
141
142 // Parse the ClientHello.
143 let legacy_ver_box: *i64 = sys_mmap(8) as *i64
144 let random_off_box: *i64 = sys_mmap(8) as *i64
145 let sid_len_box: *i64 = sys_mmap(8) as *i64
146 let suites_off_box: *i64 = sys_mmap(8) as *i64
147 let suites_len_box: *i64 = sys_mmap(8) as *i64
148 let ext_off_box: *i64 = sys_mmap(8) as *i64
149 let ext_len_box: *i64 = sys_mmap(8) as *i64
150
151 let pv: i64 = tls13_client_hello_parse(buf, n,
152 legacy_ver_box, random_off_box, sid_len_box,
153 suites_off_box, suites_len_box,
154 ext_off_box, ext_len_box)
155 if pv != NX_TLS13_HELLO_VERDICT_OK { return NX_TLS13_SSESSION_PROTOCOL_ERR }
156
157 let ext_off: i64 = *ext_off_box
158 let ext_len: i64 = *ext_len_box
159
160 // Negotiate the AEAD cipher from the client's offered list.
161 // TLS-1.2 INTEROP FIX (2026-07-31): record whether the client ACTUALLY offered
162 // supported_versions. emit_sh must not send that extension otherwise -- RFC 8446 4.2 forbids
163 // sending an extension the client did not offer, and doing so made every TLS-1.2-only client
164 // abort against every site we serve. Recorded here, beside the other captures of what the
165 // peer actually sent, because the ClientHello is the only place that truth exists.
166 let sv_off_box: *i64 = sys_mmap(8) as *i64
167 let sv_len_box: *i64 = sys_mmap(8) as *i64
168 let svv: i64 = tls13_ext_find(buf + ext_off, ext_len, EXT_SUPPORTED_VERSIONS, sv_off_box, sv_len_box)
169 session.client_offered_sup_ver = 0
170 if svv == NX_TLS13_HELLO_VERDICT_OK {
171 // CORRECTION (2026-07-31): PRESENCE OF THE EXTENSION IS NOT THE QUESTION -- 0x0304 BEING IN
172 // IT IS. My first cut set this flag on presence alone and CHANGED NOTHING, because an
173 // OpenSSL client invoked with -tls1_2 still SENDS supported_versions, listing only 0x0303.
174 // We passed the presence test, then asserted selected_version=0x0304 at a client that never
175 // offered 1.3. OpenSSL calls that u0027unsolicited extensionu0027 because in a 1.2 ServerHello the
176 // extension is a 1.3-only construct and therefore illegal. So scan the LIST.
177 // ClientHello layout (RFC 8446 4.2.1): 1-byte list length, then that many bytes of u16.
178 let sv_off: i64 = *sv_off_box
179 let sv_len: i64 = *sv_len_box
180 if sv_len >= 3 {
181 let list_len: i64 = buf[ext_off + sv_off] as i64
182 var vi: i64 = 1
183 while vi + 1 < sv_len {
184 if vi <= list_len {
185 let hi: i64 = buf[ext_off + sv_off + vi] as i64
186 let lo: i64 = buf[ext_off + sv_off + vi + 1] as i64
187 if hi == 3 { if lo == 4 { session.client_offered_sup_ver = 1 } }
188 }
189 vi = vi + 2
190 }
191 }
192 }
193
194 session.cipher_suite = nx_tls13_recv_ch_pick_cipher(buf, *suites_off_box, *suites_len_box)
195
196 // Capture client's legacy_session_id (RFC 8446 §4.1.3 -- the server
197 // MUST byte-for-byte echo this in ServerHello AND in a HRR). Done
198 // BEFORE the key_share branch so the HRR path (which also echoes the
199 // sid) has it. Layout: random_off + 32 (random) + 1 (sid_len byte).
200 let sid_len: i64 = *sid_len_box
201 let sid_off: i64 = *random_off_box + 32 + 1
202 let sid_buf: *u8 = sys_mmap(64)
203 var si: i64 = 0
204 while si < sid_len {
205 sid_buf[si] = buf[sid_off + si]
206 si = si + 1
207 }
208 session.client_session_id = sid_buf
209 session.client_session_id_len = sid_len
210
211 // Feed the ClientHello into the transcript hash (full message,
212 // including handshake header). Done in BOTH paths: the HRR rule
213 // (RFC 8446 §4.4.1) needs Hash(ClientHello1) before the transcript
214 // is rewritten, and on a malformed/HRR result the session is
215 // abandoned so the extra update is harmless.
216 nx_tls13_transcript_update(session.transcript, buf, n)
217
218 // REFUSE A NON-1.3 CLIENT HERE (2026-07-31), and specifically at THIS point in the function.
219 //
220 // WHY NOT AT THE EMITTERS: a TLS-1.2-only ClientHello carries no X25519 key_share, so the
221 // runner takes the HelloRetryRequest branch -- and an HRR is ServerHello-shaped and ALSO
222 // carries supported_versions. Gating nx_tls13_server_session_emit_sh alone changed NOTHING
223 // (verified by md5 that the gated binary was the live listener and the behaviour was
224 // identical). Version negotiation is a property of the CLIENTHELLO, not of any one response
225 // path, so deciding it once here covers emit_sh, emit_hrr and anything added later.
226 //
227 // WHY NOT EARLIER: my first cut returned before client_session_id was captured and before the
228 // transcript update above. The daemon then crash-looped -- served, took a connection that hit
229 // the refusal, and died. The sid capture is deliberately placed early (see its comment) so the
230 // HRR path has it; returning before that leaves the session half-built for whatever the caller
231 // does next. Refusing AFTER the session is fully initialised gives the runner a well-formed
232 // session carrying a plain error verdict, which is the state its error path already expects
233 // from every other PROTOCOL_ERR return in this function.
234 // ★★★★★AN ERROR RETURN IS A CONTRACT: IT MUST LEAVE THE OBJECT AS INTACT AS THE SUCCESS PATH
235 // WOULD. A correct decision at the wrong point in a constructor is still a crash.
236 if session.client_offered_sup_ver == 0 {
237 return NX_TLS13_SSESSION_PROTOCOL_ERR
238 }
239
240 // Try to extract an X25519 key_share.
241 let ks_data_off_box: *i64 = sys_mmap(8) as *i64
242 let ks_data_len_box: *i64 = sys_mmap(8) as *i64
243 let kfv: i64 = tls13_ext_find(buf + ext_off, ext_len, EXT_KEY_SHARE,
244 ks_data_off_box, ks_data_len_box)
245 let pub_buf: *u8 = sys_mmap(32)
246 var have_x25519: i64 = 0
247 if kfv == NX_TLS13_HELLO_VERDICT_OK {
248 let ks_data_off: i64 = *ks_data_off_box
249 let ks_data_len: i64 = *ks_data_len_box
250 let ev: i64 = nx_tls13_recv_ch_extract_x25519_pub(
251 buf + ext_off + ks_data_off, ks_data_len, pub_buf)
252 if ev == NX_TLS13_SSESSION_OK { have_x25519 = 1 }
253 }
254
255 if have_x25519 == 1 {
256 session.client_x25519_pub = pub_buf
257 session.kex_group = 29
258 session.state = NX_TLS13_SSTATE_CH_RECEIVED
259 return NX_TLS13_SSESSION_OK
260 }
261
262 // No X25519 share -- try a secp256r1 share before any HRR
263 // (server side of rung B4-P256-KEYSHARE: a P-256-only client,
264 // e.g. our own ch-compat census leg, completes WITHOUT a
265 // round-trip). 65-byte uncompressed point; on-curve validation
266 // happens in derive_hs via p256_ecdh_shared.
267 let p256_buf: *u8 = sys_mmap(72)
268 if kfv == NX_TLS13_HELLO_VERDICT_OK {
269 let ev2: i64 = nx_tls13_recv_ch_extract_p256_pub(
270 buf + ext_off + *ks_data_off_box, *ks_data_len_box, p256_buf)
271 if ev2 == NX_TLS13_SSESSION_OK {
272 session.client_p256_pub = p256_buf
273 session.kex_group = 23
274 session.state = NX_TLS13_SSTATE_CH_RECEIVED
275 return NX_TLS13_SSESSION_OK
276 }
277 }
278
279 // No usable X25519 key_share. If the client lists X25519 in
280 // supported_groups, request it via HelloRetryRequest (state stays
281 // INIT so the caller can read ClientHello2 through recv_ch again);
282 // otherwise we genuinely cannot serve it (server supports only X25519).
283 let sg_off_box: *i64 = sys_mmap(8) as *i64
284 let sg_len_box: *i64 = sys_mmap(8) as *i64
285 let sgv: i64 = tls13_ext_find(buf + ext_off, ext_len, EXT_SUPPORTED_GROUPS,
286 sg_off_box, sg_len_box)
287 if sgv == NX_TLS13_HELLO_VERDICT_OK {
288 let sg_off: i64 = *sg_off_box
289 let sg_len: i64 = *sg_len_box
290 if nx_tls13_recv_ch_sg_has_x25519(buf + ext_off + sg_off, sg_len) == 1 {
291 return NX_TLS13_SSESSION_NEED_HRR
292 }
293 }
294 return NX_TLS13_SSESSION_PROTOCOL_ERR
295}