nx_tls13_client_h2run.nx source
↩ module page · 174 lines · 7786 B
1// nx_tls13_client_h2run.nx -- TUTOR-BOOTSTRAP SCAFFOLD (Claude, authored under
2// the R4-H2 HTTP/2-transport-ladder workflow, capstone NBC-FETCH-001), NOT
3// credited as team self-authoring.
4//
5// THE h2-NEGOTIATING TLS 1.3 client handshake orchestrator. It is the
6// byte-for-byte twin of nx_tls13_client_session_run.nx (step 3c.6b) EXCEPT the
7// ClientHello it emits advertises ALPN ["h2"] ONLY (via the new
8// tls13_client_hello_emit2_h2only / tls13_ext_emit_alpn_h2_only). Consequence
9// (RFC 7301 ยง3.2): the server either selects "h2" -- so a CONNECTED session here
10// means the application-data channel IS HTTP/2, no encrypted-EE ALPN parse needed
11// -- or it MUST abort the handshake with no_application_protocol. This is how a
12// sovereign client makes "TLS ALPN selects h2" a yes/no the caller can branch on.
13//
14// FOUNDED ON (composes, does NOT reinvent -- anti-orphan law; each imported
15// EXACTLY ONCE, the RC6 double-import landmine avoided):
16// - nx_tls13_client_session.nx (the session struct + emit-CH internals +
17// transcript update; it transitively imports nx_tls13_hello.nx where the new
18// h2-only ClientHello emitter and ALPN-h2-only extension live)
19// - the recv_sh / recv_hs / emit_finished / derive_app step organs and
20// nx_tls13_read_record_from_fd -- the SAME set nx_tls13_client_session_run
21// imports, so the handshake body is identical to the proven HTTP/1.1 run.
22// This module deliberately does NOT import nx_tls13_client_session_run.nx (that
23// would double-define _write_n etc.); helper / const names carry an _H2 suffix so
24// a caller may import BOTH this and the http/1.1 run with no symbol clash.
25//
26// BACK-FILL: the team RE-AUTHORS this from the DATA spec
27// (knowledge/specs/2026-06-13-http2-transport-ladder.md) via the
28// emitter-of-emitters (X-AUT-006c/e/f); this hand scaffold is the sanctioned
29// one-time bootstrap only (meter-integrity).
30//
31// license_tier: INDEPENDENT_REDERIVE
32// genealogy_id: international-research-sources/ietf/rfc_8446 + rfc_7301 + rfc_9113
33// lineage_id: nishi_tls13_client_h2run_r4h2_capstone
34
35import "nx_syscalls.nx"
36import "nx_tls13.nx"
37import "nx_tls13_record.nx"
38import "nx_tls13_client_validate_certificate.nx"
39import "nx_tls13_client_session.nx"
40import "nx_tls13_client_session_recv_sh.nx"
41import "nx_tls13_client_session_recv_hs.nx"
42import "nx_tls13_client_session_emit_finished.nx"
43import "nx_tls13_client_session_derive_app.nx"
44import "nx_tls13_read_record_from_fd.nx"
45const NX_MAGIC_1024: i64 = 1024
46
47const NX_TLS13_H2RUN_OK: i64 = 1
48const NX_TLS13_H2RUN_EMIT_CH_FAIL: i64 = 2
49const NX_TLS13_H2RUN_WRITE_CH_FAIL: i64 = 3
50const NX_TLS13_H2RUN_READ_SH_FAIL: i64 = 4
51const NX_TLS13_H2RUN_RECV_SH_FAIL: i64 = 5
52const NX_TLS13_H2RUN_READ_HS_FAIL: i64 = 6
53const NX_TLS13_H2RUN_RECV_HS_FAIL: i64 = 7
54const NX_TLS13_H2RUN_EMIT_CF_FAIL: i64 = 8
55const NX_TLS13_H2RUN_WRITE_CF_FAIL: i64 = 9
56const NX_TLS13_H2RUN_DERIVE_APP_FAIL: i64 = 10
57const NX_TLS13_H2RUN_LOOP_BUDGET_EXCEEDED: i64 = 11
58
59const NX_TLS13_H2RUN_MAX_HS_RECORDS: i64 = 8
60const NX_TLS13_H2RUN_RECORD_BUF_BYTES: i64 = 16645
61
62// Write exactly n bytes to fd (loop sys_write). 0 ok, -1 error.
63func _h2run_write_n(fd: i64, buf: *u8, n: i64) -> i64 {
64 var off: i64 = 0
65 while off < n {
66 let w: i64 = sys_write(fd, (buf as i64 + off) as *u8, n - off)
67 if w <= 0 { return 0 - 1 }
68 off = off + w
69 }
70 return 0
71}
72
73func _h2run_is_ccs(buf: *u8, n: i64) -> i64 {
74 if n != NX_TLS13_RECORD_HEADER_LEN + 1 { return 0 }
75 if (buf[0] & 0xff) != NX_TLS13_CT_CHANGE_CIPHER_SPEC { return 0 }
76 if (buf[5] & 0xff) != 1 { return 0 }
77 return 1
78}
79
80// Emit the ClientHello advertising ALPN ["h2"] only, feed it into the transcript
81// hash, advance state to CH_SENT. Twin of nx_tls13_client_session_emit_ch but
82// calls tls13_client_hello_emit2_h2only. Returns bytes written or negative.
83func nx_tls13_client_session_emit_ch_h2(
84 s: *Tls13ClientSession,
85 sni: *u8, sni_len: i64,
86 out_buf: *u8, out_cap: i64
87) -> i64 {
88 if s.state != NX_TLS13_CSESSION_STATE_INIT {
89 return 0 - NX_TLS13_CSESSION_BAD_STATE
90 }
91 if out_cap < 160 { return 0 - NX_TLS13_CSESSION_BUF_OVERFLOW }
92 let n: i64 = tls13_client_hello_emit2_h2only(
93 s.client_random, sni, sni_len, s.x25519_pub, s.p256_pub,
94 out_buf, out_cap
95 )
96 if n < 0 { return 0 - NX_TLS13_CSESSION_INTERNAL }
97 nx_tls13_transcript_update(s.transcript, out_buf, n)
98 s.state = NX_TLS13_CSESSION_STATE_CH_SENT
99 return n
100}
101
102// Drive the full TLS 1.3 client handshake to CONNECTED with ALPN ["h2"] only.
103// POSITIVE pointer-as-i64 to the connected session on success (the channel IS
104// h2). NEGATIVE -NX_TLS13_H2RUN_* on failure (an h2-refusing host shows up as a
105// handshake failure here -- honestly named, never a faked 200).
106func nx_tls13_client_session_run_h2(
107 fd: i64,
108 sni: *u8, sni_len: i64,
109 client_random: *u8,
110 x25519_priv: *u8,
111 val_ctx: *TlsValidationContext
112) -> i64 {
113 let s: *Tls13ClientSession = nx_tls13_client_session_new(client_random, x25519_priv)
114
115 let ch_buf: *u8 = sys_mmap(NX_MAGIC_1024)
116 let ch_n: i64 = nx_tls13_client_session_emit_ch_h2(s, sni, sni_len, ch_buf, NX_MAGIC_1024)
117 if ch_n < 0 { return 0 - NX_TLS13_H2RUN_EMIT_CH_FAIL }
118
119 let ch_record: *u8 = sys_mmap(NX_MAGIC_1024 + NX_TLS13_RECORD_HEADER_LEN)
120 ch_record[0] = NX_TLS13_CT_HANDSHAKE & 0xff
121 ch_record[1] = 0x03; ch_record[2] = 0x01
122 ch_record[3] = ((ch_n >> 8) & 0xff) as u8
123 ch_record[4] = (ch_n & 0xff) as u8
124 var ci: i64 = 0
125 while ci < ch_n {
126 ch_record[NX_TLS13_RECORD_HEADER_LEN + ci] = ch_buf[ci]
127 ci = ci + 1
128 }
129
130 let wr_ch: i64 = _h2run_write_n(fd, ch_record, NX_TLS13_RECORD_HEADER_LEN + ch_n)
131 if wr_ch < 0 { return 0 - NX_TLS13_H2RUN_WRITE_CH_FAIL }
132
133 let sh_record: *u8 = sys_mmap(NX_TLS13_H2RUN_RECORD_BUF_BYTES)
134 let sh_total: i64 = nx_tls13_read_record_from_fd(fd, sh_record, NX_TLS13_H2RUN_RECORD_BUF_BYTES)
135 if sh_total < 0 { return 0 - NX_TLS13_H2RUN_READ_SH_FAIL }
136
137 let sh_body: *u8 = sh_record + NX_TLS13_RECORD_HEADER_LEN
138 let sh_body_len: i64 = sh_total - NX_TLS13_RECORD_HEADER_LEN
139 let rs_v: i64 = nx_tls13_client_session_recv_sh(s, sh_body, sh_body_len)
140 if rs_v != NX_TLS13_RECV_SH_OK { return 0 - NX_TLS13_H2RUN_RECV_SH_FAIL }
141
142 var loop_count: i64 = 0
143 while s.state != NX_TLS13_CSESSION_STATE_WAIT_CLIENT_FIN {
144 if loop_count >= NX_TLS13_H2RUN_MAX_HS_RECORDS {
145 return 0 - NX_TLS13_H2RUN_LOOP_BUDGET_EXCEEDED
146 }
147 let hs_record: *u8 = sys_mmap(NX_TLS13_H2RUN_RECORD_BUF_BYTES)
148 let hs_total: i64 = nx_tls13_read_record_from_fd(fd, hs_record, NX_TLS13_H2RUN_RECORD_BUF_BYTES)
149 if hs_total < 0 { return 0 - NX_TLS13_H2RUN_READ_HS_FAIL }
150 if _h2run_is_ccs(hs_record, hs_total) == 1 {
151 loop_count = loop_count + 1
152 } else {
153 let rh_v: i64 = nx_tls13_client_session_recv_hs(s, hs_record, hs_total, val_ctx)
154 if rh_v != NX_TLS13_RECV_HS_OK { return 0 - NX_TLS13_H2RUN_RECV_HS_FAIL }
155 loop_count = loop_count + 1
156 }
157 }
158
159 let cf_buf: *u8 = sys_mmap(128)
160 let cf_n: i64 = nx_tls13_client_session_emit_finished(s, cf_buf, 128)
161 if cf_n < 0 { return 0 - NX_TLS13_H2RUN_EMIT_CF_FAIL }
162 let wr_cf: i64 = _h2run_write_n(fd, cf_buf, cf_n)
163 if wr_cf < 0 { return 0 - NX_TLS13_H2RUN_WRITE_CF_FAIL }
164
165 let da_v: i64 = nx_tls13_client_session_derive_app(s)
166 if da_v != NX_TLS13_DERIVE_APP_OK { return 0 - NX_TLS13_H2RUN_DERIVE_APP_FAIL }
167
168 return s as i64
169}
170
171// Compile-only smoke. The real proof is the live h2 GET gate that composes this.
172func main() -> i64 {
173 return 0
174}