nx_acme_response.nx source
↩ module page · 228 lines · 7399 B
1// nx_acme_response.nx -- ACME (RFC 8555) HTTP response parser.
2//
3// Extracts the four fields the ACME state machine needs from any
4// CA response:
5// 1. HTTP status code (200, 201, 204, 400, etc.)
6// 2. Replay-Nonce header (RFC 8555 §6.5; the nonce we use for
7// the NEXT request -- single-use per RFC 8555 §6.5.1)
8// 3. Location header (account-URL / order-URL / challenge-URL
9// depending on the endpoint)
10// 4. Body offset + length (JSON payload for the caller to parse)
11//
12// Composes:
13// nx_http_header_find.nx -- case-insensitive header search
14//
15// Per cardinal feedback-defensive-at-boundaries-trusting-internally:
16// the parser validates the response shape ONCE; downstream callers
17// trust the parsed fields.
18//
19// Per RFC 8555 §6.4: ACME servers MAY include Replay-Nonce on ANY
20// response (success or error). We extract it always. If absent,
21// the caller treats the next request as needing a fresh /new-nonce
22// roundtrip.
23//
24// nx_capability_claims:
25// needs: [sealed_enum, http_header_search]
26// provides: [acme_response_parse, replay_nonce_extract,
27// location_header_extract]
28// safety: [no_unchecked_deref, no_floating_point, no_syscall,
29// bounded_iteration, bit_equal_reproducible]
30// verdict: [sealed_enum_6_state]
31// license: ORIGINAL
32// kind: racing_crew_specialist
33// layer: L3 (algorithm: HTTP response -> ACME-typed fields)
34
35import "nx_syscalls_x86_64.nx"
36import "nx_http_header_find.nx"
37
38// ---- Sealed enum: parse verdict ----------------------------------
39
40const NXAR_OK: i64 = 0
41const NXAR_BAD_STATUS_LINE: i64 = 1
42const NXAR_NO_HEADER_END: i64 = 2
43const NXAR_BODY_TOO_SHORT: i64 = 3
44const NXAR_BAD_ARG: i64 = 4
45const NXAR_VERDICT_N: i64 = 5
46
47func nxar_verdict_is_valid(v: i64) -> i64 {
48 if v < 0 { return 0 }
49 if v >= NXAR_VERDICT_N { return 0 }
50 return 1
51}
52
53func nxar_verdict_name(v: i64) -> *u8 {
54 if v == NXAR_OK { return "OK" as *u8 }
55 if v == NXAR_BAD_STATUS_LINE { return "BAD_STATUS_LINE" as *u8 }
56 if v == NXAR_NO_HEADER_END { return "NO_HEADER_END" as *u8 }
57 if v == NXAR_BODY_TOO_SHORT { return "BODY_TOO_SHORT" as *u8 }
58 if v == NXAR_BAD_ARG { return "BAD_ARG" as *u8 }
59 return "INVALID" as *u8
60}
61
62// ---- Status-line parse -------------------------------------------
63//
64// HTTP/1.1 200 OK\r\n
65// HTTP/1.1 201 Created\r\n
66// HTTP/1.1 204 No Content\r\n
67// HTTP/1.1 400 Bad Request\r\n
68//
69// We just want the integer. Tolerant: HTTP/1.0 or HTTP/2 also work.
70
71func nxar_parse_status_code(buf: *u8, len: i64) -> i64 {
72 if len < 12 { return -1 } // "HTTP/1.x NNN" minimum
73 // Find first space.
74 var p: i64 = 0
75 while p < len {
76 if buf[p] == 0x20 as u8 { p = p + 1; break }
77 p = p + 1
78 }
79 if p >= len { return -1 }
80 // Parse the integer.
81 var code: i64 = 0
82 var any: i64 = 0
83 while p < len {
84 let b: i64 = buf[p] as i64
85 if b >= 0x30 && b <= 0x39 {
86 code = code * 10 + (b - 0x30)
87 any = 1
88 p = p + 1
89 } else {
90 p = len + 1 // break sentinel
91 }
92 }
93 if any == 0 { return -1 }
94 if code < 100 || code > 599 { return -1 }
95 return code
96}
97
98// ---- Find header / body boundary ---------------------------------
99//
100// HTTP separates headers from body with CRLF CRLF (or LF LF tolerated).
101// Returns the offset of the FIRST byte of the body, or -1 if absent.
102
103func nxar_find_body_offset(buf: *u8, len: i64) -> i64 {
104 if len < 4 { return -1 }
105 var i: i64 = 0
106 while i + 3 < len {
107 if buf[i] == 0x0d as u8 && buf[i+1] == 0x0a as u8
108 && buf[i+2] == 0x0d as u8 && buf[i+3] == 0x0a as u8 {
109 return i + 4
110 }
111 i = i + 1
112 }
113 // LF-only tolerance.
114 var j: i64 = 0
115 while j + 1 < len {
116 if buf[j] == 0x0a as u8 && buf[j+1] == 0x0a as u8 {
117 return j + 2
118 }
119 j = j + 1
120 }
121 return -1
122}
123
124// ---- AcmeResponse struct -----------------------------------------
125//
126// Caller-allocated. Filled by nx_acme_response_parse. Out fields
127// for absent headers are zero (so callers can check
128// nonce_len > 0 / location_len > 0).
129
130struct AcmeResponse {
131 status_code: i64,
132 nonce_off: i64,
133 nonce_len: i64,
134 location_off: i64,
135 location_len: i64,
136 body_off: i64,
137 body_len: i64,
138}
139
140const ACME_RESPONSE_BYTES: i64 = 56 // 7 i64 fields
141
142// ---- Top-level parse entry --------------------------------------
143//
144// Caller supplies the FULL raw HTTP response bytes (headers + body).
145// We fill an AcmeResponse struct + return sealed verdict.
146
147func nx_acme_response_parse(
148 resp_buf: *u8, resp_n: i64,
149 out: *AcmeResponse) -> i64 {
150 if resp_buf == (0 as *u8) { return NXAR_BAD_ARG }
151 if out == (0 as *AcmeResponse) { return NXAR_BAD_ARG }
152 if resp_n <= 0 { return NXAR_BAD_ARG }
153
154 // Init out fields.
155 out.status_code = 0
156 out.nonce_off = 0
157 out.nonce_len = 0
158 out.location_off = 0
159 out.location_len = 0
160 out.body_off = 0
161 out.body_len = 0
162
163 // Parse status code.
164 let code: i64 = nxar_parse_status_code(resp_buf, resp_n)
165 if code < 0 { return NXAR_BAD_STATUS_LINE }
166 out.status_code = code
167
168 // Find body offset.
169 let body_off: i64 = nxar_find_body_offset(resp_buf, resp_n)
170 if body_off < 0 { return NXAR_NO_HEADER_END }
171 out.body_off = body_off
172 out.body_len = resp_n - body_off
173
174 // Header block is [0, body_off). Slice for header-find.
175 let headers_n: i64 = body_off
176
177 // Replay-Nonce (absence is OK; out fields stay 0).
178 let n_off: *i64 = sys_mmap(8) as *i64
179 let n_len: *i64 = sys_mmap(8) as *i64
180 let rcn: i64 = nx_http_header_find(resp_buf, headers_n,
181 "Replay-Nonce" as *u8, 12,
182 n_off, n_len)
183 if rcn == NXHF_FOUND {
184 out.nonce_off = n_off[0]
185 out.nonce_len = n_len[0]
186 }
187
188 // Location.
189 let l_off: *i64 = sys_mmap(8) as *i64
190 let l_len: *i64 = sys_mmap(8) as *i64
191 let rcl: i64 = nx_http_header_find(resp_buf, headers_n,
192 "Location" as *u8, 8,
193 l_off, l_len)
194 if rcl == NXHF_FOUND {
195 out.location_off = l_off[0]
196 out.location_len = l_len[0]
197 }
198
199 return NXAR_OK
200}
201
202// ---- Convenience predicates -------------------------------------
203//
204// Mirror RFC 8555 §6.7 status-code semantics for the state machine.
205
206// 2xx success
207func nx_acme_response_is_success(out: *AcmeResponse) -> i64 {
208 if out == (0 as *AcmeResponse) { return 0 }
209 if out.status_code >= 200 && out.status_code <= 299 { return 1 }
210 return 0
211}
212
213// 4xx client error (caller likely won't retry without payload change)
214func nx_acme_response_is_client_error(out: *AcmeResponse) -> i64 {
215 if out == (0 as *AcmeResponse) { return 0 }
216 if out.status_code >= 400 && out.status_code <= 499 { return 1 }
217 return 0
218}
219
220// 5xx server error or 429 (caller retries with backoff)
221func nx_acme_response_should_retry(out: *AcmeResponse) -> i64 {
222 if out == (0 as *AcmeResponse) { return 0 }
223 if out.status_code == 429 { return 1 }
224 if out.status_code >= 500 && out.status_code <= 599 { return 1 }
225 return 0
226}
227
228// (imports at top of file per NishiLang parser)