nx_tls13_client_session_derive_app.nx source
↩ module page · 133 lines · 5757 B
1// nx_tls13_client_session_derive_app.nx -- step 3c.5 of the
2// nx_https_client wiring arc.
3//
4// Runs the SECOND HALF of the RFC 8446 §7.1 key schedule cascade
5// to derive the application-traffic keys, advancing the session
6// from WAIT_APP_KEYS to CONNECTED. After this primitive returns
7// OK, the session is fully usable for application-data send/recv.
8//
9// Cascade (continuing from where 3c.2 left off):
10//
11// handshake_secret (from 3c.2)
12// |
13// v
14// Derive-Secret(., "derived", H_empty) = derived_2
15// |
16// v IKM = 0^HashLen
17// HKDF-Extract(salt=derived_2, IKM=0) = master_secret
18// |
19// v salt = master_secret; context = H2 = hash(CH..CF)
20// +--> Derive-Secret(., "c ap traffic", H2) = c_app_traffic_secret_0
21// +--> Derive-Secret(., "s ap traffic", H2) = s_app_traffic_secret_0
22// then per-secret expand:
23// key = HKDF-Expand-Label(secret, "key", "", 32)
24// iv = HKDF-Expand-Label(secret, "iv", "", 12)
25//
26// The transcript at this point covers CH..client_Finished
27// (CF was appended in 3c.4's emit). H2 = transcript snapshot.
28//
29// Once derived, app_seq counters are reset to 0 (per-direction
30// AEAD nonce sequence starts fresh for application data). State
31// advances to CONNECTED -- application send/recv is now safe.
32//
33// Public API:
34// nx_tls13_client_session_derive_app(session) -> verdict
35// nx_tls13_derive_app_verdict_is_valid(v) -> 0|1
36//
37// Sealed verdict:
38// NX_TLS13_DERIVE_APP_OK keys derived, state CONNECTED
39// NX_TLS13_DERIVE_APP_BAD_STATE session not at WAIT_APP_KEYS
40//
41// Per Cardinals 9 (single-responsibility -- ONE step: the final
42// derivation), 12 (defensive at boundaries -- state guard), 19
43// (composes shipped key-schedule primitives unchanged), 22
44// (composition -- 5 shipped primitives compose into one step),
45// 23 (preamble shows the cascade diagram).
46//
47// license_tier: INDEPENDENT_REDERIVE
48// genealogy_id: international-research-sources/ietf/rfc_8446
49// lineage_id: nishi_tls13_client_session_derive_app_q10
50
51// nx_safety_envelope:
52// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
53// sil_target: SIL1
54// evidence: [bulk_applied_2026-05-19, tls13-client-derive-app-step-3c5]
55// verdict: NOT_YET_EVALUATED
56
57import "nx_syscalls.nx"
58import "nx_sha256.nx"
59import "nx_tls13_transcript.nx"
60import "nx_tls13_schedule.nx"
61import "nx_tls13_client_session.nx"
62
63const NX_TLS13_DERIVE_APP_OK: i64 = 1
64const NX_TLS13_DERIVE_APP_BAD_STATE: i64 = 2
65const NX_TLS13_DERIVE_APP_VERDICT_N: i64 = 3
66
67func nx_tls13_derive_app_verdict_is_valid(v: i64) -> i64 {
68 if v < NX_TLS13_DERIVE_APP_OK { return 0 }
69 if v >= NX_TLS13_DERIVE_APP_VERDICT_N { return 0 }
70 return 1
71}
72
73// Step 3c.5: derive application-traffic keys + advance to CONNECTED.
74func nx_tls13_client_session_derive_app(s: *Tls13ClientSession) -> i64 {
75 if s.state != NX_TLS13_CSESSION_STATE_WAIT_APP_KEYS {
76 return NX_TLS13_DERIVE_APP_BAD_STATE
77 }
78
79 // R9 (2026-08-05): the application key schedule runs on the SUITE'S hash too. This was the
80 // last hardcoded SHA-256 in the client path -- missing it would have derived correct
81 // handshake keys and then WRONG application keys, i.e. a handshake that completes and then
82 // garbles the first byte of real data. Buffers 64 so the 48-byte SHA-384 secrets fit.
83 var hash_len: i64 = TLS13_HASH_LEN_SHA256
84 if s.cipher_suite == NX_TLS13_CS_AES_256_GCM_SHA384 { hash_len = 48 }
85 let zeros: *u8 = sys_mmap(64)
86
87 // ---- derived_2 = Derive-Secret(handshake_secret, "derived", H_empty) ----
88 let empty_hash: *u8 = sys_mmap(64)
89 if hash_len == 48 { sha384_digest(zeros, 0, empty_hash) } else { sha256_digest(zeros, 0, empty_hash) }
90 let derived_2: *u8 = sys_mmap(64) // R9: 48-byte secret under SHA-384
91 tls13_derived(s.handshake_secret, empty_hash, hash_len, derived_2)
92
93 // ---- master_secret = HKDF-Extract(derived_2, 0^HashLen) ----
94 tls13_master_secret(derived_2, hash_len, s.master_secret)
95
96 // ---- H2 = transcript snapshot covering CH..SF ----
97 // RFC 8446 §7.1: application-traffic secrets are derived from
98 // Hash(ClientHello...server Finished) -- the prefix BEFORE the
99 // client Finished is appended. emit_finished (step 3c.4)
100 // captured this hash into s.h_post_sf RIGHT BEFORE appending CF.
101 // Previously this code snapshotted transcript HERE, which was
102 // post-CF and produced TAG_MISMATCH on every real-server
103 // app-data record (2026-05-20 root cause).
104 let h2: *u8 = s.h_post_sf
105
106 // ---- Derive c/s app traffic SECRETS ----
107 tls13_traffic_secret(s.master_secret, NX_TLS13_LABEL_C_AP_TRAFFIC,
108 h2, hash_len, s.client_app_traffic_secret)
109 tls13_traffic_secret(s.master_secret, NX_TLS13_LABEL_S_AP_TRAFFIC,
110 h2, hash_len, s.server_app_traffic_secret)
111
112 // ---- Expand into key + IV (width tracks s.cipher_suite) ----
113 var key_len: i64 = 32
114 if s.cipher_suite == 0x1301 { key_len = 16 }
115 tls13_traffic_key(s.client_app_traffic_secret, key_len, s.client_app_traffic_key)
116 tls13_traffic_key(s.server_app_traffic_secret, key_len, s.server_app_traffic_key)
117 tls13_traffic_iv(s.client_app_traffic_secret, 12, s.client_app_iv)
118 tls13_traffic_iv(s.server_app_traffic_secret, 12, s.server_app_iv)
119
120 // ---- Reset per-direction app-record sequence numbers ----
121 s.client_app_seq = 0
122 s.server_app_seq = 0
123
124 // ---- Advance state ----
125 s.state = NX_TLS13_CSESSION_STATE_CONNECTED
126 return NX_TLS13_DERIVE_APP_OK
127}
128
129// Compile-only smoke. Real KAT in
130// nx_tls13_client_session_derive_app_test.nx.
131func main() -> i64 {
132 return 0
133}