use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::error::{CapError, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityToken {
pub version: u8,
pub issuer: Vec<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub audience: Option<Vec<u8>>,
pub scopes: Vec<String>,
pub expires_at: u64,
pub nonce: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub proofs: Vec<ProofLink>,
#[serde(with = "serde_bytes")]
pub signature: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProofLink {
pub issuer: Vec<u8>,
pub scopes: Vec<String>,
#[serde(with = "serde_bytes")]
pub signature: Vec<u8>,
}
pub const TOKEN_PREFIX: &str = "cap_";
impl CapabilityToken {
pub fn create_root(
signing_key: &SigningKey,
scopes: Vec<String>,
expires_at: u64,
audience: Option<Vec<u8>>,
) -> Result<Self> {
let issuer = signing_key.verifying_key().to_bytes().to_vec();
let nonce = uuid::Uuid::new_v4().to_string();
let mut token = Self {
version: 1,
issuer,
audience,
scopes,
expires_at,
nonce,
proofs: vec![],
signature: vec![],
};
let payload = token.signable_payload()?;
let signature = signing_key.sign(&payload);
token.signature = signature.to_bytes().to_vec();
Ok(token)
}
pub fn delegate(
&self,
child_signing_key: &SigningKey,
child_scopes: Vec<String>,
expires_at: u64,
audience: Option<Vec<u8>>,
) -> Result<Self> {
for child_scope in &child_scopes {
if !self.scope_allows(child_scope) {
return Err(CapError::AttenuationViolation(format!(
"child scope '{}' not allowed by parent scopes {:?}",
child_scope, self.scopes
)));
}
}
let child_expires = expires_at.min(self.expires_at);
let child_issuer = child_signing_key.verifying_key().to_bytes().to_vec();
let nonce = uuid::Uuid::new_v4().to_string();
let mut proofs = self.proofs.clone();
proofs.push(ProofLink {
issuer: self.issuer.clone(),
scopes: self.scopes.clone(),
signature: self.signature.clone(),
});
let mut token = Self {
version: 1,
issuer: child_issuer,
audience,
scopes: child_scopes,
expires_at: child_expires,
nonce,
proofs,
signature: vec![],
};
let payload = token.signable_payload()?;
let signature = child_signing_key.sign(&payload);
token.signature = signature.to_bytes().to_vec();
Ok(token)
}
fn scope_allows(&self, child_scope: &str) -> bool {
let Some((child_action, child_pattern)) = child_scope.split_once(':') else {
return false;
};
for parent_scope in &self.scopes {
let Some((parent_action, parent_pattern)) = parent_scope.split_once(':') else {
continue;
};
let action_ok = match parent_action {
"admin" => true,
"write" => child_action == "write" || child_action == "read",
"read" => child_action == "read",
_ => parent_action == child_action,
};
if !action_ok {
continue;
}
if pattern_is_subset(child_pattern, parent_pattern) {
return true;
}
}
false
}
pub fn verify_signature(&self) -> Result<()> {
let verifying_key = VerifyingKey::from_bytes(
self.issuer
.as_slice()
.try_into()
.map_err(|_| CapError::KeyError("invalid issuer key length".to_string()))?,
)
.map_err(|e| CapError::KeyError(e.to_string()))?;
let payload = self.signable_payload()?;
let signature = Signature::from_bytes(
self.signature
.as_slice()
.try_into()
.map_err(|_| CapError::InvalidSignature)?,
);
verifying_key
.verify(&payload, &signature)
.map_err(|_| CapError::InvalidSignature)
}
pub fn is_expired(&self) -> bool {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
now > self.expires_at
}
pub fn chain_depth(&self) -> usize {
self.proofs.len()
}
pub fn encode(&self) -> Result<String> {
use base64::Engine;
let bytes = rmp_serde::to_vec_named(self).map_err(|e| CapError::Encoding(e.to_string()))?;
Ok(format!(
"{}{}",
TOKEN_PREFIX,
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&bytes)
))
}
pub fn decode(token: &str) -> Result<Self> {
use base64::Engine;
let encoded = token
.strip_prefix(TOKEN_PREFIX)
.ok_or_else(|| CapError::Encoding("missing cap_ prefix".to_string()))?;
let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(encoded)
.map_err(|e| CapError::Encoding(e.to_string()))?;
rmp_serde::from_slice(&bytes).map_err(|e| CapError::Encoding(e.to_string()))
}
fn signable_payload(&self) -> Result<Vec<u8>> {
let signable = SignableToken {
version: self.version,
issuer: &self.issuer,
audience: self.audience.as_deref(),
scopes: &self.scopes,
expires_at: self.expires_at,
nonce: &self.nonce,
proofs: &self.proofs,
};
rmp_serde::to_vec_named(&signable).map_err(|e| CapError::Encoding(e.to_string()))
}
}
#[derive(Serialize)]
struct SignableToken<'a> {
version: u8,
issuer: &'a [u8],
audience: Option<&'a [u8]>,
scopes: &'a [String],
expires_at: u64,
nonce: &'a str,
proofs: &'a [ProofLink],
}
pub fn pattern_is_subset(child: &str, parent: &str) -> bool {
if child == parent {
return true;
}
if parent == "/**" || parent == "**" {
return true;
}
let parent_parts: Vec<&str> = parent.split('/').filter(|s| !s.is_empty()).collect();
let child_parts: Vec<&str> = child.split('/').filter(|s| !s.is_empty()).collect();
let mut pi = 0;
let mut ci = 0;
while pi < parent_parts.len() && ci < child_parts.len() {
let pp = parent_parts[pi];
let cp = child_parts[ci];
if pp == "**" {
return true;
}
if pp == "*" {
if cp == "**" {
return false;
}
pi += 1;
ci += 1;
continue;
}
if cp == "**" {
return false;
}
if cp == "*" {
return false;
}
if pp != cp {
return false;
}
pi += 1;
ci += 1;
}
if pi < parent_parts.len() && parent_parts[pi] == "**" {
return true;
}
pi >= parent_parts.len() && ci >= child_parts.len()
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::SigningKey;
fn test_key() -> SigningKey {
SigningKey::from_bytes(&[1u8; 32])
}
fn future_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
+ 3600
}
#[test]
fn test_create_root_token() {
let key = test_key();
let token = CapabilityToken::create_root(
&key,
vec!["admin:/**".to_string()],
future_timestamp(),
None,
)
.unwrap();
assert_eq!(token.version, 1);
assert_eq!(token.scopes, vec!["admin:/**"]);
assert!(token.proofs.is_empty());
assert!(!token.signature.is_empty());
}
#[test]
fn test_verify_signature() {
let key = test_key();
let token = CapabilityToken::create_root(
&key,
vec!["admin:/**".to_string()],
future_timestamp(),
None,
)
.unwrap();
assert!(token.verify_signature().is_ok());
}
#[test]
fn test_encode_decode() {
let key = test_key();
let token = CapabilityToken::create_root(
&key,
vec!["read:/**".to_string()],
future_timestamp(),
None,
)
.unwrap();
let encoded = token.encode().unwrap();
assert!(encoded.starts_with("cap_"));
let decoded = CapabilityToken::decode(&encoded).unwrap();
assert_eq!(decoded.scopes, token.scopes);
assert_eq!(decoded.issuer, token.issuer);
}
#[test]
fn test_delegation() {
let root_key = test_key();
let child_key = SigningKey::from_bytes(&[2u8; 32]);
let root = CapabilityToken::create_root(
&root_key,
vec!["admin:/**".to_string()],
future_timestamp(),
None,
)
.unwrap();
let child = root
.delegate(
&child_key,
vec!["write:/lights/**".to_string()],
future_timestamp(),
None,
)
.unwrap();
assert_eq!(child.chain_depth(), 1);
assert_eq!(child.scopes, vec!["write:/lights/**"]);
assert!(child.verify_signature().is_ok());
}
#[test]
fn test_attenuation_violation() {
let root_key = test_key();
let child_key = SigningKey::from_bytes(&[2u8; 32]);
let root = CapabilityToken::create_root(
&root_key,
vec!["write:/lights/**".to_string()],
future_timestamp(),
None,
)
.unwrap();
let result = root.delegate(
&child_key,
vec!["write:/audio/**".to_string()],
future_timestamp(),
None,
);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
CapError::AttenuationViolation(_)
));
}
#[test]
fn test_expiration_clamped() {
let root_key = test_key();
let child_key = SigningKey::from_bytes(&[2u8; 32]);
let root_expires = future_timestamp();
let root = CapabilityToken::create_root(
&root_key,
vec!["admin:/**".to_string()],
root_expires,
None,
)
.unwrap();
let child = root
.delegate(
&child_key,
vec!["read:/**".to_string()],
root_expires + 9999,
None,
)
.unwrap();
assert_eq!(child.expires_at, root_expires);
}
#[test]
fn test_pattern_is_subset() {
assert!(pattern_is_subset("/lights/room1/**", "/lights/**"));
assert!(pattern_is_subset("/lights/room1", "/lights/**"));
assert!(pattern_is_subset("/**", "/**"));
assert!(!pattern_is_subset("/audio/**", "/lights/**"));
assert!(!pattern_is_subset("/**", "/lights/**"));
assert!(pattern_is_subset("/lights/1", "/lights/*"));
assert!(!pattern_is_subset("/lights/**", "/lights/*"));
assert!(!pattern_is_subset("/**", "/*"));
}
#[test]
fn test_decode_malformed_base64() {
let result = CapabilityToken::decode("cap_!!!invalid-base64!!!");
assert!(result.is_err());
}
#[test]
fn test_decode_missing_prefix() {
let result = CapabilityToken::decode("not_a_cap_token");
assert!(result.is_err());
}
#[test]
fn test_decode_truncated_payload() {
use base64::Engine;
let truncated = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([0x92, 0x01]);
let result = CapabilityToken::decode(&format!("cap_{}", truncated));
assert!(result.is_err());
}
#[test]
fn test_decode_corrupted_msgpack() {
use base64::Engine;
let garbage =
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"this is not msgpack");
let result = CapabilityToken::decode(&format!("cap_{}", garbage));
assert!(result.is_err());
}
#[test]
fn test_signature_tampering() {
let key = test_key();
let mut token = CapabilityToken::create_root(
&key,
vec!["admin:/**".to_string()],
future_timestamp(),
None,
)
.unwrap();
token.signature[0] ^= 0xFF;
assert!(token.verify_signature().is_err());
}
#[test]
fn test_empty_scopes_delegation() {
let root_key = test_key();
let child_key = SigningKey::from_bytes(&[2u8; 32]);
let root = CapabilityToken::create_root(
&root_key,
vec!["admin:/**".to_string()],
future_timestamp(),
None,
)
.unwrap();
let child = root
.delegate(&child_key, vec![], future_timestamp(), None)
.unwrap();
assert!(child.scopes.is_empty());
assert!(child.verify_signature().is_ok());
}
#[test]
fn test_multi_hop_delegation() {
let key_a = SigningKey::from_bytes(&[1u8; 32]);
let key_b = SigningKey::from_bytes(&[2u8; 32]);
let key_c = SigningKey::from_bytes(&[3u8; 32]);
let root = CapabilityToken::create_root(
&key_a,
vec!["admin:/**".to_string()],
future_timestamp(),
None,
)
.unwrap();
let child = root
.delegate(
&key_b,
vec!["write:/lights/**".to_string()],
future_timestamp(),
None,
)
.unwrap();
let grandchild = child
.delegate(
&key_c,
vec!["write:/lights/room1/**".to_string()],
future_timestamp(),
None,
)
.unwrap();
assert_eq!(grandchild.chain_depth(), 2);
assert!(grandchild.verify_signature().is_ok());
let result = child.delegate(
&key_c,
vec!["write:/audio/**".to_string()],
future_timestamp(),
None,
);
assert!(result.is_err());
}
}