use auths_verifier::IdentityBundle;
use chrono::{DateTime, Utc};
use crate::keri::parse_trailers;
#[cfg(feature = "backend-git")]
use crate::keri::KelResolverChain;
#[cfg(feature = "backend-git")]
use crate::ports::RegistryBackend;
#[cfg(feature = "backend-git")]
use auths_crypto::CryptoProvider;
#[cfg(feature = "backend-git")]
use auths_verifier::{CommitVerdict, verify_commit_against_kel};
#[derive(Debug, thiserror::Error)]
pub enum CommitTrustError {
#[error(
"commit carries no Auths-Id/Auths-Device trailer — it was not signed by `auths` \
(or predates KEL-native signing)"
)]
MissingTrailers,
#[error("identity bundle is not a usable trust anchor: {0}")]
BundleInvalid(String),
#[error("{role} KEL for {did} could not be resolved: {reason}")]
KelUnresolved {
role: &'static str,
did: String,
reason: String,
},
#[error("org policy evaluation failed: {0}")]
Policy(#[from] crate::domains::org::error::OrgError),
}
pub fn commit_signer_trailers(raw_commit: &str) -> Option<(String, String)> {
let message = raw_commit
.split_once("\n\n")
.map(|(_, m)| m)
.unwrap_or(raw_commit);
let trailers = parse_trailers(message);
let find = |key: &str| {
trailers
.iter()
.rev()
.find(|(k, _)| k.eq_ignore_ascii_case(key))
.map(|(_, v)| v.trim().to_string())
};
Some((find("Auths-Id")?, find("Auths-Device")?))
}
pub fn trusted_root_from_bundle(
bundle: &IdentityBundle,
now: DateTime<Utc>,
) -> Result<String, CommitTrustError> {
bundle
.check_freshness(now)
.map_err(|e| CommitTrustError::BundleInvalid(e.to_string()))?;
Ok(bundle.identity_did.to_string())
}
#[cfg(feature = "backend-git")]
pub async fn verify_commit_local(
registry: &dyn RegistryBackend,
pinned_roots: &[String],
raw_commit: &[u8],
provider: &dyn CryptoProvider,
) -> Result<CommitVerdict, CommitTrustError> {
let commit_str = String::from_utf8_lossy(raw_commit);
let (root_did, device_did) =
commit_signer_trailers(&commit_str).ok_or(CommitTrustError::MissingTrailers)?;
let chain = KelResolverChain::local(registry);
let device_kel =
chain
.resolve_kel(&device_did)
.map_err(|e| CommitTrustError::KelUnresolved {
role: "device",
did: device_did.clone(),
reason: e.to_string(),
})?;
let root_kel = chain
.resolve_kel(&root_did)
.map_err(|e| CommitTrustError::KelUnresolved {
role: "root",
did: root_did.clone(),
reason: e.to_string(),
})?;
Ok(verify_commit_against_kel(raw_commit, &device_kel, &root_kel, pinned_roots, provider).await)
}
#[derive(Debug, Clone)]
pub enum PolicyOutcome {
CryptoFailed,
NoPolicy,
Evaluated(auths_id::policy::Decision),
}
#[cfg(feature = "backend-git")]
#[derive(Debug, Clone)]
pub struct CommitDecision {
pub verdict: CommitVerdict,
pub policy: PolicyOutcome,
}
#[cfg(feature = "backend-git")]
impl CommitDecision {
pub fn is_authorized(&self) -> bool {
if !self.verdict.is_valid() {
return false;
}
match &self.policy {
PolicyOutcome::CryptoFailed => false,
PolicyOutcome::NoPolicy => true,
PolicyOutcome::Evaluated(decision) => decision.is_allowed(),
}
}
}
fn signer_type_of(
ctx: &crate::context::AuthsContext,
root_prefix: &auths_id::keri::types::Prefix,
signer_prefix: &auths_id::keri::types::Prefix,
) -> Result<auths_id::policy::SignerType, CommitTrustError> {
use auths_id::keri::delegation::{DelegatedRole, list_delegated_devices};
let delegated = list_delegated_devices(ctx.registry.as_ref(), root_prefix).map_err(|e| {
CommitTrustError::Policy(crate::domains::org::error::OrgError::Delegation(e))
})?;
let is_agent = delegated.iter().any(|d| {
d.device_prefix.as_str() == signer_prefix.as_str() && d.role == DelegatedRole::Agent
});
Ok(if is_agent {
auths_id::policy::SignerType::Agent
} else {
auths_id::policy::SignerType::Human
})
}
fn commit_policy_context(
ctx: &crate::context::AuthsContext,
root_prefix: &auths_id::keri::types::Prefix,
signer_prefix: &auths_id::keri::types::Prefix,
now: DateTime<Utc>,
) -> Result<auths_id::policy::EvalContext, CommitTrustError> {
use auths_id::policy::context_from_delegated_member;
use auths_verifier::core::Role;
let root_did = format!("did:keri:{}", root_prefix.as_str());
let signer_did = format!("did:keri:{}", signer_prefix.as_str());
let authority =
crate::domains::org::delegation::resolve_member_authority(ctx, root_prefix, signer_prefix)?;
let (role, caps, expires) = match &authority {
Some(a) => (
a.role.as_ref().map(Role::as_str),
a.capabilities.clone(),
a.expires_at,
),
None => (None, Vec::new(), None),
};
let expires_dt = expires.and_then(|s| DateTime::from_timestamp(s, 0));
let mut eval_ctx =
context_from_delegated_member(&root_did, &signer_did, false, role, &caps, expires_dt, now)
.map_err(|e| {
CommitTrustError::Policy(crate::domains::org::error::OrgError::InvalidDid(
e.to_string(),
))
})?;
eval_ctx = eval_ctx.signer_type(signer_type_of(ctx, root_prefix, signer_prefix)?);
Ok(eval_ctx)
}
pub fn evaluate_commit_policy(
ctx: &crate::context::AuthsContext,
root_did: &str,
signer_did: &str,
now: DateTime<Utc>,
) -> Result<PolicyOutcome, CommitTrustError> {
use auths_id::keri::types::Prefix;
let root_prefix = Prefix::new_unchecked(
root_did
.strip_prefix("did:keri:")
.unwrap_or(root_did)
.to_string(),
);
let signer_prefix = Prefix::new_unchecked(
signer_did
.strip_prefix("did:keri:")
.unwrap_or(signer_did)
.to_string(),
);
let Some(policy) = crate::domains::org::policy::load_org_policy(ctx, &root_prefix)? else {
return Ok(PolicyOutcome::NoPolicy);
};
let eval_ctx = commit_policy_context(ctx, &root_prefix, &signer_prefix, now)?;
let decision = crate::domains::org::policy::evaluate_with_org_policy(&policy, &eval_ctx);
crate::audit::emit_policy_decision(ctx, "commit", signer_did, &decision);
Ok(PolicyOutcome::Evaluated(decision))
}
#[cfg(feature = "backend-git")]
pub async fn verify_commit_with_policy(
ctx: &crate::context::AuthsContext,
pinned_roots: &[String],
raw_commit: &[u8],
provider: &dyn CryptoProvider,
now: DateTime<Utc>,
) -> Result<CommitDecision, CommitTrustError> {
let verdict =
verify_commit_local(ctx.registry.as_ref(), pinned_roots, raw_commit, provider).await?;
let policy = match &verdict {
CommitVerdict::Valid {
signer_did,
root_did,
..
} => evaluate_commit_policy(ctx, root_did, signer_did, now)?,
_ => PolicyOutcome::CryptoFailed,
};
Ok(CommitDecision { verdict, policy })
}
#[cfg(test)]
mod tests {
use super::*;
const ROOT: &str = "did:keri:Eroot00000000000000000000000000000000000000";
const DEVICE: &str = "did:key:z6MkDevice000000000000000000000000000000000";
fn commit_with_trailers(root: &str, device: &str) -> String {
format!(
"tree abc\nauthor T <t@e.com> 0 +0000\n\nsubject\n\nAuths-Id: {root}\nAuths-Device: {device}\n"
)
}
#[test]
fn extracts_both_trailers() {
let commit = commit_with_trailers(ROOT, DEVICE);
assert_eq!(
commit_signer_trailers(&commit),
Some((ROOT.to_string(), DEVICE.to_string()))
);
}
#[test]
fn missing_device_trailer_yields_none() {
let commit = "tree abc\n\nsubject\n\nAuths-Id: did:keri:Eroot\n";
assert!(commit_signer_trailers(commit).is_none());
}
#[test]
fn no_trailers_yields_none() {
assert!(commit_signer_trailers("tree abc\n\njust a message\n").is_none());
}
#[test]
fn last_trailer_wins_on_duplicates() {
let commit = format!(
"tree abc\n\nsubject\n\nAuths-Id: did:keri:Eold\nAuths-Device: {DEVICE}\nAuths-Id: {ROOT}\n"
);
assert_eq!(
commit_signer_trailers(&commit),
Some((ROOT.to_string(), DEVICE.to_string()))
);
}
#[allow(clippy::disallowed_methods)] fn test_bundle(did: &str, ts: DateTime<Utc>, ttl: u64) -> IdentityBundle {
IdentityBundle {
identity_did: auths_verifier::IdentityDID::new_unchecked(did.to_string()),
public_key_hex: auths_verifier::PublicKeyHex::new_unchecked("00".to_string()),
curve: auths_crypto::CurveType::P256,
attestation_chain: Vec::new(),
kel: Vec::new(),
bundle_timestamp: ts,
max_valid_for_secs: ttl,
}
}
fn fixed_time() -> DateTime<Utc> {
DateTime::<Utc>::from_timestamp(1_700_000_000, 0).expect("valid timestamp")
}
#[test]
fn fresh_bundle_yields_its_root_did() {
let t = fixed_time();
let bundle = test_bundle(ROOT, t, 3600);
let now = t + chrono::Duration::seconds(100);
assert_eq!(trusted_root_from_bundle(&bundle, now).expect("fresh"), ROOT);
}
#[test]
fn stale_bundle_fails_closed() {
let t = fixed_time();
let bundle = test_bundle(ROOT, t, 3600);
let now = t + chrono::Duration::seconds(7200);
assert!(matches!(
trusted_root_from_bundle(&bundle, now),
Err(CommitTrustError::BundleInvalid(_))
));
}
}