use chrono::{DateTime, Utc};
use crate::{DTGCredential, DTGCredentialType};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum DelegationError {
#[error("delegation chain is empty")]
EmptyChain,
#[error("delegation chain is {found} deep, exceeding the maximum of {MAX_CHAIN_DEPTH}")]
TooDeep { found: usize },
#[error("credential at index {index} is a {found}, not a delegation grant")]
NotADelegationGrant { index: usize, found: String },
#[error("credential at index {index} is an acceptance, not a grant")]
AcceptanceInChain { index: usize },
#[error("delegation at index {index} carries no scope, and so appoints for nothing")]
NoScope { index: usize },
#[error("VDC at index {index} carries no validUntil, which a VDC MUST have")]
NoExpiry { index: usize },
#[error("delegation at index {index} is not valid at {at}")]
NotValidNow { index: usize, at: DateTime<Utc> },
#[error("delegation at index {index} names parent {named}, but {presented} was presented")]
BrokenLink {
index: usize,
named: String,
presented: String,
},
#[error("digest error at index {index}: {reason}")]
Digest { index: usize, reason: String },
#[error(
"delegation at index {index} is issued by {issuer}, but its parent appointed {subject}"
)]
IssuerNotParentSubject {
index: usize,
issuer: String,
subject: String,
},
#[error("delegation at index {index} adds `{act}`, which its parent does not appoint for")]
WidensScope { index: usize, act: String },
#[error(
"delegation at index {index} is valid until {until}, beyond its parent's {parent_until}"
)]
OutlivesParent {
index: usize,
until: DateTime<Utc>,
parent_until: DateTime<Utc>,
},
#[error(
"delegation at index {index} re-delegates below a parent whose maxDepth is {parent_depth}"
)]
RedelegationNotPermitted { index: usize, parent_depth: u32 },
#[error(
"delegation at index {index} bears maxDepth {depth}, above its parent's {parent_depth} - 1"
)]
DepthNotNarrowed {
index: usize,
depth: u32,
parent_depth: u32,
},
#[error("chain root is issued by {root_issuer}, not the principal {expected}")]
RootNotPrincipal {
root_issuer: String,
expected: String,
},
#[error("the delegation does not appoint for `{act}`")]
ActNotAppointed { act: String },
}
pub const MAX_CHAIN_DEPTH: usize = 8;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedDelegation {
pub delegate: String,
pub scope: Vec<String>,
pub principal: String,
}
pub fn verify_chain(
chain: &[DTGCredential],
principal: &str,
requested_act: &str,
at: DateTime<Utc>,
) -> Result<VerifiedDelegation, DelegationError> {
if chain.is_empty() {
return Err(DelegationError::EmptyChain);
}
if chain.len() > MAX_CHAIN_DEPTH {
return Err(DelegationError::TooDeep { found: chain.len() });
}
for (index, link) in chain.iter().enumerate() {
if !matches!(link.type_(), DTGCredentialType::Delegation) {
return Err(DelegationError::NotADelegationGrant {
index,
found: link.type_().to_string(),
});
}
let grant =
link.credential()
.delegation()
.ok_or_else(|| DelegationError::NotADelegationGrant {
index,
found: "DelegationCredential without a delegation object".to_string(),
})?;
if grant.accepts.is_some() {
return Err(DelegationError::AcceptanceInChain { index });
}
if grant.scope.as_ref().is_none_or(|s| s.is_empty()) {
return Err(DelegationError::NoScope { index });
}
let c = link.credential();
if c.valid_from() > at {
return Err(DelegationError::NotValidNow { index, at });
}
let Some(until) = c.valid_until() else {
return Err(DelegationError::NoExpiry { index });
};
if until < at {
return Err(DelegationError::NotValidNow { index, at });
}
}
for index in 0..chain.len() - 1 {
let link = &chain[index];
let parent = &chain[index + 1];
let grant = link.credential().delegation().expect("checked above");
let parent_grant = parent.credential().delegation().expect("checked above");
let presented_digest = parent
.digest_multibase()
.map_err(|e| DelegationError::Digest {
index: index + 1,
reason: e.to_string(),
})?;
match &grant.parent {
Some(named) => {
let matches = crate::digests_match(named, &presented_digest).map_err(|e| {
DelegationError::Digest {
index,
reason: e.to_string(),
}
})?;
if !matches {
return Err(DelegationError::BrokenLink {
index,
named: named.clone(),
presented: presented_digest,
});
}
}
None => {
return Err(DelegationError::BrokenLink {
index,
named: "<none — link claims to be a root delegation>".to_string(),
presented: presented_digest,
});
}
}
if link.credential().issuer() != parent.credential().subject() {
return Err(DelegationError::IssuerNotParentSubject {
index,
issuer: link.credential().issuer().to_string(),
subject: parent.credential().subject().to_string(),
});
}
let parent_depth = parent_grant.max_depth.unwrap_or(0);
if parent_depth == 0 {
return Err(DelegationError::RedelegationNotPermitted {
index,
parent_depth,
});
}
if let Some(depth) = grant.max_depth
&& depth > parent_depth - 1
{
return Err(DelegationError::DepthNotNarrowed {
index,
depth,
parent_depth,
});
}
let parent_scope = parent_grant.scope.as_deref().expect("checked above");
for act in grant.scope.as_deref().expect("checked above") {
if !parent_scope.contains(act) {
return Err(DelegationError::WidensScope {
index,
act: act.clone(),
});
}
}
if let (Some(until), Some(parent_until)) = (
link.credential().valid_until(),
parent.credential().valid_until(),
) && until > parent_until
{
return Err(DelegationError::OutlivesParent {
index,
until,
parent_until,
});
}
}
let root = chain.last().expect("non-empty");
let root_grant = root.credential().delegation().expect("checked above");
if root.credential().issuer() != principal {
return Err(DelegationError::RootNotPrincipal {
root_issuer: root.credential().issuer().to_string(),
expected: principal.to_string(),
});
}
if let Some(named) = &root_grant.parent {
return Err(DelegationError::BrokenLink {
index: chain.len() - 1,
named: named.clone(),
presented: "<nothing — chain ends here>".to_string(),
});
}
let leaf = &chain[0];
let leaf_grant = leaf.credential().delegation().expect("checked above");
let scope = leaf_grant.scope.clone().expect("checked above");
if !scope.iter().any(|a| a == requested_act) {
return Err(DelegationError::ActNotAppointed {
act: requested_act.to_string(),
});
}
Ok(VerifiedDelegation {
delegate: leaf.credential().subject().to_string(),
scope,
principal: principal.to_string(),
})
}