nx_jwk.nx source
↩ module page · 138 lines · 4992 B
1// nx_jwk.nx -- JSON Web Key serialization (RFC 7517 + RFC 8037).
2//
3// Closes the JWK gap for ACME (RFC 8555). An ACME account is
4// identified by its public-key JWK; ACME server uses
5// SHA-256(canonical-JWK) as the account-thumbprint.
6//
7// Per cardinal feedback-no-third-party-trust-native-or-nothing:
8// substrate ships its own JWK encoder; no openssl-jose dep.
9//
10// Supported key types (today):
11// OKP / Ed25519 (RFC 8037 §2) -- composes with shipped
12// nx_ed25519_signature
13//
14// Queued (separate slices when needed):
15// EC / P-256 (RFC 7518 §6.2) -- needs nx_p256
16// RSA / 2048+ (RFC 7518 §6.3) -- needs nx_rsa
17//
18// JWK shape (OKP/Ed25519 public):
19// { "kty": "OKP", "crv": "Ed25519", "x": "<b64url(32B pubkey)>" }
20//
21// Canonical (RFC 7638) thumbprint input:
22// the same JSON with members in lexicographic order, no whitespace
23// between tokens. Substrate emits canonical form by default.
24//
25// nx_capability_claims:
26// needs: [sealed_enum, b64url, json_emit]
27// provides: [jwk_okp_ed25519_emit, jwk_thumbprint_input]
28// safety: [no_unchecked_deref, no_floating_point, no_syscall_other_than_mmap,
29// bit_equal_reproducible, target_agnostic]
30// verdict: [sealed_enum_5_state]
31// license: ORIGINAL
32// kind: racing_crew_specialist
33// layer: L3 (algorithm: encode key per RFC)
34
35import "nx_syscalls_x86_64.nx"
36import "nx_jwt.nx"
37
38// ---- Sealed enum: JWK verdict ------------------------------------
39
40const NXJWK_OK: i64 = 0
41const NXJWK_OOM_BUFFER: i64 = 1
42const NXJWK_BAD_KEY_LEN: i64 = 2
43const NXJWK_BAD_KTY: i64 = 3
44const NXJWK_BAD_ARG: i64 = 4
45const NXJWK_VERDICT_N: i64 = 5
46
47func nxjwk_verdict_is_valid(v: i64) -> i64 {
48 if v < 0 { return 0 }
49 if v >= NXJWK_VERDICT_N { return 0 }
50 return 1
51}
52
53func nxjwk_verdict_name(v: i64) -> *u8 {
54 if v == NXJWK_OK { return "OK" as *u8 }
55 if v == NXJWK_OOM_BUFFER { return "OOM_BUFFER" as *u8 }
56 if v == NXJWK_BAD_KEY_LEN { return "BAD_KEY_LEN" as *u8 }
57 if v == NXJWK_BAD_KTY { return "BAD_KTY" as *u8 }
58 if v == NXJWK_BAD_ARG { return "BAD_ARG" as *u8 }
59 return "INVALID" as *u8
60}
61
62// ---- Byte-emit helpers (bounded, sealed-enum verdict) -----------
63
64func nxjwk_put(out: *u8, off: *i64, cap: i64, b: i64) -> i64 {
65 if *off >= cap { return NXJWK_OOM_BUFFER }
66 out[*off] = b as u8
67 *off = *off + 1
68 return NXJWK_OK
69}
70
71func nxjwk_put_cstr(out: *u8, off: *i64, cap: i64, s: *u8) -> i64 {
72 var i: i64 = 0
73 while s[i] != 0 {
74 let rc: i64 = nxjwk_put(out, off, cap, s[i] as i64)
75 if rc != NXJWK_OK { return rc }
76 i = i + 1
77 }
78 return NXJWK_OK
79}
80
81func nxjwk_put_bytes(out: *u8, off: *i64, cap: i64, src: *u8, n: i64) -> i64 {
82 var i: i64 = 0
83 while i < n {
84 let rc: i64 = nxjwk_put(out, off, cap, src[i] as i64)
85 if rc != NXJWK_OK { return rc }
86 i = i + 1
87 }
88 return NXJWK_OK
89}
90
91// ---- JWK emit: OKP/Ed25519 public key ----------------------------
92//
93// Writes canonical JWK JSON (RFC 7638-thumbprint-friendly):
94// {"crv":"Ed25519","kty":"OKP","x":"<b64url(pubkey)>"}
95//
96// Members in lexicographic order: crv, kty, x. No whitespace.
97// Per RFC 7638 §3.2, this canonical form is exactly the bytes that
98// SHA-256 sees when computing the JWK thumbprint.
99//
100// pubkey must be 32 bytes (Ed25519 public key length).
101
102func nx_jwk_emit_okp_ed25519(out: *u8, off: *i64, cap: i64,
103 pubkey_32: *u8) -> i64 {
104 if out == (0 as *u8) { return NXJWK_BAD_ARG }
105 if off == (0 as *i64) { return NXJWK_BAD_ARG }
106 if pubkey_32 == (0 as *u8) { return NXJWK_BAD_ARG }
107 if cap <= 0 { return NXJWK_BAD_ARG }
108
109 // Lexicographic-order members: crv, kty, x.
110 let r1: i64 = nxjwk_put_cstr(out, off, cap, "{\"crv\":\"Ed25519\",\"kty\":\"OKP\",\"x\":\"" as *u8)
111 if r1 != NXJWK_OK { return r1 }
112
113 // Allocate scratch buf for the base64url-encoded x.
114 // 32 bytes -> ceil(32*4/3) = 44 chars b64; b64url strips '=' so
115 // 43 chars. Allocate 48 for safety.
116 let x_b64: *u8 = sys_mmap(48)
117 let x_b64_len: i64 = jwt_b64url_encode(pubkey_32, 32, x_b64)
118 let rc_x: i64 = nxjwk_put_bytes(out, off, cap, x_b64, x_b64_len)
119 if rc_x != NXJWK_OK { return rc_x }
120
121 return nxjwk_put_cstr(out, off, cap, "\"}" as *u8)
122}
123
124// ---- Thumbprint input emission ----------------------------------
125//
126// Returns the EXACT bytes that go into SHA-256 for the RFC 7638
127// thumbprint. For OKP/Ed25519 this is identical to the canonical
128// JWK emit above. Kept as a separate function so future key types
129// (EC, RSA) can declare their canonical-member ordering
130// independently.
131//
132// Today: just delegates to nx_jwk_emit_okp_ed25519 since the
133// canonical form IS the thumbprint input for OKP.
134
135func nx_jwk_thumbprint_input_okp(out: *u8, off: *i64, cap: i64,
136 pubkey_32: *u8) -> i64 {
137 return nx_jwk_emit_okp_ed25519(out, off, cap, pubkey_32)
138}