code wiki / (root) / nx_tls13_client_session_recv_sh.nx

nx_tls13_client_session_recv_sh.nx source

↩ module page · 270 lines · 12786 B

1// nx_tls13_client_session_recv_sh.nx -- step 3c.2 of the 2// nx_https_client wiring arc. 3// 4// Consumes the bytes of a TLS 1.3 ServerHello handshake message 5// (provided by the caller -- this step doesn't do fd IO; that's 6// the orchestrator wrapper 3c.6 in a later commit) and walks the 7// full handshake-key-derivation cascade: 8// 9// 1. Parse the ServerHello message (via shipped tls13_server_hello_parse) 10// 2. Find the key_share extension + extract server's X25519 pubkey 11// 3. Compute shared_secret = x25519(my_priv, server_pub) 12// 4. Run TLS 1.3 key schedule cascade (RFC 8446 §7.1): 13// early_secret = HKDF-Extract(0, 0) 14// derived_1 = Derive-Secret(early, "derived", "") 15// handshake_secret = HKDF-Extract(derived_1, shared_secret) 16// 5. Feed ServerHello bytes into transcript (so H1 = hash(CH || SH)) 17// 6. Snapshot transcript -> H1 18// 7. Derive c_hs_traffic_secret + s_hs_traffic_secret using H1 19// 8. Expand each into AEAD key (32B) + IV (12B for ChaCha20-Poly1305 20// or AES-GCM) 21// 9. Reset per-direction sequence numbers to 0 22// 10. Advance state CH_SENT -> WAIT_EE 23// 24// After this primitive returns OK, the session is ready for the 25// 3c.3 step (recv encrypted handshake records, decrypt with 26// server_hs_traffic_key, dispatch via tls13_client_dispatch_with_validation). 27// 28// Public API: 29// nx_tls13_client_session_recv_sh(session, sh_bytes, sh_len) -> verdict 30// nx_tls13_recv_sh_verdict_is_valid(v) -> 0|1 31// 32// Sealed verdict enum: 33// NX_TLS13_RECV_SH_OK keys derived, state advanced 34// NX_TLS13_RECV_SH_BAD_STATE session not in CH_SENT 35// NX_TLS13_RECV_SH_BAD_FORMAT SH parse failed 36// NX_TLS13_RECV_SH_NO_KEYSHARE SH extensions missing key_share 37// NX_TLS13_RECV_SH_BAD_GROUP key_share group != X25519 (29) 38// NX_TLS13_RECV_SH_BAD_KEYSHARE_LEN server keyshare != 32 bytes 39// NX_TLS13_RECV_SH_INTERNAL HKDF/SHA-256/x25519 failed 40// 41// Per Cardinals 9 (single-responsibility -- ONE handshake step), 42// 12 (defensive at boundaries -- state check + every parse-step 43// verdict mapped explicitly), 19 (API contract stability -- 44// composes shipped primitives without modifying them), 22 45// (composition -- 7 shipped primitives compose into one step), 23 46// (preamble names the queued next step). 47// 48// license_tier: INDEPENDENT_REDERIVE 49// genealogy_id: international-research-sources/ietf/rfc_8446 50// lineage_id: nishi_tls13_client_session_recv_sh_q10 51 52// nx_safety_envelope: 53// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 54// sil_target: SIL1 55// evidence: [bulk_applied_2026-05-19, tls13-client-recv-sh-step-3c2] 56// verdict: NOT_YET_EVALUATED 57 58import "nx_syscalls.nx" 59import "nx_sha256.nx" 60import "nx_x25519.nx" 61import "nx_tls13.nx" 62import "nx_tls13_hello.nx" 63import "nx_tls13_transcript.nx" 64import "nx_tls13_kdf.nx" 65import "nx_tls13_schedule.nx" 66import "nx_tls13_client_session.nx" 67import "nx_p256_ecdh.nx" 68 69const NX_TLS13_RECV_SH_OK: i64 = 1 70const NX_TLS13_RECV_SH_BAD_STATE: i64 = 2 71const NX_TLS13_RECV_SH_BAD_FORMAT: i64 = 3 72const NX_TLS13_RECV_SH_NO_KEYSHARE: i64 = 4 73const NX_TLS13_RECV_SH_BAD_GROUP: i64 = 5 74const NX_TLS13_RECV_SH_BAD_KEYSHARE_LEN: i64 = 6 75const NX_TLS13_RECV_SH_INTERNAL: i64 = 7 76const NX_TLS13_RECV_SH_BAD_CIPHER: i64 = 8 // server picked cipher we don't speak 77// A TLS-1.2 suite in a ServerHello is NOT a cipher gap -- it means the server DOES NOT SPEAK TLS 1.3 and 78// negotiated down. Folding that into BAD_CIPHER is what sent two sessions hunting a cipher implementation 79// for graphis.ne.jp (Apache 2.2.31 / OpenSSL 1.0.0-fips, which predates RFC 8446 by eight years and can 80// never do 1.3). The two conditions have OPPOSITE remedies -- BAD_CIPHER means BUILD the suite, NOT_TLS13 81// means STOP and use the 1.2 leg -- so a verdict that cannot tell them apart sends the reader the wrong way. 82// ★A DIAGNOSTIC THAT MERGES TWO CAUSES WITH OPPOSITE FIXES IS WORSE THAN NO DIAGNOSTIC. 83const NX_TLS13_RECV_SH_NOT_TLS13: i64 = 9 // server negotiated DOWN: chose a non-0x13xx suite 84const NX_TLS13_RECV_SH_VERDICT_N: i64 = 10 85 86const NX_TLS13_GROUP_X25519: i64 = 29 87const NX_TLS13_GROUP_SECP256R1: i64 = 23 88 89func nx_tls13_recv_sh_verdict_is_valid(v: i64) -> i64 { 90 if v < NX_TLS13_RECV_SH_OK { return 0 } 91 if v >= NX_TLS13_RECV_SH_VERDICT_N { return 0 } 92 return 1 93} 94 95// Step 3c.2: consume ServerHello + derive handshake keys. 96func nx_tls13_client_session_recv_sh( 97 s: *Tls13ClientSession, 98 sh_bytes: *u8, sh_len: i64 99) -> i64 { 100 if s.state != NX_TLS13_CSESSION_STATE_CH_SENT { 101 return NX_TLS13_RECV_SH_BAD_STATE 102 } 103 104 // ---- Parse ServerHello structure ---- 105 let p_lv: *i64 = sys_mmap(16) as *i64 106 let p_ro: *i64 = sys_mmap(16) as *i64 107 let p_cs: *i64 = sys_mmap(16) as *i64 108 let p_eo: *i64 = sys_mmap(16) as *i64 109 let p_el: *i64 = sys_mmap(16) as *i64 110 let parse_v: i64 = tls13_server_hello_parse( 111 sh_bytes, sh_len, p_lv, p_ro, p_cs, p_eo, p_el 112 ) 113 if parse_v != NX_TLS13_HELLO_VERDICT_OK { 114 return NX_TLS13_RECV_SH_BAD_FORMAT 115 } 116 117 // ---- Accept the server's chosen cipher_suite iff it is one we 118 // OFFERED and can decrypt: ChaCha20-Poly1305 (0x1303) or 119 // AES-128-GCM (0x1301) -- BOTH are fully wired in the record 120 // layer (nx_tls13_record dispatches per s.cipher_suite). 121 // [Corrected 2026-07-03: previously claimed AES-128-GCM 122 // "dispatch is not wired" -- stale; it is live + decrypts.] 123 // Accept either advertised cipher: AES-128-GCM (0x1301) or 124 // ChaCha20-Poly1305 (0x1303). Record layer dispatches via 125 // tls13_record_aead_seal/open per s.cipher_suite. 126 var cipher_ok: i64 = 0 127 if *p_cs == NX_TLS13_CS_CHACHA20_POLY1305_SHA256 { cipher_ok = 1 } 128 if *p_cs == NX_TLS13_CS_AES_128_GCM_SHA256 { cipher_ok = 1 } 129 // R9 (2026-08-05): 0x1302 AES-256-GCM-SHA384 is now REAL, not just advertised. It is 130 // nginx's DEFAULT preference, so refusing it made a large slice of the web unreachable -- 131 // and the crawler then retired those hosts, laundering our capability gap into permanent 132 // coverage loss. Record layer: nx_tls13_record dispatches to nx_aes256_gcm (gate 4/4). 133 // Transcript: dual-hash from byte one (gate 4/4). Schedule/HKDF were ALREADY hash- 134 // parameterized. ★A CLIENTHELLO IS A PROMISE -- now we can keep the one we were making. 135 if *p_cs == NX_TLS13_CS_AES_256_GCM_SHA384 { cipher_ok = 1 } 136 // R9b: record the server's CHOICE BEFORE judging it. A refusal that cannot name the suite it 137 // refused forces every caller's diagnostic to GUESS -- and the guess ("it must be 0x1302") 138 // outlived its truth and sent a session hunting a cipher the server may never have chosen. 139 s.cipher_suite = *p_cs 140 if cipher_ok != 1 { 141 // Split the refusal by CAUSE. Every TLS 1.3 suite is 0x13xx; anything else means the server 142 // answered our 1.3 hello with a 1.2 negotiation, which no client-side cipher work can change. 143 if (*p_cs / 256) != 0x13 { return NX_TLS13_RECV_SH_NOT_TLS13 } 144 return NX_TLS13_RECV_SH_BAD_CIPHER 145 } 146 147 // ---- Find key_share extension ---- 148 let p_ks_off: *i64 = sys_mmap(16) as *i64 149 let p_ks_len: *i64 = sys_mmap(16) as *i64 150 let find_v: i64 = tls13_ext_find( 151 sh_bytes + *p_eo, *p_el, 152 EXT_KEY_SHARE, 153 p_ks_off, p_ks_len 154 ) 155 if find_v != NX_TLS13_HELLO_VERDICT_OK { 156 return NX_TLS13_RECV_SH_NO_KEYSHARE 157 } 158 159 // ---- Parse key_share entry: group(2) + ke_len(2) + ke ---- 160 // In SH the key_share is a SINGLE KeyShareEntry (RFC 8446 §4.2.8.2) 161 let ks_data_off: i64 = *p_eo + *p_ks_off 162 if *p_ks_len < 4 { return NX_TLS13_RECV_SH_BAD_FORMAT } 163 let group: i64 = ((sh_bytes[ks_data_off] & 0xff) << 8) 164 | (sh_bytes[ks_data_off + 1] & 0xff) 165 var group_ok: i64 = 0 166 if group == NX_TLS13_GROUP_X25519 { group_ok = 1 } 167 if group == NX_TLS13_GROUP_SECP256R1 { group_ok = 1 } 168 if group_ok != 1 { 169 return NX_TLS13_RECV_SH_BAD_GROUP 170 } 171 let ke_len: i64 = ((sh_bytes[ks_data_off + 2] & 0xff) << 8) 172 | (sh_bytes[ks_data_off + 3] & 0xff) 173 if 4 + ke_len > *p_ks_len { return NX_TLS13_RECV_SH_BAD_FORMAT } 174 175 // ---- Compute shared secret per the server's chosen group ---- 176 // (B4: the CH carries both an x25519 and a secp256r1 share, so 177 // either branch completes without a HelloRetryRequest.) 178 let shared: *u8 = sys_mmap(32) 179 var i: i64 = 0 180 if group == NX_TLS13_GROUP_X25519 { 181 if ke_len != 32 { 182 return NX_TLS13_RECV_SH_BAD_KEYSHARE_LEN 183 } 184 // Copy server X25519 pubkey into session. 185 while i < 32 { 186 s.server_x25519_pub[i] = sh_bytes[ks_data_off + 4 + i] 187 i = i + 1 188 } 189 let xv: i64 = x25519(s.x25519_priv, s.server_x25519_pub, shared) 190 if xv != 0 { return NX_TLS13_RECV_SH_INTERNAL } 191 } 192 if group == NX_TLS13_GROUP_SECP256R1 { 193 if ke_len != 65 { 194 return NX_TLS13_RECV_SH_BAD_KEYSHARE_LEN 195 } 196 // Off-curve / malformed server share is peer data -> BAD_FORMAT; 197 // a bad own scalar would be INTERNAL. 198 let pv: i64 = p256_ecdh_shared( 199 s.p256_priv, sh_bytes + ks_data_off + 4, 65, shared 200 ) 201 if pv == NX_P256_ECDH_BAD_POINT { return NX_TLS13_RECV_SH_BAD_FORMAT } 202 if pv != NX_P256_ECDH_OK { return NX_TLS13_RECV_SH_INTERNAL } 203 } 204 205 // ---- TLS 1.3 key schedule cascade ---- 206 let zeros: *u8 = sys_mmap(64) 207 // R9: the key schedule width IS the suite's hash. Buffers are 64 (not 32) so the 48-byte 208 // SHA-384 secrets fit -- a 32-byte buffer taking a 48-byte write is the silent-corruption 209 // shape this estate has been bitten by before. 210 var hash_len: i64 = TLS13_HASH_LEN_SHA256 211 if *p_cs == NX_TLS13_CS_AES_256_GCM_SHA384 { hash_len = 48 } 212 213 // Stage 1: early_secret = HKDF-Extract(0, 0) 214 let early: *u8 = sys_mmap(64) 215 tls13_early_secret(zeros, hash_len, early) 216 217 // Stage 2: derived_1 = Derive-Secret(early, "derived", "") with H_empty 218 // H_empty must be the SUITE'S hash of the empty string, not always SHA-256. 219 let empty_hash: *u8 = sys_mmap(64) 220 if hash_len == 48 { sha384_digest(zeros, 0, empty_hash) } else { sha256_digest(zeros, 0, empty_hash) } 221 let derived_1: *u8 = sys_mmap(64) 222 tls13_derived(early, empty_hash, hash_len, derived_1) 223 224 // Stage 3: handshake_secret = HKDF-Extract(derived_1, shared) 225 tls13_handshake_secret(derived_1, hash_len, shared, 32, s.handshake_secret) 226 227 // ---- Feed ServerHello into transcript so H1 = hash(CH || SH) ---- 228 nx_tls13_transcript_update(s.transcript, sh_bytes, sh_len) 229 230 // ---- H1 = snapshot of transcript hash (CH || SH) ---- 231 // R9: H1 must be hashed with the SUITE'S transcript hash. 232 let h1: *u8 = sys_mmap(64) 233 if hash_len == 48 { nx_tls13_transcript_snapshot384(s.transcript, h1) } else { nx_tls13_transcript_snapshot(s.transcript, h1) } 234 235 // ---- Derive c/s handshake traffic SECRETS via Derive-Secret ---- 236 // Stored in session because both the SF MAC verify (in 237 // dispatcher's WAIT_SF) AND the client_Finished emit (in 3c.4) 238 // need the SECRETS, not just the derived keys. Key updates 239 // during long sessions also need the parent secrets. 240 tls13_traffic_secret(s.handshake_secret, NX_TLS13_LABEL_C_HS_TRAFFIC, 241 h1, hash_len, s.client_hs_traffic_secret) 242 tls13_traffic_secret(s.handshake_secret, NX_TLS13_LABEL_S_HS_TRAFFIC, 243 h1, hash_len, s.server_hs_traffic_secret) 244 245 // ---- Expand each traffic secret into AEAD key + IV ---- 246 // Key width tracks the negotiated cipher_suite: 247 // AES-128-GCM-SHA256 (0x1301) -> key_len = 16 248 // ChaCha20-Poly1305-SHA256 (0x1303) -> key_len = 32 249 // Both use iv_len = 12. 250 var key_len: i64 = 32 251 if s.cipher_suite == NX_TLS13_CS_AES_128_GCM_SHA256 { key_len = 16 } 252 tls13_traffic_key(s.client_hs_traffic_secret, key_len, s.client_hs_traffic_key) 253 tls13_traffic_key(s.server_hs_traffic_secret, key_len, s.server_hs_traffic_key) 254 tls13_traffic_iv(s.client_hs_traffic_secret, 12, s.client_hs_iv) 255 tls13_traffic_iv(s.server_hs_traffic_secret, 12, s.server_hs_iv) 256 257 // ---- Reset per-direction record-layer sequence numbers ---- 258 s.client_seq = 0 259 s.server_seq = 0 260 261 // ---- Advance state ---- 262 s.state = NX_TLS13_CSESSION_STATE_WAIT_EE 263 return NX_TLS13_RECV_SH_OK 264} 265 266// Compile-only smoke. Real KAT in 267// nx_tls13_client_session_recv_sh_test.nx. 268func main() -> i64 { 269 return 0 270}