code wiki / (root) / oauth2_pkce.nx

oauth2_pkce.nx source

↩ module page · 136 lines · 4876 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 33import "syscalls.nx" 34import "rand.nx" 35import "sha256.nx" 36import "base64.nx" 37 38const PKCE_VERIFIER_DEFAULT_LEN: i64 = 43 39const PKCE_CHALLENGE_LEN: i64 = 43 // base64url(sha256(x)) w/o pad 40 41// Map a 6-bit random value to an unreserved char. 42func pkce_map_char(v: i64) -> i64 { 43 if v < 26 { return 0x41 + v } // A-Z 44 if v < 52 { return 0x61 + (v - 26) } // a-z 45 if v < 62 { return 0x30 + (v - 52) } // 0-9 46 if v == 62 { return 0x2D } // - 47 if v == 63 { return 0x2E } // . 48 return 0x5F // _ (shouldn't hit; 6 bits max 63) 49} 50 51// Generate a 43-char PKCE verifier. Caller supplies output 52// buffer (must have room for PKCE_VERIFIER_DEFAULT_LEN bytes). 53func pkce_verifier_new(out: *u8) -> i64 { 54 // Pull 43 random bytes; mask each to 6 bits; map to alphabet. 55 rand_bytes(out, PKCE_VERIFIER_DEFAULT_LEN) 56 var i: i64 = 0 57 while i < PKCE_VERIFIER_DEFAULT_LEN { 58 let v: i64 = out[i] & 0x3F 59 out[i] = pkce_map_char(v) 60 i = i + 1 61 } 62 return PKCE_VERIFIER_DEFAULT_LEN 63} 64 65// Derive the S256 challenge from a verifier. out must have room 66// for PKCE_CHALLENGE_LEN (43) bytes. Returns bytes written. 67func pkce_challenge(verifier: *u8, verifier_len: i64, 68 out: *u8) -> i64 { 69 let digest: *u8 = sys_mmap(64) 70 sha256_digest(verifier, verifier_len, digest) 71 72 // Standard base64 encode -> 44 chars with 1 trailing '=' 73 // (since 32 bytes is not a multiple of 3). Patch + / to - _ 74 // in place, strip '=' padding. 75 let b64_out: *u8 = sys_mmap(64) 76 let b64_len: i64 = b64_encode(digest, 32, b64_out) 77 var stripped: i64 = b64_len 78 while stripped > 0 { 79 if b64_out[stripped - 1] != 0x3D { break } 80 stripped = stripped - 1 81 } 82 var i: i64 = 0 83 while i < stripped { 84 var c: i64 = b64_out[i] 85 if c == 0x2B { c = 0x2D } // '+' -> '-' 86 if c == 0x2F { c = 0x5F } // '/' -> '_' 87 out[i] = c 88 i = i + 1 89 } 90 return stripped 91} 92 93// Compile-only smoke. Uses RFC 7636 Appendix B test vectors 94// indirectly: any verifier should produce a 43-char URL-safe 95// challenge that matches the expected shape. 96func main() -> i64 { 97 // Generate a verifier. 98 let v: *u8 = sys_mmap(64) 99 let vlen: i64 = pkce_verifier_new(v) 100 if vlen != 43 { return 1 } 101 // All chars unreserved. 102 var i: i64 = 0 103 while i < vlen { 104 let c: i64 = v[i] 105 var ok: i64 = 0 106 if c >= 0x41 { 107 if c <= 0x5A { ok = 1 } 108 } 109 if c >= 0x61 { 110 if c <= 0x7A { ok = 1 } 111 } 112 if c >= 0x30 { 113 if c <= 0x39 { ok = 1 } 114 } 115 if c == 0x2D { ok = 1 } 116 if c == 0x2E { ok = 1 } 117 if c == 0x5F { ok = 1 } 118 if ok == 0 { return 2 } 119 i = i + 1 120 } 121 122 // Derive challenge from the RFC 7636 example verifier: 123 // "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" 124 // expected challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" 125 let ref_v: *u8 = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" 126 let out: *u8 = sys_mmap(64) 127 let n: i64 = pkce_challenge(ref_v, 43, out) 128 if n != 43 { return 3 } 129 let expect: *u8 = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" 130 i = 0 131 while i < 43 { 132 if out[i] != expect[i] { return 10 + i } 133 i = i + 1 134 } 135 return 0 136}