code wiki / (root) / nx_tls13_client_session_recv_hs.nx

nx_tls13_client_session_recv_hs.nx source

↩ module page · 266 lines · 11992 B

1// nx_tls13_client_session_recv_hs.nx -- step 3c.3 of the 2// nx_https_client wiring arc. 3// 4// Consumes ONE encrypted TLS record (the caller has already read 5// it from fd into a buffer, including the 5-byte TLSCiphertext 6// header) and walks the dispatch loop: 7// 8// 1. Decrypt record with session.server_hs_traffic_key + IV + 9// session.server_seq via shipped nx_tls13_record_decrypt 10// -> produces inner plaintext + real ContentType 11// 2. Increment session.server_seq for next record 12// 3. Reject record if ContentType != 22 (HANDSHAKE) 13// 4. Walk the inner plaintext extracting handshake messages 14// (each is [type:1][length:3][body:length]) 15// 5. For each handshake message: 16// - Call shipped tls13_client_dispatch_with_validation 17// (which enforces cert validation at WAIT_CERT via the 18// bridge shipped in c5ddbf81) 19// - Update session.state from out_new_state 20// - If dispatcher returned non-OK -> bail with mapped verdict 21// 6. Return OK after processing all messages in the record 22// 23// Caller invokes this once per record received from fd. After 24// multiple calls the session reaches CONNECTED state (server 25// Finished verified), at which point step 3c.4 emits the client 26// Finished. 27// 28// Public API: 29// nx_tls13_client_session_recv_hs( 30// session, record_bytes, record_len, val_ctx 31// ) -> verdict 32// nx_tls13_recv_hs_verdict_is_valid(v) -> 0|1 33// 34// Sealed verdict enum: 35// NX_TLS13_RECV_HS_OK record processed (may be partial 36// handshake progress; check 37// session.state) 38// NX_TLS13_RECV_HS_BAD_STATE session not in WAIT_EE..WAIT_SF 39// NX_TLS13_RECV_HS_RECORD_FAIL record decrypt verdict non-OK 40// NX_TLS13_RECV_HS_WRONG_CT decrypted ContentType != 22 41// NX_TLS13_RECV_HS_MSG_TOO_SHORT handshake message header invalid 42// NX_TLS13_RECV_HS_DISPATCH_FAIL dispatcher rejected (cert verify 43// failed / bad MAC / bad format etc) 44// NX_TLS13_RECV_HS_INTERNAL out_of_band parse failure 45// 46// Per Cardinals 9 (single-responsibility -- ONE record per call), 47// 12 (defensive at boundaries -- 6 distinct verdict mappings; 48// server_seq incremented BEFORE any error return so replay 49// counters track delivered records), 19 (composes shipped 50// dispatcher unchanged), 22 (composition -- 3 shipped primitives 51// compose into one orchestrator step), 23 (preamble names what 52// the caller does between calls + the multi-call contract). 53// 54// license_tier: INDEPENDENT_REDERIVE 55// genealogy_id: international-research-sources/ietf/rfc_8446 56// lineage_id: nishi_tls13_client_session_recv_hs_q10 57 58// nx_safety_envelope: 59// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 60// sil_target: SIL1 61// evidence: [bulk_applied_2026-05-19, tls13-client-recv-hs-step-3c3] 62// verdict: NOT_YET_EVALUATED 63 64import "nx_syscalls.nx" 65import "nx_tls13_record.nx" 66import "nx_tls13_client.nx" 67import "nx_tls13_client_validate_certificate.nx" 68import "nx_tls13_client_dispatch_with_validation.nx" 69import "nx_tls13_client_session.nx" 70import "nx_tls13_client_verify_cv.nx" // CertificateVerify signature check (closes the client auth-bypass stub) 71const NX_MAGIC_8192: i64 = 8192 72 73const NX_TLS13_RECV_HS_OK: i64 = 1 74const NX_TLS13_RECV_HS_BAD_STATE: i64 = 2 75const NX_TLS13_RECV_HS_RECORD_FAIL: i64 = 3 76const NX_TLS13_RECV_HS_WRONG_CT: i64 = 4 77const NX_TLS13_RECV_HS_MSG_TOO_SHORT: i64 = 5 78const NX_TLS13_RECV_HS_DISPATCH_FAIL: i64 = 6 79const NX_TLS13_RECV_HS_INTERNAL: i64 = 7 80const NX_TLS13_RECV_HS_VERDICT_N: i64 = 8 81 82func nx_tls13_recv_hs_verdict_is_valid(v: i64) -> i64 { 83 if v < NX_TLS13_RECV_HS_OK { return 0 } 84 if v >= NX_TLS13_RECV_HS_VERDICT_N { return 0 } 85 return 1 86} 87 88// Map the underlying dispatch-with-validation enum into the 89// recv-hs vocabulary. OK passes through; all others collapse to 90// DISPATCH_FAIL since the recv-hs caller doesn't need to 91// distinguish (it just knows the handshake stalled). 92// 93// Internally the dispatcher's exact verdict is observable via 94// session.state (unchanged on failure) + the underlying KAT. 95func _map_dwv_verdict(v: i64) -> i64 { 96 if v == NX_TLS13_DWV_OK { return NX_TLS13_RECV_HS_OK } 97 return NX_TLS13_RECV_HS_DISPATCH_FAIL 98} 99 100// Process ONE encrypted record. Caller has read the record off 101// fd into record_bytes; record_len is the total byte count (5-byte 102// header + ciphertext + 16-byte AEAD tag). 103func nx_tls13_client_session_recv_hs( 104 s: *Tls13ClientSession, 105 record_bytes: *u8, record_len: i64, 106 val_ctx: *TlsValidationContext 107) -> i64 { 108 // Session must be in one of the four "expecting handshake 109 // messages" states. 110 if s.state < NX_TLS13_CSESSION_STATE_WAIT_EE { return NX_TLS13_RECV_HS_BAD_STATE } 111 if s.state > NX_TLS13_CSESSION_STATE_WAIT_SF { return NX_TLS13_RECV_HS_BAD_STATE } 112 113 if record_len < NX_TLS13_RECORD_HEADER_LEN + 1 + NX_TLS13_RECORD_TAG_LEN { 114 return NX_TLS13_RECV_HS_RECORD_FAIL 115 } 116 117 // Split record_bytes into (header[0..5], ciphertext[5..n-16], tag[n-16..n]). 118 let header: *u8 = record_bytes 119 let ct: *u8 = record_bytes + NX_TLS13_RECORD_HEADER_LEN 120 let ct_len: i64 = record_len - NX_TLS13_RECORD_HEADER_LEN - NX_TLS13_RECORD_TAG_LEN 121 let tag: *u8 = record_bytes + record_len - NX_TLS13_RECORD_TAG_LEN 122 123 let inner: *u8 = sys_mmap(ct_len + 16) 124 let real_ct_p: *i64 = sys_mmap(16) as *i64 125 let real_len_p: *i64 = sys_mmap(16) as *i64 126 127 let dv: i64 = nx_tls13_record_decrypt_v2( 128 s.cipher_suite, 129 s.server_hs_traffic_key, 130 s.server_hs_iv, 131 s.server_seq, 132 header, 133 ct, ct_len, 134 tag, 135 inner, 136 real_ct_p, 137 real_len_p 138 ) 139 140 // Increment server_seq AFTER use (the next record uses seq+1). 141 s.server_seq = s.server_seq + 1 142 143 if dv != NX_TLS13_REC_VERDICT_OK { return NX_TLS13_RECV_HS_RECORD_FAIL } 144 145 if *real_ct_p != NX_TLS13_CT_HANDSHAKE { return NX_TLS13_RECV_HS_WRONG_CT } 146 147 // ---- REASSEMBLY (RFC 8446 ยง5.1): append this record's decrypted plaintext to the cross-record 148 // accumulator, then parse only COMPLETE handshake messages; carry any partial-message remainder to 149 // the next record. Meta/CDN servers fragment the Certificate across records; without this we bailed. ---- 150 let plaintext_len: i64 = *real_len_p 151 let hb: *u8 = s.hs_reasm 152 if s.hs_reasm_len + plaintext_len > NX_TLS13_HS_REASM_BYTES { 153 return NX_TLS13_RECV_HS_MSG_TOO_SHORT // flight exceeds our 64KB bound -> reject (DoS guard) 154 } 155 var ai: i64 = 0 156 while ai < plaintext_len { hb[s.hs_reasm_len + ai] = inner[ai]; ai = ai + 1 } 157 let total: i64 = s.hs_reasm_len + plaintext_len 158 var off: i64 = 0 159 let new_state_p: *i64 = sys_mmap(16) as *i64 160 161 while off < total { 162 // Fewer than 4 header bytes buffered -> carry the remainder, wait for the next record. 163 if off + 4 > total { 164 var c1: i64 = 0 165 while c1 < total - off { hb[c1] = hb[off + c1]; c1 = c1 + 1 } 166 s.hs_reasm_len = total - off 167 return NX_TLS13_RECV_HS_OK 168 } 169 let body_len: i64 = ((hb[off + 1] & 0xff) << 16) 170 | ((hb[off + 2] & 0xff) << 8) 171 | (hb[off + 3] & 0xff) 172 let msg_len: i64 = 4 + body_len 173 // Message fragmented across records -> carry [off..total] to the front, wait for the next record. 174 if off + msg_len > total { 175 var c2: i64 = 0 176 while c2 < total - off { hb[c2] = hb[off + c2]; c2 = c2 + 1 } 177 s.hs_reasm_len = total - off 178 return NX_TLS13_RECV_HS_OK 179 } 180 181 let cur_state: i64 = s.state 182 183 // WAIT_CV: verify the server's CertificateVerify signature over Transcript-Hash(CH..Certificate) 184 // BEFORE the dispatcher folds CV into the transcript. This is the possession-proof the old dispatcher 185 // stubbed out; without it a MITM presenting the (public) real chain + a garbage CV would be accepted. 186 if cur_state == NX_TLS13_CSESSION_STATE_WAIT_CV { 187 // R9: the CertificateVerify transcript hash is the SUITE'S hash (48B for 0x1302). 188 let th_cv: *u8 = sys_mmap(64) 189 if s.cipher_suite == NX_TLS13_CS_AES_256_GCM_SHA384 { 190 if nx_tls13_transcript_snapshot384(s.transcript, th_cv) != NX_TLS13_TX_VERDICT_OK { return NX_TLS13_RECV_HS_INTERNAL } 191 } else { 192 if nx_tls13_transcript_snapshot(s.transcript, th_cv) != NX_TLS13_TX_VERDICT_OK { return NX_TLS13_RECV_HS_INTERNAL } 193 } 194 let cvv: i64 = nx_tls13_client_verify_server_cv(hb + off, msg_len, s.leaf_cert, s.leaf_cert_len, th_cv) 195 if cvv != NX_CVV_OK { return NX_TLS13_RECV_HS_DISPATCH_FAIL } 196 } 197 198 // Dispatch via the validation-enforcing wrapper. Map the 199 // CSESSION state space to the CSTATE space the dispatcher 200 // expects (they happen to be identical for WAIT_* states). 201 let cstate: i64 = s.state - NX_TLS13_CSESSION_STATE_WAIT_EE + NX_TLS13_CSTATE_WAIT_EE 202 203 let dv2: i64 = tls13_client_dispatch_with_validation( 204 cstate, 205 hb + off, msg_len, 206 s.server_hs_traffic_secret, 207 s.transcript, 208 new_state_p, 209 val_ctx 210 ) 211 if dv2 != NX_TLS13_DWV_OK { 212 return _map_dwv_verdict(dv2) 213 } 214 215 // Convert dispatcher's new CSTATE back into CSESSION state. 216 if *new_state_p == NX_TLS13_CSTATE_WAIT_EE { 217 s.state = NX_TLS13_CSESSION_STATE_WAIT_EE 218 } else { if *new_state_p == NX_TLS13_CSTATE_WAIT_CERT { 219 s.state = NX_TLS13_CSESSION_STATE_WAIT_CERT 220 } else { if *new_state_p == NX_TLS13_CSTATE_WAIT_CV { 221 s.state = NX_TLS13_CSESSION_STATE_WAIT_CV 222 } else { if *new_state_p == NX_TLS13_CSTATE_WAIT_SF { 223 s.state = NX_TLS13_CSESSION_STATE_WAIT_SF 224 } else { if *new_state_p == NX_TLS13_CSTATE_CONNECTED { 225 // RFC 8446: server Finished verified; client must 226 // still emit its own Finished (step 3c.4) before the 227 // session is fully usable for app data. We mark a 228 // distinct WAIT_CLIENT_FIN state so 3c.4 knows. 229 s.state = NX_TLS13_CSESSION_STATE_WAIT_CLIENT_FIN 230 } else { return NX_TLS13_RECV_HS_INTERNAL } } } } } 231 232 // WAIT_CERT just processed (dispatch validated the chain): capture the server leaf cert DER so the 233 // next message (CertificateVerify) can be verified against the leaf public key. 234 if cur_state == NX_TLS13_CSESSION_STATE_WAIT_CERT { 235 let p_co: *i64 = sys_mmap(16) as *i64 236 let p_cl: *i64 = sys_mmap(16) as *i64 237 let p_cert_off: *i64 = sys_mmap(16) as *i64 238 let p_cert_len: *i64 = sys_mmap(16) as *i64 239 let p_ce_off: *i64 = sys_mmap(16) as *i64 240 let p_ce_len: *i64 = sys_mmap(16) as *i64 241 let p_chain: *i64 = sys_mmap(16) as *i64 242 if tls13_parse_certificate_chain_first(hb + off, msg_len, 243 p_co, p_cl, p_cert_off, p_cert_len, p_ce_off, p_ce_len, p_chain) == NX_TLS13_AUTH_VERDICT_OK { 244 let clen: i64 = p_cert_len[0] 245 if clen > 0 { if clen <= NX_MAGIC_8192 { 246 let src: *u8 = (hb as i64 + off + p_cert_off[0]) as *u8 247 var ci: i64 = 0 248 while ci < clen { s.leaf_cert[ci] = src[ci]; ci = ci + 1 } 249 s.leaf_cert_len = clen 250 } } 251 } 252 } 253 254 off = off + msg_len 255 } 256 257 // All buffered messages consumed exactly on a record boundary -> accumulator drained. 258 s.hs_reasm_len = 0 259 return NX_TLS13_RECV_HS_OK 260} 261 262// Compile-only smoke. Real KAT in 263// nx_tls13_client_session_recv_hs_test.nx. 264func main() -> i64 { 265 return 0 266}