nx_https_post_complete.nx source
↩ module page · 184 lines · 6741 B
1// nx_https_post_complete.nx -- HTTPS POST companion to
2// nx_https_get_complete. Sends an HTTP/1.1 POST request with a
3// caller-supplied body (typically application/jose+json for ACME)
4// over a CONNECTED TLS 1.3 session, then drains the encrypted
5// response into out_buf.
6//
7// Composes:
8// nx_http_client_build_request_post -- POST line + headers + body
9// nx_tls13_record_encrypt_v2 -- AEAD wrap (one request rec)
10// nx_tls13_read_record_from_fd -- partial-read framer
11// nx_tls13_record_decrypt_v2 -- AEAD unwrap per response rec
12//
13// Body size limit: NX_HTTPS_PC_BODY_BYTES (8 KB). ACME requests are
14// typically <2 KB (JWS-wrapped JSON over a 32-byte EdDSA key) so 8 KB
15// gives a comfortable margin. Caller should fragment larger payloads
16// across multiple TLS records explicitly (not auto-fragment here --
17// callers needing >8 KB request bodies are sufficiently rare that
18// they should opt in).
19//
20// Verdict mirrors NX_HTTPS_GC_* with a PC prefix; same sealed-enum
21// shape (positive bytes on success, negative -verdict on failure).
22//
23// Per Cardinals 9 (single-responsibility -- ONE round trip), 22
24// (composition of shipped primitives), 25 (build intelligence, don't
25// strip features -- adds POST capability without weakening GET).
26//
27// license_tier: INDEPENDENT_REDERIVE
28// genealogy_id: international-research-sources/ietf/rfc_8446 + rfc_9112 + rfc_8555
29// lineage_id: nishi_https_post_complete_q1
30
31import "nx_syscalls.nx"
32import "nx_tls13.nx"
33import "nx_tls13_record.nx"
34import "nx_tls13_read_record_from_fd.nx"
35import "nx_tls13_client_session.nx"
36import "nx_http_client.nx"
37
38const NX_HTTPS_PC_OK: i64 = 1
39const NX_HTTPS_PC_BAD_STATE: i64 = 2
40const NX_HTTPS_PC_BUILD_FAIL: i64 = 3
41const NX_HTTPS_PC_ENCRYPT_FAIL: i64 = 4
42const NX_HTTPS_PC_WRITE_FAIL: i64 = 5
43const NX_HTTPS_PC_READ_FAIL: i64 = 6
44const NX_HTTPS_PC_DECRYPT_FAIL: i64 = 7
45const NX_HTTPS_PC_BUF_OVERFLOW: i64 = 8
46const NX_HTTPS_PC_BODY_TOO_BIG: i64 = 9
47const NX_HTTPS_PC_VERDICT_N: i64 = 10
48
49const NX_HTTPS_PC_REQ_BUF_BYTES: i64 = 16384 // header (~512) + body (<= 8 KB) + slack
50const NX_HTTPS_PC_BODY_BYTES: i64 = 8192
51const NX_HTTPS_PC_RESP_RECORD_BYTES: i64 = 16645
52
53func nx_https_pc_verdict_is_valid(v: i64) -> i64 {
54 if v < NX_HTTPS_PC_OK { return 0 }
55 if v >= NX_HTTPS_PC_VERDICT_N { return 0 }
56 return 1
57}
58
59func _pc_write_n(fd: i64, buf: *u8, n: i64) -> i64 {
60 var off: i64 = 0
61 while off < n {
62 let w: i64 = sys_write(fd, (buf as i64 + off) as *u8, n - off)
63 if w <= 0 { return 0 - 1 }
64 off = off + w
65 }
66 return 0
67}
68
69func nx_https_post_complete(
70 s: *Tls13ClientSession,
71 fd: i64,
72 path: *u8, path_len: i64,
73 host: *u8, host_len: i64,
74 content_type: *u8, content_type_len: i64,
75 body: *u8, body_len: i64,
76 out_buf: *u8, out_cap: i64
77) -> i64 {
78 if s.state != NX_TLS13_CSESSION_STATE_CONNECTED {
79 return 0 - NX_HTTPS_PC_BAD_STATE
80 }
81 if body_len > NX_HTTPS_PC_BODY_BYTES {
82 return 0 - NX_HTTPS_PC_BODY_TOO_BIG
83 }
84
85 // ---- Build HTTP POST request ----
86 let req: *u8 = sys_mmap(NX_HTTPS_PC_REQ_BUF_BYTES)
87 let req_len: i64 = nx_http_client_build_request_post(
88 path, path_len,
89 host, host_len,
90 content_type, content_type_len,
91 body, body_len,
92 req
93 )
94 if req_len <= 0 { return 0 - NX_HTTPS_PC_BUILD_FAIL }
95
96 // ---- Encrypt as TLS application_data record ----
97 let rec_buf: *u8 = sys_mmap(req_len + 64)
98 let header_out: *u8 = rec_buf
99 let ct_out: *u8 = rec_buf + NX_TLS13_RECORD_HEADER_LEN
100 let tag_out: *u8 = rec_buf + NX_TLS13_RECORD_HEADER_LEN + req_len + 1
101
102 let enc_v: i64 = nx_tls13_record_encrypt_v2(
103 s.cipher_suite,
104 s.client_app_traffic_key,
105 s.client_app_iv,
106 s.client_app_seq,
107 req, req_len,
108 NX_TLS13_CT_APPLICATION_DATA,
109 0,
110 header_out, ct_out, tag_out
111 )
112 s.client_app_seq = s.client_app_seq + 1
113 if enc_v != NX_TLS13_REC_VERDICT_OK { return 0 - NX_HTTPS_PC_ENCRYPT_FAIL }
114
115 // ---- Write encrypted request record ----
116 let total_rec_len: i64 = NX_TLS13_RECORD_HEADER_LEN + req_len + 1 + NX_TLS13_RECORD_TAG_LEN
117 let wr_v: i64 = _pc_write_n(fd, rec_buf, total_rec_len)
118 if wr_v < 0 { return 0 - NX_HTTPS_PC_WRITE_FAIL }
119
120 // ---- Read + decrypt response records (same shape as get_complete) ----
121 var accumulated: i64 = 0
122 while accumulated < out_cap {
123 let rec_in: *u8 = sys_mmap(NX_HTTPS_PC_RESP_RECORD_BYTES)
124 let rec_in_total: i64 = nx_tls13_read_record_from_fd(
125 fd, rec_in, NX_HTTPS_PC_RESP_RECORD_BYTES
126 )
127 if rec_in_total < 0 {
128 let nv: i64 = 0 - rec_in_total
129 if nv == NX_TLS13_READ_REC_EOF { return accumulated }
130 if nv == NX_TLS13_READ_REC_PAYLOAD_EOF { return accumulated }
131 return 0 - NX_HTTPS_PC_READ_FAIL
132 }
133
134 let rec_in_header: *u8 = rec_in
135 let rec_in_ct: *u8 = rec_in + NX_TLS13_RECORD_HEADER_LEN
136 let rec_in_ct_len: i64 = rec_in_total - NX_TLS13_RECORD_HEADER_LEN - NX_TLS13_RECORD_TAG_LEN
137 let rec_in_tag: *u8 = rec_in + rec_in_total - NX_TLS13_RECORD_TAG_LEN
138
139 let plaintext: *u8 = sys_mmap(rec_in_ct_len + 16)
140 let plaintext_ct_p: *i64 = sys_mmap(16) as *i64
141 let plaintext_len_p: *i64 = sys_mmap(16) as *i64
142
143 let dec_v: i64 = nx_tls13_record_decrypt_v2(
144 s.cipher_suite,
145 s.server_app_traffic_key,
146 s.server_app_iv,
147 s.server_app_seq,
148 rec_in_header,
149 rec_in_ct, rec_in_ct_len,
150 rec_in_tag,
151 plaintext,
152 plaintext_ct_p, plaintext_len_p
153 )
154 s.server_app_seq = s.server_app_seq + 1
155 if dec_v != NX_TLS13_REC_VERDICT_OK { return 0 - NX_HTTPS_PC_DECRYPT_FAIL }
156
157 if *plaintext_ct_p == NX_TLS13_CT_ALERT {
158 if *plaintext_len_p >= 2 {
159 if (plaintext[1] & 0xff) == 0 { return accumulated }
160 }
161 return accumulated
162 }
163
164 if *plaintext_ct_p == NX_TLS13_CT_APPLICATION_DATA {
165 let avail: i64 = out_cap - accumulated
166 let to_copy: i64 = *plaintext_len_p
167 if to_copy > avail { return 0 - NX_HTTPS_PC_BUF_OVERFLOW }
168 var i: i64 = 0
169 while i < to_copy {
170 out_buf[accumulated + i] = plaintext[i]
171 i = i + 1
172 }
173 accumulated = accumulated + to_copy
174 }
175 }
176
177 return accumulated
178}
179
180// Compile-only smoke. Live exercise belongs to a separate end-to-end
181// test against ACME staging (queued: nx_acme_directory_live_smoke).
182func main() -> i64 {
183 return 0
184}