1use 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
16const PROOF_TYPE: &str = "AegisDelegationProof2025";
18
19#[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
35pub 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
91pub 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}