Skip to main content

aegis_delegate/
proof.rs

1// AEGIS Delegate — Delegation Proofs
2//
3// Reference: AEGIS Specification v1.0.0 §9.7
4//
5// Creates and verifies delegation proofs using Ed25519 signatures over
6// JCS-canonicalized delegation objects.
7
8use openagent_aegis_core::{Delegation, DelegationError, DelegationProof, DelegationScope};
9use base64::engine::general_purpose::URL_SAFE_NO_PAD;
10use base64::Engine;
11use chrono::{DateTime, Utc};
12use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
13use serde::{Deserialize, Serialize};
14use uuid::Uuid;
15
16/// Proof type constant per AEGIS Spec §9.7.
17const PROOF_TYPE: &str = "AegisDelegationProof2025";
18
19/// Intermediate structure used for canonicalization.
20///
21/// Contains every field of a [`Delegation`] except the `proof`, so we can
22/// serialize, canonicalize (JCS), and sign before the proof exists.
23#[derive(Serialize, Deserialize)]
24struct DelegationForSigning {
25    id: String,
26    delegator: String,
27    delegate: String,
28    scope: DelegationScope,
29    created: DateTime<Utc>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    expires: Option<DateTime<Utc>>,
32    revocable: bool,
33}
34
35/// Creates a delegation proof by signing the delegation with the delegator's key.
36///
37/// Steps per spec §9.7:
38/// 1. Construct delegation object (excluding proof field)
39/// 2. Canonicalize via JCS (RFC 8785)
40/// 3. Sign canonical bytes with delegator's delegation key (Ed25519)
41///
42/// Returns a fully formed [`Delegation`] with an attached cryptographic proof.
43pub fn create_delegation_proof(
44    delegator_did: &str,
45    delegate_did: &str,
46    scope: DelegationScope,
47    expires: Option<DateTime<Utc>>,
48    signing_key: &SigningKey,
49    verification_method: &str,
50) -> Result<Delegation, DelegationError> {
51    let now = Utc::now();
52    let id = Uuid::now_v7().to_string();
53
54    let for_signing = DelegationForSigning {
55        id: id.clone(),
56        delegator: delegator_did.to_string(),
57        delegate: delegate_did.to_string(),
58        scope: scope.clone(),
59        created: now,
60        expires,
61        revocable: true,
62    };
63
64    let canonical =
65        serde_jcs::to_string(&for_signing).map_err(|e| DelegationError::InvalidProof {
66            reason: format!("JCS canonicalization failed: {e}"),
67        })?;
68
69    let signature: Signature = signing_key.sign(canonical.as_bytes());
70    let jws = URL_SAFE_NO_PAD.encode(signature.to_bytes());
71
72    let proof = DelegationProof {
73        proof_type: PROOF_TYPE.to_string(),
74        verification_method: verification_method.to_string(),
75        created: now,
76        jws,
77    };
78
79    Ok(Delegation {
80        id,
81        delegator: delegator_did.to_string(),
82        delegate: delegate_did.to_string(),
83        scope,
84        created: now,
85        expires,
86        revocable: true,
87        proof,
88    })
89}
90
91/// Verifies a delegation proof against the delegator's public key.
92///
93/// Reconstructs the canonical form of the delegation (without the proof),
94/// then verifies the Ed25519 signature contained in the proof's JWS field.
95pub fn verify_delegation_proof(
96    delegation: &Delegation,
97    delegator_public_key: &VerifyingKey,
98) -> Result<bool, DelegationError> {
99    let for_signing = DelegationForSigning {
100        id: delegation.id.clone(),
101        delegator: delegation.delegator.clone(),
102        delegate: delegation.delegate.clone(),
103        scope: delegation.scope.clone(),
104        created: delegation.created,
105        expires: delegation.expires,
106        revocable: delegation.revocable,
107    };
108
109    let canonical =
110        serde_jcs::to_string(&for_signing).map_err(|e| DelegationError::InvalidProof {
111            reason: format!("JCS canonicalization failed: {e}"),
112        })?;
113
114    let sig_bytes = URL_SAFE_NO_PAD.decode(&delegation.proof.jws).map_err(|e| {
115        DelegationError::InvalidProof {
116            reason: format!("base64 decode failed: {e}"),
117        }
118    })?;
119
120    let signature =
121        Signature::from_slice(&sig_bytes).map_err(|e| DelegationError::InvalidProof {
122            reason: format!("invalid signature bytes: {e}"),
123        })?;
124
125    match delegator_public_key.verify(canonical.as_bytes(), &signature) {
126        Ok(()) => Ok(true),
127        Err(_) => Ok(false),
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use rand::rngs::OsRng;
135
136    fn test_scope() -> DelegationScope {
137        DelegationScope {
138            actions: vec!["transfer".to_string(), "approve".to_string()],
139            resources: vec!["0xabc".to_string()],
140            chains: vec!["ethereum".to_string()],
141            limits: None,
142            temporal: None,
143        }
144    }
145
146    #[test]
147    fn proof_creation_and_verification_round_trip() {
148        let signing_key = SigningKey::generate(&mut OsRng);
149        let verifying_key = signing_key.verifying_key();
150
151        let result = create_delegation_proof(
152            "did:oas:delegator123",
153            "did:oas:delegate456",
154            test_scope(),
155            None,
156            &signing_key,
157            "did:oas:delegator123#delegation-key",
158        );
159
160        assert!(result.is_ok());
161        let delegation = result.ok();
162
163        if let Some(ref d) = delegation {
164            assert_eq!(d.delegator, "did:oas:delegator123");
165            assert_eq!(d.delegate, "did:oas:delegate456");
166            assert_eq!(d.proof.proof_type, PROOF_TYPE);
167
168            let verified = verify_delegation_proof(d, &verifying_key);
169            assert!(verified.is_ok());
170            assert_eq!(verified.ok(), Some(true));
171        }
172    }
173
174    #[test]
175    fn proof_fails_with_wrong_key() {
176        let signing_key = SigningKey::generate(&mut OsRng);
177        let wrong_key = SigningKey::generate(&mut OsRng);
178        let wrong_verifying = wrong_key.verifying_key();
179
180        let result = create_delegation_proof(
181            "did:oas:delegator",
182            "did:oas:delegate",
183            test_scope(),
184            None,
185            &signing_key,
186            "did:oas:delegator#key",
187        );
188
189        assert!(result.is_ok());
190        if let Ok(ref d) = result {
191            let verified = verify_delegation_proof(d, &wrong_verifying);
192            assert!(verified.is_ok());
193            assert_eq!(verified.ok(), Some(false));
194        }
195    }
196
197    #[test]
198    fn proof_with_expiry() {
199        let signing_key = SigningKey::generate(&mut OsRng);
200        let verifying_key = signing_key.verifying_key();
201        let expires = Utc::now() + chrono::Duration::hours(1);
202
203        let result = create_delegation_proof(
204            "did:oas:a",
205            "did:oas:b",
206            test_scope(),
207            Some(expires),
208            &signing_key,
209            "did:oas:a#key",
210        );
211
212        assert!(result.is_ok());
213        if let Ok(ref d) = result {
214            assert!(d.expires.is_some());
215            let verified = verify_delegation_proof(d, &verifying_key);
216            assert_eq!(verified.ok(), Some(true));
217        }
218    }
219
220    #[test]
221    fn delegation_has_uuid_v7_id() {
222        let signing_key = SigningKey::generate(&mut OsRng);
223
224        let result = create_delegation_proof(
225            "did:oas:a",
226            "did:oas:b",
227            test_scope(),
228            None,
229            &signing_key,
230            "did:oas:a#key",
231        );
232
233        assert!(result.is_ok());
234        if let Ok(ref d) = result {
235            let parsed = Uuid::parse_str(&d.id);
236            assert!(parsed.is_ok());
237        }
238    }
239}