1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
14
15use acdp_did::web::WebResolver;
16use acdp_primitives::error::AcdpError;
17use acdp_types::body::FullContext;
18use acdp_types::log::{LogCheckpoint, LogInclusion, LogLeaf};
19use acdp_types::receipt::RegistryReceipt;
20
21use crate::log::verify_log_checkpoint_value;
22use crate::witness::{evaluate_witness_quorum, WitnessPolicy, WitnessQuorumReport};
23
24pub const DEFAULT_LOG_MAX_CLOCK_SKEW_SECONDS: u32 = 120;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum LogInclusionPolicy {
34 #[default]
40 Ignore,
41 VerifyIfPresent,
45 Require,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
59#[non_exhaustive]
60pub enum WitnessEnforcement {
61 #[default]
63 Report,
64 RequireQuorum,
68 RequireFreshQuorum,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
90#[non_exhaustive]
91pub struct WitnessQuorumPolicy {
92 pub trusted_witnesses: BTreeSet<String>,
94 pub pinned_documents: BTreeMap<String, serde_json::Value>,
97 pub witness: WitnessPolicy,
100 pub enforcement: WitnessEnforcement,
103}
104
105impl WitnessQuorumPolicy {
106 #[must_use]
108 pub fn new<I, S>(witnesses: I) -> Self
109 where
110 I: IntoIterator<Item = S>,
111 S: Into<String>,
112 {
113 Self {
114 trusted_witnesses: witnesses.into_iter().map(Into::into).collect(),
115 pinned_documents: BTreeMap::new(),
116 witness: WitnessPolicy::default(),
117 enforcement: WitnessEnforcement::Report,
118 }
119 }
120
121 #[must_use]
123 pub fn with_document(
124 mut self,
125 witness_id: impl Into<String>,
126 document: serde_json::Value,
127 ) -> Self {
128 self.pinned_documents.insert(witness_id.into(), document);
129 self
130 }
131
132 #[must_use]
134 pub fn with_min_witnesses(mut self, n: u32) -> Self {
135 self.witness.min_witnesses = n;
136 self
137 }
138
139 #[must_use]
141 pub fn with_witness_policy(mut self, witness: WitnessPolicy) -> Self {
142 self.witness = witness;
143 self
144 }
145
146 #[must_use]
148 pub fn enforce(mut self, enforcement: WitnessEnforcement) -> Self {
149 self.enforcement = enforcement;
150 self
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158#[non_exhaustive]
159pub enum CannotVerifyReason {
160 ReceiptAbsent,
162 ReceiptInvalid,
166 ReceiptNotVerified,
170}
171
172impl CannotVerifyReason {
173 #[must_use]
175 pub fn as_str(self) -> &'static str {
176 match self {
177 Self::ReceiptAbsent => "receipt_absent",
178 Self::ReceiptInvalid => "receipt_invalid",
179 Self::ReceiptNotVerified => "receipt_not_verified",
180 }
181 }
182}
183
184impl std::fmt::Display for CannotVerifyReason {
185 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 f.write_str(self.as_str())
187 }
188}
189
190#[derive(Debug, Clone)]
204#[non_exhaustive]
205pub enum LogInclusionVerdict {
206 Included(Box<LogInclusion>),
209 CannotVerify(CannotVerifyReason),
211 Invalid(AcdpError),
214}
215
216impl LogInclusionVerdict {
217 #[must_use]
219 pub fn is_included(&self) -> bool {
220 matches!(self, Self::Included(_))
221 }
222
223 #[must_use]
225 pub fn inclusion(&self) -> Option<&LogInclusion> {
226 match self {
227 Self::Included(inclusion) => Some(inclusion),
228 _ => None,
229 }
230 }
231
232 #[must_use]
234 pub fn cannot_verify_reason(&self) -> Option<CannotVerifyReason> {
235 match self {
236 Self::CannotVerify(reason) => Some(*reason),
237 _ => None,
238 }
239 }
240
241 #[must_use]
243 pub fn error(&self) -> Option<&AcdpError> {
244 match self {
245 Self::Invalid(e) => Some(e),
246 _ => None,
247 }
248 }
249
250 #[must_use]
255 pub fn as_str(&self) -> &'static str {
256 match self {
257 Self::Included(_) => "included",
258 Self::CannotVerify(_) => "cannot_verify",
259 Self::Invalid(AcdpError::InvalidLogProof(_)) => "invalid_log_proof",
260 Self::Invalid(_) => "error",
261 }
262 }
263}
264
265#[derive(Debug, Clone, Default)]
267pub(crate) struct TransparencyVerdicts {
268 pub(crate) log: Option<LogInclusionVerdict>,
270 pub(crate) witness: Option<Result<WitnessQuorumReport, AcdpError>>,
272}
273
274pub(crate) struct LeafMaterial<'a> {
276 pub(crate) receipt: &'a RegistryReceipt,
277 pub(crate) receipt_value: &'a serde_json::Value,
278}
279
280pub(crate) async fn evaluate(
287 client: &crate::RegistryClient,
288 ctx: &FullContext,
289 material: Result<LeafMaterial<'_>, CannotVerifyReason>,
290 serving_authority: &str,
291 log_policy: LogInclusionPolicy,
292 witness_policy: Option<&WitnessQuorumPolicy>,
293 resolver: &WebResolver,
294) -> Result<TransparencyVerdicts, AcdpError> {
295 let evaluate_log = match log_policy {
296 LogInclusionPolicy::Ignore => witness_policy.is_some(),
297 LogInclusionPolicy::VerifyIfPresent | LogInclusionPolicy::Require => true,
298 };
299 let mut out = TransparencyVerdicts::default();
300 let mut checkpoint: Option<Result<LogCheckpoint, AcdpError>> = None;
302
303 if evaluate_log {
304 out.log = match &ctx.log_inclusion {
305 Some(value) => {
306 let (verdict, cp) = verify_inclusion(
307 client,
308 ctx,
309 value,
310 material,
311 serving_authority,
312 witness_policy.is_some(),
313 resolver,
314 )
315 .await;
316 checkpoint = cp;
317 Some(verdict)
318 }
319 None if log_policy == LogInclusionPolicy::Require => {
320 return Err(AcdpError::InvalidLogProof(
321 "policy requires a transparency-log inclusion proof but the response \
322 carries no log_inclusion (RFC-ACDP-0012 §10)"
323 .into(),
324 ));
325 }
326 None => None,
327 };
328 if log_policy == LogInclusionPolicy::Require {
329 match &out.log {
330 Some(LogInclusionVerdict::Invalid(e)) => return Err(e.clone()),
331 Some(LogInclusionVerdict::CannotVerify(reason)) => {
334 return Err(AcdpError::InvalidReceipt(format!(
335 "policy requires a verified transparency-log inclusion proof, but \
336 it cannot be verified without a verified registry receipt \
337 ({reason}; RFC-ACDP-0012 §9.1 step 1)"
338 )));
339 }
340 _ => {}
341 }
342 }
343 }
344
345 if let Some(policy) = witness_policy {
346 let verdict = match checkpoint {
347 Some(Ok(cp)) => Ok(evaluate_witnesses(ctx, &cp, policy, resolver).await),
348 Some(Err(e)) => Err(AcdpError::InvalidLogProof(format!(
349 "witness quorum not evaluated: the checkpoint did not verify ({e})"
350 ))),
351 None => Err(AcdpError::InvalidLogProof(
352 "witness quorum not evaluated: the response carries no log_inclusion \
353 checkpoint to corroborate (RFC-ACDP-0015 §6.1)"
354 .into(),
355 )),
356 };
357 enforce_witnesses(&verdict, policy)?;
358 out.witness = Some(verdict);
359 }
360 Ok(out)
361}
362
363async fn verify_inclusion(
371 client: &crate::RegistryClient,
372 ctx: &FullContext,
373 value: &serde_json::Value,
374 material: Result<LeafMaterial<'_>, CannotVerifyReason>,
375 serving_authority: &str,
376 need_checkpoint: bool,
377 resolver: &WebResolver,
378) -> (
379 LogInclusionVerdict,
380 Option<Result<LogCheckpoint, AcdpError>>,
381) {
382 if let Err(reason) = &material {
383 if !need_checkpoint {
384 return (LogInclusionVerdict::CannotVerify(*reason), None);
385 }
386 }
387 let checkpoint = verify_checkpoint(client, value, serving_authority, resolver).await;
388 let material = match material {
389 Ok(material) => material,
390 Err(reason) => return (LogInclusionVerdict::CannotVerify(reason), Some(checkpoint)),
391 };
392 let verdict = match &checkpoint {
393 Err(e) => LogInclusionVerdict::Invalid(e.clone()),
394 Ok(_) => match check_leaf(ctx, value, &material) {
395 Ok(inclusion) => LogInclusionVerdict::Included(Box::new(inclusion)),
396 Err(e) => LogInclusionVerdict::Invalid(e),
397 },
398 };
399 (verdict, Some(checkpoint))
400}
401
402async fn verify_checkpoint(
405 client: &crate::RegistryClient,
406 value: &serde_json::Value,
407 serving_authority: &str,
408 resolver: &WebResolver,
409) -> Result<LogCheckpoint, AcdpError> {
410 LogInclusion::from_value(value)?;
412 let checkpoint_value = value.get("log_checkpoint").ok_or_else(|| {
413 AcdpError::InvalidLogProof("log_inclusion has no log_checkpoint member".into())
414 })?;
415 let caps = client.capabilities().await?;
416 verify_log_checkpoint_value(
417 checkpoint_value,
418 serving_authority,
419 &caps.registry_did,
420 chrono::Duration::seconds(i64::from(DEFAULT_LOG_MAX_CLOCK_SKEW_SECONDS)),
421 resolver,
422 )
423 .await
424}
425
426fn check_leaf(
428 ctx: &FullContext,
429 value: &serde_json::Value,
430 material: &LeafMaterial<'_>,
431) -> Result<LogInclusion, AcdpError> {
432 let leaf = LogLeaf::for_publish(
436 &ctx.body,
437 &ctx.body.content_hash,
438 &material.receipt.key_fingerprint,
439 material.receipt_value,
440 )?;
441 let inclusion = LogInclusion::from_value(value)?;
442 inclusion.verify_reconstructed_leaf(&leaf)?;
443 Ok(inclusion)
444}
445
446async fn evaluate_witnesses(
447 ctx: &FullContext,
448 checkpoint: &LogCheckpoint,
449 policy: &WitnessQuorumPolicy,
450 resolver: &WebResolver,
451) -> WitnessQuorumReport {
452 let cosignatures: Vec<serde_json::Value> = match ctx.extensions.get("witness_signatures") {
453 Some(serde_json::Value::Array(items)) => items.clone(),
454 _ => Vec::new(),
455 };
456 let named: HashSet<&str> = cosignatures
459 .iter()
460 .filter_map(|c| c.get("witness_id").and_then(|v| v.as_str()))
461 .collect();
462 let mut docs: HashMap<String, serde_json::Value> = HashMap::new();
463 let mut resolution_failures = Vec::new();
464 for witness in policy
465 .trusted_witnesses
466 .iter()
467 .filter(|w| named.contains(w.as_str()))
468 {
469 match witness_document(witness, policy, resolver).await {
470 Ok(doc) => {
471 docs.insert(witness.clone(), doc);
472 }
473 Err(e) => resolution_failures.push(AcdpError::InvalidWitnessCosignature(format!(
474 "could not obtain the DID document of trusted witness '{witness}': {e}"
475 ))),
476 }
477 }
478 let trusted: HashSet<String> = policy.trusted_witnesses.iter().cloned().collect();
479 let mut report = evaluate_witness_quorum(
480 &cosignatures,
481 &docs,
482 &trusted,
483 checkpoint,
484 &policy.witness,
485 None,
486 );
487 resolution_failures.append(&mut report.failures);
490 report.failures = resolution_failures;
491 report
492}
493
494async fn witness_document(
495 witness: &str,
496 policy: &WitnessQuorumPolicy,
497 resolver: &WebResolver,
498) -> Result<serde_json::Value, AcdpError> {
499 if let Some(doc) = policy.pinned_documents.get(witness) {
500 return Ok(doc.clone());
501 }
502 if witness.starts_with("did:key:") {
503 return Ok(serde_json::to_value(acdp_did::key::did_key_document(
504 witness,
505 )?)?);
506 }
507 Ok(serde_json::to_value(resolver.resolve(witness).await?)?)
508}
509
510fn enforce_witnesses(
511 verdict: &Result<WitnessQuorumReport, AcdpError>,
512 policy: &WitnessQuorumPolicy,
513) -> Result<(), AcdpError> {
514 let (needed, fresh) = match policy.enforcement {
515 WitnessEnforcement::Report => return Ok(()),
516 WitnessEnforcement::RequireQuorum => (policy.witness.min_witnesses, false),
517 WitnessEnforcement::RequireFreshQuorum => (policy.witness.min_witnesses, true),
518 };
519 let report = verdict.as_ref().map_err(Clone::clone)?;
520 let (met, count) = if fresh {
521 (report.meets_fresh_quorum, report.fresh_witnessed_count)
522 } else {
523 (report.meets_quorum, report.witnessed_count)
524 };
525 if met {
526 return Ok(());
527 }
528 Err(AcdpError::InvalidWitnessCosignature(format!(
529 "witness quorum not met: {count} of {needed} required distinct trusted witnesses{} \
530 cosigned the checkpoint (RFC-ACDP-0015 §8.1){}",
531 if fresh {
532 " (within max_age_seconds)"
533 } else {
534 ""
535 },
536 match report.failures.first() {
537 Some(e) => format!("; first failure: {e}"),
538 None => String::new(),
539 }
540 )))
541}