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}