turbomcp_auth/api_key_validation.rs
1//! Secure API Key Validation with Constant-Time Comparison
2//!
3//! This module provides timing-attack resistant API key validation using:
4//! - `blake3` for fast cryptographic hashing
5//! - `subtle` for constant-time comparison
6//!
7//! ## Security Properties
8//!
9//! - **Timing Attack Resistance**: Uses constant-time comparison to prevent character-by-character
10//! guessing of API keys through timing side-channels.
11//! - **Pre-hashing**: Hashes keys before comparison to ensure comparison time is independent of
12//! actual key values.
13//! - **Length Independence**: Comparison time is independent of key length due to fixed hash size.
14//!
15//! ## Attack Scenario Prevented
16//!
17//! Without constant-time comparison, an attacker could measure response times:
18//! ```text
19//! Attempt: "a..." → 0.1ms (wrong first char, fails fast)
20//! Attempt: "s..." → 0.2ms (correct first char, continues comparison)
21//! Attempt: "sk..." → 0.3ms (correct first two chars, continues longer)
22//! ```
23//!
24//! With constant-time comparison, all attempts take the same time regardless of correctness.
25//!
26//! ## Usage
27//!
28//! ```rust
29//! use turbomcp_auth::api_key_validation::validate_api_key;
30//!
31//! let provided_key = "sk_live_abc123";
32//! let expected_key = "sk_live_abc123";
33//!
34//! if validate_api_key(provided_key, expected_key) {
35//! // Authenticated
36//! } else {
37//! // Invalid key
38//! }
39//! ```
40//!
41//! ## Implementation Notes
42//!
43//! - Uses `blake3` instead of SHA-256 for performance (10x faster, still cryptographically secure)
44//! - Hash size: 32 bytes (256 bits)
45//! - Comparison time: ~1-2 nanoseconds (constant regardless of input)
46
47use blake3;
48use subtle::ConstantTimeEq;
49
50/// Hash an API key using BLAKE3
51///
52/// BLAKE3 provides:
53/// - Cryptographically secure hashing
54/// - 10x faster than SHA-256
55/// - Fixed 256-bit output
56/// - Collision resistance
57#[inline]
58fn hash_api_key(key: &str) -> [u8; 32] {
59 blake3::hash(key.as_bytes()).into()
60}
61
62/// Validate an API key using constant-time comparison
63///
64/// This function is timing-attack resistant. The comparison time is constant
65/// regardless of:
66/// - Which characters are correct
67/// - Where the mismatch occurs
68/// - The length of the keys (both are hashed to 32 bytes)
69///
70/// ## Security Guarantees
71///
72/// - **Constant Time**: Uses `subtle::ConstantTimeEq` for timing-safe comparison
73/// - **Pre-hashing**: Both keys are hashed before comparison
74/// - **No Early Exit**: Comparison continues even after finding a mismatch
75///
76/// ## Performance
77///
78/// - Hashing: ~50-100ns per key (BLAKE3 is very fast)
79/// - Comparison: ~1-2ns (constant time)
80/// - Total: ~100-200ns per validation
81///
82/// ## Example
83///
84/// ```rust
85/// use turbomcp_auth::api_key_validation::validate_api_key;
86///
87/// let provided = "sk_live_correct_key";
88/// let expected = "sk_live_correct_key";
89///
90/// assert!(validate_api_key(provided, expected));
91///
92/// let wrong_key = "sk_live_wrong_key";
93/// assert!(!validate_api_key(wrong_key, expected));
94/// ```
95#[must_use]
96#[inline]
97pub fn validate_api_key(provided: &str, expected: &str) -> bool {
98 // Hash both keys to fixed 32-byte size
99 let provided_hash = hash_api_key(provided);
100 let expected_hash = hash_api_key(expected);
101
102 // Constant-time comparison using subtle crate
103 // This prevents timing attacks by ensuring comparison time is independent
104 // of where the mismatch occurs
105 provided_hash.ct_eq(&expected_hash).into()
106}
107
108/// Validate an API key against multiple possible keys (constant-time)
109///
110/// This function checks if the provided key matches any of the expected keys,
111/// while maintaining constant-time properties. The total comparison time is
112/// proportional to the number of keys checked, not to which key matches or where
113/// mismatches occur.
114///
115/// ## Security Note
116///
117/// While this maintains constant-time comparison for each individual key,
118/// the total time is `O(n)` where `n` is the number of keys. This means:
119/// - An attacker can determine approximately how many keys are stored
120/// - But cannot determine which character positions are correct
121/// - Cannot perform character-by-character guessing attacks
122///
123/// For systems with many API keys (>1000), consider using a pre-hashed lookup
124/// table to avoid the linear scan.
125///
126/// ## Example
127///
128/// ```rust
129/// use turbomcp_auth::api_key_validation::validate_api_key_multiple;
130///
131/// let provided = "sk_live_key2";
132/// let valid_keys = vec![
133/// "sk_live_key1",
134/// "sk_live_key2",
135/// "sk_live_key3",
136/// ];
137///
138/// assert!(validate_api_key_multiple(provided, &valid_keys));
139/// ```
140#[must_use]
141pub fn validate_api_key_multiple(provided: &str, expected_keys: &[&str]) -> bool {
142 let provided_hash = hash_api_key(provided);
143
144 // Check all keys in constant time per key
145 // Note: Total time is O(n) but each comparison is constant-time
146 for expected_key in expected_keys {
147 let expected_hash = hash_api_key(expected_key);
148 if provided_hash.ct_eq(&expected_hash).into() {
149 return true;
150 }
151 }
152
153 false
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use std::time::Instant;
160
161 #[test]
162 fn test_validate_correct_key() {
163 let key = "sk_live_1234567890abcdef";
164 assert!(validate_api_key(key, key));
165 }
166
167 #[test]
168 fn test_validate_incorrect_key() {
169 let correct = "sk_live_1234567890abcdef";
170 let wrong = "sk_live_0000000000000000";
171 assert!(!validate_api_key(wrong, correct));
172 }
173
174 #[test]
175 fn test_validate_prefix_mismatch() {
176 let correct = "sk_live_1234567890abcdef";
177 let wrong_prefix = "sk_test_1234567890abcdef";
178 assert!(!validate_api_key(wrong_prefix, correct));
179 }
180
181 #[test]
182 fn test_validate_suffix_mismatch() {
183 let correct = "sk_live_1234567890abcdef";
184 let wrong_suffix = "sk_live_1234567890abcdex";
185 assert!(!validate_api_key(wrong_suffix, correct));
186 }
187
188 #[test]
189 fn test_validate_empty_keys() {
190 assert!(validate_api_key("", ""));
191 assert!(!validate_api_key("key", ""));
192 assert!(!validate_api_key("", "key"));
193 }
194
195 #[test]
196 fn test_validate_case_sensitive() {
197 let lower = "sk_live_abcdef";
198 let upper = "SK_LIVE_ABCDEF";
199 assert!(!validate_api_key(lower, upper));
200 }
201
202 #[test]
203 fn test_validate_multiple_keys_first_match() {
204 let provided = "sk_live_key1";
205 let valid_keys = vec!["sk_live_key1", "sk_live_key2", "sk_live_key3"];
206 assert!(validate_api_key_multiple(provided, &valid_keys));
207 }
208
209 #[test]
210 fn test_validate_multiple_keys_middle_match() {
211 let provided = "sk_live_key2";
212 let valid_keys = vec!["sk_live_key1", "sk_live_key2", "sk_live_key3"];
213 assert!(validate_api_key_multiple(provided, &valid_keys));
214 }
215
216 #[test]
217 fn test_validate_multiple_keys_last_match() {
218 let provided = "sk_live_key3";
219 let valid_keys = vec!["sk_live_key1", "sk_live_key2", "sk_live_key3"];
220 assert!(validate_api_key_multiple(provided, &valid_keys));
221 }
222
223 #[test]
224 fn test_validate_multiple_keys_no_match() {
225 let provided = "sk_live_wrong";
226 let valid_keys = vec!["sk_live_key1", "sk_live_key2", "sk_live_key3"];
227 assert!(!validate_api_key_multiple(provided, &valid_keys));
228 }
229
230 #[test]
231 fn test_validate_multiple_keys_empty_list() {
232 let provided = "sk_live_key1";
233 let valid_keys: Vec<&str> = vec![];
234 assert!(!validate_api_key_multiple(provided, &valid_keys));
235 }
236
237 #[test]
238 fn test_timing_attack_resistance() {
239 // This test verifies that comparison time is independent of where mismatch occurs
240 let correct_key = "sk_live_1234567890abcdef";
241
242 // Key with mismatch in first character
243 let wrong_prefix = "xk_live_1234567890abcdef";
244
245 // Key with mismatch in last character
246 let wrong_suffix = "sk_live_1234567890abcdex";
247
248 // Warm up
249 for _ in 0..1000 {
250 let _ = validate_api_key(wrong_prefix, correct_key);
251 let _ = validate_api_key(wrong_suffix, correct_key);
252 }
253
254 // Measure timing for prefix mismatch
255 let start = Instant::now();
256 for _ in 0..10000 {
257 let _ = validate_api_key(wrong_prefix, correct_key);
258 }
259 let prefix_time = start.elapsed();
260
261 // Measure timing for suffix mismatch
262 let start = Instant::now();
263 for _ in 0..10000 {
264 let _ = validate_api_key(wrong_suffix, correct_key);
265 }
266 let suffix_time = start.elapsed();
267
268 // Calculate difference in nanoseconds
269 let diff_ns = (prefix_time.as_nanos() as i128 - suffix_time.as_nanos() as i128).abs();
270 let avg_diff_ns = diff_ns / 10000;
271
272 // Timing difference should be negligible (< 10ns per comparison on average)
273 // This is much smaller than network jitter (~1ms = 1,000,000ns)
274 //
275 // Note: This test may be flaky on heavily loaded systems.
276 // If it fails, it doesn't necessarily mean timing attack is possible,
277 // just that system noise exceeded threshold.
278 println!(
279 "Average timing difference: {}ns per comparison",
280 avg_diff_ns
281 );
282
283 // Allow up to 100ns difference (generous margin for system noise)
284 assert!(
285 avg_diff_ns < 100,
286 "Timing difference too large: {}ns (threshold: 100ns). \
287 This suggests potential timing attack vulnerability.",
288 avg_diff_ns
289 );
290 }
291
292 #[test]
293 fn test_blake3_hash_consistency() {
294 // Verify that hashing is deterministic
295 let key = "sk_live_test";
296 let hash1 = hash_api_key(key);
297 let hash2 = hash_api_key(key);
298 assert_eq!(hash1, hash2);
299 }
300
301 #[test]
302 fn test_blake3_hash_collision_resistance() {
303 // Different keys should produce different hashes
304 let key1 = "sk_live_1234567890abcdef";
305 let key2 = "sk_live_1234567890abcdeg"; // Last char different
306
307 let hash1 = hash_api_key(key1);
308 let hash2 = hash_api_key(key2);
309
310 assert_ne!(hash1, hash2);
311 }
312
313 #[test]
314 fn test_long_keys() {
315 // Test with very long keys
316 let long_key = "sk_live_".to_string() + &"a".repeat(1000);
317 assert!(validate_api_key(&long_key, &long_key));
318 }
319
320 #[test]
321 fn test_special_characters() {
322 // Test with special characters
323 let key = "sk_live_!@#$%^&*()_+-={}[]|:;<>?,./";
324 assert!(validate_api_key(key, key));
325 }
326
327 #[test]
328 fn test_unicode_keys() {
329 // Test with Unicode characters
330 let key = "sk_live_你好世界🔒";
331 assert!(validate_api_key(key, key));
332 }
333}