use std::ops::ControlFlow;
use std::sync::Arc;
use auths_core::signing::StorageSigner;
use auths_core::storage::keychain::{IdentityDID, KeyAlias, extract_public_key_bytes};
use auths_id::attestation::create::{AttestationInput, create_signed_attestation};
use auths_id::keri::delegation::{
BulkAgentSpec, DelegatedRole, incept_delegated_agents_bulk, incept_delegated_device,
list_delegated_devices, mark_agent_scope, mark_delegated_agent, read_agent_scope,
revoke_delegated_device, revoke_delegated_devices_batch, rotate_delegated_device,
};
use auths_id::keri::{Event, anchor_and_persist_via_backend, parse_did_keri};
use auths_id::storage::git_refs::AttestationMetadata;
use auths_keri::{AgentScope, Capability};
use auths_verifier::core::SignerType;
use auths_verifier::types::CanonicalDid;
use crate::context::AuthsContext;
use crate::domains::agents::error::AgentError;
#[derive(Debug, Clone)]
pub struct AgentDelegationResult {
pub agent_did: String,
pub agent_prefix: String,
}
pub fn add(
ctx: &AuthsContext,
root_alias: &KeyAlias,
agent_alias: &KeyAlias,
agent_curve: auths_crypto::CurveType,
) -> Result<AgentDelegationResult, AgentError> {
add_scoped(ctx, root_alias, agent_alias, agent_curve, &[], None)
}
pub fn add_scoped(
ctx: &AuthsContext,
root_alias: &KeyAlias,
agent_alias: &KeyAlias,
agent_curve: auths_crypto::CurveType,
scope: &[Capability],
expires_at: Option<i64>,
) -> Result<AgentDelegationResult, AgentError> {
if ctx.key_storage.load_key(agent_alias).is_ok() {
return Err(AgentError::AlreadyDelegated {
alias: agent_alias.as_str().to_string(),
});
}
let managed =
ctx.identity_storage
.load_identity()
.map_err(|e| AgentError::IdentityNotFound {
did: format!("identity load failed: {e}"),
})?;
let root_prefix = parse_did_keri(managed.controller_did.as_str()).map_err(|e| {
AgentError::IdentityNotFound {
did: format!("invalid root did:keri: {e}"),
}
})?;
let (_pk, root_curve) = extract_public_key_bytes(
ctx.key_storage.as_ref(),
root_alias,
ctx.passphrase_provider.as_ref(),
)
.map_err(AgentError::CryptoError)?;
enforce_scope_subset(ctx, &root_prefix, root_alias, scope)?;
let agent = incept_delegated_device(
Arc::clone(&ctx.registry),
&root_prefix,
root_alias,
root_curve,
agent_alias,
agent_curve,
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)
.map_err(AgentError::DelegationError)?;
mark_delegated_agent(
ctx.registry.as_ref(),
&root_prefix,
root_alias,
root_curve,
&agent.device_prefix,
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)
.map_err(AgentError::DelegationError)?;
if !scope.is_empty() || expires_at.is_some() {
mark_agent_scope(
ctx.registry.as_ref(),
&root_prefix,
root_alias,
root_curve,
&agent.device_prefix,
&AgentScope {
capabilities: scope.to_vec(),
expires_at,
},
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)
.map_err(AgentError::DelegationError)?;
}
record_delegation_attestation(
ctx,
&managed.controller_did,
&managed.storage_id,
root_alias,
&root_prefix,
agent_alias,
&agent.device_did,
expires_at,
)?;
Ok(AgentDelegationResult {
agent_did: agent.device_did.as_str().to_string(),
agent_prefix: agent.device_prefix.as_str().to_string(),
})
}
pub fn add_bulk(
ctx: &AuthsContext,
root_alias: &KeyAlias,
agent_aliases: &[KeyAlias],
agent_curve: auths_crypto::CurveType,
batch_size: usize,
) -> Result<Vec<AgentDelegationResult>, AgentError> {
for alias in agent_aliases {
if ctx.key_storage.load_key(alias).is_ok() {
return Err(AgentError::AlreadyDelegated {
alias: alias.as_str().to_string(),
});
}
}
let managed =
ctx.identity_storage
.load_identity()
.map_err(|e| AgentError::IdentityNotFound {
did: format!("identity load failed: {e}"),
})?;
let root_prefix = parse_did_keri(managed.controller_did.as_str()).map_err(|e| {
AgentError::IdentityNotFound {
did: format!("invalid root did:keri: {e}"),
}
})?;
let (_pk, root_curve) = extract_public_key_bytes(
ctx.key_storage.as_ref(),
root_alias,
ctx.passphrase_provider.as_ref(),
)
.map_err(AgentError::CryptoError)?;
let signer = StorageSigner::new(Arc::clone(&ctx.key_storage));
let issuer_canonical = CanonicalDid::from(managed.controller_did.clone());
let mut out = Vec::with_capacity(agent_aliases.len());
for chunk in agent_aliases.chunks(batch_size.max(1)) {
let specs: Vec<BulkAgentSpec> = chunk
.iter()
.map(|alias| BulkAgentSpec {
device_alias: alias.clone(),
device_curve: agent_curve,
})
.collect();
let mut idx = 0usize;
let bulk = incept_delegated_agents_bulk(
ctx.registry.as_ref(),
&root_prefix,
root_alias,
root_curve,
&specs,
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
&ctx.witness_params(),
ctx.clock.now(),
|bundle, batch| {
let agent_alias = &chunk[idx];
idx += 1;
let (agent_pk, agent_pk_curve) = extract_public_key_bytes(
ctx.key_storage.as_ref(),
agent_alias,
ctx.passphrase_provider.as_ref(),
)
.map_err(|e| auths_id::error::InitError::Crypto(e.to_string()))?;
let now = ctx.clock.now();
let meta = AttestationMetadata {
timestamp: Some(now),
expires_at: None,
note: None,
};
let subject = CanonicalDid::from(bundle.device_did.clone());
let attestation = create_signed_attestation(
now,
AttestationInput {
rid: &managed.storage_id,
issuer: &issuer_canonical,
subject: &subject,
device_public_key: &agent_pk,
device_curve: agent_pk_curve,
payload: None,
meta: &meta,
identity_alias: Some(root_alias),
device_alias: Some(agent_alias),
delegated_by: None,
commit_sha: None,
signer_type: Some(SignerType::Agent),
oidc_binding: None,
},
&signer,
ctx.passphrase_provider.as_ref(),
)
.map_err(|e| auths_id::error::InitError::Keri(e.to_string()))?;
let said = auths_id::keri::anchor::attestation_said(&attestation)
.map_err(|e| auths_id::error::InitError::Keri(e.to_string()))?;
batch.stage_attestation(attestation);
Ok(vec![auths_id::keri::Seal::digest(said.as_str())])
},
)
.map_err(AgentError::DelegationError)?;
for device in bulk.devices {
out.push(AgentDelegationResult {
agent_did: device.device_did.as_str().to_string(),
agent_prefix: device.device_prefix.as_str().to_string(),
});
}
}
Ok(out)
}
#[allow(clippy::too_many_arguments)]
fn record_delegation_attestation(
ctx: &AuthsContext,
identity_did: &IdentityDID,
rid: &str,
root_alias: &KeyAlias,
root_prefix: &auths_id::keri::types::Prefix,
agent_alias: &KeyAlias,
agent_did: &IdentityDID,
expires_at: Option<i64>,
) -> Result<(), AgentError> {
let (agent_pk, agent_pk_curve) = extract_public_key_bytes(
ctx.key_storage.as_ref(),
agent_alias,
ctx.passphrase_provider.as_ref(),
)
.map_err(AgentError::CryptoError)?;
let now = ctx.clock.now();
let meta = AttestationMetadata {
timestamp: Some(now),
expires_at: expires_at.and_then(|secs| chrono::DateTime::from_timestamp(secs, 0)),
note: None,
};
let subject = CanonicalDid::from(agent_did.clone());
let issuer_canonical = CanonicalDid::from(identity_did.clone());
let signer = StorageSigner::new(Arc::clone(&ctx.key_storage));
let attestation = create_signed_attestation(
now,
AttestationInput {
rid,
issuer: &issuer_canonical,
subject: &subject,
device_public_key: &agent_pk,
device_curve: agent_pk_curve,
payload: None,
meta: &meta,
identity_alias: Some(root_alias),
device_alias: Some(agent_alias),
delegated_by: None,
commit_sha: None,
signer_type: Some(SignerType::Agent),
oidc_binding: None,
},
&signer,
ctx.passphrase_provider.as_ref(),
)
.map_err(AgentError::AttestationError)?;
let mut batch = auths_id::storage::registry::backend::AtomicWriteBatch::new();
batch.stage_attestation(attestation.clone());
anchor_and_persist_via_backend(
ctx.registry.as_ref(),
&signer,
root_alias,
ctx.passphrase_provider.as_ref(),
root_prefix,
&attestation,
&mut batch,
&ctx.witness_params(),
now,
)
.map_err(AgentError::AnchorError)?;
Ok(())
}
fn collect_kel(ctx: &AuthsContext, prefix: &auths_id::keri::types::Prefix) -> Vec<Event> {
let mut events = Vec::new();
let _ = ctx.registry.visit_events(prefix, 0, &mut |e| {
events.push(e.clone());
ControlFlow::Continue(())
});
events
}
fn enforce_scope_subset(
ctx: &AuthsContext,
root_prefix: &auths_id::keri::types::Prefix,
root_alias: &KeyAlias,
requested: &[Capability],
) -> Result<(), AgentError> {
if requested.is_empty() {
return Ok(());
}
let (delegator_did, _role, _key) = ctx
.key_storage
.load_key(root_alias)
.map_err(AgentError::CryptoError)?;
let delegator_prefix =
parse_did_keri(delegator_did.as_str()).map_err(|e| AgentError::IdentityNotFound {
did: format!("invalid delegator did:keri for alias {root_alias}: {e}"),
})?;
let root_kel = collect_kel(ctx, root_prefix);
let is_registry_root = delegator_prefix == *root_prefix;
match resolve_delegator_authority(
is_registry_root,
read_agent_scope(&root_kel, &delegator_prefix),
) {
DelegatorAuthority::Root => Ok(()),
DelegatorAuthority::Scoped(scope) => {
crate::domains::agents::scope::validate_capability_subset(
&scope.capabilities,
requested,
)
.map_err(|e| AgentError::OutsideDelegatorScope {
capability: match e {
crate::domains::agents::scope::DelegationError::CapabilityNotGranted(cap) => {
cap
}
other => other.to_string(),
},
})
}
DelegatorAuthority::NoAuthority => Err(AgentError::OutsideDelegatorScope {
capability: format!("delegator {delegator_prefix} presents no anchored scope seal"),
}),
}
}
enum DelegatorAuthority {
Root,
Scoped(AgentScope),
NoAuthority,
}
fn resolve_delegator_authority(
is_registry_root: bool,
anchored_seal: Option<AgentScope>,
) -> DelegatorAuthority {
match (is_registry_root, anchored_seal) {
(_, Some(scope)) => DelegatorAuthority::Scoped(scope),
(true, None) => DelegatorAuthority::Root,
(false, None) => DelegatorAuthority::NoAuthority,
}
}
#[derive(Debug, Clone)]
pub struct AgentInfo {
pub agent_did: String,
pub revoked: bool,
}
pub fn list(ctx: &AuthsContext) -> Result<Vec<AgentInfo>, AgentError> {
let managed =
ctx.identity_storage
.load_identity()
.map_err(|e| AgentError::IdentityNotFound {
did: format!("identity load failed: {e}"),
})?;
let root_prefix = parse_did_keri(managed.controller_did.as_str()).map_err(|e| {
AgentError::IdentityNotFound {
did: format!("invalid root did:keri: {e}"),
}
})?;
let delegated = list_delegated_devices(ctx.registry.as_ref(), &root_prefix)
.map_err(AgentError::DelegationError)?;
Ok(delegated
.into_iter()
.filter(|d| d.role == DelegatedRole::Agent)
.map(|d| AgentInfo {
agent_did: format!("did:keri:{}", d.device_prefix),
revoked: d.revoked,
})
.collect())
}
pub fn revoke(
ctx: &AuthsContext,
root_alias: &KeyAlias,
agent_did: &str,
) -> Result<(), AgentError> {
let managed =
ctx.identity_storage
.load_identity()
.map_err(|e| AgentError::IdentityNotFound {
did: format!("identity load failed: {e}"),
})?;
let root_prefix = parse_did_keri(managed.controller_did.as_str()).map_err(|e| {
AgentError::IdentityNotFound {
did: format!("invalid root did:keri: {e}"),
}
})?;
let agent_prefix = parse_did_keri(agent_did).map_err(|_| AgentError::AgentNotFound {
did: agent_did.to_string(),
})?;
let (_pk, root_curve) = extract_public_key_bytes(
ctx.key_storage.as_ref(),
root_alias,
ctx.passphrase_provider.as_ref(),
)
.map_err(AgentError::CryptoError)?;
revoke_delegated_device(
ctx.registry.as_ref(),
&root_prefix,
root_alias,
root_curve,
&agent_prefix,
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)
.map_err(AgentError::DelegationError)
}
#[derive(Debug, Clone)]
pub struct BatchRevocation {
pub revoked: Vec<String>,
pub anchored_at_seq: Option<u128>,
}
pub fn revoke_batch(
ctx: &AuthsContext,
root_alias: &KeyAlias,
agent_dids: &[String],
) -> Result<BatchRevocation, AgentError> {
let managed =
ctx.identity_storage
.load_identity()
.map_err(|e| AgentError::IdentityNotFound {
did: format!("identity load failed: {e}"),
})?;
let root_prefix = parse_did_keri(managed.controller_did.as_str()).map_err(|e| {
AgentError::IdentityNotFound {
did: format!("invalid root did:keri: {e}"),
}
})?;
let mut prefixes = Vec::with_capacity(agent_dids.len());
for did in agent_dids {
prefixes
.push(parse_did_keri(did).map_err(|_| AgentError::AgentNotFound { did: did.clone() })?);
}
let (_pk, root_curve) = extract_public_key_bytes(
ctx.key_storage.as_ref(),
root_alias,
ctx.passphrase_provider.as_ref(),
)
.map_err(AgentError::CryptoError)?;
let (_newly, ixn) = revoke_delegated_devices_batch(
ctx.registry.as_ref(),
&root_prefix,
root_alias,
root_curve,
&prefixes,
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)
.map_err(AgentError::DelegationError)?;
Ok(BatchRevocation {
revoked: agent_dids.to_vec(),
anchored_at_seq: ixn.map(|e| e.s.value()),
})
}
pub fn rotate(
ctx: &AuthsContext,
root_alias: &KeyAlias,
agent_did: &str,
) -> Result<(), AgentError> {
let managed =
ctx.identity_storage
.load_identity()
.map_err(|e| AgentError::IdentityNotFound {
did: format!("identity load failed: {e}"),
})?;
let root_prefix = parse_did_keri(managed.controller_did.as_str()).map_err(|e| {
AgentError::IdentityNotFound {
did: format!("invalid root did:keri: {e}"),
}
})?;
let agent_prefix = parse_did_keri(agent_did).map_err(|_| AgentError::AgentNotFound {
did: agent_did.to_string(),
})?;
let delegated = list_delegated_devices(ctx.registry.as_ref(), &root_prefix)
.map_err(AgentError::DelegationError)?;
let info = delegated
.iter()
.find(|d| d.device_prefix.as_str() == agent_prefix.as_str())
.ok_or_else(|| AgentError::AgentNotFound {
did: agent_did.to_string(),
})?;
if info.revoked {
return Err(AgentError::Revoked {
did: agent_did.to_string(),
});
}
let agent_did_typed =
IdentityDID::try_from(&agent_prefix).map_err(|e| AgentError::AgentNotFound {
did: format!("invalid agent did:keri: {e}"),
})?;
let agent_alias = ctx
.key_storage
.list_aliases_for_identity(&agent_did_typed)
.map_err(AgentError::CryptoError)?
.into_iter()
.find(|a| !a.as_str().contains("--next-"))
.ok_or_else(|| AgentError::AgentNotFound {
did: format!("no local key for agent {agent_did}"),
})?;
let (_pk, agent_curve) = extract_public_key_bytes(
ctx.key_storage.as_ref(),
&agent_alias,
ctx.passphrase_provider.as_ref(),
)
.map_err(AgentError::CryptoError)?;
let (_pk, root_curve) = extract_public_key_bytes(
ctx.key_storage.as_ref(),
root_alias,
ctx.passphrase_provider.as_ref(),
)
.map_err(AgentError::CryptoError)?;
rotate_delegated_device(
ctx.registry.as_ref(),
&root_prefix,
root_alias,
root_curve,
&agent_prefix,
&agent_alias,
agent_curve,
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)
.map_err(AgentError::DelegationError)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_non_root_delegator_without_a_seal_has_no_authority() {
assert!(matches!(
resolve_delegator_authority(false, None),
DelegatorAuthority::NoAuthority
));
assert!(matches!(
resolve_delegator_authority(true, None),
DelegatorAuthority::Root
));
}
}