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}