use openagent_aegis_core::{Delegation, DelegationError, DelegationProof, DelegationScope};
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use chrono::{DateTime, Utc};
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
const PROOF_TYPE: &str = "AegisDelegationProof2025";
#[derive(Serialize, Deserialize)]
struct DelegationForSigning {
id: String,
delegator: String,
delegate: String,
scope: DelegationScope,
created: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
expires: Option<DateTime<Utc>>,
revocable: bool,
}
pub fn create_delegation_proof(
delegator_did: &str,
delegate_did: &str,
scope: DelegationScope,
expires: Option<DateTime<Utc>>,
signing_key: &SigningKey,
verification_method: &str,
) -> Result<Delegation, DelegationError> {
let now = Utc::now();
let id = Uuid::now_v7().to_string();
let for_signing = DelegationForSigning {
id: id.clone(),
delegator: delegator_did.to_string(),
delegate: delegate_did.to_string(),
scope: scope.clone(),
created: now,
expires,
revocable: true,
};
let canonical =
serde_jcs::to_string(&for_signing).map_err(|e| DelegationError::InvalidProof {
reason: format!("JCS canonicalization failed: {e}"),
})?;
let signature: Signature = signing_key.sign(canonical.as_bytes());
let jws = URL_SAFE_NO_PAD.encode(signature.to_bytes());
let proof = DelegationProof {
proof_type: PROOF_TYPE.to_string(),
verification_method: verification_method.to_string(),
created: now,
jws,
};
Ok(Delegation {
id,
delegator: delegator_did.to_string(),
delegate: delegate_did.to_string(),
scope,
created: now,
expires,
revocable: true,
proof,
})
}
pub fn verify_delegation_proof(
delegation: &Delegation,
delegator_public_key: &VerifyingKey,
) -> Result<bool, DelegationError> {
let for_signing = DelegationForSigning {
id: delegation.id.clone(),
delegator: delegation.delegator.clone(),
delegate: delegation.delegate.clone(),
scope: delegation.scope.clone(),
created: delegation.created,
expires: delegation.expires,
revocable: delegation.revocable,
};
let canonical =
serde_jcs::to_string(&for_signing).map_err(|e| DelegationError::InvalidProof {
reason: format!("JCS canonicalization failed: {e}"),
})?;
let sig_bytes = URL_SAFE_NO_PAD.decode(&delegation.proof.jws).map_err(|e| {
DelegationError::InvalidProof {
reason: format!("base64 decode failed: {e}"),
}
})?;
let signature =
Signature::from_slice(&sig_bytes).map_err(|e| DelegationError::InvalidProof {
reason: format!("invalid signature bytes: {e}"),
})?;
match delegator_public_key.verify(canonical.as_bytes(), &signature) {
Ok(()) => Ok(true),
Err(_) => Ok(false),
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::rngs::OsRng;
fn test_scope() -> DelegationScope {
DelegationScope {
actions: vec!["transfer".to_string(), "approve".to_string()],
resources: vec!["0xabc".to_string()],
chains: vec!["ethereum".to_string()],
limits: None,
temporal: None,
}
}
#[test]
fn proof_creation_and_verification_round_trip() {
let signing_key = SigningKey::generate(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let result = create_delegation_proof(
"did:oas:delegator123",
"did:oas:delegate456",
test_scope(),
None,
&signing_key,
"did:oas:delegator123#delegation-key",
);
assert!(result.is_ok());
let delegation = result.ok();
if let Some(ref d) = delegation {
assert_eq!(d.delegator, "did:oas:delegator123");
assert_eq!(d.delegate, "did:oas:delegate456");
assert_eq!(d.proof.proof_type, PROOF_TYPE);
let verified = verify_delegation_proof(d, &verifying_key);
assert!(verified.is_ok());
assert_eq!(verified.ok(), Some(true));
}
}
#[test]
fn proof_fails_with_wrong_key() {
let signing_key = SigningKey::generate(&mut OsRng);
let wrong_key = SigningKey::generate(&mut OsRng);
let wrong_verifying = wrong_key.verifying_key();
let result = create_delegation_proof(
"did:oas:delegator",
"did:oas:delegate",
test_scope(),
None,
&signing_key,
"did:oas:delegator#key",
);
assert!(result.is_ok());
if let Ok(ref d) = result {
let verified = verify_delegation_proof(d, &wrong_verifying);
assert!(verified.is_ok());
assert_eq!(verified.ok(), Some(false));
}
}
#[test]
fn proof_with_expiry() {
let signing_key = SigningKey::generate(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let expires = Utc::now() + chrono::Duration::hours(1);
let result = create_delegation_proof(
"did:oas:a",
"did:oas:b",
test_scope(),
Some(expires),
&signing_key,
"did:oas:a#key",
);
assert!(result.is_ok());
if let Ok(ref d) = result {
assert!(d.expires.is_some());
let verified = verify_delegation_proof(d, &verifying_key);
assert_eq!(verified.ok(), Some(true));
}
}
#[test]
fn delegation_has_uuid_v7_id() {
let signing_key = SigningKey::generate(&mut OsRng);
let result = create_delegation_proof(
"did:oas:a",
"did:oas:b",
test_scope(),
None,
&signing_key,
"did:oas:a#key",
);
assert!(result.is_ok());
if let Ok(ref d) = result {
let parsed = Uuid::parse_str(&d.id);
assert!(parsed.is_ok());
}
}
}