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}