agent_id_core/
delegation.rs1use crate::{signing, Did, Error, Result, RootKey};
4use chrono::{DateTime, Utc};
5use ed25519_dalek::{Signature, Verifier};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum DelegationType {
13 Session,
15 Recovery,
17 Service,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum Capability {
25 Sign,
27 Handshake,
29 Delegate,
31 RotateRoot,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Delegation {
38 #[serde(rename = "type")]
40 pub type_: String,
41
42 pub version: String,
44
45 pub root_did: String,
47
48 pub delegate_pubkey: String,
50
51 pub delegate_type: DelegationType,
53
54 pub issued_at: i64,
56
57 pub expires_at: i64,
59
60 pub capabilities: Vec<Capability>,
62
63 pub revocation_id: String,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub signature: Option<String>,
69}
70
71impl Delegation {
72 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 pub fn sign(mut self, root_key: &RootKey) -> Result<Self> {
96 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 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 let root_did: Did = self.root_did.parse()?;
120 let public_key = root_did.public_key()?;
121
122 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 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 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), );
188
189 let result = delegation.is_valid_at(Utc::now());
190 assert!(matches!(result, Err(Error::DelegationExpired)));
191 }
192}