code wiki / (root) / nx_oauth2_pkce.nx

nx_oauth2_pkce.nx source

↩ module page · 142 lines · 5003 B

1// oauth2_pkce.nx -- Proof Key for Code Exchange (RFC 7636). 2// 3// Mandatory OAuth 2.0 extension for public clients (mobile apps, 4// SPAs, CLI tools) that can't keep a client_secret secret. Also 5// recommended for confidential clients per OAuth 2.1 draft. 6// 7// Flow: 8// 1. Client generates a random 43-128 char verifier. 9// 2. Client sends challenge = BASE64URL(SHA-256(verifier)) to 10// authorize endpoint along with code_challenge_method=S256. 11// 3. Server stores challenge + issues auth code. 12// 4. Client redeems code + sends original verifier to token 13// endpoint. 14// 5. Server re-derives challenge from verifier, compares; if 15// equal, issues access token. 16// 17// This module ships: 18// - pkce_verifier_new(out) -> random 43-char 19// - pkce_challenge(verifier, verifier_len, out) -> challenge 20// 21// Composes rand.nx + sha256.nx + base64.nx (with URL-safe + 22// unpadded normalisation inline). 23// 24// Invariants: 25// PKCE1 Verifier chars drawn from [A-Z a-z 0-9 - . _ ~] per 26// RFC 7636 ยง4.1 unreserved set. 43 chars default ~= 27// 256 bits of entropy. 28// PKCE2 Challenge = BASE64URL(SHA256(verifier)) without 29// padding ('=' stripped). 30// PKCE3 Output is ASCII only; safe to embed in URLs without 31// further encoding. 32 33// nx_safety_envelope: 34// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 35// sil_target: SIL1 36// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 37// verdict: NOT_YET_EVALUATED 38 39import "nx_syscalls.nx" 40import "nx_rand.nx" 41import "nx_sha256.nx" 42import "nx_base64.nx" 43 44const PKCE_VERIFIER_DEFAULT_LEN: i64 = 43 45const PKCE_CHALLENGE_LEN: i64 = 43 // base64url(sha256(x)) w/o pad 46 47// Map a 6-bit random value to an unreserved char. 48func pkce_map_char(v: i64) -> i64 { 49 if v < 26 { return 0x41 + v } // A-Z 50 if v < 52 { return 0x61 + (v - 26) } // a-z 51 if v < 62 { return 0x30 + (v - 52) } // 0-9 52 if v == 62 { return 0x2D } // - 53 if v == 63 { return 0x2E } // . 54 return 0x5F // _ (shouldn't hit; 6 bits max 63) 55} 56 57// Generate a 43-char PKCE verifier. Caller supplies output 58// buffer (must have room for PKCE_VERIFIER_DEFAULT_LEN bytes). 59func pkce_verifier_new(out: *u8) -> i64 { 60 // Pull 43 random bytes; mask each to 6 bits; map to alphabet. 61 rand_bytes(out, PKCE_VERIFIER_DEFAULT_LEN) 62 var i: i64 = 0 63 while i < PKCE_VERIFIER_DEFAULT_LEN { 64 let v: i64 = out[i] & 0x3F 65 out[i] = pkce_map_char(v) 66 i = i + 1 67 } 68 return PKCE_VERIFIER_DEFAULT_LEN 69} 70 71// Derive the S256 challenge from a verifier. out must have room 72// for PKCE_CHALLENGE_LEN (43) bytes. Returns bytes written. 73func pkce_challenge(verifier: *u8, verifier_len: i64, 74 out: *u8) -> i64 { 75 let digest: *u8 = sys_mmap(64) 76 sha256_digest(verifier, verifier_len, digest) 77 78 // Standard base64 encode -> 44 chars with 1 trailing '=' 79 // (since 32 bytes is not a multiple of 3). Patch + / to - _ 80 // in place, strip '=' padding. 81 let b64_out: *u8 = sys_mmap(64) 82 let b64_len: i64 = b64_encode(digest, 32, b64_out) 83 var stripped: i64 = b64_len 84 while stripped > 0 { 85 if b64_out[stripped - 1] != 0x3D { break } 86 stripped = stripped - 1 87 } 88 var i: i64 = 0 89 while i < stripped { 90 var c: i64 = b64_out[i] 91 if c == 0x2B { c = 0x2D } // '+' -> '-' 92 if c == 0x2F { c = 0x5F } // '/' -> '_' 93 out[i] = c 94 i = i + 1 95 } 96 return stripped 97} 98 99// Compile-only smoke. Uses RFC 7636 Appendix B test vectors 100// indirectly: any verifier should produce a 43-char URL-safe 101// challenge that matches the expected shape. 102func main() -> i64 { 103 // Generate a verifier. 104 let v: *u8 = sys_mmap(64) 105 let vlen: i64 = pkce_verifier_new(v) 106 if vlen != 43 { return 1 } 107 // All chars unreserved. 108 var i: i64 = 0 109 while i < vlen { 110 let c: i64 = v[i] 111 var ok: i64 = 0 112 if c >= 0x41 { 113 if c <= 0x5A { ok = 1 } 114 } 115 if c >= 0x61 { 116 if c <= 0x7A { ok = 1 } 117 } 118 if c >= 0x30 { 119 if c <= 0x39 { ok = 1 } 120 } 121 if c == 0x2D { ok = 1 } 122 if c == 0x2E { ok = 1 } 123 if c == 0x5F { ok = 1 } 124 if ok == 0 { return 2 } 125 i = i + 1 126 } 127 128 // Derive challenge from the RFC 7636 example verifier: 129 // "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" 130 // expected challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" 131 let ref_v: *u8 = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" 132 let out: *u8 = sys_mmap(64) 133 let n: i64 = pkce_challenge(ref_v, 43, out) 134 if n != 43 { return 3 } 135 let expect: *u8 = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" 136 i = 0 137 while i < 43 { 138 if out[i] != expect[i] { return 10 + i } 139 i = i + 1 140 } 141 return 0 142}