nx_acme_es256.nx source
↩ module page · 283 lines · 13179 B
1// nx_acme_es256.nx -- ACME (RFC 8555) request builders for the ES256
2// account-key path, on the nx_syscalls graph.
3//
4// WHY a second ACME file: the legacy nx_acme.nx is EdDSA + sits on the
5// nx_syscalls_x86_64 graph (via nx_jose/nx_jwk). Let's Encrypt's
6// production CA does NOT accept Ed25519 account keys -- it requires ES256
7// (ECDSA P-256) or RS256 -- and the whole HTTPS client + P-256 crypto +
8// CSR stack lives on the nx_syscalls graph. Two syscall variants define
9// duplicate sys_* symbols and cannot link together, so the ES256 ACME
10// client must be its own file on the nx_syscalls graph. This supersedes
11// nx_acme.nx for live issuance.
12//
13// Composes (all nx_syscalls graph):
14// nx_jose_es256.nx -- ES256 JWS flattened sign + nxes_put_* + pubkey
15// nx_jwk_ec.nx -- EC P-256 JWK emit + RFC 7638 thumbprint
16// nx_acme_csr.nx -- ECDSA P-256 PKCS#10 CSR (pulls nx_x509_build hub)
17// nx_https_client.nx-- live HTTPS POST/GET to the CA (next slice)
18//
19// This file is the REQUEST-BUILDER + DNS-01 layer. All functions here
20// are offline-deterministic and KAT-verifiable; the live state-machine
21// drive (newNonce -> newAccount -> newOrder -> authz -> dns-01 -> poll ->
22// finalize -> download) layers on top using nx_https_client.
23//
24// license_tier: ORIGINAL (composes RFC 8555 + RFC 7515/7518/7638 + P-256)
25
26import "nx_jose_es256.nx"
27import "nx_jwk_ec.nx"
28import "nx_acme_csr.nx"
29const NXACMEES_MAGIC_1024: i64 = 1024
30const NXACMEES_MAGIC_4096: i64 = 4096
31
32const NXACMEES_OK: i64 = 0
33const NXACMEES_OOM: i64 = 1
34const NXACMEES_BAD_ARG: i64 = 2
35const NXACMEES_SIGN_ERR: i64 = 3
36
37// ---- ES256 protected-header builders -----------------------------
38// Canonical lexicographic member order (alg < jwk/kid < nonce < url).
39
40// {"alg":"ES256","jwk":<jwk>,"nonce":"<nonce>","url":"<url>"}
41func nx_acme_es256_protected_jwk_form(
42 out: *u8, off: *i64, cap: i64,
43 jwk_bytes: *u8, jwk_n: i64,
44 nonce: *u8, nonce_n: i64,
45 url: *u8, url_n: i64) -> i64 {
46 if out == (0 as *u8) { return NXACMEES_BAD_ARG }
47 if off == (0 as *i64) { return NXACMEES_BAD_ARG }
48 if jwk_bytes == (0 as *u8) { return NXACMEES_BAD_ARG }
49 if nonce == (0 as *u8) { return NXACMEES_BAD_ARG }
50 if url == (0 as *u8) { return NXACMEES_BAD_ARG }
51 if cap <= 0 { return NXACMEES_BAD_ARG }
52
53 if nxes_put_cstr(out, off, cap, "{\"alg\":\"ES256\",\"jwk\":" as *u8) != NXES_OK { return NXACMEES_OOM }
54 if nxes_put_bytes(out, off, cap, jwk_bytes, jwk_n) != NXES_OK { return NXACMEES_OOM }
55 if nxes_put_cstr(out, off, cap, ",\"nonce\":\"" as *u8) != NXES_OK { return NXACMEES_OOM }
56 if nxes_put_bytes(out, off, cap, nonce, nonce_n) != NXES_OK { return NXACMEES_OOM }
57 if nxes_put_cstr(out, off, cap, "\",\"url\":\"" as *u8) != NXES_OK { return NXACMEES_OOM }
58 if nxes_put_bytes(out, off, cap, url, url_n) != NXES_OK { return NXACMEES_OOM }
59 if nxes_put_cstr(out, off, cap, "\"}" as *u8) != NXES_OK { return NXACMEES_OOM }
60 return NXACMEES_OK
61}
62
63// {"alg":"ES256","kid":"<kid>","nonce":"<nonce>","url":"<url>"}
64func nx_acme_es256_protected_kid_form(
65 out: *u8, off: *i64, cap: i64,
66 kid: *u8, kid_n: i64,
67 nonce: *u8, nonce_n: i64,
68 url: *u8, url_n: i64) -> i64 {
69 if out == (0 as *u8) { return NXACMEES_BAD_ARG }
70 if off == (0 as *i64) { return NXACMEES_BAD_ARG }
71 if kid == (0 as *u8) { return NXACMEES_BAD_ARG }
72 if nonce == (0 as *u8) { return NXACMEES_BAD_ARG }
73 if url == (0 as *u8) { return NXACMEES_BAD_ARG }
74 if cap <= 0 { return NXACMEES_BAD_ARG }
75
76 if nxes_put_cstr(out, off, cap, "{\"alg\":\"ES256\",\"kid\":\"" as *u8) != NXES_OK { return NXACMEES_OOM }
77 if nxes_put_bytes(out, off, cap, kid, kid_n) != NXES_OK { return NXACMEES_OOM }
78 if nxes_put_cstr(out, off, cap, "\",\"nonce\":\"" as *u8) != NXES_OK { return NXACMEES_OOM }
79 if nxes_put_bytes(out, off, cap, nonce, nonce_n) != NXES_OK { return NXACMEES_OOM }
80 if nxes_put_cstr(out, off, cap, "\",\"url\":\"" as *u8) != NXES_OK { return NXACMEES_OOM }
81 if nxes_put_bytes(out, off, cap, url, url_n) != NXES_OK { return NXACMEES_OOM }
82 if nxes_put_cstr(out, off, cap, "\"}" as *u8) != NXES_OK { return NXACMEES_OOM }
83 return NXACMEES_OK
84}
85
86// ---- Payload builders --------------------------------------------
87
88// {"contact":["mailto:<email>"],"termsOfServiceAgreed":true}
89func nx_acme_payload_new_account(
90 out: *u8, off: *i64, cap: i64,
91 email: *u8, email_n: i64) -> i64 {
92 if out == (0 as *u8) { return NXACMEES_BAD_ARG }
93 if off == (0 as *i64) { return NXACMEES_BAD_ARG }
94 if email == (0 as *u8) { return NXACMEES_BAD_ARG }
95 if nxes_put_cstr(out, off, cap, "{\"contact\":[\"mailto:" as *u8) != NXES_OK { return NXACMEES_OOM }
96 if nxes_put_bytes(out, off, cap, email, email_n) != NXES_OK { return NXACMEES_OOM }
97 if nxes_put_cstr(out, off, cap, "\"],\"termsOfServiceAgreed\":true}" as *u8) != NXES_OK { return NXACMEES_OOM }
98 return NXACMEES_OK
99}
100
101// {"identifiers":[{"type":"dns","value":"<d0>"},{"type":"dns","value":"<d1>"}...]}
102func nx_acme_payload_new_order(
103 out: *u8, off: *i64, cap: i64,
104 dom_ptrs: *i64, dom_lens: *i64, dom_count: i64) -> i64 {
105 if out == (0 as *u8) { return NXACMEES_BAD_ARG }
106 if off == (0 as *i64) { return NXACMEES_BAD_ARG }
107 if dom_count < 1 { return NXACMEES_BAD_ARG }
108 if nxes_put_cstr(out, off, cap, "{\"identifiers\":[" as *u8) != NXES_OK { return NXACMEES_OOM }
109 var i: i64 = 0
110 while i < dom_count {
111 if i > 0 {
112 if nxes_put_cstr(out, off, cap, "," as *u8) != NXES_OK { return NXACMEES_OOM }
113 }
114 if nxes_put_cstr(out, off, cap, "{\"type\":\"dns\",\"value\":\"" as *u8) != NXES_OK { return NXACMEES_OOM }
115 if nxes_put_bytes(out, off, cap, dom_ptrs[i] as *u8, dom_lens[i]) != NXES_OK { return NXACMEES_OOM }
116 if nxes_put_cstr(out, off, cap, "\"}" as *u8) != NXES_OK { return NXACMEES_OOM }
117 i = i + 1
118 }
119 if nxes_put_cstr(out, off, cap, "]}" as *u8) != NXES_OK { return NXACMEES_OOM }
120 return NXACMEES_OK
121}
122
123// {"csr":"<base64url(DER CSR)>"}
124func nx_acme_payload_finalize(
125 out: *u8, off: *i64, cap: i64,
126 csr_der: *u8, csr_n: i64) -> i64 {
127 if out == (0 as *u8) { return NXACMEES_BAD_ARG }
128 if off == (0 as *i64) { return NXACMEES_BAD_ARG }
129 if csr_der == (0 as *u8) { return NXACMEES_BAD_ARG }
130 let b64: *u8 = sys_mmap((csr_n * 4 / 3) + 16)
131 let b64n: i64 = jwt_b64url_encode(csr_der, csr_n, b64)
132 if nxes_put_cstr(out, off, cap, "{\"csr\":\"" as *u8) != NXES_OK { return NXACMEES_OOM }
133 if nxes_put_bytes(out, off, cap, b64, b64n) != NXES_OK { return NXACMEES_OOM }
134 if nxes_put_cstr(out, off, cap, "\"}" as *u8) != NXES_OK { return NXACMEES_OOM }
135 return NXACMEES_OK
136}
137
138// ---- DNS-01 challenge (RFC 8555 §8.1, §8.4) ----------------------
139//
140// keyAuthorization = token "." base64url(SHA-256(canonical account JWK))
141// DNS TXT record value (_acme-challenge.<domain>) =
142// base64url(SHA-256(keyAuthorization))
143//
144// HTTP-01 key authorization (RFC 8555 §8.1): token "." base64url(JWK
145// thumbprint), served VERBATIM (no extra hash) as the body of
146// http://<domain>/.well-known/acme-challenge/<token>. Writes into out;
147// returns length via out_n.
148func nx_acme_key_authorization(
149 token: *u8, token_n: i64,
150 pub_x: *i64, pub_y: *i64,
151 out: *u8, cap: i64, out_n: *i64) -> i64 {
152 if token == (0 as *u8) { return NXACMEES_BAD_ARG }
153 if out == (0 as *u8) { return NXACMEES_BAD_ARG }
154 let thumb_raw: *u8 = sys_mmap(32)
155 if nx_jwk_ec_p256_thumbprint(pub_x, pub_y, thumb_raw) != NXJWK_OK { return NXACMEES_SIGN_ERR }
156 let thumb_b64: *u8 = sys_mmap(64)
157 let thumb_b64n: i64 = jwt_b64url_encode(thumb_raw, 32, thumb_b64)
158 var k: i64 = 0
159 var j: i64 = 0
160 while j < token_n { out[k] = token[j]; k = k + 1; j = j + 1 }
161 out[k] = 0x2e as u8; k = k + 1 // '.'
162 j = 0
163 while j < thumb_b64n { out[k] = thumb_b64[j]; k = k + 1; j = j + 1 }
164 out_n[0] = k
165 return NXACMEES_OK
166}
167
168// Writes the TXT value bytes into out; returns its length via out_n.
169func nx_acme_dns01_txt_value(
170 token: *u8, token_n: i64,
171 pub_x: *i64, pub_y: *i64,
172 out: *u8, cap: i64, out_n: *i64) -> i64 {
173 if token == (0 as *u8) { return NXACMEES_BAD_ARG }
174 if out == (0 as *u8) { return NXACMEES_BAD_ARG }
175
176 // 1. JWK thumbprint (raw 32B) -> base64url.
177 let thumb_raw: *u8 = sys_mmap(32)
178 if nx_jwk_ec_p256_thumbprint(pub_x, pub_y, thumb_raw) != NXJWK_OK { return NXACMEES_SIGN_ERR }
179 let thumb_b64: *u8 = sys_mmap(64)
180 let thumb_b64n: i64 = jwt_b64url_encode(thumb_raw, 32, thumb_b64)
181
182 // 2. keyAuthorization = token "." thumb_b64
183 let keyauth: *u8 = sys_mmap(token_n + thumb_b64n + 4)
184 var k: i64 = 0
185 var j: i64 = 0
186 while j < token_n { keyauth[k] = token[j]; k = k + 1; j = j + 1 }
187 keyauth[k] = 0x2e as u8; k = k + 1 // '.'
188 j = 0
189 while j < thumb_b64n { keyauth[k] = thumb_b64[j]; k = k + 1; j = j + 1 }
190
191 // 3. TXT value = base64url(SHA-256(keyAuthorization))
192 let kdg: *u8 = sys_mmap(32)
193 sha256_digest(keyauth, k, kdg)
194 let txt_n: i64 = jwt_b64url_encode(kdg, 32, out)
195 out_n[0] = txt_n
196 return NXACMEES_OK
197}
198
199// ---- new-account JWS (top-level convenience) ---------------------
200// jwk-emit -> ES256 protected-jwk-form -> new-account payload -> sign.
201func nx_acme_build_new_account_jws_es256(
202 priv_limbs: *i64, pub_x: *i64, pub_y: *i64,
203 nonce: *u8, nonce_n: i64,
204 url: *u8, url_n: i64,
205 email: *u8, email_n: i64,
206 out: *u8, cap: i64, out_n: *i64) -> i64 {
207 let jwk: *u8 = sys_mmap(256)
208 let jwk_off: *i64 = sys_mmap(8) as *i64
209 jwk_off[0] = 0
210 if nx_jwk_ec_p256_emit(jwk, jwk_off, 256, pub_x, pub_y) != NXJWK_OK { return NXACMEES_OOM }
211
212 let prot: *u8 = sys_mmap(NXACMEES_MAGIC_1024)
213 let prot_off: *i64 = sys_mmap(8) as *i64
214 prot_off[0] = 0
215 if nx_acme_es256_protected_jwk_form(prot, prot_off, NXACMEES_MAGIC_1024, jwk, jwk_off[0],
216 nonce, nonce_n, url, url_n) != NXACMEES_OK { return NXACMEES_OOM }
217
218 let pay: *u8 = sys_mmap(512)
219 let pay_off: *i64 = sys_mmap(8) as *i64
220 pay_off[0] = 0
221 if nx_acme_payload_new_account(pay, pay_off, 512, email, email_n) != NXACMEES_OK { return NXACMEES_OOM }
222
223 if nx_jose_sign_es256_flattened(prot, prot_off[0], pay, pay_off[0],
224 priv_limbs, out, cap, out_n) != NXES_OK { return NXACMEES_SIGN_ERR }
225 return NXACMEES_OK
226}
227
228// ---- KAT (fully offline) -----------------------------------------
229func main() -> i64 {
230 let priv_be: *u8 = sys_mmap(32)
231 var i: i64 = 0
232 while i < 32 { priv_be[i] = (i + 7) as u8; i = i + 1 }
233 let priv: *i64 = u256_alloc()
234 u256_load_be(priv, priv_be)
235 let px: *i64 = u256_alloc()
236 let py: *i64 = u256_alloc()
237 nx_p256_pubkey_from_priv(priv, px, py)
238
239 // 1. new-account JWS (ES256) -> verify the flattened JWS sig roundtrips.
240 let nonce: *u8 = "Xj4-nonce-abc" as *u8
241 let url: *u8 = "https://acme-staging-v02.api.letsencrypt.org/acme/new-acct" as *u8
242 let email: *u8 = "elderwesto@gmail.com" as *u8
243 let jws: *u8 = sys_mmap(NXACMEES_MAGIC_4096)
244 let jws_n: *i64 = sys_mmap(8) as *i64
245 if nx_acme_build_new_account_jws_es256(priv, px, py, nonce, 13, url, 58, email, 20,
246 jws, NXACMEES_MAGIC_4096, jws_n) != NXACMEES_OK {
247 sys_write(2, "new-account JWS FAIL\n" as *u8, 21); return 1
248 }
249 sys_write(1, "new-acct JWS: " as *u8, 14); sys_write(1, jws, jws_n[0]); sys_write(1, "\n" as *u8, 1)
250
251 // 2. new-order payload (2 domains).
252 let dp: *i64 = sys_mmap(16) as *i64
253 let dl: *i64 = sys_mmap(16) as *i64
254 dp[0] = ("andelinwest.com" as *u8) as i64; dl[0] = 15
255 dp[1] = ("www.andelinwest.com" as *u8) as i64; dl[1] = 19
256 let ord: *u8 = sys_mmap(512)
257 let ord_off: *i64 = sys_mmap(8) as *i64
258 ord_off[0] = 0
259 if nx_acme_payload_new_order(ord, ord_off, 512, dp, dl, 2) != NXACMEES_OK {
260 sys_write(2, "new-order payload FAIL\n" as *u8, 23); return 2
261 }
262 sys_write(1, "new-order: " as *u8, 11); sys_write(1, ord, ord_off[0]); sys_write(1, "\n" as *u8, 1)
263
264 // 3. DNS-01 TXT value -- determinism + non-empty + correct length (43 b64url chars of a 32B digest).
265 let tok: *u8 = "evaGxfkz_qY-token-1234" as *u8
266 let txt1: *u8 = sys_mmap(64)
267 let txt2: *u8 = sys_mmap(64)
268 let t1n: *i64 = sys_mmap(8) as *i64
269 let t2n: *i64 = sys_mmap(8) as *i64
270 if nx_acme_dns01_txt_value(tok, 22, px, py, txt1, 64, t1n) != NXACMEES_OK {
271 sys_write(2, "dns01 txt FAIL\n" as *u8, 15); return 3
272 }
273 nx_acme_dns01_txt_value(tok, 22, px, py, txt2, 64, t2n)
274 if t1n[0] != 43 { sys_write(2, "dns01 txt wrong len FAIL\n" as *u8, 25); return 4 }
275 var same: i64 = 1
276 var z: i64 = 0
277 while z < t1n[0] { if txt1[z] != txt2[z] { same = 0 }; z = z + 1 }
278 if same != 1 { sys_write(2, "dns01 txt nondeterministic FAIL\n" as *u8, 32); return 5 }
279 sys_write(1, "dns01 TXT: " as *u8, 11); sys_write(1, txt1, t1n[0]); sys_write(1, "\n" as *u8, 1)
280
281 sys_write(1, "ACME ES256 builders + DNS-01 KAT PASS\n" as *u8, 38)
282 return 0
283}