code wiki / (root) / nx_tls13_server_session.nx

nx_tls13_server_session.nx source

↩ module page · 271 lines · 12176 B

1// nx_tls13_server_session.nx -- TLS 1.3 SERVER session state + state 2// machine. The inverse of nx_tls13_client_session. 3// 4// ===================================================================== 5// SUPERIOR-CAPABILITY MOTIVATION (per the 2026-05-20 cardinal): 6// 7// The substrate already ships full TLS 1.3 CLIENT side (handshake + 8// ALPN + record layer + auth) -- nishifamily.com fetches HTTPS today 9// bits-up. But to SERVE nishifamily.com itself bits-up we need 10// server-side handshake. No substrate currently does TLS 1.3 server 11// in NishiLang; this file is the first stone. 12// 13// Per docs/NISHIFAMILY_COM_ALL_BITS_UP_STATUS_2026_05_20.md, TLS 14// 1.3 server is the single biggest gap remaining for "all things 15// bits-up on nishifamily.com." Once landed, Phase D-E of the 16// master roadmap composes the shipped HTTP server + ACME + DNS + 17// static-site-builder into a full sovereign HTTPS stack. 18// ===================================================================== 19// 20// State machine (inverse of client; RFC 8446 §4): 21// 22// SSTATE_INIT 23// -> SSTATE_CH_RECEIVED (peer ClientHello parsed) 24// -> SSTATE_SH_SENT (we emitted ServerHello + key share) 25// -> SSTATE_EE_SENT (EncryptedExtensions written) 26// -> SSTATE_CERT_SENT (server Certificate) 27// -> SSTATE_CV_SENT (CertificateVerify) 28// -> SSTATE_SF_SENT (server Finished) 29// -> SSTATE_CLIENT_FIN_RECV (peer Finished parsed + verified) 30// -> SSTATE_CONNECTED (app data flow OK) 31// 32// Public API (this commit): 33// struct Tls13ServerSession { /* see below */ } 34// nx_tls13_server_session_new(server_random, server_x25519_priv) 35// -> *Tls13ServerSession 36// nx_tls13_server_state_is_valid(s) -> 0|1 37// 38// Queued for next session (each its own commit, matching the 39// client-session arc structure): 40// nx_tls13_server_session_recv_ch parse ClientHello 41// nx_tls13_server_session_emit_sh emit ServerHello + KEX 42// nx_tls13_server_session_emit_ee_cert emit EE + Certificate 43// nx_tls13_server_session_emit_cv_sf emit CertificateVerify + Finished 44// nx_tls13_server_session_recv_cf parse client Finished 45// nx_tls13_server_session_derive_app handshake -> app traffic keys 46// 47// Sealed verdict enum (mirrors client-session pattern): 48// NX_TLS13_SSESSION_OK positive return / bytes-written / success 49// NX_TLS13_SSESSION_BAD_STATE called step outside expected state 50// NX_TLS13_SSESSION_BUF_OVERFLOW output buffer too small 51// NX_TLS13_SSESSION_INTERNAL internal emit/parse returned negative 52// NX_TLS13_SSESSION_PROTOCOL_ERR peer sent malformed handshake bytes 53// 54// license_tier: ORIGINAL 55 56import "nx_syscalls.nx" 57import "nx_x25519.nx" 58import "nx_x25519_ephemeral.nx" 59import "nx_tls13.nx" 60import "nx_tls13_transcript.nx" 61import "nx_p256_ecdh.nx" 62 63// ===== state sealed enum ========================================== 64 65const NX_TLS13_SSTATE_INIT: i64 = 0 66const NX_TLS13_SSTATE_CH_RECEIVED: i64 = 1 67const NX_TLS13_SSTATE_SH_SENT: i64 = 2 68const NX_TLS13_SSTATE_EE_SENT: i64 = 3 69const NX_TLS13_SSTATE_CERT_SENT: i64 = 4 70const NX_TLS13_SSTATE_CV_SENT: i64 = 5 71const NX_TLS13_SSTATE_SF_SENT: i64 = 6 72const NX_TLS13_SSTATE_CLIENT_FIN_RECV: i64 = 7 73const NX_TLS13_SSTATE_CONNECTED: i64 = 8 74const NX_TLS13_SSTATE_N: i64 = 9 75 76func nx_tls13_server_state_is_valid(s: i64) -> i64 { 77 if s < 0 { return 0 } 78 if s >= NX_TLS13_SSTATE_N { return 0 } 79 return 1 80} 81 82// ===== verdicts ==================================================== 83 84const NX_TLS13_SSESSION_OK: i64 = 0 85const NX_TLS13_SSESSION_BAD_STATE: i64 = 1 86const NX_TLS13_SSESSION_BUF_OVERFLOW: i64 = 2 87const NX_TLS13_SSESSION_INTERNAL: i64 = 3 88const NX_TLS13_SSESSION_PROTOCOL_ERR: i64 = 4 89// recv_ch: ClientHello carried no usable X25519 key_share but DID list 90// X25519 in supported_groups -> caller must send a HelloRetryRequest 91// and read a second ClientHello. Not an error; a handshake fork. 92const NX_TLS13_SSESSION_NEED_HRR: i64 = 5 93 94// ===== struct ====================================================== 95// 96// Mirrors Tls13ClientSession (208 bytes, 26 fields) with the 97// perspective flipped (server's own random + key, peer's = client_*). 98// Most fields are zero at session-new and populated by subsequent 99// orchestrator steps as the handshake progresses. 100 101struct Tls13ServerSession { 102 // Filled at session-new: 103 server_random: *u8, // 32 bytes 104 x25519_priv: *u8, // 32 bytes (server's ephemeral) 105 x25519_pub: *u8, // 32 bytes (derived) 106 transcript: *u8, // running transcript-hash state 107 108 // Populated by recv_ch step: 109 state: i64, 110 client_x25519_pub: *u8, // peer key share extension value 111 // RFC 8446 §4.1.3 requires the server to byte-for-byte echo the 112 // client's legacy_session_id in the ServerHello. Captured here 113 // by recv_ch + replayed by emit_sh. Added 2026-05-20 -- without 114 // this, real clients (openssl, browsers) abort with "invalid 115 // session id" mid-handshake. 116 client_session_id: *u8, // up to 32 bytes 117 client_session_id_len: i64, 118 119 // Populated by SH-derive step (after ServerHello sent): 120 handshake_secret: *u8, 121 client_hs_traffic_secret: *u8, 122 server_hs_traffic_secret: *u8, 123 client_hs_traffic_key: *u8, 124 server_hs_traffic_key: *u8, 125 client_hs_iv: *u8, 126 server_hs_iv: *u8, 127 128 // Record-layer sequence numbers per direction: 129 client_seq: i64, 130 server_seq: i64, 131 132 // App-traffic state derived after CONNECTED: 133 master_secret: *u8, 134 client_app_traffic_secret: *u8, 135 server_app_traffic_secret: *u8, 136 client_app_traffic_key: *u8, 137 server_app_traffic_key: *u8, 138 client_app_iv: *u8, 139 server_app_iv: *u8, 140 client_app_seq: i64, 141 server_app_seq: i64, 142 143 // Negotiated AEAD cipher suite (TLS 1.3 wire value): 0x1303 144 // ChaCha20-Poly1305 (default) or 0x1301 AES-128-GCM. Set by 145 // recv_ch from the client's offered list; both use SHA-256 so the 146 // key schedule/transcript are identical -- only the AEAD + key 147 // length (32 vs 16) differ. Added at the END so existing field 148 // offsets are unchanged. 149 cipher_suite: i64, 150 151 // P-256 key exchange (rung: server-side of B4-P256-KEYSHARE, 152 // 2026-06-10). kex_group records which group recv_ch accepted 153 // (29 = x25519 default, 23 = secp256r1 fallback); emit_sh and 154 // derive_hs branch on it. The server P-256 ephemeral is derived 155 // in session-new from the same caller-supplied 32 priv bytes 156 // (domain-separated SHA-256 mod n) so the 2-arg constructor 157 // contract is unchanged. Added at the END: prior offsets stable. 158 kex_group: i64, 159 client_p256_pub: *u8, // 65 bytes, 0x04 || X || Y (recv_ch) 160 p256_priv: *u8, // 32 bytes (derived) 161 p256_pub: *u8, // 65 bytes (derived) 162 163 // TLS-1.2 INTEROP FIX (2026-07-31). RFC 8446 4.2 and RFC 5246 both forbid a server from 164 // sending an extension the client did not offer. We emitted supported_versions 165 // UNCONDITIONALLY in ServerHello, so a TLS-1.2-only ClientHello got back a 1.3-only 166 // extension and every conformant client aborted -- OpenSSL with 167 // u0027tls_collect_extensions: unsolicited extensionu0027. Net effect: NO 1.2-only client could 168 // reach ANY site we serve, and it was invisible because curl/browsers/our own client all 169 // negotiate 1.3 and never exercise that path. recv_ch now records what was actually 170 // offered and emit_sh refuses to invent it. Same failure family as client_session_id 171 // above, and the same fix: echo/answer only what the peer actually sent. 172 // Added at the END so prior field offsets are unchanged. 173 client_offered_sup_ver: i64, 174} 175 176const NX_TLS13_SSESSION_BYTES: i64 = 272 // 34 fields × 8 bytes (added client_offered_sup_ver 2026-07-31) 177 178// ===== construction ================================================ 179// 180// Caller provides server-side entropy (server_random) + ephemeral 181// X25519 private key. We derive the matching public key. 182// Transcript hash state is initialised but empty (first input will 183// be the peer's ClientHello bytes in the recv_ch step). 184// 185// Returns 0-ptr on bad input. 186 187func nx_tls13_server_session_new(server_random: *u8, 188 server_x25519_priv: *u8) -> *Tls13ServerSession { 189 if (server_random as i64) == 0 { return 0 as *Tls13ServerSession } 190 if (server_x25519_priv as i64) == 0 { return 0 as *Tls13ServerSession } 191 192 let s: *Tls13ServerSession = (sys_mmap(NX_TLS13_SSESSION_BYTES)) as *Tls13ServerSession 193 194 // Copy caller-supplied random + priv into owned buffers. 195 let rnd_buf: *u8 = sys_mmap(32) 196 let pri_buf: *u8 = sys_mmap(32) 197 var i: i64 = 0 198 while i < 32 { 199 rnd_buf[i] = server_random[i] 200 pri_buf[i] = server_x25519_priv[i] 201 i = i + 1 202 } 203 204 // Derive server's X25519 public key from the private key. 205 let pub_buf: *u8 = sys_mmap(32) 206 x25519_keypair_public(pri_buf, pub_buf) 207 208 // Allocate transcript hash state via the canonical constructor. 209 // BUG-FIX 2026-05-20: previously used raw sys_mmap(256) which left 210 // the SHA-256 IV at all-zero instead of the FIPS 180-4 constants 211 // (0x6a09e667 ..). All transcript-hash-dependent derivations 212 // (handshake_secret -> traffic keys -> AEAD key/iv) silently 213 // diverged from openssl, producing "decryption failed or bad 214 // record mac" on the first encrypted record (EncryptedExtensions). 215 // Composes the canonical nx_tls13_transcript_new which calls 216 // sha256_init internally (same path the client session uses). 217 // See [[project-tls-server-session-id-fix-2026-05-20]] + 218 // [[feedback-no-false-ok-substrate-honesty-audit]]. 219 let trans_buf: *u8 = nx_tls13_transcript_new() 220 221 s.server_random = rnd_buf 222 s.x25519_priv = pri_buf 223 s.x25519_pub = pub_buf 224 s.transcript = trans_buf 225 226 s.state = NX_TLS13_SSTATE_INIT 227 s.client_x25519_pub = 0 as *u8 228 229 // Zero-init derived-secret fields (populated by later steps). 230 s.handshake_secret = 0 as *u8 231 s.client_hs_traffic_secret = 0 as *u8 232 s.server_hs_traffic_secret = 0 as *u8 233 s.client_hs_traffic_key = 0 as *u8 234 s.server_hs_traffic_key = 0 as *u8 235 s.client_hs_iv = 0 as *u8 236 s.server_hs_iv = 0 as *u8 237 s.client_seq = 0 238 s.server_seq = 0 239 s.master_secret = 0 as *u8 240 s.client_app_traffic_secret = 0 as *u8 241 s.server_app_traffic_secret = 0 as *u8 242 s.client_app_traffic_key = 0 as *u8 243 s.server_app_traffic_key = 0 as *u8 244 s.client_app_iv = 0 as *u8 245 s.server_app_iv = 0 as *u8 246 s.client_app_seq = 0 247 s.server_app_seq = 0 248 // Safe default: the proven ChaCha20-Poly1305 path. recv_ch only 249 // negotiates AES-128-GCM when the client doesn't offer ChaCha20, so 250 // any negotiation gap leaves browsers on the unchanged path. 251 s.cipher_suite = 0x1303 252 253 // P-256 ephemeral (server side of B4): derived, not separately 254 // supplied. kex_group stays x25519 unless recv_ch flips it. 255 s.kex_group = 29 256 s.client_p256_pub = 0 as *u8 257 s.p256_priv = sys_mmap(32) 258 s.p256_pub = sys_mmap(72) 259 p256_ecdh_derive_priv(pri_buf, s.p256_priv) 260 p256_ecdh_pub(s.p256_priv, s.p256_pub) 261 262 return s 263} 264 265// AEAD key length for a TLS 1.3 cipher suite: AES-128-GCM (0x1301) = 16 266// bytes, ChaCha20-Poly1305 (0x1303) = 32. Nonce/IV is 12 for both; both 267// use SHA-256, so secrets/transcript are identical across the two. 268func nx_tls13_aead_key_len(cipher_suite: i64) -> i64 { 269 if cipher_suite == 0x1301 { return 16 } 270 return 32 271}