code wiki / (root) / nx_acme_es256.nx

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}