1use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
7use serde::{Deserialize, Serialize};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use crate::error::{CapError, Result};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct CapabilityToken {
20 pub version: u8,
22 pub issuer: Vec<u8>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub audience: Option<Vec<u8>>,
27 pub scopes: Vec<String>,
29 pub expires_at: u64,
31 pub nonce: String,
33 #[serde(default, skip_serializing_if = "Vec::is_empty")]
35 pub proofs: Vec<ProofLink>,
36 #[serde(with = "serde_bytes")]
38 pub signature: Vec<u8>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ProofLink {
44 pub issuer: Vec<u8>,
46 pub scopes: Vec<String>,
48 #[serde(with = "serde_bytes")]
50 pub signature: Vec<u8>,
51}
52
53pub const TOKEN_PREFIX: &str = "cap_";
55
56impl CapabilityToken {
57 pub fn create_root(
59 signing_key: &SigningKey,
60 scopes: Vec<String>,
61 expires_at: u64,
62 audience: Option<Vec<u8>>,
63 ) -> Result<Self> {
64 let issuer = signing_key.verifying_key().to_bytes().to_vec();
65 let nonce = uuid::Uuid::new_v4().to_string();
66
67 let mut token = Self {
68 version: 1,
69 issuer,
70 audience,
71 scopes,
72 expires_at,
73 nonce,
74 proofs: vec![],
75 signature: vec![],
76 };
77
78 let payload = token.signable_payload()?;
80 let signature = signing_key.sign(&payload);
81 token.signature = signature.to_bytes().to_vec();
82
83 Ok(token)
84 }
85
86 pub fn delegate(
90 &self,
91 child_signing_key: &SigningKey,
92 child_scopes: Vec<String>,
93 expires_at: u64,
94 audience: Option<Vec<u8>>,
95 ) -> Result<Self> {
96 for child_scope in &child_scopes {
99 if !self.scope_allows(child_scope) {
100 return Err(CapError::AttenuationViolation(format!(
101 "child scope '{}' not allowed by parent scopes {:?}",
102 child_scope, self.scopes
103 )));
104 }
105 }
106
107 let child_expires = expires_at.min(self.expires_at);
109
110 let child_issuer = child_signing_key.verifying_key().to_bytes().to_vec();
111 let nonce = uuid::Uuid::new_v4().to_string();
112
113 let mut proofs = self.proofs.clone();
115 proofs.push(ProofLink {
116 issuer: self.issuer.clone(),
117 scopes: self.scopes.clone(),
118 signature: self.signature.clone(),
119 });
120
121 let mut token = Self {
122 version: 1,
123 issuer: child_issuer,
124 audience,
125 scopes: child_scopes,
126 expires_at: child_expires,
127 nonce,
128 proofs,
129 signature: vec![],
130 };
131
132 let payload = token.signable_payload()?;
133 let signature = child_signing_key.sign(&payload);
134 token.signature = signature.to_bytes().to_vec();
135
136 Ok(token)
137 }
138
139 fn scope_allows(&self, child_scope: &str) -> bool {
143 let Some((child_action, child_pattern)) = child_scope.split_once(':') else {
145 return false;
146 };
147
148 for parent_scope in &self.scopes {
149 let Some((parent_action, parent_pattern)) = parent_scope.split_once(':') else {
150 continue;
151 };
152
153 let action_ok = match parent_action {
155 "admin" => true,
156 "write" => child_action == "write" || child_action == "read",
157 "read" => child_action == "read",
158 _ => parent_action == child_action,
159 };
160
161 if !action_ok {
162 continue;
163 }
164
165 if pattern_is_subset(child_pattern, parent_pattern) {
167 return true;
168 }
169 }
170
171 false
172 }
173
174 pub fn verify_signature(&self) -> Result<()> {
176 let verifying_key = VerifyingKey::from_bytes(
177 self.issuer
178 .as_slice()
179 .try_into()
180 .map_err(|_| CapError::KeyError("invalid issuer key length".to_string()))?,
181 )
182 .map_err(|e| CapError::KeyError(e.to_string()))?;
183
184 let payload = self.signable_payload()?;
185 let signature = Signature::from_bytes(
186 self.signature
187 .as_slice()
188 .try_into()
189 .map_err(|_| CapError::InvalidSignature)?,
190 );
191
192 verifying_key
193 .verify(&payload, &signature)
194 .map_err(|_| CapError::InvalidSignature)
195 }
196
197 pub fn is_expired(&self) -> bool {
199 let now = SystemTime::now()
200 .duration_since(UNIX_EPOCH)
201 .unwrap_or_default()
202 .as_secs();
203 now > self.expires_at
204 }
205
206 pub fn chain_depth(&self) -> usize {
209 self.proofs.len()
210 }
211
212 pub fn encode(&self) -> Result<String> {
214 use base64::Engine;
215 let bytes = rmp_serde::to_vec_named(self).map_err(|e| CapError::Encoding(e.to_string()))?;
216 Ok(format!(
217 "{}{}",
218 TOKEN_PREFIX,
219 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&bytes)
220 ))
221 }
222
223 pub fn decode(token: &str) -> Result<Self> {
225 use base64::Engine;
226 let encoded = token
227 .strip_prefix(TOKEN_PREFIX)
228 .ok_or_else(|| CapError::Encoding("missing cap_ prefix".to_string()))?;
229
230 let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
231 .decode(encoded)
232 .map_err(|e| CapError::Encoding(e.to_string()))?;
233
234 rmp_serde::from_slice(&bytes).map_err(|e| CapError::Encoding(e.to_string()))
235 }
236
237 fn signable_payload(&self) -> Result<Vec<u8>> {
239 let signable = SignableToken {
241 version: self.version,
242 issuer: &self.issuer,
243 audience: self.audience.as_deref(),
244 scopes: &self.scopes,
245 expires_at: self.expires_at,
246 nonce: &self.nonce,
247 proofs: &self.proofs,
248 };
249
250 rmp_serde::to_vec_named(&signable).map_err(|e| CapError::Encoding(e.to_string()))
251 }
252}
253
254#[derive(Serialize)]
256struct SignableToken<'a> {
257 version: u8,
258 issuer: &'a [u8],
259 audience: Option<&'a [u8]>,
260 scopes: &'a [String],
261 expires_at: u64,
262 nonce: &'a str,
263 proofs: &'a [ProofLink],
264}
265
266pub fn pattern_is_subset(child: &str, parent: &str) -> bool {
273 if child == parent {
275 return true;
276 }
277
278 if parent == "/**" || parent == "**" {
280 return true;
281 }
282
283 let parent_parts: Vec<&str> = parent.split('/').filter(|s| !s.is_empty()).collect();
284 let child_parts: Vec<&str> = child.split('/').filter(|s| !s.is_empty()).collect();
285
286 let mut pi = 0;
288 let mut ci = 0;
289
290 while pi < parent_parts.len() && ci < child_parts.len() {
291 let pp = parent_parts[pi];
292 let cp = child_parts[ci];
293
294 if pp == "**" {
295 return true;
297 }
298
299 if pp == "*" {
300 if cp == "**" {
303 return false;
304 }
305 pi += 1;
307 ci += 1;
308 continue;
309 }
310
311 if cp == "**" {
312 return false;
314 }
315
316 if cp == "*" {
317 return false;
320 }
321
322 if pp != cp {
324 return false;
325 }
326
327 pi += 1;
328 ci += 1;
329 }
330
331 if pi < parent_parts.len() && parent_parts[pi] == "**" {
333 return true;
334 }
335
336 pi >= parent_parts.len() && ci >= child_parts.len()
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use ed25519_dalek::SigningKey;
344
345 fn test_key() -> SigningKey {
346 SigningKey::from_bytes(&[1u8; 32])
347 }
348
349 fn future_timestamp() -> u64 {
350 SystemTime::now()
351 .duration_since(UNIX_EPOCH)
352 .unwrap()
353 .as_secs()
354 + 3600
355 }
356
357 #[test]
358 fn test_create_root_token() {
359 let key = test_key();
360 let token = CapabilityToken::create_root(
361 &key,
362 vec!["admin:/**".to_string()],
363 future_timestamp(),
364 None,
365 )
366 .unwrap();
367
368 assert_eq!(token.version, 1);
369 assert_eq!(token.scopes, vec!["admin:/**"]);
370 assert!(token.proofs.is_empty());
371 assert!(!token.signature.is_empty());
372 }
373
374 #[test]
375 fn test_verify_signature() {
376 let key = test_key();
377 let token = CapabilityToken::create_root(
378 &key,
379 vec!["admin:/**".to_string()],
380 future_timestamp(),
381 None,
382 )
383 .unwrap();
384
385 assert!(token.verify_signature().is_ok());
386 }
387
388 #[test]
389 fn test_encode_decode() {
390 let key = test_key();
391 let token = CapabilityToken::create_root(
392 &key,
393 vec!["read:/**".to_string()],
394 future_timestamp(),
395 None,
396 )
397 .unwrap();
398
399 let encoded = token.encode().unwrap();
400 assert!(encoded.starts_with("cap_"));
401
402 let decoded = CapabilityToken::decode(&encoded).unwrap();
403 assert_eq!(decoded.scopes, token.scopes);
404 assert_eq!(decoded.issuer, token.issuer);
405 }
406
407 #[test]
408 fn test_delegation() {
409 let root_key = test_key();
410 let child_key = SigningKey::from_bytes(&[2u8; 32]);
411
412 let root = CapabilityToken::create_root(
413 &root_key,
414 vec!["admin:/**".to_string()],
415 future_timestamp(),
416 None,
417 )
418 .unwrap();
419
420 let child = root
421 .delegate(
422 &child_key,
423 vec!["write:/lights/**".to_string()],
424 future_timestamp(),
425 None,
426 )
427 .unwrap();
428
429 assert_eq!(child.chain_depth(), 1);
430 assert_eq!(child.scopes, vec!["write:/lights/**"]);
431 assert!(child.verify_signature().is_ok());
432 }
433
434 #[test]
435 fn test_attenuation_violation() {
436 let root_key = test_key();
437 let child_key = SigningKey::from_bytes(&[2u8; 32]);
438
439 let root = CapabilityToken::create_root(
440 &root_key,
441 vec!["write:/lights/**".to_string()],
442 future_timestamp(),
443 None,
444 )
445 .unwrap();
446
447 let result = root.delegate(
449 &child_key,
450 vec!["write:/audio/**".to_string()],
451 future_timestamp(),
452 None,
453 );
454 assert!(result.is_err());
455 assert!(matches!(
456 result.unwrap_err(),
457 CapError::AttenuationViolation(_)
458 ));
459 }
460
461 #[test]
462 fn test_expiration_clamped() {
463 let root_key = test_key();
464 let child_key = SigningKey::from_bytes(&[2u8; 32]);
465
466 let root_expires = future_timestamp();
467 let root = CapabilityToken::create_root(
468 &root_key,
469 vec!["admin:/**".to_string()],
470 root_expires,
471 None,
472 )
473 .unwrap();
474
475 let child = root
477 .delegate(
478 &child_key,
479 vec!["read:/**".to_string()],
480 root_expires + 9999,
481 None,
482 )
483 .unwrap();
484
485 assert_eq!(child.expires_at, root_expires);
487 }
488
489 #[test]
490 fn test_pattern_is_subset() {
491 assert!(pattern_is_subset("/lights/room1/**", "/lights/**"));
492 assert!(pattern_is_subset("/lights/room1", "/lights/**"));
493 assert!(pattern_is_subset("/**", "/**"));
494 assert!(!pattern_is_subset("/audio/**", "/lights/**"));
495 assert!(!pattern_is_subset("/**", "/lights/**"));
496 assert!(pattern_is_subset("/lights/1", "/lights/*"));
497 assert!(!pattern_is_subset("/lights/**", "/lights/*"));
499 assert!(!pattern_is_subset("/**", "/*"));
500 }
501
502 #[test]
505 fn test_decode_malformed_base64() {
506 let result = CapabilityToken::decode("cap_!!!invalid-base64!!!");
507 assert!(result.is_err());
508 }
509
510 #[test]
511 fn test_decode_missing_prefix() {
512 let result = CapabilityToken::decode("not_a_cap_token");
513 assert!(result.is_err());
514 }
515
516 #[test]
517 fn test_decode_truncated_payload() {
518 use base64::Engine;
519 let truncated = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([0x92, 0x01]);
521 let result = CapabilityToken::decode(&format!("cap_{}", truncated));
522 assert!(result.is_err());
523 }
524
525 #[test]
526 fn test_decode_corrupted_msgpack() {
527 use base64::Engine;
528 let garbage =
530 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"this is not msgpack");
531 let result = CapabilityToken::decode(&format!("cap_{}", garbage));
532 assert!(result.is_err());
533 }
534
535 #[test]
536 fn test_signature_tampering() {
537 let key = test_key();
538 let mut token = CapabilityToken::create_root(
539 &key,
540 vec!["admin:/**".to_string()],
541 future_timestamp(),
542 None,
543 )
544 .unwrap();
545
546 token.signature[0] ^= 0xFF;
548 assert!(token.verify_signature().is_err());
549 }
550
551 #[test]
552 fn test_empty_scopes_delegation() {
553 let root_key = test_key();
554 let child_key = SigningKey::from_bytes(&[2u8; 32]);
555
556 let root = CapabilityToken::create_root(
557 &root_key,
558 vec!["admin:/**".to_string()],
559 future_timestamp(),
560 None,
561 )
562 .unwrap();
563
564 let child = root
566 .delegate(&child_key, vec![], future_timestamp(), None)
567 .unwrap();
568 assert!(child.scopes.is_empty());
569 assert!(child.verify_signature().is_ok());
570 }
571
572 #[test]
573 fn test_multi_hop_delegation() {
574 let key_a = SigningKey::from_bytes(&[1u8; 32]);
575 let key_b = SigningKey::from_bytes(&[2u8; 32]);
576 let key_c = SigningKey::from_bytes(&[3u8; 32]);
577
578 let root = CapabilityToken::create_root(
579 &key_a,
580 vec!["admin:/**".to_string()],
581 future_timestamp(),
582 None,
583 )
584 .unwrap();
585
586 let child = root
587 .delegate(
588 &key_b,
589 vec!["write:/lights/**".to_string()],
590 future_timestamp(),
591 None,
592 )
593 .unwrap();
594
595 let grandchild = child
596 .delegate(
597 &key_c,
598 vec!["write:/lights/room1/**".to_string()],
599 future_timestamp(),
600 None,
601 )
602 .unwrap();
603
604 assert_eq!(grandchild.chain_depth(), 2);
605 assert!(grandchild.verify_signature().is_ok());
606
607 let result = child.delegate(
609 &key_c,
610 vec!["write:/audio/**".to_string()],
611 future_timestamp(),
612 None,
613 );
614 assert!(result.is_err());
615 }
616}