code wiki / (root) / nx_jose.nx

nx_jose.nx source

↩ module page · 230 lines · 8422 B

1// nx_jose.nx -- JOSE JWS flattened-JSON signing (RFC 7515 + RFC 8037). 2// 3// Closes the JWS gap for ACME (RFC 8555). ACME requests are wrapped 4// in JWS flattened-JSON-serialization, with EdDSA (RFC 8037) being 5// one accepted signing algorithm. 6// 7// The JWS flattened-JSON shape (RFC 7515 §7.2.2): 8// 9// { 10// "protected": "<b64url(protected_header)>", 11// "payload": "<b64url(payload_bytes)>", 12// "signature": "<b64url(signature_bytes)>" 13// } 14// 15// Signature input (RFC 7515 §5.1): 16// 17// b64url(protected_header) || "." || b64url(payload) 18// 19// (yes -- the literal ASCII dot is included in the signed bytes). 20// 21// Per cardinal feedback-no-third-party-trust-native-or-nothing: 22// substrate ships its own JWS signer; no openssl-jose / pyjwt dep. 23// 24// Supported algorithms today: 25// EdDSA (RFC 8037 §3.1) -- composes ed25519_sign_full 26// 27// Queued (separate slices): 28// ES256 (ECDSA P-256 + SHA-256) -- needs nx_p256 29// RS256 (RSA + SHA-256) -- needs nx_rsa 30// 31// nx_capability_claims: 32// needs: [sealed_enum, b64url, ed25519_sign, json_emit] 33// provides: [jws_flattened_json_sign_eddsa, jws_signature_input] 34// safety: [no_unchecked_deref, no_floating_point, 35// bit_equal_reproducible, target_agnostic, 36// caller_supplies_protected_header] 37// verdict: [sealed_enum_6_state] 38// license: ORIGINAL 39// kind: racing_crew_specialist 40// layer: L4 (composite over L3 ed25519 + b64url + json) 41 42import "nx_syscalls_x86_64.nx" 43import "nx_jwt.nx" 44import "nx_ed25519_signature.nx" 45 46// ---- Sealed enum: JOSE verdict ----------------------------------- 47 48const NXJOSE_OK: i64 = 0 49const NXJOSE_OOM_BUFFER: i64 = 1 50const NXJOSE_BAD_KEY_LEN: i64 = 2 51const NXJOSE_BAD_ALG: i64 = 3 52const NXJOSE_SIGN_ERR: i64 = 4 53const NXJOSE_BAD_ARG: i64 = 5 54const NXJOSE_VERDICT_N: i64 = 6 55 56func nxjose_verdict_is_valid(v: i64) -> i64 { 57 if v < 0 { return 0 } 58 if v >= NXJOSE_VERDICT_N { return 0 } 59 return 1 60} 61 62func nxjose_verdict_name(v: i64) -> *u8 { 63 if v == NXJOSE_OK { return "OK" as *u8 } 64 if v == NXJOSE_OOM_BUFFER { return "OOM_BUFFER" as *u8 } 65 if v == NXJOSE_BAD_KEY_LEN { return "BAD_KEY_LEN" as *u8 } 66 if v == NXJOSE_BAD_ALG { return "BAD_ALG" as *u8 } 67 if v == NXJOSE_SIGN_ERR { return "SIGN_ERR" as *u8 } 68 if v == NXJOSE_BAD_ARG { return "BAD_ARG" as *u8 } 69 return "INVALID" as *u8 70} 71 72// ---- Sealed enum: algorithm -------------------------------------- 73 74const NXJOSE_ALG_EDDSA: i64 = 0 75const NXJOSE_ALG_N: i64 = 1 // ES256, RS256 queued 76 77func nxjose_alg_is_valid(a: i64) -> i64 { 78 if a < 0 { return 0 } 79 if a >= NXJOSE_ALG_N { return 0 } 80 return 1 81} 82 83// ---- Byte-emit helpers ------------------------------------------- 84 85func nxjose_put(out: *u8, off: *i64, cap: i64, b: i64) -> i64 { 86 if *off >= cap { return NXJOSE_OOM_BUFFER } 87 out[*off] = b as u8 88 *off = *off + 1 89 return NXJOSE_OK 90} 91 92func nxjose_put_cstr(out: *u8, off: *i64, cap: i64, s: *u8) -> i64 { 93 var i: i64 = 0 94 while s[i] != 0 { 95 let rc: i64 = nxjose_put(out, off, cap, s[i] as i64) 96 if rc != NXJOSE_OK { return rc } 97 i = i + 1 98 } 99 return NXJOSE_OK 100} 101 102func nxjose_put_bytes(out: *u8, off: *i64, cap: i64, src: *u8, n: i64) -> i64 { 103 var i: i64 = 0 104 while i < n { 105 let rc: i64 = nxjose_put(out, off, cap, src[i] as i64) 106 if rc != NXJOSE_OK { return rc } 107 i = i + 1 108 } 109 return NXJOSE_OK 110} 111 112// ---- Compute the JWS signature input ----------------------------- 113// 114// Per RFC 7515 §5.1, the bytes that are signed are: 115// 116// b64url(protected) || "." || b64url(payload) 117// 118// Caller supplies an output buffer; this writes the joined bytes 119// into out_buf and returns *out_n. Useful for callers that want 120// to verify the signature input matches their expectation. 121 122func nx_jose_signature_input( 123 protected_json: *u8, protected_n: i64, 124 payload: *u8, payload_n: i64, 125 out_buf: *u8, out_cap: i64, 126 out_n: *i64) -> i64 { 127 if protected_json == (0 as *u8) { return NXJOSE_BAD_ARG } 128 if payload == (0 as *u8) && payload_n > 0 { return NXJOSE_BAD_ARG } 129 if out_buf == (0 as *u8) { return NXJOSE_BAD_ARG } 130 if out_n == (0 as *i64) { return NXJOSE_BAD_ARG } 131 if protected_n < 0 { return NXJOSE_BAD_ARG } 132 if payload_n < 0 { return NXJOSE_BAD_ARG } 133 if out_cap <= 0 { return NXJOSE_BAD_ARG } 134 135 var off: i64 = 0 136 137 // b64url(protected_json) 138 let p_scratch: *u8 = sys_mmap(((protected_n * 4 / 3) + 16)) 139 let p_b64_n: i64 = jwt_b64url_encode(protected_json, protected_n, p_scratch) 140 let r1: i64 = nxjose_put_bytes(out_buf, &off, out_cap, p_scratch, p_b64_n) 141 if r1 != NXJOSE_OK { return r1 } 142 143 // literal ASCII dot 144 let r2: i64 = nxjose_put(out_buf, &off, out_cap, 0x2e) 145 if r2 != NXJOSE_OK { return r2 } 146 147 // b64url(payload) 148 let pay_scratch: *u8 = sys_mmap(((payload_n * 4 / 3) + 16)) 149 let pay_b64_n: i64 = jwt_b64url_encode(payload, payload_n, pay_scratch) 150 let r3: i64 = nxjose_put_bytes(out_buf, &off, out_cap, pay_scratch, pay_b64_n) 151 if r3 != NXJOSE_OK { return r3 } 152 153 *out_n = off 154 return NXJOSE_OK 155} 156 157// ---- Sign with EdDSA + emit flattened JSON ----------------------- 158// 159// Caller supplies: 160// - protected_json (already-serialized JSON header bytes; must 161// include "alg":"EdDSA" and either "jwk" or "kid") 162// - payload (the request body, any bytes) 163// - priv_32 (Ed25519 private seed, 32 bytes) 164// 165// We emit: 166// {"protected":"<b64url>","payload":"<b64url>","signature":"<b64url>"} 167// 168// out_n holds the bytes written. 169 170func nx_jose_sign_eddsa_flattened( 171 protected_json: *u8, protected_n: i64, 172 payload: *u8, payload_n: i64, 173 priv_32: *u8, 174 out_buf: *u8, out_cap: i64, 175 out_n: *i64) -> i64 { 176 if priv_32 == (0 as *u8) { return NXJOSE_BAD_ARG } 177 if protected_json == (0 as *u8) { return NXJOSE_BAD_ARG } 178 if payload == (0 as *u8) && payload_n > 0 { return NXJOSE_BAD_ARG } 179 if out_buf == (0 as *u8) { return NXJOSE_BAD_ARG } 180 if out_n == (0 as *i64) { return NXJOSE_BAD_ARG } 181 if protected_n < 0 { return NXJOSE_BAD_ARG } 182 if payload_n < 0 { return NXJOSE_BAD_ARG } 183 if out_cap <= 0 { return NXJOSE_BAD_ARG } 184 185 // ---- Step 1: build signature input ---- 186 let sig_input_cap: i64 = (protected_n + payload_n + 64) * 2 187 let sig_input: *u8 = sys_mmap(sig_input_cap) 188 let sig_input_n: *i64 = sys_mmap(8) as *i64 189 let r_si: i64 = nx_jose_signature_input(protected_json, protected_n, 190 payload, payload_n, 191 sig_input, sig_input_cap, sig_input_n) 192 if r_si != NXJOSE_OK { return r_si } 193 194 // ---- Step 2: ed25519_sign_full ---- 195 let sig_64: *u8 = sys_mmap(64) 196 let sign_rc: i64 = ed25519_sign_full(priv_32, sig_input, sig_input_n[0], sig_64) 197 if sign_rc != 0 { return NXJOSE_SIGN_ERR } 198 199 // ---- Step 3: b64url the protected header, payload, signature ---- 200 let h_b64_cap: i64 = (protected_n * 4 / 3) + 16 201 let h_b64: *u8 = sys_mmap(h_b64_cap) 202 let h_b64_n: i64 = jwt_b64url_encode(protected_json, protected_n, h_b64) 203 204 let p_b64_cap: i64 = (payload_n * 4 / 3) + 16 205 let p_b64: *u8 = sys_mmap(p_b64_cap) 206 let p_b64_n: i64 = jwt_b64url_encode(payload, payload_n, p_b64) 207 208 let s_b64: *u8 = sys_mmap(128) 209 let s_b64_n: i64 = jwt_b64url_encode(sig_64, 64, s_b64) 210 211 // ---- Step 4: emit flattened JSON ---- 212 var off: i64 = 0 213 let r1: i64 = nxjose_put_cstr(out_buf, &off, out_cap, "{\"protected\":\"" as *u8) 214 if r1 != NXJOSE_OK { return r1 } 215 let r2: i64 = nxjose_put_bytes(out_buf, &off, out_cap, h_b64, h_b64_n) 216 if r2 != NXJOSE_OK { return r2 } 217 let r3: i64 = nxjose_put_cstr(out_buf, &off, out_cap, "\",\"payload\":\"" as *u8) 218 if r3 != NXJOSE_OK { return r3 } 219 let r4: i64 = nxjose_put_bytes(out_buf, &off, out_cap, p_b64, p_b64_n) 220 if r4 != NXJOSE_OK { return r4 } 221 let r5: i64 = nxjose_put_cstr(out_buf, &off, out_cap, "\",\"signature\":\"" as *u8) 222 if r5 != NXJOSE_OK { return r5 } 223 let r6: i64 = nxjose_put_bytes(out_buf, &off, out_cap, s_b64, s_b64_n) 224 if r6 != NXJOSE_OK { return r6 } 225 let r7: i64 = nxjose_put_cstr(out_buf, &off, out_cap, "\"}" as *u8) 226 if r7 != NXJOSE_OK { return r7 } 227 228 *out_n = off 229 return NXJOSE_OK 230}