Skip to main content

acdp_client/
verified.rs

1//! VerifiedContext: retrieve + verify in one call.
2
3use super::data_ref::{fetch_and_verify_data_ref, DataRefFetcher};
4use super::registry::RegistryClient;
5use acdp_did::WebResolver;
6use acdp_primitives::error::AcdpError;
7use acdp_types::{body::FullContext, primitives::CtxId};
8use acdp_verify::Verifier;
9
10/// Consumer-tunable strictness for [`VerifiedContext::fetch_with_policy`].
11///
12/// For ACDP v0.1.0 the verification profile is **always strict**:
13///
14/// - `did:web` is required for every producer identity — enforced
15///   unconditionally by `verify_signature_envelope`
16///   (RFC-ACDP-0001 §5.4), regardless of any policy field.
17/// - Embedded `DataRef` hashes are verified by
18///   [`acdp_validation::validate_body`] whenever `validate_body_schema`
19///   is set.
20///
21/// Only the fields below have real effect in this version; there are no
22/// relaxed-mode `did:web` or embedded-hash knobs.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct VerificationPolicy {
25    /// If true, run [`acdp_validation::validate_body`] (structural
26    /// schema checks plus embedded-`DataRef` hash verification) before
27    /// any cryptographic check. Default `true`. Set `false` only in
28    /// diagnostic paths that want to attempt signature verification
29    /// despite a body known to fail structural checks.
30    pub validate_body_schema: bool,
31
32    /// If true, accept `Status::Other` values (degrade to active per
33    /// RFC-ACDP-0004 §4.1). When false, reject unknown statuses.
34    /// Default `true`.
35    pub allow_unknown_status: bool,
36
37    /// Registry-receipt handling (ACDP 0.2, RFC-ACDP-0010).
38    /// Default [`ReceiptPolicy::VerifyIfPresent`].
39    pub receipts: ReceiptPolicy,
40
41    /// Historical-key handling (ACDP 0.2, WS-B). Default
42    /// [`HistoricalKeyPolicy::AcceptWithReceipt`].
43    pub historical_keys: HistoricalKeyPolicy,
44}
45
46impl Default for VerificationPolicy {
47    fn default() -> Self {
48        Self {
49            validate_body_schema: true,
50            allow_unknown_status: true,
51            receipts: ReceiptPolicy::VerifyIfPresent,
52            historical_keys: HistoricalKeyPolicy::AcceptWithReceipt,
53        }
54    }
55}
56
57/// How to treat the optional `registry_receipt` on retrieval
58/// (RFC-ACDP-0010).
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60pub enum ReceiptPolicy {
61    /// Skip receipt verification entirely (0.1.0 behavior). The
62    /// receipt value is still preserved verbatim on the context.
63    Ignore,
64    /// Verify the receipt when one is present; absence is not an
65    /// error (the registry may simply be a 0.1.0 registry). Default.
66    #[default]
67    VerifyIfPresent,
68    /// Fail closed unless a receipt is present AND verifies. Use when
69    /// the deployment requires audit-grade provenance — registry
70    /// claims (`ctx_id`, `created_at`, `origin_registry`) are
71    /// assertions, not proofs, without a receipt.
72    Require,
73}
74
75/// How to treat a producer key that is present in the DID document's
76/// `verificationMethod` but no longer in `assertionMethod` — i.e. a
77/// key the producer rotated out but retained per the RFC-ACDP-0010
78/// key-retention rule.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
80pub enum HistoricalKeyPolicy {
81    /// Strict 0.1.0 behavior: only `assertionMethod` keys verify.
82    /// Every context signed by a rotated-out key fails.
83    Reject,
84    /// Accept a retained key **only** when a verified registry receipt
85    /// attests (via `key_fingerprint`) that this exact key was the
86    /// authorized one at publish time. Without a verified receipt the
87    /// historical path never activates — fail closed. Default.
88    #[default]
89    AcceptWithReceipt,
90}
91
92/// How the producer key that verified the body relates to the
93/// producer's *current* DID document.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum KeyAuthorization {
96    /// The signing key is currently listed in `assertionMethod`.
97    CurrentlyAuthorized,
98    /// The signing key was rotated out of `assertionMethod` but is
99    /// retained in `verificationMethod`, and a verified registry
100    /// receipt attests it was the authorized key at publish time
101    /// (RFC-ACDP-0010). Weigh accordingly: valid history, not a
102    /// current endorsement.
103    HistoricallyAuthorized,
104}
105
106impl VerificationPolicy {
107    /// The v0.1.0 strict verification profile (RFC-ACDP-0001 §5.11, §9.2).
108    ///
109    /// Runs the full §5.11 pipeline: body schema validation, `content_hash`
110    /// recomputation, `did:web` key resolution, signature verification, and
111    /// embedded `data_ref.content_hash` checks. Returns on the first failure.
112    ///
113    /// This is the **only** mode covered by the `acdp-consumer` conformance
114    /// profile. Relaxed modes (`Diagnostic`, `UnsafeForTests`) are NOT
115    /// available in this crate in v0.1.0 — they would be separately-named
116    /// opt-ins per §9.2, and are not currently implemented.
117    ///
118    /// NOT identical to [`Default::default()`] as of 0.2: the default
119    /// policy is receipt-aware (`VerifyIfPresent` + `AcceptWithReceipt`),
120    /// while this named profile preserves the exact v0.1.0 semantics —
121    /// receipts inert ([`ReceiptPolicy::Ignore`]) and only
122    /// `assertionMethod` keys accepted
123    /// ([`HistoricalKeyPolicy::Reject`]). Callers pinned to this
124    /// constructor keep v0.1.0 behavior across the 0.2 upgrade.
125    pub fn strict_v0_1_0() -> Self {
126        Self {
127            validate_body_schema: true,
128            allow_unknown_status: true,
129            receipts: ReceiptPolicy::Ignore,
130            historical_keys: HistoricalKeyPolicy::Reject,
131        }
132    }
133}
134
135/// A retrieved context that has been cryptographically verified.
136#[derive(Debug)]
137pub struct VerifiedContext {
138    pub inner: FullContext,
139    /// Whether the body verified against a currently authorized key or
140    /// a receipt-attested historical one (ACDP 0.2, WS-B).
141    pub key_status: KeyAuthorization,
142    /// The verified registry receipt, when one was present and the
143    /// policy verified it (RFC-ACDP-0010). `None` under
144    /// [`ReceiptPolicy::Ignore`] or when the registry minted none.
145    pub verified_receipt: Option<acdp_types::receipt::RegistryReceipt>,
146}
147
148impl VerifiedContext {
149    /// Retrieve a context and verify its signature using the strict
150    /// default [`VerificationPolicy`].
151    pub async fn fetch(
152        client: &RegistryClient,
153        resolver: &WebResolver,
154        ctx_id: &CtxId,
155    ) -> Result<Self, AcdpError> {
156        Self::fetch_with_policy(client, resolver, ctx_id, &VerificationPolicy::default()).await
157    }
158
159    /// Retrieve a context and verify its signature with caller-controlled
160    /// strictness.
161    ///
162    /// 1. Fetches `body + registry_state` from the registry.
163    /// 2. Optionally runs `validate_body` — structural schema checks
164    ///    plus embedded-`DataRef` hash verification (policy-controlled).
165    /// 3. Recomputes `content_hash` over ProducerContent.
166    /// 4. Resolves the producer's DID document. `did:web` is required
167    ///    unconditionally for v0.1.0 (RFC-ACDP-0001 §5.4).
168    /// 5. Verifies the Ed25519 signature (or other supported algorithm).
169    /// 6. Optionally verifies the `registry_receipt` placeholder.
170    /// 7. Optionally rejects unknown statuses.
171    pub async fn fetch_with_policy(
172        client: &RegistryClient,
173        resolver: &WebResolver,
174        ctx_id: &CtxId,
175        policy: &VerificationPolicy,
176    ) -> Result<Self, AcdpError> {
177        let ctx = client.retrieve(ctx_id).await?;
178
179        if policy.validate_body_schema {
180            acdp_validation::validate_body(&ctx.body)?;
181        }
182
183        // Hash recomputation first: from here on `ctx.body.content_hash`
184        // IS the independently recomputed value, which the receipt
185        // cross-check below relies on.
186        let verifier = Verifier::new(resolver);
187        verifier.verify_body_hash(&ctx.body)?;
188
189        // ── Receipt phase (RFC-ACDP-0010) ───────────────────────────
190        // Verified BEFORE the signature phase because the historical-
191        // key path is gated on a verified receipt.
192        let serving_authority = client
193            .authority()
194            .unwrap_or_else(|| ctx_id.authority().to_string());
195        let verified_receipt = match (policy.receipts, &ctx.registry_receipt) {
196            (ReceiptPolicy::Ignore, _) | (ReceiptPolicy::VerifyIfPresent, None) => None,
197            (ReceiptPolicy::Require, None) => {
198                return Err(AcdpError::InvalidReceipt(
199                    "policy requires a registry receipt but the response carries none \
200                     (registry without the acdp-registry-receipts profile, or a \
201                     pre-receipts context)"
202                        .into(),
203                ));
204            }
205            (_, Some(value)) => {
206                let fingerprint = acdp_crypto::fingerprint::fingerprint_for_key_id(
207                    &ctx.body.signature.key_id,
208                    &ctx.body.signature.algorithm,
209                    resolver,
210                )
211                .await?;
212                Some(
213                    super::receipt::verify_receipt_value(
214                        value,
215                        ctx_id,
216                        &ctx.body,
217                        &ctx.body.content_hash,
218                        &fingerprint,
219                        &serving_authority,
220                        resolver,
221                    )
222                    .await?,
223                )
224            }
225        };
226
227        // ── Signature phase ──────────────────────────────────────────
228        // Standard path enforces assertionMethod membership. A
229        // KeyNotAuthorized failure falls back to the historical path
230        // only under AcceptWithReceipt AND a verified receipt — the
231        // receipt's key_fingerprint (already cross-checked against this
232        // exact key above) is what attests publish-time authorization.
233        let key_status = match verifier.verify_body_signature(&ctx.body).await {
234            Ok(()) => KeyAuthorization::CurrentlyAuthorized,
235            Err(AcdpError::KeyNotAuthorized(_))
236                if policy.historical_keys == HistoricalKeyPolicy::AcceptWithReceipt
237                    && verified_receipt.is_some() =>
238            {
239                acdp_verify::verify_body_signature_historical(&ctx.body, resolver).await?;
240                KeyAuthorization::HistoricallyAuthorized
241            }
242            Err(e) => return Err(e),
243        };
244
245        if !policy.allow_unknown_status {
246            if let Some(other) = ctx.registry_state.status.as_other() {
247                return Err(AcdpError::SchemaViolation(format!(
248                    "policy.allow_unknown_status=false; registry returned '{other}'"
249                )));
250            }
251        }
252
253        Ok(Self {
254            inner: ctx,
255            key_status,
256            verified_receipt,
257        })
258    }
259
260    /// Retrieve + verify, returning a structured [`VerificationReport`]
261    /// alongside the verified context. Does NOT attempt external
262    /// `DataRef` fetches — use [`Self::fetch_report_with_fetcher`] for
263    /// that. Each `data_ref_external` slot in the returned report is
264    /// `None`.
265    ///
266    /// Unlike [`Self::fetch_with_policy`], per-`DataRef` embedded-hash
267    /// failures are recorded in the report instead of aborting the
268    /// verification. The top-level checks (schema, body hash,
269    /// signature) remain hard-fail: if any of them fails, the method
270    /// returns an `AcdpError` and produces no report.
271    ///
272    /// For diagnostic callers that want a populated report even when
273    /// a top-level check fails (e.g. an audit walker that needs to
274    /// distinguish "wrong hash" from "wrong signature"), use
275    /// [`Self::fetch_report_diagnose`] instead.
276    pub async fn fetch_report(
277        client: &RegistryClient,
278        resolver: &WebResolver,
279        ctx_id: &CtxId,
280        policy: &VerificationPolicy,
281    ) -> Result<(Self, VerificationReport), AcdpError> {
282        Self::fetch_report_inner::<NoFetcher>(client, resolver, ctx_id, policy, None).await
283    }
284
285    /// Diagnostic variant of [`Self::fetch_report`] that never
286    /// short-circuits on a top-level failure — schema, body-hash, and
287    /// signature outcomes are each recorded individually in the
288    /// returned [`VerificationReport`]. Returns `Ok((None, report))`
289    /// when any top-level stage failed (the report shows which one);
290    /// `Ok((Some(verified), report))` only when every check passed
291    /// (FEAT-05).
292    ///
293    /// Use cases:
294    /// - Audit walkers that need to classify failures by stage.
295    /// - Admin tooling that wants to distinguish "hash mismatch"
296    ///   (probable tampering / encoding drift) from "signature
297    ///   verification failed" (key compromise / DID resolution
298    ///   problem).
299    ///
300    /// Network errors (retrieve, DID resolution) still propagate as
301    /// `Err` — there's no body to inspect when the registry is
302    /// unreachable.
303    pub async fn fetch_report_diagnose(
304        client: &RegistryClient,
305        resolver: &WebResolver,
306        ctx_id: &CtxId,
307        policy: &VerificationPolicy,
308    ) -> Result<(Option<Self>, VerificationReport), AcdpError> {
309        let ctx = client.retrieve(ctx_id).await?;
310        let mut report = VerificationReport {
311            body_hash_ok: false,
312            signature_ok: false,
313            schema_ok: false,
314            data_ref_embedded: Vec::with_capacity(ctx.body.data_refs.len()),
315            data_ref_external: Vec::with_capacity(ctx.body.data_refs.len()),
316        };
317
318        // Schema (structural) — record pass/fail.
319        if policy.validate_body_schema {
320            match acdp_validation::validate_body_structural(&ctx.body) {
321                Ok(()) => report.schema_ok = true,
322                Err(_) => { /* keep schema_ok=false; continue collecting */ }
323            }
324        } else {
325            report.schema_ok = true;
326        }
327
328        // Per-DataRef embedded hashes — same as fetch_report_inner.
329        for dr in &ctx.body.data_refs {
330            if let (Some(emb), Some(_)) = (&dr.embedded, &dr.content_hash) {
331                let outcome = acdp_validation::verify_embedded_hash(dr)
332                    .and_then(|()| acdp_validation::embedded_decoded_bytes(emb).map(|b| b.len()));
333                report.data_ref_embedded.push(outcome);
334            } else {
335                report.data_ref_embedded.push(Ok(0));
336            }
337        }
338
339        // Hash + signature recorded independently (FEAT-05).
340        let verifier = Verifier::new(resolver);
341        report.body_hash_ok = verifier.verify_body_hash(&ctx.body).is_ok();
342        report.signature_ok = verifier.verify_body_signature(&ctx.body).await.is_ok();
343
344        // External fetches were not attempted (this method has no
345        // fetcher param — diagnostic callers can wire their own).
346        for _ in &ctx.body.data_refs {
347            report.data_ref_external.push(None);
348        }
349
350        // Decide whether to surface the verified handle. Report paths
351        // run the strict assertionMethod check only (no receipt /
352        // historical handling — use `fetch_with_policy` for those).
353        let all_top_level_pass = report.schema_ok && report.body_hash_ok && report.signature_ok;
354        let verified = if all_top_level_pass {
355            Some(Self {
356                inner: ctx,
357                key_status: KeyAuthorization::CurrentlyAuthorized,
358                verified_receipt: None,
359            })
360        } else {
361            None
362        };
363        Ok((verified, report))
364    }
365
366    /// Retrieve + verify like [`Self::fetch_report`], and additionally
367    /// fetch every `DataRef` whose `location` resolves through `fetcher`.
368    /// Each external fetch outcome is recorded in `report.data_ref_external`.
369    pub async fn fetch_report_with_fetcher<F: DataRefFetcher>(
370        client: &RegistryClient,
371        resolver: &WebResolver,
372        ctx_id: &CtxId,
373        policy: &VerificationPolicy,
374        fetcher: &F,
375    ) -> Result<(Self, VerificationReport), AcdpError> {
376        Self::fetch_report_inner(client, resolver, ctx_id, policy, Some(fetcher)).await
377    }
378
379    async fn fetch_report_inner<F: DataRefFetcher>(
380        client: &RegistryClient,
381        resolver: &WebResolver,
382        ctx_id: &CtxId,
383        policy: &VerificationPolicy,
384        fetcher: Option<&F>,
385    ) -> Result<(Self, VerificationReport), AcdpError> {
386        let ctx = client.retrieve(ctx_id).await?;
387        let mut report = VerificationReport {
388            body_hash_ok: false,
389            signature_ok: false,
390            schema_ok: false,
391            data_ref_embedded: Vec::with_capacity(ctx.body.data_refs.len()),
392            data_ref_external: Vec::with_capacity(ctx.body.data_refs.len()),
393        };
394
395        // Structural-only schema validation — embedded-hash checks are
396        // intentionally skipped here so per-DataRef hash failures land
397        // in the report (below) instead of short-circuiting the whole
398        // verification. That's the diagnostic shape `fetch_report`
399        // promises in its docstring.
400        if policy.validate_body_schema {
401            acdp_validation::validate_body_structural(&ctx.body)?;
402        }
403        report.schema_ok = true;
404
405        // Per-DataRef embedded-hash outcomes — recorded individually.
406        for dr in &ctx.body.data_refs {
407            if let (Some(emb), Some(_)) = (&dr.embedded, &dr.content_hash) {
408                let outcome = acdp_validation::verify_embedded_hash(dr)
409                    .and_then(|()| acdp_validation::embedded_decoded_bytes(emb).map(|b| b.len()));
410                report.data_ref_embedded.push(outcome);
411            } else {
412                report.data_ref_embedded.push(Ok(0));
413            }
414        }
415
416        // `verify_body_signed` recomputes content_hash + verifies the
417        // signature WITHOUT re-running the schema validator (we already
418        // ran the structural part above, and embedded-hash failures are
419        // recorded per-DataRef rather than aborting). It still enforces
420        // `did:web` for the producer key (RFC-ACDP-0001 §5.4).
421        Verifier::new(resolver)
422            .verify_body_signed(&ctx.body)
423            .await?;
424        report.body_hash_ok = true;
425        report.signature_ok = true;
426
427        if !policy.allow_unknown_status {
428            if let Some(other) = ctx.registry_state.status.as_other() {
429                return Err(AcdpError::SchemaViolation(format!(
430                    "policy.allow_unknown_status=false; registry returned '{other}'"
431                )));
432            }
433        }
434
435        // External fetches — record per-ref outcomes when a fetcher is
436        // supplied; otherwise leave each slot as `None` so callers can
437        // distinguish "skipped" from "failed".
438        for dr in &ctx.body.data_refs {
439            let slot: Option<Result<usize, AcdpError>> = match (fetcher, &dr.location) {
440                (Some(f), Some(_)) => Some(fetch_and_verify_data_ref(dr, f).await.map(|b| b.len())),
441                _ => None,
442            };
443            report.data_ref_external.push(slot);
444        }
445
446        Ok((
447            Self {
448                inner: ctx,
449                key_status: KeyAuthorization::CurrentlyAuthorized,
450                verified_receipt: None,
451            },
452            report,
453        ))
454    }
455
456    pub fn body(&self) -> &acdp_types::body::Body {
457        &self.inner.body
458    }
459
460    pub fn registry_state(&self) -> &acdp_types::body::RegistryState {
461        &self.inner.registry_state
462    }
463
464    /// Raw registry receipt value as served on the wire
465    /// (RFC-ACDP-0010), preserved verbatim. For the verified, typed
466    /// form see [`Self::verified_receipt`].
467    pub fn receipt(&self) -> Option<&serde_json::Value> {
468        self.inner.registry_receipt.as_ref()
469    }
470
471    /// Verify the registry receipt, when one is present
472    /// (RFC-ACDP-0010).
473    ///
474    /// Standalone variant for contexts obtained via the report paths or
475    /// constructed externally; `fetch_with_policy` already does this
476    /// under [`ReceiptPolicy::VerifyIfPresent`]/`Require`. The serving
477    /// authority is taken from the context's own `ctx_id` — correct
478    /// when the context was fetched from its home registry, which is
479    /// the only retrieval shape v0.2 defines.
480    ///
481    /// Returns `Ok(None)` when no receipt is present, `Ok(Some(_))`
482    /// with the verified receipt otherwise.
483    pub async fn verify_receipt(
484        &self,
485        resolver: &WebResolver,
486    ) -> Result<Option<acdp_types::receipt::RegistryReceipt>, AcdpError> {
487        let Some(value) = &self.inner.registry_receipt else {
488            return Ok(None);
489        };
490        // Recompute the body hash rather than trusting the echoed
491        // `body.content_hash`: all fields of this type are public, so a
492        // caller may have constructed it around an unverified
493        // FullContext, and the receipt cross-check is only meaningful
494        // against an independently recomputed hash (RFC-ACDP-0010 §8
495        // step 4).
496        let body_val = serde_json::to_value(&self.inner.body)?;
497        let recomputed = acdp_crypto::compute_content_hash(&body_val)?;
498        if recomputed != self.inner.body.content_hash {
499            return Err(AcdpError::HashMismatch {
500                stored: self.inner.body.content_hash.clone(),
501                recomputed,
502            });
503        }
504        let fingerprint = acdp_crypto::fingerprint::fingerprint_for_key_id(
505            &self.inner.body.signature.key_id,
506            &self.inner.body.signature.algorithm,
507            resolver,
508        )
509        .await?;
510        let receipt = super::receipt::verify_receipt_value(
511            value,
512            &self.inner.body.ctx_id,
513            &self.inner.body,
514            &self.inner.body.content_hash,
515            &fingerprint,
516            self.inner.body.ctx_id.authority(),
517            resolver,
518        )
519        .await?;
520        Ok(Some(receipt))
521    }
522}
523
524/// Structured diagnostic outcome from [`VerifiedContext::fetch_report`].
525///
526/// Top-level booleans report the per-stage outcome of the verification
527/// pipeline. Per-`DataRef` slots track outcomes for each entry in
528/// `body.data_refs`, in declaration order:
529///
530/// - `data_ref_embedded[i]` — `Ok(decoded_size_bytes)` when the embedded
531///   payload's `content_hash` matched; `Err` when it didn't (or the
532///   embedded was malformed). Refs without an embedded payload or
533///   without a declared `content_hash` produce `Ok(0)`.
534/// - `data_ref_external[i]` — `None` when no external fetch was
535///   attempted (either no `location` or no `fetcher` was provided);
536///   `Some(Ok(bytes_len))` when the fetch + hash succeeded;
537///   `Some(Err(_))` on any failure (SSRF rejection, hash mismatch,
538///   timeout, …).
539///
540/// `AcdpError` doesn't implement `Clone`, so the report is move-only.
541#[derive(Debug)]
542pub struct VerificationReport {
543    /// `content_hash` recomputed from the body matches the declared one.
544    pub body_hash_ok: bool,
545    /// The producer signature verified against the resolved DID key.
546    pub signature_ok: bool,
547    /// `validate_body` passed (or was disabled by policy).
548    pub schema_ok: bool,
549    /// Per-`DataRef` embedded-hash outcome, in `body.data_refs` order.
550    pub data_ref_embedded: Vec<Result<usize, AcdpError>>,
551    /// Per-`DataRef` external-fetch outcome, in `body.data_refs` order.
552    /// `None` indicates "not attempted" (no fetcher provided or no
553    /// `location` to fetch from).
554    pub data_ref_external: Vec<Option<Result<usize, AcdpError>>>,
555}
556
557/// Sentinel `DataRefFetcher` used as the type parameter for
558/// `fetch_report_inner` when no fetcher is supplied. `fetch` is never
559/// actually called — the option is matched out before that — but
560/// providing a real impl lets the generic monomorphize cleanly without
561/// requiring `fetch_report`'s callers to name a type.
562struct NoFetcher;
563
564impl DataRefFetcher for NoFetcher {
565    async fn fetch(
566        &self,
567        _location: &acdp_types::data_ref::Location,
568    ) -> Result<Vec<u8>, AcdpError> {
569        Err(AcdpError::NotImplemented(
570            "NoFetcher should never be called — this is a fetch_report sentinel".into(),
571        ))
572    }
573}
574
575#[cfg(test)]
576mod tests {
577    use super::{HistoricalKeyPolicy, ReceiptPolicy, VerificationPolicy};
578
579    /// The RFC-ACDP-0001 §9.2 named constructor preserves exact v0.1.0
580    /// semantics: receipts inert, assertionMethod-only keys. It is
581    /// deliberately NOT the 0.2 default (which is receipt-aware).
582    #[test]
583    fn strict_v0_1_0_preserves_v0_1_0_semantics() {
584        let strict = VerificationPolicy::strict_v0_1_0();
585        assert!(strict.validate_body_schema);
586        assert!(strict.allow_unknown_status);
587        assert_eq!(strict.receipts, ReceiptPolicy::Ignore);
588        assert_eq!(strict.historical_keys, HistoricalKeyPolicy::Reject);
589        assert_ne!(
590            strict,
591            VerificationPolicy::default(),
592            "the 0.2 default is receipt-aware; the v0.1.0 profile is not"
593        );
594    }
595}