nx_tls13_client_session_emit_finished.nx source
↩ module page · 158 lines · 6236 B
1// nx_tls13_client_session_emit_finished.nx -- step 3c.4 of the
2// nx_https_client wiring arc.
3//
4// Builds the client_Finished handshake message + wraps it in an
5// AEAD-encrypted TLS record under the client handshake traffic
6// key. Caller writes the resulting bytes to fd. After this
7// primitive returns OK, the session is ready for step 3c.5
8// (derive application traffic keys; state -> CONNECTED).
9//
10// Composes 2 shipped primitives:
11// tls13_client_emit_finished -- 36-byte Finished + transcript
12// update (CRITICAL: appends CF to
13// transcript so app-key derivation
14// matches server's view)
15// nx_tls13_record_encrypt -- ChaCha20-Poly1305 record wrap
16//
17// Pipeline:
18// transcript_state at WAIT_CLIENT_FIN (covers CH..server_Finished)
19// ↓ snapshot
20// transcript_hash_CH..SF
21// ↓ tls13_finished_key(client_hs_traffic_secret) -> finished_key
22// ↓ HMAC(finished_key, transcript_hash)
23// verify_data (32 bytes)
24// ↓ wrap in handshake message: [HT_FINISHED][3-byte len=32][verify_data]
25// client_Finished_msg (36 bytes)
26// ↓ append to transcript (for app-key derivation hash)
27// ↓ AEAD encrypt under client_hs_traffic_key + client_hs_iv +
28// client_seq
29// encrypted_record (5-byte header + 36+1 inner + 16-byte tag = 58 bytes)
30//
31// Caller calls sys_write(fd, record_bytes, record_len) to send.
32//
33// Public API:
34// nx_tls13_client_session_emit_finished(
35// session, out_buf, out_cap
36// ) -> positive bytes-written | negative -NX_TLS13_EMIT_CF_* code
37// nx_tls13_emit_cf_verdict_is_valid(v) -> 0|1
38//
39// Sealed verdict:
40// NX_TLS13_EMIT_CF_OK positive rc = bytes written
41// NX_TLS13_EMIT_CF_BAD_STATE session not at WAIT_CLIENT_FIN
42// NX_TLS13_EMIT_CF_BUF_OVERFLOW out_buf too small (need ~58 bytes)
43// NX_TLS13_EMIT_CF_INTERNAL emit or encrypt returned non-OK
44//
45// Per Cardinals 9 (single-responsibility -- ONE handshake step),
46// 12 (defensive at boundaries -- state guard + buf cap check +
47// client_seq increment post-use to maintain nonce uniqueness),
48// 19 (composes shipped primitives unchanged), 22 (composition --
49// 2 shipped primitives stack into one step), 23 (preamble names
50// the transcript-update criticality from the loopback-test bug
51// history).
52//
53// license_tier: INDEPENDENT_REDERIVE
54// genealogy_id: international-research-sources/ietf/rfc_8446
55// lineage_id: nishi_tls13_client_session_emit_finished_q10
56
57// nx_safety_envelope:
58// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
59// sil_target: SIL1
60// evidence: [bulk_applied_2026-05-19, tls13-client-emit-cf-step-3c4]
61// verdict: NOT_YET_EVALUATED
62
63import "nx_syscalls.nx"
64import "nx_tls13.nx"
65import "nx_tls13_record.nx"
66import "nx_tls13_client.nx"
67import "nx_tls13_client_session.nx"
68import "nx_tls13_transcript.nx"
69
70const NX_TLS13_EMIT_CF_OK: i64 = 1
71const NX_TLS13_EMIT_CF_BAD_STATE: i64 = 2
72const NX_TLS13_EMIT_CF_BUF_OVERFLOW: i64 = 3
73const NX_TLS13_EMIT_CF_INTERNAL: i64 = 4
74const NX_TLS13_EMIT_CF_VERDICT_N: i64 = 5
75
76// client_Finished encrypted record byte budget:
77// header(5) + inner(36 + 1 type-byte) + tag(16) = 58 bytes
78const NX_TLS13_CF_RECORD_BYTES: i64 = 58
79
80func nx_tls13_emit_cf_verdict_is_valid(v: i64) -> i64 {
81 if v < NX_TLS13_EMIT_CF_OK { return 0 }
82 if v >= NX_TLS13_EMIT_CF_VERDICT_N { return 0 }
83 return 1
84}
85
86// Step 3c.4: build encrypted client_Finished record into out_buf.
87//
88// Returns POSITIVE bytes-written (= 58) on success. Returns
89// NEGATIVE (-NX_TLS13_EMIT_CF_*) on failure.
90func nx_tls13_client_session_emit_finished(
91 s: *Tls13ClientSession,
92 out_buf: *u8, out_cap: i64
93) -> i64 {
94 if s.state != NX_TLS13_CSESSION_STATE_WAIT_CLIENT_FIN {
95 return 0 - NX_TLS13_EMIT_CF_BAD_STATE
96 }
97 if out_cap < NX_TLS13_CF_RECORD_BYTES {
98 return 0 - NX_TLS13_EMIT_CF_BUF_OVERFLOW
99 }
100
101 // ---- Snapshot transcript at hash(CH..SF) BEFORE building CF ----
102 // RFC 8446 §7.1: application-traffic-secrets use Hash(ClientHello
103 // ...server Finished) -- the SAME prefix the CF verify_data uses,
104 // captured before the CF transcript update inside the emitter.
105 // R9: h_post_sf feeds the application-traffic derivation, so it must be the SUITE'S hash.
106 if s.cipher_suite == NX_TLS13_CS_AES_256_GCM_SHA384 {
107 nx_tls13_transcript_snapshot384(s.transcript, s.h_post_sf)
108 } else {
109 nx_tls13_transcript_snapshot(s.transcript, s.h_post_sf)
110 }
111
112 // ---- Build the 36-byte client_Finished handshake message ----
113 let cf_msg: *u8 = sys_mmap(64)
114 let cf_len: i64 = tls13_client_emit_finished(
115 s.client_hs_traffic_secret,
116 s.transcript,
117 cf_msg
118 )
119 if cf_len != 36 { return 0 - NX_TLS13_EMIT_CF_INTERNAL }
120
121 // ---- Encrypt as TLS record under client_hs_traffic_key ----
122 // Inner plaintext = cf_msg || real_content_type(1) = 37 bytes
123 // Ciphertext payload = inner + tag = 37 + 16 = 53 bytes
124 // Record total = header(5) + payload(53) = 58 bytes
125 let header_out: *u8 = out_buf // bytes 0..5
126 let ct_out: *u8 = out_buf + NX_TLS13_RECORD_HEADER_LEN // bytes 5..42
127 let tag_out: *u8 = out_buf + NX_TLS13_RECORD_HEADER_LEN + cf_len + 1 // bytes 42..58
128
129 let enc_v: i64 = nx_tls13_record_encrypt_v2(
130 s.cipher_suite,
131 s.client_hs_traffic_key,
132 s.client_hs_iv,
133 s.client_seq,
134 cf_msg, cf_len,
135 NX_TLS13_CT_HANDSHAKE,
136 0, // no padding
137 header_out,
138 ct_out,
139 tag_out
140 )
141
142 // Increment client_seq AFTER use regardless of verdict (same
143 // nonce-uniqueness discipline as recv_hs's server_seq).
144 s.client_seq = s.client_seq + 1
145
146 if enc_v != NX_TLS13_REC_VERDICT_OK { return 0 - NX_TLS13_EMIT_CF_INTERNAL }
147
148 // ---- Advance state ----
149 s.state = NX_TLS13_CSESSION_STATE_WAIT_APP_KEYS
150
151 return NX_TLS13_CF_RECORD_BYTES
152}
153
154// Compile-only smoke. Real KAT in
155// nx_tls13_client_session_emit_finished_test.nx.
156func main() -> i64 {
157 return 0
158}