#[non_exhaustive]pub struct RevocationDiscovery {
pub include_registry_attested: bool,
pub on_failure: DiscoveryFailurePolicy,
pub total_timeout: Duration,
pub max_requests: Option<NonZeroUsize>,
pub max_bytes: Option<u64>,
pub freshness: Duration,
}Expand description
RFC-ACDP-0014 §8 revocation auto-discovery configuration.
When set on RevocationPolicy::discover, this instructs
verification to look up revocations itself — via
find_revocations and,
when Self::include_registry_attested is true, additionally
find_registry_attested_revocations
— instead of relying solely on RevocationPolicy::known.
§Cost
Discovery is expensive, and every request it issues is serial.
MAX_SEARCH_PAGES = 10 (crate::revocation) bounds search
round-trips per (type_form, status) pair, and there are 6
such pairs (2 type forms × 3 statuses) — so up to 60 search
requests, each of which can name up to 100 per-candidate context
retrieves (GET /contexts/{id}, capped at 1 MB apiece), for up to
6,000 + 60 + 100 = 6,160 requests / ~6.1 GB in the worst case
for one of the two discovery functions. The retrieve fan-out is
not bounded by MAX_LINEAGE_WALKS = 100 — that cap is only
checked after the retrieves have already gone out. With
Self::include_registry_attested set, both functions run:
≈12,321 requests / ~12.2 GB worst case for the pair (the extra
1 is the unconditional client.capabilities() fetch
find_registry_attested_revocations makes).
Self::total_timeout is an availability bound, not a bytes or
memory bound: it stops verification from hanging forever against
a slow or hostile registry, but a hostile registry on a fast link
can still serve gigabytes of legitimate-looking traffic inside the
window — the 1 MB cap applies per request, not in aggregate, and
verified revocations accumulate in a Vec for the call’s duration.
Self::max_requests and Self::max_bytes (issue #258) close
that gap: set either (or both) to bound the two lookups combined
— enabling Self::include_registry_attested does not double the
ceiling — checked before each request is issued, so a would-be
request that would exceed the budget is never sent. Exhaustion
raises AcdpError::RevocationDiscoveryBudgetExceeded through the
same Self::on_failure path as AcdpError::SearchTruncated — it
is permanent for the same request shape and is never transient.
Both knobs bound registry traffic only: DID-document fetches
issued via WebResolver are not counted. Self::max_bytes counts
only successfully-parsed response bodies — an error-envelope
read on a non-success response is not charged. Self::max_requests
has no such exemption: the request slot is reserved before the
request is issued, so a 503, a parse failure, or a PayloadTooLarge
still consumes it. Leaving both None (as both
Self::producer_signed_only and Self::all_trust_classes do)
preserves pre-#258 behavior exactly: unbounded requests and bytes,
bounded only by Self::total_timeout.
Issue #257 adds an opt-in cache (crate::RevocationCache, attached
to the crate::RegistryClient passed in via
crate::RegistryClient::with_revocation_cache) — but attaching one
does NOT, by itself, turn “re-discovers from scratch” into “sometimes
skips discovery.” It is two independent things: verified revocations
(“facts”) are always unioned into classification, indefinitely,
regardless of Self::freshness — a caller verifying many contexts
against the same producer benefits from this immediately, with zero
extra configuration, simply by attaching a cache and reusing the
client. Whether a repeat lookup is skipped entirely (saving requests)
is governed separately by Self::freshness, which defaults to
Duration::ZERO — i.e. off. See Self::freshness’s own doc and
crate::revocation_cache for the full model. Without a cache attached
at all, this crate behaves exactly as before #257: every call
re-discovers from scratch, budgeted or not. A caller that does not
want to manage a RevocationCache can still discover once itself and
pass the results via RevocationPolicy::known instead of setting
discover on every call — the same hoisting guidance
crate::revocation’s find_registry_attested_revocations doc already
gives callers of that function directly (see its “Cost note for
callers verifying many contexts”).
§Reentrancy
Discovery calls back into verification, and that reentrancy has two consequences worth stating explicitly rather than leaving implicit:
- Each candidate body
find_revocationsturns up is verified viaVerifier::new(resolver).verify_body— notverify_retrieved— so discovered revocation bodies are themselves checked without revocation checking of their own. That is defensible under RFC-ACDP-0014 §5 step 1’s “currently authorized key,” but it is an assumption this type is making on the caller’s behalf, not an accident. - Verifying a
key-revocationcontext now also triggers discovery against the same producer, so the revocation-fetch path itself becomes fragile underDiscoveryFailurePolicy::FailClosed: a producer whose revocation search is briefly unreachable can no longer be verified as revoked, either.
§No Default
This type deliberately has no Default impl — construct it
via Self::producer_signed_only or Self::all_trust_classes.
RFC-ACDP-0014 §6’s “lost-everything” fallback means a producer that
has lost every key it could sign a revocation with can only be
revoked registry-attested — so the catastrophic case is exactly the
one a silently-defaulted-off trust class would skip. A quiet
Default::default() that leaves include_registry_attested: false
would make that skip invisible at every call site; forcing a named
constructor puts the choice at the type level instead, where a
reviewer (and git grep) can see it. Callers protecting against
key loss, or otherwise unwilling to assume a producer always
retains signing capacity, MUST use Self::all_trust_classes.
Fields (Non-exhaustive)§
This struct is marked as non-exhaustive
Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.include_registry_attested: boolWhether to also run
find_registry_attested_revocations
(the §6 registry-attested trust class), in addition to the
producer-signed search every discovery configuration runs.
Requires the registry to serve /.well-known/acdp.json; under
DiscoveryFailurePolicy::FailClosed a registry that serves no
capabilities document fails verification when this is true.
on_failure: DiscoveryFailurePolicyWhat to do when discovery itself fails (a transient transport
error from either search, or the search-safety-cap error
AcdpError::SearchTruncated). Default DiscoveryFailurePolicy::FailClosed.
SearchTruncated and a transport error (e.g. a 503) are NOT
equivalent, even though both take this same on_failure path.
SearchTruncated means “this producer has more revocations than
we will page through” (MAX_SEARCH_PAGES) — an
attacker-inducible security downgrade, since a hostile
producer or registry can pad the result set specifically to
exhaust the page cap and hide a real revocation from discovery.
A 503 is an ordinary availability blip. A third case is
attacker-inducible the same way SearchTruncated is (issue
#258): AcdpError::RevocationDiscoveryBudgetExceeded, raised
when Self::max_requests or Self::max_bytes is exhausted —
a hostile registry that learns a caller’s budget can pad
harmless-looking traffic specifically to exhaust it before a
real revocation is found, just as padding the result set exhausts
MAX_SEARCH_PAGES. Under DiscoveryFailurePolicy::ProceedWithKnown
all three are treated the same way (proceed on
RevocationPolicy::known alone, record the failure) — choose
ProceedWithKnown knowing it also waives truncation and budget
exhaustion, not just transient unavailability.
total_timeout: DurationWall-clock budget for the whole discovery step (both searches,
if Self::include_registry_attested is set). An availability
bound only — see the type-level cost section above. Matches this
crate’s existing ResolverOptions::total_timeout precedent
(crate::cross_registry), defaulting to the same 30 s rather
than exceeding it on the core verify path.
Enforced via tokio::time::timeout, which requires the
executing Tokio runtime to have its time driver enabled
(#[tokio::main] and #[tokio::test] enable it by default;
a hand-built Builder::new_current_thread() runtime does
not unless .enable_time() or .enable_all() is called).
Calling verify_retrieved with discover: Some(..) from a
runtime without the time driver panics — it does not
return Err — the same requirement ResolverOptions::total_timeout
(crate::cross_registry) already carries on its opt-in walk,
but here it sits on the core verify path whenever discovery is
configured, not just on an explicit cross-registry walk.
max_requests: Option<NonZeroUsize>Issue #258: cap on the total number of registry requests the
discovery step may issue, combined across both lookups (the
producer-signed search always, plus the registry-attested search
when Self::include_registry_attested is set) — not a ceiling
per lookup, so turning on the second trust class does not double
the allowance. None (the default from both named constructors)
is unbounded, matching every version before #258. Checked
before each request is issued (RegistryClient::capabilities,
::retrieve, ::lineage, ::search); exceeding it raises
AcdpError::RevocationDiscoveryBudgetExceeded through
Self::on_failure, the same path AcdpError::SearchTruncated
already takes. Counts registry requests only — DID-document
fetches via WebResolver are not counted.
max_bytes: Option<u64>Issue #258: cap on the cumulative bytes of successfully-parsed
response bodies the discovery step may read, combined across
both lookups, same combination rule as Self::max_requests.
None (the default from both named constructors) is unbounded,
matching every version before #258. Checked before each
request is issued, using the running total from requests that
already completed — the size of an in-flight request cannot be
known (and therefore reserved) in advance, so up to two
requests can push the total past max_bytes before the next
check observes the overrun: the two trust-class lookups run
concurrently under tokio::try_join!, and both can pass a
not-yet-updated check before either’s response is recorded — see
Self::max_requests’s doc for the same race on the request
count, where it is closed by an atomic reservation; there is no
equivalent reservation for bytes, since a response’s size is not
known until after it is read. Counts a non-success response’s
error-envelope read not at all — only bytes read on the
success path are charged.
Some(0) is representable (unlike Self::max_requests, which
is guarded by NonZeroUsize) and is not special-cased: it trips
the >= check on the very first request of either lookup,
before that request is ever issued, so discovery fails
immediately with zero registry traffic. This is a deliberate
consequence of keeping this field a plain u64 (matching the
wave plan’s chosen types) rather than a reason to reach for
NonZeroU64.
freshness: DurationIssue #257: how long a discovery-freshness marker stays valid on
a crate::RevocationCache attached to the crate::RegistryClient
passed to verify_retrieved (via
crate::RegistryClient::with_revocation_cache). A marker records
“vantage V completed a full, untruncated discovery for this
producer/trust-class at time T”; while one is within freshness of
T, that lookup is skipped entirely (zero registry requests) rather
than merely supplemented.
Default Duration::ZERO from both named constructors — markers
never suppress a lookup unless a caller explicitly raises this
above zero. This is the safe default per RFC-ACDP-0014 §8’s own
warning (“absence of search results is not evidence of absence”): a
cached absence is not licensed the way a cached, verified
revocation is (§7:114 licenses only the latter, indefinitely). No
cache attached makes this field inert regardless of its value.
Distinct from — and orthogonal to — caching verified revocations
themselves (“facts”), which a crate::RevocationCache does
unconditionally and indefinitely whenever one is attached,
independent of this field: facts are always unioned into
classification (never gated behind freshness), because a
revocation is monotone (more revocations ⇒ an earlier effective
boundary ⇒ strictly more fail-closed verdicts), so seeding from
them can only tighten a verdict, never loosen one. freshness
governs only whether a lookup that would otherwise re-confirm “no
NEW revocation” is skipped. See crate::revocation_cache for the
full two-object model.
That “unconditionally” is exact for a producer-signed fact (§8: self-contained, applies at any vantage) but is scoped for a registry-attested one (§6): the cache additionally filters those to the vantage that minted them, so attaching one cache to clients for two different registries does not let registry A’s attestation apply to a context served by registry B.
A recommended ceiling, not enforced: RFC-ACDP-0006 §4.2 caps
WebResolver’s own DID-document cache TTL at 3600 s, and that is a
reasonable order-of-magnitude anchor for this field too — this
crate already accepts bounded key-material staleness at that
order. Left unenforced deliberately: a hard cap on a knob whose
safe default is ZERO would add a failure mode without adding
safety.
Implementations§
Source§impl RevocationDiscovery
impl RevocationDiscovery
Sourcepub fn producer_signed_only() -> Self
pub fn producer_signed_only() -> Self
Discover producer-signed revocations only
(include_registry_attested: false). Cheapest of the two
constructors, and the default choice for callers that are not
specifically defending against a producer that has lost every
signing key — see the “No Default” section above for who must
NOT stop here.
Sourcepub fn all_trust_classes() -> Self
pub fn all_trust_classes() -> Self
Discover both trust classes: producer-signed AND registry-attested
(include_registry_attested: true). Required to catch RFC-ACDP-0014
§6’s “lost-everything” fallback, where a producer with no signing
key left can only be revoked registry-attested. Requires the
registry to serve a capabilities document.
Trait Implementations§
Source§impl Clone for RevocationDiscovery
impl Clone for RevocationDiscovery
Source§fn clone(&self) -> RevocationDiscovery
fn clone(&self) -> RevocationDiscovery
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreimpl Copy for RevocationDiscovery
Source§impl Debug for RevocationDiscovery
impl Debug for RevocationDiscovery
impl Eq for RevocationDiscovery
Source§impl PartialEq for RevocationDiscovery
impl PartialEq for RevocationDiscovery
impl StructuralPartialEq for RevocationDiscovery
Auto Trait Implementations§
impl Freeze for RevocationDiscovery
impl RefUnwindSafe for RevocationDiscovery
impl Send for RevocationDiscovery
impl Sync for RevocationDiscovery
impl Unpin for RevocationDiscovery
impl UnsafeUnpin for RevocationDiscovery
impl UnwindSafe for RevocationDiscovery
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
key and return true if they are equal.