code wiki / (root) / nx_tls13_server_session_recv_ch.nx

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}