use super::data_ref::{fetch_and_verify_data_ref, DataRefFetcher};
use super::registry::RegistryClient;
use acdp_did::WebResolver;
use acdp_primitives::error::AcdpError;
use acdp_types::{body::FullContext, primitives::CtxId};
use acdp_verify::Verifier;
#[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,
pub lineage_head: LineageHeadPolicy,
pub revocations: RevocationPolicy,
}
impl Default for VerificationPolicy {
fn default() -> Self {
Self {
validate_body_schema: true,
allow_unknown_status: true,
receipts: ReceiptPolicy::VerifyIfPresent,
historical_keys: HistoricalKeyPolicy::AcceptWithReceipt,
lineage_head: LineageHeadPolicy::default(),
revocations: RevocationPolicy::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RevocationPolicy {
pub known: Vec<acdp_types::revocation::KeyRevocation>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ReceiptPolicy {
Ignore,
#[default]
VerifyIfPresent,
Require,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LineageHeadPolicy {
pub receipts: ReceiptPolicy,
pub max_clock_skew_seconds: u32,
pub max_age_seconds: Option<u32>,
}
impl Default for LineageHeadPolicy {
fn default() -> Self {
Self {
receipts: ReceiptPolicy::VerifyIfPresent,
max_clock_skew_seconds: 120,
max_age_seconds: Some(300),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HistoricalKeyPolicy {
Reject,
#[default]
AcceptWithReceipt,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyAuthorization {
CurrentlyAuthorized,
HistoricallyAuthorized,
HistoricallyAuthorizedPreCompromise,
}
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,
lineage_head: LineageHeadPolicy {
receipts: ReceiptPolicy::Ignore,
..LineageHeadPolicy::default()
},
revocations: RevocationPolicy::default(),
}
}
}
#[derive(Debug)]
pub struct VerifiedContext {
inner: FullContext,
key_status: KeyAuthorization,
verified_receipt: Option<acdp_types::receipt::RegistryReceipt>,
verified_head_receipt: Option<acdp_types::receipt::LineageHeadReceipt>,
head_receipt_stale: Option<bool>,
}
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?;
let (key_status, verified_receipt) =
Self::verify_retrieved(client, resolver, &ctx, ctx_id, policy).await?;
Ok(Self {
inner: ctx,
key_status,
verified_receipt,
verified_head_receipt: None,
head_receipt_stale: None,
})
}
pub async fn fetch_current(
client: &RegistryClient,
resolver: &WebResolver,
lineage_id: &acdp_types::primitives::LineageId,
) -> Result<Self, AcdpError> {
Self::fetch_current_with_policy(
client,
resolver,
lineage_id,
&VerificationPolicy::default(),
)
.await
}
pub async fn fetch_current_with_policy(
client: &RegistryClient,
resolver: &WebResolver,
lineage_id: &acdp_types::primitives::LineageId,
policy: &VerificationPolicy,
) -> Result<Self, AcdpError> {
let ctx = client.current(lineage_id).await?;
let served_ctx_id = ctx.body.ctx_id.clone();
let (key_status, verified_receipt) =
Self::verify_retrieved(client, resolver, &ctx, &served_ctx_id, policy).await?;
let (verified_head_receipt, head_receipt_stale) =
match (policy.lineage_head.receipts, &ctx.lineage_head_receipt) {
(ReceiptPolicy::Ignore, _) | (ReceiptPolicy::VerifyIfPresent, None) => (None, None),
(ReceiptPolicy::Require, None) => {
return Err(AcdpError::InvalidReceipt(
"policy requires a lineage-head receipt but the /current response \
carries none (registry without the acdp-registry-head-receipts \
profile?)"
.into(),
));
}
(_, Some(value)) => {
let serving_authority = client
.authority()
.unwrap_or_else(|| served_ctx_id.authority().to_string());
let caps = client.capabilities().await?;
let receipt = super::receipt::verify_lineage_head_receipt_value(
value,
lineage_id,
&served_ctx_id,
ctx.body.version,
&ctx.registry_state.status,
true, &serving_authority,
&caps.registry_did,
chrono::Duration::seconds(
policy.lineage_head.max_clock_skew_seconds as i64,
),
resolver,
)
.await?;
let stale = policy.lineage_head.max_age_seconds.map(|max| {
receipt.age_at(chrono::Utc::now()) > chrono::Duration::seconds(max as i64)
});
(Some(receipt), stale)
}
};
Ok(Self {
inner: ctx,
key_status,
verified_receipt,
verified_head_receipt,
head_receipt_stale,
})
}
async fn verify_retrieved(
client: &RegistryClient,
resolver: &WebResolver,
ctx: &FullContext,
expected_ctx_id: &CtxId,
policy: &VerificationPolicy,
) -> Result<
(
KeyAuthorization,
Option<acdp_types::receipt::RegistryReceipt>,
),
AcdpError,
> {
if policy.validate_body_schema {
acdp_validation::validate_body(&ctx.body)?;
}
let verifier = Verifier::new(resolver);
verifier.verify_body_hash(&ctx.body)?;
let serving_authority = client
.authority()
.unwrap_or_else(|| expected_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 = acdp_crypto::fingerprint::fingerprint_for_key_id(
&ctx.body.signature.key_id,
&ctx.body.signature.algorithm,
resolver,
)
.await?;
Some(
super::receipt::verify_receipt_value(
value,
expected_ctx_id,
&ctx.body,
&ctx.body.content_hash,
&fingerprint,
&serving_authority,
resolver,
)
.await?,
)
}
};
let revocation_verdict = if policy.revocations.known.is_empty() {
None
} else {
let fingerprint = acdp_crypto::fingerprint::fingerprint_for_key_id(
&ctx.body.signature.key_id,
&ctx.body.signature.algorithm,
resolver,
)
.await?;
super::revocation::classify_under_revocation(
&policy.revocations.known,
&fingerprint,
verified_receipt.as_ref().map(|r| r.created_at),
)?
};
let key_status = match revocation_verdict {
Some(pre_compromise) => {
if ctx.body.agent_id.as_str().starts_with("did:key:") {
verifier.verify_body_signature(&ctx.body).await?;
} else {
acdp_verify::verify_body_signature_historical(&ctx.body, resolver).await?;
}
pre_compromise
}
None => match verifier.verify_body_signature(&ctx.body).await {
Ok(()) => KeyAuthorization::CurrentlyAuthorized,
Err(AcdpError::KeyNotAuthorized(_))
if policy.historical_keys == HistoricalKeyPolicy::AcceptWithReceipt
&& verified_receipt.is_some() =>
{
acdp_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((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 acdp_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 = acdp_validation::verify_embedded_hash(dr)
.and_then(|()| acdp_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,
verified_head_receipt: None,
head_receipt_stale: 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 {
acdp_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 = acdp_validation::verify_embedded_hash(dr)
.and_then(|()| acdp_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,
verified_head_receipt: None,
head_receipt_stale: None,
},
report,
))
}
pub fn body(&self) -> &acdp_types::body::Body {
&self.inner.body
}
pub fn registry_state(&self) -> &acdp_types::body::RegistryState {
&self.inner.registry_state
}
pub fn full_context(&self) -> &FullContext {
&self.inner
}
pub fn key_status(&self) -> KeyAuthorization {
self.key_status
}
pub fn verified_receipt(&self) -> Option<&acdp_types::receipt::RegistryReceipt> {
self.verified_receipt.as_ref()
}
pub fn verified_head_receipt(&self) -> Option<&acdp_types::receipt::LineageHeadReceipt> {
self.verified_head_receipt.as_ref()
}
pub fn head_receipt_stale(&self) -> Option<bool> {
self.head_receipt_stale
}
pub fn receipt(&self) -> Option<&serde_json::Value> {
self.inner.registry_receipt.as_ref()
}
pub fn lineage_head_receipt(&self) -> Option<&serde_json::Value> {
self.inner.lineage_head_receipt.as_ref()
}
pub async fn verify_receipt(
&self,
resolver: &WebResolver,
) -> Result<Option<acdp_types::receipt::RegistryReceipt>, AcdpError> {
let Some(value) = &self.inner.registry_receipt else {
return Ok(None);
};
let fingerprint = acdp_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: &acdp_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!(
strict.revocations.known.is_empty(),
"a 0.1.0-pinned consumer is unaffected by RFC-ACDP-0014"
);
assert_ne!(
strict,
VerificationPolicy::default(),
"the 0.2 default is receipt-aware; the v0.1.0 profile is not"
);
}
}