Skip to main content

agent_id_core/
lifecycle.rs

1//! Key lifecycle management: rotation and revocation.
2
3use crate::{signing, Did, Error, Result, RootKey};
4use chrono::{DateTime, Duration, Utc};
5use ed25519_dalek::{Signature, Verifier};
6use serde::{Deserialize, Serialize};
7
8/// Type of key being rotated.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum RotationType {
12    /// Root key rotation.
13    Root,
14    /// Session key rotation.
15    Session,
16}
17
18/// Reason for key rotation.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum RotationReason {
22    /// Scheduled rotation.
23    Scheduled,
24    /// Suspected compromise.
25    Compromise,
26    /// Algorithm upgrade.
27    AlgorithmUpgrade,
28    /// Operational requirements.
29    Operational,
30}
31
32/// A new key being rotated to.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub struct NewKey {
36    /// Key identifier (e.g., "did:key:...#root-2").
37    pub id: String,
38    /// Key type.
39    #[serde(rename = "type")]
40    pub key_type: String,
41    /// Public key in multibase format.
42    pub public_key_multibase: String,
43}
44
45/// A key rotation event.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct KeyRotation {
49    /// Type identifier.
50    #[serde(rename = "type")]
51    pub type_: String,
52
53    /// Protocol version.
54    pub version: String,
55
56    /// The DID this rotation is for.
57    pub did: String,
58
59    /// Type of rotation.
60    pub rotation_type: RotationType,
61
62    /// The new key.
63    pub new_key: NewKey,
64
65    /// Reference to the previous key.
66    pub previous_key: String,
67
68    /// When this rotation takes effect (unix ms).
69    pub effective_at: i64,
70
71    /// When the overlap period ends (unix ms).
72    pub overlap_until: i64,
73
74    /// Reason for rotation.
75    pub reason: RotationReason,
76
77    /// Signature from the previous key.
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub signature: Option<String>,
80}
81
82impl KeyRotation {
83    /// Default overlap period (24 hours).
84    pub const DEFAULT_OVERLAP: Duration = Duration::hours(24);
85
86    /// Create a new unsigned key rotation.
87    pub fn new(
88        did: Did,
89        rotation_type: RotationType,
90        new_key: NewKey,
91        previous_key: String,
92        reason: RotationReason,
93    ) -> Self {
94        let now = Utc::now();
95        Self {
96            type_: "KeyRotation".to_string(),
97            version: "1.0".to_string(),
98            did: did.to_string(),
99            rotation_type,
100            new_key,
101            previous_key,
102            effective_at: now.timestamp_millis(),
103            overlap_until: (now + Self::DEFAULT_OVERLAP).timestamp_millis(),
104            reason,
105            signature: None,
106        }
107    }
108
109    /// Set custom effective time.
110    pub fn effective_at(mut self, time: DateTime<Utc>) -> Self {
111        self.effective_at = time.timestamp_millis();
112        self
113    }
114
115    /// Set custom overlap period.
116    pub fn overlap_duration(mut self, duration: Duration) -> Self {
117        let effective = DateTime::from_timestamp_millis(self.effective_at).unwrap_or_else(Utc::now);
118        self.overlap_until = (effective + duration).timestamp_millis();
119        self
120    }
121
122    /// Sign this rotation with the previous (old) key.
123    pub fn sign(mut self, old_key: &RootKey) -> Result<Self> {
124        self.signature = None;
125        let canonical = signing::canonicalize(&self)?;
126        let sig = old_key.sign(&canonical);
127        self.signature = Some(base64::Engine::encode(
128            &base64::engine::general_purpose::STANDARD,
129            sig.to_bytes(),
130        ));
131        Ok(self)
132    }
133
134    /// Verify this rotation's signature against the DID's public key.
135    pub fn verify(&self) -> Result<()> {
136        let sig_b64 = self.signature.as_ref().ok_or(Error::InvalidSignature)?;
137        let sig_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, sig_b64)
138            .map_err(|_| Error::InvalidSignature)?;
139
140        let signature =
141            Signature::from_bytes(&sig_bytes.try_into().map_err(|_| Error::InvalidSignature)?);
142
143        let did: Did = self.did.parse()?;
144        let public_key = did.public_key()?;
145
146        let mut unsigned = self.clone();
147        unsigned.signature = None;
148        let canonical = signing::canonicalize(&unsigned)?;
149
150        public_key
151            .verify(&canonical, &signature)
152            .map_err(|_| Error::InvalidSignature)
153    }
154
155    /// Check if a key is valid at a given time (considering overlap).
156    pub fn is_old_key_valid_at(&self, time: DateTime<Utc>) -> bool {
157        time.timestamp_millis() <= self.overlap_until
158    }
159
160    /// Check if the new key is active at a given time.
161    pub fn is_new_key_active_at(&self, time: DateTime<Utc>) -> bool {
162        time.timestamp_millis() >= self.effective_at
163    }
164}
165
166/// Reason for key revocation.
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
168#[serde(rename_all = "snake_case")]
169pub enum RevocationReason {
170    /// Key believed stolen.
171    Compromised,
172    /// Replaced by rotation.
173    Superseded,
174    /// Natural delegation expiry.
175    Expired,
176    /// Operator decision.
177    Administrative,
178}
179
180/// A key revocation event.
181#[derive(Debug, Clone, Serialize, Deserialize)]
182#[serde(rename_all = "camelCase")]
183pub struct Revocation {
184    /// Type identifier.
185    #[serde(rename = "type")]
186    pub type_: String,
187
188    /// Protocol version.
189    pub version: String,
190
191    /// The DID this revocation is for.
192    pub did: String,
193
194    /// Key being revoked (key ID).
195    pub revoked_key: String,
196
197    /// Revocation ID (matches delegation revocation_id if revoking a delegation).
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub revocation_id: Option<String>,
200
201    /// Reason for revocation.
202    pub reason: RevocationReason,
203
204    /// When this revocation takes effect (unix ms).
205    pub effective_at: i64,
206
207    /// Signature from root key.
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub signature: Option<String>,
210}
211
212impl Revocation {
213    /// Create a new unsigned revocation.
214    pub fn new(did: Did, revoked_key: String, reason: RevocationReason) -> Self {
215        Self {
216            type_: "Revocation".to_string(),
217            version: "1.0".to_string(),
218            did: did.to_string(),
219            revoked_key,
220            revocation_id: None,
221            reason,
222            effective_at: Utc::now().timestamp_millis(),
223            signature: None,
224        }
225    }
226
227    /// Set revocation ID (for revoking delegations).
228    pub fn with_revocation_id(mut self, id: String) -> Self {
229        self.revocation_id = Some(id);
230        self
231    }
232
233    /// Sign this revocation with a root key.
234    pub fn sign(mut self, signing_key: &RootKey) -> Result<Self> {
235        self.signature = None;
236        let canonical = signing::canonicalize(&self)?;
237        let sig = signing_key.sign(&canonical);
238        self.signature = Some(base64::Engine::encode(
239            &base64::engine::general_purpose::STANDARD,
240            sig.to_bytes(),
241        ));
242        Ok(self)
243    }
244
245    /// Verify this revocation's signature.
246    pub fn verify(&self, verifying_did: &Did) -> Result<()> {
247        let sig_b64 = self.signature.as_ref().ok_or(Error::InvalidSignature)?;
248        let sig_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, sig_b64)
249            .map_err(|_| Error::InvalidSignature)?;
250
251        let signature =
252            Signature::from_bytes(&sig_bytes.try_into().map_err(|_| Error::InvalidSignature)?);
253
254        let public_key = verifying_did.public_key()?;
255
256        let mut unsigned = self.clone();
257        unsigned.signature = None;
258        let canonical = signing::canonicalize(&unsigned)?;
259
260        public_key
261            .verify(&canonical, &signature)
262            .map_err(|_| Error::InvalidSignature)
263    }
264
265    /// Check if this revocation is effective at a given time.
266    pub fn is_effective_at(&self, time: DateTime<Utc>) -> bool {
267        time.timestamp_millis() >= self.effective_at
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    fn make_new_key(root: &RootKey, suffix: &str) -> NewKey {
276        NewKey {
277            id: format!("{}#{}", root.did(), suffix),
278            key_type: "Ed25519VerificationKey2020".to_string(),
279            public_key_multibase: format!(
280                "z{}",
281                root.did().to_string().split(':').next_back().unwrap()
282            ),
283        }
284    }
285
286    #[test]
287    fn test_key_rotation() {
288        let old_key = RootKey::generate();
289        let new_key = RootKey::generate();
290
291        let rotation = KeyRotation::new(
292            old_key.did(),
293            RotationType::Root,
294            make_new_key(&new_key, "root-2"),
295            format!("{}#root", old_key.did()),
296            RotationReason::Scheduled,
297        );
298
299        let signed = rotation.sign(&old_key).unwrap();
300        assert!(signed.signature.is_some());
301        signed.verify().unwrap();
302    }
303
304    #[test]
305    fn test_key_rotation_overlap() {
306        let key = RootKey::generate();
307        let new_key = RootKey::generate();
308
309        let rotation = KeyRotation::new(
310            key.did(),
311            RotationType::Root,
312            make_new_key(&new_key, "root-2"),
313            format!("{}#root", key.did()),
314            RotationReason::Scheduled,
315        );
316
317        // Old key should be valid during overlap
318        assert!(rotation.is_old_key_valid_at(Utc::now()));
319        assert!(rotation.is_new_key_active_at(Utc::now()));
320
321        // Old key should be invalid after overlap
322        let after_overlap = Utc::now() + Duration::hours(25);
323        assert!(!rotation.is_old_key_valid_at(after_overlap));
324    }
325
326    #[test]
327    fn test_revocation() {
328        let root = RootKey::generate();
329
330        let revocation = Revocation::new(
331            root.did(),
332            format!("{}#session-1", root.did()),
333            RevocationReason::Compromised,
334        );
335
336        let signed = revocation.sign(&root).unwrap();
337        assert!(signed.signature.is_some());
338        signed.verify(&root.did()).unwrap();
339    }
340
341    #[test]
342    fn test_revocation_with_id() {
343        let root = RootKey::generate();
344
345        let revocation = Revocation::new(
346            root.did(),
347            format!("{}#session-1", root.did()),
348            RevocationReason::Administrative,
349        )
350        .with_revocation_id("delegation-uuid-123".to_string());
351
352        assert_eq!(
353            revocation.revocation_id,
354            Some("delegation-uuid-123".to_string())
355        );
356    }
357}