use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use acdp_did::web::WebResolver;
use acdp_primitives::error::AcdpError;
use acdp_types::body::FullContext;
use acdp_types::log::{LogCheckpoint, LogInclusion, LogLeaf};
use acdp_types::receipt::RegistryReceipt;
use crate::log::verify_log_checkpoint_value;
use crate::witness::{evaluate_witness_quorum, WitnessPolicy, WitnessQuorumReport};
pub const DEFAULT_LOG_MAX_CLOCK_SKEW_SECONDS: u32 = 120;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LogInclusionPolicy {
#[default]
Ignore,
VerifyIfPresent,
Require,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum WitnessEnforcement {
#[default]
Report,
RequireQuorum,
RequireFreshQuorum,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct WitnessQuorumPolicy {
pub trusted_witnesses: BTreeSet<String>,
pub pinned_documents: BTreeMap<String, serde_json::Value>,
pub witness: WitnessPolicy,
pub enforcement: WitnessEnforcement,
}
impl WitnessQuorumPolicy {
#[must_use]
pub fn new<I, S>(witnesses: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
trusted_witnesses: witnesses.into_iter().map(Into::into).collect(),
pinned_documents: BTreeMap::new(),
witness: WitnessPolicy::default(),
enforcement: WitnessEnforcement::Report,
}
}
#[must_use]
pub fn with_document(
mut self,
witness_id: impl Into<String>,
document: serde_json::Value,
) -> Self {
self.pinned_documents.insert(witness_id.into(), document);
self
}
#[must_use]
pub fn with_min_witnesses(mut self, n: u32) -> Self {
self.witness.min_witnesses = n;
self
}
#[must_use]
pub fn with_witness_policy(mut self, witness: WitnessPolicy) -> Self {
self.witness = witness;
self
}
#[must_use]
pub fn enforce(mut self, enforcement: WitnessEnforcement) -> Self {
self.enforcement = enforcement;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CannotVerifyReason {
ReceiptAbsent,
ReceiptInvalid,
ReceiptNotVerified,
}
impl CannotVerifyReason {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::ReceiptAbsent => "receipt_absent",
Self::ReceiptInvalid => "receipt_invalid",
Self::ReceiptNotVerified => "receipt_not_verified",
}
}
}
impl std::fmt::Display for CannotVerifyReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum LogInclusionVerdict {
Included(Box<LogInclusion>),
CannotVerify(CannotVerifyReason),
Invalid(AcdpError),
}
impl LogInclusionVerdict {
#[must_use]
pub fn is_included(&self) -> bool {
matches!(self, Self::Included(_))
}
#[must_use]
pub fn inclusion(&self) -> Option<&LogInclusion> {
match self {
Self::Included(inclusion) => Some(inclusion),
_ => None,
}
}
#[must_use]
pub fn cannot_verify_reason(&self) -> Option<CannotVerifyReason> {
match self {
Self::CannotVerify(reason) => Some(*reason),
_ => None,
}
}
#[must_use]
pub fn error(&self) -> Option<&AcdpError> {
match self {
Self::Invalid(e) => Some(e),
_ => None,
}
}
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Included(_) => "included",
Self::CannotVerify(_) => "cannot_verify",
Self::Invalid(AcdpError::InvalidLogProof(_)) => "invalid_log_proof",
Self::Invalid(_) => "error",
}
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct TransparencyVerdicts {
pub(crate) log: Option<LogInclusionVerdict>,
pub(crate) witness: Option<Result<WitnessQuorumReport, AcdpError>>,
}
pub(crate) struct LeafMaterial<'a> {
pub(crate) receipt: &'a RegistryReceipt,
pub(crate) receipt_value: &'a serde_json::Value,
}
pub(crate) async fn evaluate(
client: &crate::RegistryClient,
ctx: &FullContext,
material: Result<LeafMaterial<'_>, CannotVerifyReason>,
serving_authority: &str,
log_policy: LogInclusionPolicy,
witness_policy: Option<&WitnessQuorumPolicy>,
resolver: &WebResolver,
) -> Result<TransparencyVerdicts, AcdpError> {
let evaluate_log = match log_policy {
LogInclusionPolicy::Ignore => witness_policy.is_some(),
LogInclusionPolicy::VerifyIfPresent | LogInclusionPolicy::Require => true,
};
let mut out = TransparencyVerdicts::default();
let mut checkpoint: Option<Result<LogCheckpoint, AcdpError>> = None;
if evaluate_log {
out.log = match &ctx.log_inclusion {
Some(value) => {
let (verdict, cp) = verify_inclusion(
client,
ctx,
value,
material,
serving_authority,
witness_policy.is_some(),
resolver,
)
.await;
checkpoint = cp;
Some(verdict)
}
None if log_policy == LogInclusionPolicy::Require => {
return Err(AcdpError::InvalidLogProof(
"policy requires a transparency-log inclusion proof but the response \
carries no log_inclusion (RFC-ACDP-0012 §10)"
.into(),
));
}
None => None,
};
if log_policy == LogInclusionPolicy::Require {
match &out.log {
Some(LogInclusionVerdict::Invalid(e)) => return Err(e.clone()),
Some(LogInclusionVerdict::CannotVerify(reason)) => {
return Err(AcdpError::InvalidReceipt(format!(
"policy requires a verified transparency-log inclusion proof, but \
it cannot be verified without a verified registry receipt \
({reason}; RFC-ACDP-0012 §9.1 step 1)"
)));
}
_ => {}
}
}
}
if let Some(policy) = witness_policy {
let verdict = match checkpoint {
Some(Ok(cp)) => Ok(evaluate_witnesses(ctx, &cp, policy, resolver).await),
Some(Err(e)) => Err(AcdpError::InvalidLogProof(format!(
"witness quorum not evaluated: the checkpoint did not verify ({e})"
))),
None => Err(AcdpError::InvalidLogProof(
"witness quorum not evaluated: the response carries no log_inclusion \
checkpoint to corroborate (RFC-ACDP-0015 §6.1)"
.into(),
)),
};
enforce_witnesses(&verdict, policy)?;
out.witness = Some(verdict);
}
Ok(out)
}
async fn verify_inclusion(
client: &crate::RegistryClient,
ctx: &FullContext,
value: &serde_json::Value,
material: Result<LeafMaterial<'_>, CannotVerifyReason>,
serving_authority: &str,
need_checkpoint: bool,
resolver: &WebResolver,
) -> (
LogInclusionVerdict,
Option<Result<LogCheckpoint, AcdpError>>,
) {
if let Err(reason) = &material {
if !need_checkpoint {
return (LogInclusionVerdict::CannotVerify(*reason), None);
}
}
let checkpoint = verify_checkpoint(client, value, serving_authority, resolver).await;
let material = match material {
Ok(material) => material,
Err(reason) => return (LogInclusionVerdict::CannotVerify(reason), Some(checkpoint)),
};
let verdict = match &checkpoint {
Err(e) => LogInclusionVerdict::Invalid(e.clone()),
Ok(_) => match check_leaf(ctx, value, &material) {
Ok(inclusion) => LogInclusionVerdict::Included(Box::new(inclusion)),
Err(e) => LogInclusionVerdict::Invalid(e),
},
};
(verdict, Some(checkpoint))
}
async fn verify_checkpoint(
client: &crate::RegistryClient,
value: &serde_json::Value,
serving_authority: &str,
resolver: &WebResolver,
) -> Result<LogCheckpoint, AcdpError> {
LogInclusion::from_value(value)?;
let checkpoint_value = value.get("log_checkpoint").ok_or_else(|| {
AcdpError::InvalidLogProof("log_inclusion has no log_checkpoint member".into())
})?;
let caps = client.capabilities().await?;
verify_log_checkpoint_value(
checkpoint_value,
serving_authority,
&caps.registry_did,
chrono::Duration::seconds(i64::from(DEFAULT_LOG_MAX_CLOCK_SKEW_SECONDS)),
resolver,
)
.await
}
fn check_leaf(
ctx: &FullContext,
value: &serde_json::Value,
material: &LeafMaterial<'_>,
) -> Result<LogInclusion, AcdpError> {
let leaf = LogLeaf::for_publish(
&ctx.body,
&ctx.body.content_hash,
&material.receipt.key_fingerprint,
material.receipt_value,
)?;
let inclusion = LogInclusion::from_value(value)?;
inclusion.verify_reconstructed_leaf(&leaf)?;
Ok(inclusion)
}
async fn evaluate_witnesses(
ctx: &FullContext,
checkpoint: &LogCheckpoint,
policy: &WitnessQuorumPolicy,
resolver: &WebResolver,
) -> WitnessQuorumReport {
let cosignatures: Vec<serde_json::Value> = match ctx.extensions.get("witness_signatures") {
Some(serde_json::Value::Array(items)) => items.clone(),
_ => Vec::new(),
};
let named: HashSet<&str> = cosignatures
.iter()
.filter_map(|c| c.get("witness_id").and_then(|v| v.as_str()))
.collect();
let mut docs: HashMap<String, serde_json::Value> = HashMap::new();
let mut resolution_failures = Vec::new();
for witness in policy
.trusted_witnesses
.iter()
.filter(|w| named.contains(w.as_str()))
{
match witness_document(witness, policy, resolver).await {
Ok(doc) => {
docs.insert(witness.clone(), doc);
}
Err(e) => resolution_failures.push(AcdpError::InvalidWitnessCosignature(format!(
"could not obtain the DID document of trusted witness '{witness}': {e}"
))),
}
}
let trusted: HashSet<String> = policy.trusted_witnesses.iter().cloned().collect();
let mut report = evaluate_witness_quorum(
&cosignatures,
&docs,
&trusted,
checkpoint,
&policy.witness,
None,
);
resolution_failures.append(&mut report.failures);
report.failures = resolution_failures;
report
}
async fn witness_document(
witness: &str,
policy: &WitnessQuorumPolicy,
resolver: &WebResolver,
) -> Result<serde_json::Value, AcdpError> {
if let Some(doc) = policy.pinned_documents.get(witness) {
return Ok(doc.clone());
}
if witness.starts_with("did:key:") {
return Ok(serde_json::to_value(acdp_did::key::did_key_document(
witness,
)?)?);
}
Ok(serde_json::to_value(resolver.resolve(witness).await?)?)
}
fn enforce_witnesses(
verdict: &Result<WitnessQuorumReport, AcdpError>,
policy: &WitnessQuorumPolicy,
) -> Result<(), AcdpError> {
let (needed, fresh) = match policy.enforcement {
WitnessEnforcement::Report => return Ok(()),
WitnessEnforcement::RequireQuorum => (policy.witness.min_witnesses, false),
WitnessEnforcement::RequireFreshQuorum => (policy.witness.min_witnesses, true),
};
let report = verdict.as_ref().map_err(Clone::clone)?;
let (met, count) = if fresh {
(report.meets_fresh_quorum, report.fresh_witnessed_count)
} else {
(report.meets_quorum, report.witnessed_count)
};
if met {
return Ok(());
}
Err(AcdpError::InvalidWitnessCosignature(format!(
"witness quorum not met: {count} of {needed} required distinct trusted witnesses{} \
cosigned the checkpoint (RFC-ACDP-0015 §8.1){}",
if fresh {
" (within max_age_seconds)"
} else {
""
},
match report.failures.first() {
Some(e) => format!("; first failure: {e}"),
None => String::new(),
}
)))
}