1use clasp_core::security::{Action, Scope, TokenInfo, TokenValidator, ValidationResult};
7use std::collections::HashMap;
8use std::time::{Duration, UNIX_EPOCH};
9
10use crate::error::CapError;
11use crate::token::{CapabilityToken, TOKEN_PREFIX};
12
13pub struct CapabilityValidator {
18 trust_anchors: Vec<Vec<u8>>,
20 max_depth: usize,
22}
23
24impl CapabilityValidator {
25 pub fn new(trust_anchors: Vec<Vec<u8>>, max_depth: usize) -> Self {
31 Self {
32 trust_anchors,
33 max_depth,
34 }
35 }
36
37 pub fn add_trust_anchor(&mut self, public_key: Vec<u8>) {
39 self.trust_anchors.push(public_key);
40 }
41
42 fn validate_token(&self, token_str: &str) -> std::result::Result<CapabilityToken, CapError> {
44 let token = CapabilityToken::decode(token_str)?;
46
47 if token.is_expired() {
49 return Err(CapError::Expired);
50 }
51
52 if token.chain_depth() > self.max_depth {
54 return Err(CapError::ChainTooDeep {
55 depth: token.chain_depth(),
56 max: self.max_depth,
57 });
58 }
59
60 token.verify_signature()?;
62
63 let root_issuer = if token.proofs.is_empty() {
65 &token.issuer
66 } else {
67 &token.proofs[0].issuer
68 };
69
70 if !self
71 .trust_anchors
72 .iter()
73 .any(|anchor| anchor == root_issuer)
74 {
75 return Err(CapError::UntrustedIssuer(hex::encode(root_issuer)));
76 }
77
78 if !token.proofs.is_empty() {
80 for i in 1..token.proofs.len() {
82 let parent = &token.proofs[i - 1];
83 let child = &token.proofs[i];
84 for scope in &child.scopes {
85 if !scope_within_parent(scope, &parent.scopes) {
86 return Err(CapError::AttenuationViolation(format!(
87 "scope '{}' at depth {} exceeds parent",
88 scope, i
89 )));
90 }
91 }
92 }
93
94 let last_proof = token.proofs.last().unwrap();
96 for scope in &token.scopes {
97 if !scope_within_parent(scope, &last_proof.scopes) {
98 return Err(CapError::AttenuationViolation(format!(
99 "token scope '{}' exceeds last delegation",
100 scope
101 )));
102 }
103 }
104 }
105
106 Ok(token)
107 }
108}
109
110fn scope_within_parent(scope: &str, parent_scopes: &[String]) -> bool {
112 let Some((child_action, child_pattern)) = scope.split_once(':') else {
113 return false;
114 };
115
116 for parent in parent_scopes {
117 let Some((parent_action, parent_pattern)) = parent.split_once(':') else {
118 continue;
119 };
120
121 let action_ok = match parent_action {
122 "admin" => true,
123 "write" => child_action == "write" || child_action == "read",
124 "read" => child_action == "read",
125 _ => parent_action == child_action,
126 };
127
128 if action_ok && crate::token::pattern_is_subset(child_pattern, parent_pattern) {
129 return true;
130 }
131 }
132
133 false
134}
135
136mod hex {
138 pub fn encode(bytes: &[u8]) -> String {
139 bytes.iter().map(|b| format!("{:02x}", b)).collect()
140 }
141}
142
143impl TokenValidator for CapabilityValidator {
144 fn validate(&self, token: &str) -> ValidationResult {
145 if !token.starts_with(TOKEN_PREFIX) {
147 return ValidationResult::NotMyToken;
148 }
149
150 match self.validate_token(token) {
151 Ok(cap_token) => {
152 let scopes: Vec<Scope> = cap_token
154 .scopes
155 .iter()
156 .filter_map(|s| {
157 let (action_str, pattern) = s.split_once(':')?;
158 let action = match action_str {
159 "admin" => Action::Admin,
160 "write" => Action::Write,
161 "read" => Action::Read,
162 _ => return None,
163 };
164 Scope::new(action, pattern).ok()
165 })
166 .collect();
167
168 let expires_at = if cap_token.expires_at > 0 {
169 Some(UNIX_EPOCH + Duration::from_secs(cap_token.expires_at))
170 } else {
171 None
172 };
173
174 let mut metadata = HashMap::new();
175 metadata.insert(
176 "chain_depth".to_string(),
177 cap_token.chain_depth().to_string(),
178 );
179
180 let info = TokenInfo {
181 token_id: cap_token.nonce.clone(),
182 subject: None, scopes,
184 expires_at,
185 metadata,
186 };
187
188 ValidationResult::Valid(info)
189 }
190 Err(CapError::Expired) => ValidationResult::Expired,
191 Err(e) => ValidationResult::Invalid(e.to_string()),
192 }
193 }
194
195 fn name(&self) -> &str {
196 "Capability"
197 }
198
199 fn as_any(&self) -> &dyn std::any::Any {
200 self
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::token::CapabilityToken;
208 use ed25519_dalek::SigningKey;
209 use std::time::{SystemTime, UNIX_EPOCH};
210
211 fn future_timestamp() -> u64 {
212 SystemTime::now()
213 .duration_since(UNIX_EPOCH)
214 .unwrap()
215 .as_secs()
216 + 3600
217 }
218
219 fn root_key() -> SigningKey {
220 SigningKey::from_bytes(&[1u8; 32])
221 }
222
223 fn make_validator() -> CapabilityValidator {
224 let key = root_key();
225 let pub_key = key.verifying_key().to_bytes().to_vec();
226 CapabilityValidator::new(vec![pub_key], 5)
227 }
228
229 #[test]
230 fn test_validate_root_token() {
231 let validator = make_validator();
232 let key = root_key();
233
234 let token = CapabilityToken::create_root(
235 &key,
236 vec!["admin:/**".to_string()],
237 future_timestamp(),
238 None,
239 )
240 .unwrap();
241
242 let encoded = token.encode().unwrap();
243 match validator.validate(&encoded) {
244 ValidationResult::Valid(info) => {
245 assert!(!info.scopes.is_empty());
246 assert!(info.has_scope(Action::Admin, "/anything"));
247 }
248 other => panic!("expected Valid, got {:?}", other),
249 }
250 }
251
252 #[test]
253 fn test_validate_delegated_token() {
254 let validator = make_validator();
255 let root_key = root_key();
256 let child_key = SigningKey::from_bytes(&[2u8; 32]);
257
258 let root = CapabilityToken::create_root(
259 &root_key,
260 vec!["admin:/**".to_string()],
261 future_timestamp(),
262 None,
263 )
264 .unwrap();
265
266 let child = root
267 .delegate(
268 &child_key,
269 vec!["write:/lights/**".to_string()],
270 future_timestamp(),
271 None,
272 )
273 .unwrap();
274
275 let encoded = child.encode().unwrap();
276 match validator.validate(&encoded) {
277 ValidationResult::Valid(info) => {
278 assert!(info.has_scope(Action::Write, "/lights/room1"));
279 assert!(!info.has_scope(Action::Write, "/audio/channel1"));
280 }
281 other => panic!("expected Valid, got {:?}", other),
282 }
283 }
284
285 #[test]
286 fn test_reject_untrusted_issuer() {
287 let validator = make_validator();
288 let untrusted_key = SigningKey::from_bytes(&[99u8; 32]);
289
290 let token = CapabilityToken::create_root(
291 &untrusted_key,
292 vec!["admin:/**".to_string()],
293 future_timestamp(),
294 None,
295 )
296 .unwrap();
297
298 let encoded = token.encode().unwrap();
299 match validator.validate(&encoded) {
300 ValidationResult::Invalid(msg) => {
301 assert!(msg.contains("untrusted"));
302 }
303 other => panic!("expected Invalid, got {:?}", other),
304 }
305 }
306
307 #[test]
308 fn test_not_my_token() {
309 let validator = make_validator();
310 match validator.validate("cpsk_something") {
311 ValidationResult::NotMyToken => {}
312 other => panic!("expected NotMyToken, got {:?}", other),
313 }
314 }
315
316 #[test]
317 fn test_expired_token() {
318 let validator = make_validator();
319 let key = root_key();
320
321 let token = CapabilityToken::create_root(
323 &key,
324 vec!["admin:/**".to_string()],
325 0, None,
327 )
328 .unwrap();
329
330 let encoded = token.encode().unwrap();
331 match validator.validate(&encoded) {
332 ValidationResult::Expired => {}
333 other => panic!("expected Expired, got {:?}", other),
334 }
335 }
336
337 #[test]
340 fn test_malformed_token_bad_base64() {
341 let validator = make_validator();
342 match validator.validate("cap_!!!not-valid-base64!!!") {
343 ValidationResult::Invalid(msg) => {
344 assert!(
345 msg.contains("encoding"),
346 "expected encoding error, got: {}",
347 msg
348 );
349 }
350 other => panic!("expected Invalid, got {:?}", other),
351 }
352 }
353
354 #[test]
355 fn test_malformed_token_truncated() {
356 let validator = make_validator();
357 use base64::Engine;
359 let truncated = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([0x92, 0x01, 0x02]);
360 match validator.validate(&format!("cap_{}", truncated)) {
361 ValidationResult::Invalid(_) => {}
362 other => panic!("expected Invalid, got {:?}", other),
363 }
364 }
365
366 #[test]
367 fn test_signature_tampered_token() {
368 let validator = make_validator();
369 let key = root_key();
370
371 let mut token = CapabilityToken::create_root(
372 &key,
373 vec!["admin:/**".to_string()],
374 future_timestamp(),
375 None,
376 )
377 .unwrap();
378
379 token.signature[0] ^= 0xFF;
381 let encoded = token.encode().unwrap();
382 match validator.validate(&encoded) {
383 ValidationResult::Invalid(msg) => {
384 assert!(
385 msg.contains("signature"),
386 "expected signature error, got: {}",
387 msg
388 );
389 }
390 other => panic!("expected Invalid, got {:?}", other),
391 }
392 }
393
394 #[test]
395 fn test_chain_depth_exceeds_max() {
396 let key = root_key();
398 let pub_key = key.verifying_key().to_bytes().to_vec();
399 let validator = CapabilityValidator::new(vec![pub_key], 1);
400
401 let key_b = SigningKey::from_bytes(&[2u8; 32]);
402 let key_c = SigningKey::from_bytes(&[3u8; 32]);
403
404 let root = CapabilityToken::create_root(
405 &key,
406 vec!["admin:/**".to_string()],
407 future_timestamp(),
408 None,
409 )
410 .unwrap();
411
412 let child = root
413 .delegate(
414 &key_b,
415 vec!["write:/**".to_string()],
416 future_timestamp(),
417 None,
418 )
419 .unwrap();
420
421 let grandchild = child
422 .delegate(
423 &key_c,
424 vec!["read:/**".to_string()],
425 future_timestamp(),
426 None,
427 )
428 .unwrap();
429
430 let encoded = grandchild.encode().unwrap();
432 match validator.validate(&encoded) {
433 ValidationResult::Invalid(msg) => {
434 assert!(
435 msg.contains("deep") || msg.contains("chain"),
436 "expected chain depth error, got: {}",
437 msg
438 );
439 }
440 other => panic!("expected Invalid, got {:?}", other),
441 }
442 }
443
444 #[test]
445 fn test_multiple_trust_anchors() {
446 let key_a = root_key();
447 let key_b = SigningKey::from_bytes(&[42u8; 32]);
448
449 let pub_a = key_a.verifying_key().to_bytes().to_vec();
450 let pub_b = key_b.verifying_key().to_bytes().to_vec();
451
452 let validator = CapabilityValidator::new(vec![pub_a, pub_b], 5);
453
454 let token_a = CapabilityToken::create_root(
456 &key_a,
457 vec!["admin:/**".to_string()],
458 future_timestamp(),
459 None,
460 )
461 .unwrap();
462 let encoded_a = token_a.encode().unwrap();
463 assert!(matches!(
464 validator.validate(&encoded_a),
465 ValidationResult::Valid(_)
466 ));
467
468 let token_b = CapabilityToken::create_root(
470 &key_b,
471 vec!["read:/**".to_string()],
472 future_timestamp(),
473 None,
474 )
475 .unwrap();
476 let encoded_b = token_b.encode().unwrap();
477 assert!(matches!(
478 validator.validate(&encoded_b),
479 ValidationResult::Valid(_)
480 ));
481
482 let key_c = SigningKey::from_bytes(&[99u8; 32]);
484 let token_c = CapabilityToken::create_root(
485 &key_c,
486 vec!["admin:/**".to_string()],
487 future_timestamp(),
488 None,
489 )
490 .unwrap();
491 let encoded_c = token_c.encode().unwrap();
492 assert!(matches!(
493 validator.validate(&encoded_c),
494 ValidationResult::Invalid(_)
495 ));
496 }
497}