code wiki / (root) / nx_tls13_client_session.nx

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}