Skip to main content

agent_id_core/
delegation.rs

1//! Delegation tokens for session and service keys.
2
3use crate::{signing, Did, Error, Result, RootKey};
4use chrono::{DateTime, Utc};
5use ed25519_dalek::{Signature, Verifier};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9/// Type of delegation.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum DelegationType {
13    /// Short-lived key for routine operations.
14    Session,
15    /// Long-lived key for root recovery.
16    Recovery,
17    /// Key scoped to specific services.
18    Service,
19}
20
21/// Capabilities granted by a delegation.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum Capability {
25    /// Can sign messages.
26    Sign,
27    /// Can perform handshakes.
28    Handshake,
29    /// Can issue sub-delegations.
30    Delegate,
31    /// Can rotate the root key (recovery only).
32    RotateRoot,
33}
34
35/// A delegation token authorizing a key to act on behalf of an identity.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Delegation {
38    /// Token type identifier.
39    #[serde(rename = "type")]
40    pub type_: String,
41
42    /// Protocol version.
43    pub version: String,
44
45    /// The root DID this delegation is for.
46    pub root_did: String,
47
48    /// Public key being delegated to (base58).
49    pub delegate_pubkey: String,
50
51    /// Type of delegation.
52    pub delegate_type: DelegationType,
53
54    /// When this delegation was issued (unix ms).
55    pub issued_at: i64,
56
57    /// When this delegation expires (unix ms).
58    pub expires_at: i64,
59
60    /// Capabilities granted.
61    pub capabilities: Vec<Capability>,
62
63    /// Unique ID for revocation.
64    pub revocation_id: String,
65
66    /// Signature from the root key.
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub signature: Option<String>,
69}
70
71impl Delegation {
72    /// Create a new unsigned delegation.
73    pub fn new(
74        root_did: Did,
75        delegate_pubkey: String,
76        delegate_type: DelegationType,
77        capabilities: Vec<Capability>,
78        expires_at: DateTime<Utc>,
79    ) -> Self {
80        Self {
81            type_: "KeyDelegation".to_string(),
82            version: "1.0".to_string(),
83            root_did: root_did.to_string(),
84            delegate_pubkey,
85            delegate_type,
86            issued_at: Utc::now().timestamp_millis(),
87            expires_at: expires_at.timestamp_millis(),
88            capabilities,
89            revocation_id: Uuid::now_v7().to_string(),
90            signature: None,
91        }
92    }
93
94    /// Sign this delegation with a root key.
95    pub fn sign(mut self, root_key: &RootKey) -> Result<Self> {
96        // Clear signature before hashing
97        self.signature = None;
98
99        let canonical = signing::canonicalize(&self)?;
100        let sig = root_key.sign(&canonical);
101        self.signature = Some(base64::Engine::encode(
102            &base64::engine::general_purpose::STANDARD,
103            sig.to_bytes(),
104        ));
105
106        Ok(self)
107    }
108
109    /// Verify this delegation's signature.
110    pub fn verify(&self) -> Result<()> {
111        let sig_b64 = self.signature.as_ref().ok_or(Error::InvalidSignature)?;
112        let sig_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, sig_b64)
113            .map_err(|_| Error::InvalidSignature)?;
114
115        let signature =
116            Signature::from_bytes(&sig_bytes.try_into().map_err(|_| Error::InvalidSignature)?);
117
118        // Parse the root DID to get the public key
119        let root_did: Did = self.root_did.parse()?;
120        let public_key = root_did.public_key()?;
121
122        // Canonicalize without signature
123        let mut unsigned = self.clone();
124        unsigned.signature = None;
125        let canonical = signing::canonicalize(&unsigned)?;
126
127        public_key
128            .verify(&canonical, &signature)
129            .map_err(|_| Error::InvalidSignature)
130    }
131
132    /// Check if this delegation is currently valid (not expired, not before issued).
133    pub fn is_valid_at(&self, now: DateTime<Utc>) -> Result<()> {
134        let now_ms = now.timestamp_millis();
135
136        if now_ms < self.issued_at {
137            return Err(Error::DelegationNotYetValid);
138        }
139
140        if now_ms > self.expires_at {
141            return Err(Error::DelegationExpired);
142        }
143
144        Ok(())
145    }
146
147    /// Check if this delegation has a specific capability.
148    pub fn has_capability(&self, cap: &Capability) -> bool {
149        self.capabilities.contains(cap)
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use chrono::Duration;
157
158    #[test]
159    fn test_delegation_roundtrip() {
160        let root = RootKey::generate();
161        let session_pubkey = "test_session_key_base58".to_string();
162
163        let delegation = Delegation::new(
164            root.did(),
165            session_pubkey,
166            DelegationType::Session,
167            vec![Capability::Sign, Capability::Handshake],
168            Utc::now() + Duration::hours(24),
169        );
170
171        let signed = delegation.sign(&root).unwrap();
172        assert!(signed.signature.is_some());
173
174        signed.verify().unwrap();
175    }
176
177    #[test]
178    fn test_delegation_expiry() {
179        let root = RootKey::generate();
180
181        let delegation = Delegation::new(
182            root.did(),
183            "test".to_string(),
184            DelegationType::Session,
185            vec![Capability::Sign],
186            Utc::now() - Duration::hours(1), // Already expired
187        );
188
189        let result = delegation.is_valid_at(Utc::now());
190        assert!(matches!(result, Err(Error::DelegationExpired)));
191    }
192}