code wiki / (root) / nx_jwk.nx

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}