1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
//! A helper for generating PKCE (Proof Key for Code Exchange) pairs.
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use rand::TryRng as _;
use sha2::{Digest, Sha256};
/// The PKCE pair generated using the `S256` method of RFC 7636.
pub struct Pkce {
/// Verifier
pub verifier: String,
/// Challenge
pub challenge: String,
/// Method
pub method: &'static str,
}
impl Pkce {
/// Creates a new PKCE verifier and challenger pair using the `S256` method of RFC 7636.
#[must_use]
pub fn generate_s256_pair() -> Self {
let mut verifier_bytes = [0u8; 32];
rand::rng()
.try_fill_bytes(&mut verifier_bytes)
.unwrap_or_else(|e: std::convert::Infallible| match e {});
let verifier = URL_SAFE_NO_PAD.encode(verifier_bytes);
let mut hasher = Sha256::new();
hasher.update(verifier.as_bytes());
let challenge_bytes = hasher.finalize();
let challenge = URL_SAFE_NO_PAD.encode(challenge_bytes);
Self {
verifier,
challenge,
method: "S256",
}
}
/// Creates a new PKCE verifier and challenger pair using the `plain` method of RFC 7636.
#[must_use]
pub fn generate_plain_pair() -> Self {
let mut verifier_bytes = [0u8; 32];
rand::rng()
.try_fill_bytes(&mut verifier_bytes)
.unwrap_or_else(|e: std::convert::Infallible| match e {});
let verifier = URL_SAFE_NO_PAD.encode(verifier_bytes);
Self {
challenge: verifier.clone(),
verifier,
method: "plain",
}
}
}
#[cfg(all(
test,
all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))
))]
mod wasm_tests {
use wasm_bindgen_test::*;
use super::*;
/// Tests that PKCE S256 pair generation works correctly on WASM, exercising
/// the WASM RNG, SHA-256, and base64url encoding.
#[wasm_bindgen_test]
fn test_s256_pair_on_wasm() {
let pkce = Pkce::generate_s256_pair();
// Verifier length (RFC 7636 §4.1)
assert!(pkce.verifier.len() >= 43);
assert!(pkce.verifier.len() <= 128);
// Challenge is BASE64URL(SHA256(verifier)) (RFC 7636 §4.2)
let mut hasher = sha2::Sha256::new();
sha2::Digest::update(&mut hasher, pkce.verifier.as_bytes());
let expected = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hasher.finalize());
assert_eq!(pkce.challenge, expected);
// Two pairs must differ (RNG is working)
let pkce2 = Pkce::generate_s256_pair();
assert_ne!(pkce.verifier, pkce2.verifier);
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Tests RFC 7636 §4.1 - Code Verifier length validation
///
/// Per RFC 7636, the code verifier MUST have a minimum length of 43 characters
/// and a maximum length of 128 characters.
///
/// Reference: <https://tools.ietf.org/html/rfc7636#section-4.1>
#[test]
fn test_rfc7636_4_1_verifier_length_validation() {
// Generate multiple PKCE pairs and verify length constraints
for _ in 0..10 {
let pkce = Pkce::generate_s256_pair();
let verifier_len = pkce.verifier.len();
assert!(
verifier_len >= 43,
"code_verifier length {verifier_len} must be at least 43 characters (RFC 7636 §4.1)"
);
assert!(
verifier_len <= 128,
"code_verifier length {verifier_len} must be at most 128 characters (RFC 7636 §4.1)"
);
}
}
/// Tests RFC 7636 §4.1 - Code Verifier character set validation
///
/// Per RFC 7636, the code verifier MUST use characters from the set
/// [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" (unreserved characters)
///
/// Reference: <https://tools.ietf.org/html/rfc7636#section-4.1>
#[test]
fn test_rfc7636_4_1_verifier_charset_validation() {
for _ in 0..10 {
let pkce = Pkce::generate_s256_pair();
// Verify all characters are from the allowed set (base64url without padding)
// Base64url uses: A-Z, a-z, 0-9, -, _
for ch in pkce.verifier.chars() {
assert!(
ch.is_ascii_alphanumeric() || ch == '-' || ch == '_',
"code_verifier contains invalid character '{ch}' (RFC 7636 §4.1)"
);
}
}
}
/// Tests RFC 7636 §4.1 - Code Verifier randomness
///
/// The code verifier MUST be generated using cryptographically secure
/// random bytes to prevent attacks.
///
/// Reference: <https://tools.ietf.org/html/rfc7636#section-4.1>
#[test]
fn test_rfc7636_4_1_verifier_randomness() {
// Generate multiple verifiers and ensure they're all different
let mut verifiers = std::collections::HashSet::new();
for _ in 0..100 {
let pkce = Pkce::generate_s256_pair();
verifiers.insert(pkce.verifier.clone());
}
// All 100 verifiers should be unique (probability of collision is astronomically low)
assert_eq!(
verifiers.len(),
100,
"code_verifier must be cryptographically random (RFC 7636 §4.1)"
);
}
/// Tests RFC 7636 §4.1 - Reject invalid verifiers
///
/// This test documents what would constitute an invalid verifier
/// according to RFC 7636.
///
/// Reference: <https://tools.ietf.org/html/rfc7636#section-4.1>
#[test]
fn test_rfc7636_4_1_reject_invalid_verifier() {
// Test that our implementation generates valid verifiers
// Invalid cases (for documentation):
// Too short (< 43 chars)
let too_short = "a".repeat(42);
assert!(
too_short.len() < 43,
"verifiers shorter than 43 chars would be invalid"
);
// Too long (> 128 chars)
let too_long = "a".repeat(129);
assert!(
too_long.len() > 128,
"verifiers longer than 128 chars would be invalid"
);
// Invalid characters (examples)
let invalid_chars = vec![
"valid+verifier", // '+' not allowed
"valid/verifier", // '/' not allowed
"valid=verifier", // '=' not allowed (no padding)
"valid verifier", // space not allowed
"valid$verifier", // '$' not allowed
];
for invalid in invalid_chars {
assert!(
invalid.contains(|c: char| !c.is_ascii_alphanumeric() && c != '-' && c != '_'),
"'{invalid}' contains invalid characters"
);
}
}
/// Tests RFC 7636 §4.2 - Code Challenge Method S256
///
/// For the S256 method:
/// `code_challenge` = `BASE64URL(SHA256(ASCII(code_verifier)))`
///
/// Reference: <https://tools.ietf.org/html/rfc7636#section-4.2>
#[test]
fn test_rfc7636_4_2_challenge_s256_method() {
let pkce = Pkce::generate_s256_pair();
// Manually compute the expected challenge
let mut hasher = Sha256::new();
hasher.update(pkce.verifier.as_bytes());
let hash = hasher.finalize();
let expected_challenge = URL_SAFE_NO_PAD.encode(hash);
assert_eq!(
pkce.challenge, expected_challenge,
"S256 challenge must be BASE64URL(SHA256(verifier))"
);
}
/// Tests RFC 7636 §4.2 - Code Challenge encoding
///
/// The code challenge MUST be BASE64URL encoded (without padding).
///
/// Reference: <https://tools.ietf.org/html/rfc7636#section-4.2>
#[test]
fn test_rfc7636_4_2_challenge_base64url_encoding() {
let pkce = Pkce::generate_s256_pair();
// Verify challenge uses base64url character set (no padding)
for ch in pkce.challenge.chars() {
assert!(
ch.is_ascii_alphanumeric() || ch == '-' || ch == '_',
"challenge must use base64url encoding without padding"
);
}
// Verify no padding characters
assert!(
!pkce.challenge.contains('='),
"challenge must not contain padding '='"
);
// Verify it's valid base64url by decoding
let decoded = URL_SAFE_NO_PAD.decode(&pkce.challenge);
assert!(
decoded.is_ok(),
"challenge must be valid base64url: {:?}",
decoded.err()
);
// For S256, the decoded value should be 32 bytes (SHA-256 output)
assert_eq!(
decoded.unwrap().len(),
32,
"S256 challenge should decode to 32 bytes (SHA-256 hash)"
);
}
/// Tests RFC 7636 §4.2 - Code Challenge plain method (discouraged)
///
/// The plain method sets `code_challenge` = `code_verifier`.
/// This method is NOT RECOMMENDED and should only be used if S256 is not possible.
///
/// Note: Our implementation only supports S256 (the secure method).
///
/// Reference: <https://tools.ietf.org/html/rfc7636#section-4.2>
#[test]
fn test_rfc7636_4_2_challenge_plain_method_not_used() {
let pkce = Pkce::generate_s256_pair();
// Verify we're NOT using plain method (challenge != verifier)
assert_ne!(
pkce.challenge, pkce.verifier,
"implementation should use S256, not plain method (RFC 7636 §4.2)"
);
// Verify challenge is longer than verifier (base64 encoding of hash)
// SHA-256 hash (32 bytes) -> base64url (43 chars)
// Our verifier is also 43 chars (from 32 random bytes)
// So they should be equal length, but the challenge should be
// the hash of verifier, not the verifier itself
// Compute what plain method would produce
let plain_challenge = &pkce.verifier;
assert_ne!(
&pkce.challenge, plain_challenge,
"must use S256 method, not plain (RFC 7636 §4.2)"
);
}
}