Consuming & Verifying Contexts

This page covers the consumer side: fetching contexts, verifying them end-to-end, following acdp:// references, and fetching data refs. Everything here requires the client feature (the default).

This crate implements the acdp-consumer profile. The verification algorithm is specified in RFC-ACDP-0001 §5.11; retrieval semantics in RFC-ACDP-0004; cross-registry resolution in RFC-ACDP-0006.

Two layers: transport and verification

The crate separates getting bytes from trusting them:

  • RegistryClient — the HTTP driver. Talks to one registry. Returns wire objects (FullContext, CapabilitiesDocument, SearchResponse) without verifying signatures.
  • VerifiedContext — the trust layer. Wraps a fetch with the full verification pipeline and only hands you back a context that passed.

Use VerifiedContext unless you have a specific reason not to. A raw RegistryClient::retrieve gives you a structurally-parsed body, but a registry is not a trusted party — only the producer's signature is.

RegistryClient

# #[cfg(feature = "client")]
# async fn run() -> Result<(), acdp::AcdpError> {
use acdp::client::RegistryClient;

let client = RegistryClient::new("https://registry.example.com")?;   // HTTPS-only
# Ok(()) }

new applies the security defaults automatically: HTTPS-only, IP-literal rejection, DNS-time SSRF filtering, 1 MB body cap, 3-redirect same-authority limit, 5 s connect / 30 s total timeouts.

MethodEndpointReturns
capabilities()GET /.well-known/acdp.jsonCapabilitiesDocument
retrieve(ctx_id)GET /contexts/{ctx_id}FullContext (body + registry_state)
retrieve_body(ctx_id)GET /contexts/{ctx_id}/bodybody only
lineage(lineage_id)GET /lineages/{lineage_id}Vec<FullContext> (all versions)
current(lineage_id)GET /lineages/{lineage_id}/currentlatest active version
search(&params)GET /contexts/searchSearchResponse
publish(&req)POST /contextsPublishResponse

Conditional and idempotent variants exist too: retrieve_with_metadata, retrieve_if_none_match (ETag), capabilities_with_ttl, publish_idempotent, and publish_with_retry. The client is Clone (cheap — an inner Arc).

Searching

Use the builder for ergonomic queries:

# #[cfg(feature = "client")]
# async fn run(client: &acdp::client::RegistryClient) -> Result<(), acdp::AcdpError> {
let results = client
    .search_builder()
    .q("revenue")
    .context_type("data_snapshot")
    .domain("finance")
    .tag("q1-2026")
    .limit(50)
    .send()
    .await?;

for m in &results.matches {
    println!("{}", m.ctx_id);
}
if let Some(cursor) = results.next_cursor {
    // pass `.cursor(cursor)` on the next page
}
# Ok(()) }

Search ranking is registry-defined (RFC-ACDP-0005); the crate does not re-rank. See Discovery in the spec.

VerifiedContext — the verification pipeline

VerifiedContext::fetch runs the whole pipeline and returns only on full success:

# #[cfg(feature = "client")]
# async fn run() -> Result<(), acdp::AcdpError> {
use acdp::{client::{RegistryClient, VerifiedContext}, did::WebResolver, types::CtxId};

let client   = RegistryClient::new("https://registry.example.com")?;
let resolver = WebResolver::new();
let ctx_id   = CtxId("acdp://registry.example.com/…".into());

let ctx = VerifiedContext::fetch(&client, &resolver, &ctx_id).await?;
println!("{} — {:?}", ctx.body().title, ctx.registry_state().status);
# Ok(()) }

The stages, in order (it returns on the first failure):

#StageFailure error
1Schema validation (validate_body) — structural + embedded data_ref hashesSchemaViolation, DataRefHashMismatch
2content_hash recomputesha256(JCS(ProducerContent)) vs declaredHashMismatch
3did:web key resolution via WebResolverKeyResolution, KeyResolutionUnreachable
4Signature verification against the resolved key (algorithm must match)InvalidSignature, UnsupportedAlgorithm
5Status check per policy

This is exactly what the offline cargo run --example consumer demonstrates, step by step.

Verification policy

VerificationPolicy tunes the pipeline. The default is the strict v0.1.0 profile — VerificationPolicy::strict_v0_1_0() is an alias for Default.

use acdp::client::VerificationPolicy;

let policy = VerificationPolicy::strict_v0_1_0();   // == VerificationPolicy::default()
FieldDefaultEffect
validate_body_schematrueRun stage 1. Set false only in diagnostics that want to attempt signature checks on a body known to fail structural checks.
allow_unknown_statustrueAccept Status::Other and degrade to active (RFC-ACDP-0004 §4.1). false rejects unknown statuses.
verify_registry_receiptfalseReserved for v0.1+ (RFC-ACDP-0009 §2.7); no-op today.

There is no "relaxed did:web" or "skip-hash" mode in v0.1.0. The strict profile is the only one the acdp-consumer conformance suite covers. To apply a custom policy use fetch_with_policy(&client, &resolver, &ctx_id, &policy).

Diagnostics: fetch_report

When you need to know which stage failed rather than just that it did, use fetch_report. It runs the same pipeline but returns a structured VerificationReport alongside the context:

# #[cfg(feature = "client")]
# async fn run(client: &acdp::client::RegistryClient, resolver: &acdp::did::WebResolver, ctx_id: &acdp::types::CtxId) -> Result<(), acdp::AcdpError> {
use acdp::client::VerificationPolicy;

let policy = VerificationPolicy::strict_v0_1_0();
let (verified, report) =
    VerifiedContext::fetch_report(client, resolver, ctx_id, &policy).await?;

assert!(report.schema_ok && report.body_hash_ok && report.signature_ok);
# Ok(()) }

VerificationReport fields:

FieldMeaning
schema_okvalidate_body passed (or was disabled by policy).
body_hash_okrecomputed content_hash matched the declared one.
signature_okproducer signature verified against the resolved DID key.
data_ref_embeddedper-DataRef embedded-hash outcome, in body.data_refs order.
data_ref_externalper-DataRef external-fetch outcome; None = not attempted.

fetch_report_with_fetcher additionally fetches and verifies external data_ref locations (see below).

Fetching data references

A verified context tells you where its data lives; fetching that data is a separate, SSRF-guarded step. HttpsDataRefFetcher applies the same network defenses as the registry client.

# #[cfg(feature = "client")]
# async fn run(data_ref: &acdp::types::DataRef) -> Result<(), acdp::AcdpError> {
use acdp::client::{fetch_and_verify_data_ref, HttpsDataRefFetcher};

let fetcher = HttpsDataRefFetcher::default();
let bytes = fetch_and_verify_data_ref(&fetcher, data_ref).await?;
# Ok(()) }

If the data_ref carries a content_hash, the fetched bytes are verified against it — a mismatch is DataRefHashMismatch. Fetches are size-capped (DEFAULT_MAX_BYTES). DataRefFetcher is a trait, so you can supply your own transport (e.g. for s3:// or authenticated origins).

Cross-registry resolution

CrossRegistryResolver walks derived_from provenance edges across registries, following the seven-step algorithm in RFC-ACDP-0006 §4.1 with cycle detection, depth/node/fan-out caps, and a wall-clock budget.

# #[cfg(feature = "client")]
# async fn run() -> Result<(), acdp::AcdpError> {
use acdp::client::CrossRegistryResolver;

let resolver = CrossRegistryResolver::new()
    .with_max_depth(5);                    // tighten the default 10

// (resolution methods walk the derived_from graph from a starting context,
//  verifying each hop's signature and registry-DID web binding)
# Ok(()) }

ResolverOptions (via .with_options(...)) bounds the walk:

OptionDefaultPurpose
max_depth10per-edge depth limit
max_nodes100hard ceiling on total contexts verified
max_fanout32max derived_from entries on any single context (reject hostile fan-out)
total_timeout30 swall-clock budget for the whole walk
capabilities_ttl5 minhow long to cache each foreign registry's /.well-known/acdp.json

Use .with_allowlist([...]) to restrict which authorities the resolver will contact, and .seed_client(authority, client) to pre-wire a configured client (e.g. with a custom CA) for a known authority. Every URL the resolver builds is checked against its SsrfPolicy — see Security.

Publishing from the client

The producer (Producing contexts) builds the request; the client transmits it:

# #[cfg(feature = "client")]
# async fn run(client: &acdp::client::RegistryClient, req: &acdp::PublishRequest) -> Result<(), acdp::AcdpError> {
let resp = client.publish(req).await?;
println!("assigned ctx_id: {}", resp.ctx_id);

// or, idempotent / with retry on transient failures:
// client.publish_idempotent(req, "my-idempotency-key").await?;
// client.publish_with_retry(req, "my-idempotency-key", 4).await?;  // bounded backoff
# Ok(()) }

publish_with_retry retries only transient errors — see Errors & retries.