use super::data_ref::{fetch_and_verify_data_ref, DataRefFetcher};
use super::registry::RegistryClient;
use crate::crypto::verify::Verifier;
use crate::did::WebResolver;
use crate::error::AcdpError;
use crate::types::{body::FullContext, primitives::CtxId};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerificationPolicy {
pub validate_body_schema: bool,
pub allow_unknown_status: bool,
pub receipts: ReceiptPolicy,
pub historical_keys: HistoricalKeyPolicy,
}
impl Default for VerificationPolicy {
fn default() -> Self {
Self {
validate_body_schema: true,
allow_unknown_status: true,
receipts: ReceiptPolicy::VerifyIfPresent,
historical_keys: HistoricalKeyPolicy::AcceptWithReceipt,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ReceiptPolicy {
Ignore,
#[default]
VerifyIfPresent,
Require,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HistoricalKeyPolicy {
Reject,
#[default]
AcceptWithReceipt,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyAuthorization {
CurrentlyAuthorized,
HistoricallyAuthorized,
}
impl VerificationPolicy {
pub fn strict_v0_1_0() -> Self {
Self {
validate_body_schema: true,
allow_unknown_status: true,
receipts: ReceiptPolicy::Ignore,
historical_keys: HistoricalKeyPolicy::Reject,
}
}
}
#[derive(Debug)]
pub struct VerifiedContext {
pub inner: FullContext,
pub key_status: KeyAuthorization,
pub verified_receipt: Option<crate::types::receipt::RegistryReceipt>,
}
impl VerifiedContext {
pub async fn fetch(
client: &RegistryClient,
resolver: &WebResolver,
ctx_id: &CtxId,
) -> Result<Self, AcdpError> {
Self::fetch_with_policy(client, resolver, ctx_id, &VerificationPolicy::default()).await
}
pub async fn fetch_with_policy(
client: &RegistryClient,
resolver: &WebResolver,
ctx_id: &CtxId,
policy: &VerificationPolicy,
) -> Result<Self, AcdpError> {
let ctx = client.retrieve(ctx_id).await?;
if policy.validate_body_schema {
crate::validation::validate_body(&ctx.body)?;
}
let verifier = Verifier::new(resolver);
verifier.verify_body_hash(&ctx.body)?;
let serving_authority = client
.authority()
.unwrap_or_else(|| ctx_id.authority().to_string());
let verified_receipt = match (policy.receipts, &ctx.registry_receipt) {
(ReceiptPolicy::Ignore, _) | (ReceiptPolicy::VerifyIfPresent, None) => None,
(ReceiptPolicy::Require, None) => {
return Err(AcdpError::InvalidReceipt(
"policy requires a registry receipt but the response carries none \
(registry without the acdp-registry-receipts profile, or a \
pre-receipts context)"
.into(),
));
}
(_, Some(value)) => {
let fingerprint = crate::crypto::fingerprint::fingerprint_for_key_id(
&ctx.body.signature.key_id,
&ctx.body.signature.algorithm,
resolver,
)
.await?;
Some(
super::receipt::verify_receipt_value(
value,
ctx_id,
&ctx.body,
&ctx.body.content_hash,
&fingerprint,
&serving_authority,
resolver,
)
.await?,
)
}
};
let key_status = match verifier.verify_body_signature(&ctx.body).await {
Ok(()) => KeyAuthorization::CurrentlyAuthorized,
Err(AcdpError::KeyNotAuthorized(_))
if policy.historical_keys == HistoricalKeyPolicy::AcceptWithReceipt
&& verified_receipt.is_some() =>
{
crate::crypto::verify::verify_body_signature_historical(&ctx.body, resolver)
.await?;
KeyAuthorization::HistoricallyAuthorized
}
Err(e) => return Err(e),
};
if !policy.allow_unknown_status {
if let Some(other) = ctx.registry_state.status.as_other() {
return Err(AcdpError::SchemaViolation(format!(
"policy.allow_unknown_status=false; registry returned '{other}'"
)));
}
}
Ok(Self {
inner: ctx,
key_status,
verified_receipt,
})
}
pub async fn fetch_report(
client: &RegistryClient,
resolver: &WebResolver,
ctx_id: &CtxId,
policy: &VerificationPolicy,
) -> Result<(Self, VerificationReport), AcdpError> {
Self::fetch_report_inner::<NoFetcher>(client, resolver, ctx_id, policy, None).await
}
pub async fn fetch_report_diagnose(
client: &RegistryClient,
resolver: &WebResolver,
ctx_id: &CtxId,
policy: &VerificationPolicy,
) -> Result<(Option<Self>, VerificationReport), AcdpError> {
let ctx = client.retrieve(ctx_id).await?;
let mut report = VerificationReport {
body_hash_ok: false,
signature_ok: false,
schema_ok: false,
data_ref_embedded: Vec::with_capacity(ctx.body.data_refs.len()),
data_ref_external: Vec::with_capacity(ctx.body.data_refs.len()),
};
if policy.validate_body_schema {
match crate::validation::validate_body_structural(&ctx.body) {
Ok(()) => report.schema_ok = true,
Err(_) => { }
}
} else {
report.schema_ok = true;
}
for dr in &ctx.body.data_refs {
if let (Some(emb), Some(_)) = (&dr.embedded, &dr.content_hash) {
let outcome = crate::validation::verify_embedded_hash(dr)
.and_then(|()| crate::validation::embedded_decoded_bytes(emb).map(|b| b.len()));
report.data_ref_embedded.push(outcome);
} else {
report.data_ref_embedded.push(Ok(0));
}
}
let verifier = Verifier::new(resolver);
report.body_hash_ok = verifier.verify_body_hash(&ctx.body).is_ok();
report.signature_ok = verifier.verify_body_signature(&ctx.body).await.is_ok();
for _ in &ctx.body.data_refs {
report.data_ref_external.push(None);
}
let all_top_level_pass = report.schema_ok && report.body_hash_ok && report.signature_ok;
let verified = if all_top_level_pass {
Some(Self {
inner: ctx,
key_status: KeyAuthorization::CurrentlyAuthorized,
verified_receipt: None,
})
} else {
None
};
Ok((verified, report))
}
pub async fn fetch_report_with_fetcher<F: DataRefFetcher>(
client: &RegistryClient,
resolver: &WebResolver,
ctx_id: &CtxId,
policy: &VerificationPolicy,
fetcher: &F,
) -> Result<(Self, VerificationReport), AcdpError> {
Self::fetch_report_inner(client, resolver, ctx_id, policy, Some(fetcher)).await
}
async fn fetch_report_inner<F: DataRefFetcher>(
client: &RegistryClient,
resolver: &WebResolver,
ctx_id: &CtxId,
policy: &VerificationPolicy,
fetcher: Option<&F>,
) -> Result<(Self, VerificationReport), AcdpError> {
let ctx = client.retrieve(ctx_id).await?;
let mut report = VerificationReport {
body_hash_ok: false,
signature_ok: false,
schema_ok: false,
data_ref_embedded: Vec::with_capacity(ctx.body.data_refs.len()),
data_ref_external: Vec::with_capacity(ctx.body.data_refs.len()),
};
if policy.validate_body_schema {
crate::validation::validate_body_structural(&ctx.body)?;
}
report.schema_ok = true;
for dr in &ctx.body.data_refs {
if let (Some(emb), Some(_)) = (&dr.embedded, &dr.content_hash) {
let outcome = crate::validation::verify_embedded_hash(dr)
.and_then(|()| crate::validation::embedded_decoded_bytes(emb).map(|b| b.len()));
report.data_ref_embedded.push(outcome);
} else {
report.data_ref_embedded.push(Ok(0));
}
}
Verifier::new(resolver)
.verify_body_signed(&ctx.body)
.await?;
report.body_hash_ok = true;
report.signature_ok = true;
if !policy.allow_unknown_status {
if let Some(other) = ctx.registry_state.status.as_other() {
return Err(AcdpError::SchemaViolation(format!(
"policy.allow_unknown_status=false; registry returned '{other}'"
)));
}
}
for dr in &ctx.body.data_refs {
let slot: Option<Result<usize, AcdpError>> = match (fetcher, &dr.location) {
(Some(f), Some(_)) => Some(fetch_and_verify_data_ref(dr, f).await.map(|b| b.len())),
_ => None,
};
report.data_ref_external.push(slot);
}
Ok((
Self {
inner: ctx,
key_status: KeyAuthorization::CurrentlyAuthorized,
verified_receipt: None,
},
report,
))
}
pub fn body(&self) -> &crate::types::body::Body {
&self.inner.body
}
pub fn registry_state(&self) -> &crate::types::body::RegistryState {
&self.inner.registry_state
}
pub fn receipt(&self) -> Option<&serde_json::Value> {
self.inner.registry_receipt.as_ref()
}
pub async fn verify_receipt(
&self,
resolver: &WebResolver,
) -> Result<Option<crate::types::receipt::RegistryReceipt>, AcdpError> {
let Some(value) = &self.inner.registry_receipt else {
return Ok(None);
};
let body_val = serde_json::to_value(&self.inner.body)?;
let recomputed = crate::crypto::compute_content_hash(&body_val)?;
if recomputed != self.inner.body.content_hash {
return Err(AcdpError::HashMismatch {
stored: self.inner.body.content_hash.clone(),
recomputed,
});
}
let fingerprint = crate::crypto::fingerprint::fingerprint_for_key_id(
&self.inner.body.signature.key_id,
&self.inner.body.signature.algorithm,
resolver,
)
.await?;
let receipt = super::receipt::verify_receipt_value(
value,
&self.inner.body.ctx_id,
&self.inner.body,
&self.inner.body.content_hash,
&fingerprint,
self.inner.body.ctx_id.authority(),
resolver,
)
.await?;
Ok(Some(receipt))
}
}
#[derive(Debug)]
pub struct VerificationReport {
pub body_hash_ok: bool,
pub signature_ok: bool,
pub schema_ok: bool,
pub data_ref_embedded: Vec<Result<usize, AcdpError>>,
pub data_ref_external: Vec<Option<Result<usize, AcdpError>>>,
}
struct NoFetcher;
impl DataRefFetcher for NoFetcher {
async fn fetch(
&self,
_location: &crate::types::data_ref::Location,
) -> Result<Vec<u8>, AcdpError> {
Err(AcdpError::NotImplemented(
"NoFetcher should never be called — this is a fetch_report sentinel".into(),
))
}
}
#[cfg(test)]
mod tests {
use super::{HistoricalKeyPolicy, ReceiptPolicy, VerificationPolicy};
#[test]
fn strict_v0_1_0_preserves_v0_1_0_semantics() {
let strict = VerificationPolicy::strict_v0_1_0();
assert!(strict.validate_body_schema);
assert!(strict.allow_unknown_status);
assert_eq!(strict.receipts, ReceiptPolicy::Ignore);
assert_eq!(strict.historical_keys, HistoricalKeyPolicy::Reject);
assert_ne!(
strict,
VerificationPolicy::default(),
"the 0.2 default is receipt-aware; the v0.1.0 profile is not"
);
}
}