use chrono::{DateTime, Utc};
use crate::{DTGCredential, DTGCredentialType};
pub const MAX_CHAIN_DEPTH: usize = 8;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum AuthorityError {
#[error("authority chain is empty")]
EmptyChain,
#[error("digest error at index {index}: {reason}")]
Digest { index: usize, reason: String },
#[error("VAC at index {index} carries no validUntil, which a VAC MUST have")]
NoExpiry { index: usize },
#[error("authority chain is {found} deep, exceeding the maximum of {MAX_CHAIN_DEPTH}")]
TooDeep {
found: usize,
},
#[error("chain link {index} is a {found}, not an AuthorityCredential")]
NotAuthority {
index: usize,
found: String,
},
#[error(
"chain root was issued by `{root_issuer}`, not by `{expected}` which governs the scope"
)]
RootNotGoverning {
root_issuer: String,
expected: String,
},
#[error("chain link {index} names parent `{named}`, but was presented after `{presented}`")]
BrokenLink {
index: usize,
named: String,
presented: String,
},
#[error("chain link {index} was issued by `{issuer}`, but its parent granted to `{subject}`")]
IssuerNotParentSubject {
index: usize,
issuer: String,
subject: String,
},
#[error("chain link {index} adds action `{action}`, which its parent does not confer")]
WidensActions {
index: usize,
action: String,
},
#[error("chain link {index} has scope `{scope}`, its parent `{parent_scope}`")]
WidensScope {
index: usize,
scope: String,
parent_scope: String,
},
#[error("chain link {index} is valid until {until}, beyond its parent's {parent_until}")]
OutlivesParent {
index: usize,
until: DateTime<Utc>,
parent_until: DateTime<Utc>,
},
#[error("chain confers on scope `{granted}`, but `{requested}` was requested")]
ScopeMismatch {
granted: String,
requested: String,
},
#[error("chain does not confer action `{action}`")]
ActionNotGranted {
action: String,
},
#[error("the chain's leaf grants to `{subject}`, but it was presented by `{presenter}`")]
NotThePresenter {
subject: String,
presenter: String,
},
#[error("chain link {index} is not valid at {at}")]
NotValidNow {
index: usize,
at: DateTime<Utc>,
},
#[error("chain link {index} confers no actions")]
NoActions {
index: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedAuthority {
pub subject: String,
pub scope: String,
pub actions: Vec<String>,
pub governing_party: String,
}
pub fn verify_chain(
chain: &[DTGCredential],
governing_party: &str,
requested_scope: &str,
requested_action: &str,
presenter: &str,
at: DateTime<Utc>,
) -> Result<VerifiedAuthority, AuthorityError> {
if chain.is_empty() {
return Err(AuthorityError::EmptyChain);
}
if chain.len() > MAX_CHAIN_DEPTH {
return Err(AuthorityError::TooDeep { found: chain.len() });
}
for (index, link) in chain.iter().enumerate() {
if !matches!(link.type_(), DTGCredentialType::Authority) {
return Err(AuthorityError::NotAuthority {
index,
found: link.type_().to_string(),
});
}
let grant = link
.credential()
.authority()
.ok_or_else(|| AuthorityError::NotAuthority {
index,
found: "AuthorityCredential without an authority grant".to_string(),
})?;
if grant.actions.is_empty() {
return Err(AuthorityError::NoActions { index });
}
let c = link.credential();
if c.valid_from() > at {
return Err(AuthorityError::NotValidNow { index, at });
}
let Some(until) = c.valid_until() else {
return Err(AuthorityError::NoExpiry { index });
};
if until < at {
return Err(AuthorityError::NotValidNow { index, at });
}
}
let leaf = &chain[0];
let leaf_grant = leaf.credential().authority().expect("checked above");
let leaf_subject = leaf.credential().subject();
if leaf_subject != presenter {
return Err(AuthorityError::NotThePresenter {
subject: leaf_subject.to_string(),
presenter: presenter.to_string(),
});
}
for index in 0..chain.len() - 1 {
let link = &chain[index];
let parent = &chain[index + 1];
let grant = link.credential().authority().expect("checked above");
let parent_grant = parent.credential().authority().expect("checked above");
let presented_digest = parent
.digest_multibase()
.map_err(|e| AuthorityError::Digest {
index: index + 1,
reason: e.to_string(),
})?;
match &grant.parent {
Some(named) => {
let matches = crate::digests_match(named, &presented_digest).map_err(|e| {
AuthorityError::Digest {
index,
reason: e.to_string(),
}
})?;
if !matches {
return Err(AuthorityError::BrokenLink {
index,
named: named.clone(),
presented: presented_digest,
});
}
}
None => {
return Err(AuthorityError::BrokenLink {
index,
named: "<none — link claims to be a root>".to_string(),
presented: presented_digest,
});
}
}
if link.credential().issuer() != parent.credential().subject() {
return Err(AuthorityError::IssuerNotParentSubject {
index,
issuer: link.credential().issuer().to_string(),
subject: parent.credential().subject().to_string(),
});
}
if grant.scope != parent_grant.scope {
return Err(AuthorityError::WidensScope {
index,
scope: grant.scope.clone(),
parent_scope: parent_grant.scope.clone(),
});
}
for action in &grant.actions {
if !parent_grant.actions.contains(action) {
return Err(AuthorityError::WidensActions {
index,
action: action.clone(),
});
}
}
if let (Some(until), Some(parent_until)) = (
link.credential().valid_until(),
parent.credential().valid_until(),
) && until > parent_until
{
return Err(AuthorityError::OutlivesParent {
index,
until,
parent_until,
});
}
}
let root = chain.last().expect("non-empty");
let root_grant = root.credential().authority().expect("checked above");
if root.credential().issuer() != governing_party {
return Err(AuthorityError::RootNotGoverning {
root_issuer: root.credential().issuer().to_string(),
expected: governing_party.to_string(),
});
}
if root_grant.parent.is_some() {
return Err(AuthorityError::BrokenLink {
index: chain.len() - 1,
named: root_grant.parent.clone().unwrap_or_default(),
presented: "<nothing — chain ends here>".to_string(),
});
}
if leaf_grant.scope != requested_scope {
return Err(AuthorityError::ScopeMismatch {
granted: leaf_grant.scope.clone(),
requested: requested_scope.to_string(),
});
}
if !leaf_grant.actions.iter().any(|a| a == requested_action) {
return Err(AuthorityError::ActionNotGranted {
action: requested_action.to_string(),
});
}
Ok(VerifiedAuthority {
subject: leaf.credential().subject().to_string(),
scope: leaf_grant.scope.clone(),
actions: leaf_grant.actions.clone(),
governing_party: governing_party.to_string(),
})
}