Skip to main content

acdp_client/
keyless.rs

1//! Keyless-signed bodies, consumer side (RFC-ACDP-0021 §8, §11; 0.5.0
2//! Draft).
3//!
4//! [`KeylessPolicy`] says which registries this consumer accepts as
5//! attesters (§11). It exists in every build so that
6//! [`VerificationPolicy`](crate::VerificationPolicy) has one shape; the
7//! default accepts none, and without the `keyless` feature it is not
8//! consulted at all: a keyless key id then fails closed in
9//! [`VerifiedContext::fetch`](crate::VerifiedContext::fetch) like any key
10//! the DID document does not list.
11//!
12//! With the `keyless` feature, `VerifiedContext` runs §8.1 in place of
13//! RFC-ACDP-0001 §5.11 for a keyless key id, in this order (the first
14//! failure decides, and its reason token rides on the error, see
15//! [`KeylessReason::of`](acdp_types::keyless::KeylessReason::of)):
16//!
17//! 1. the attestation, fetched from the serving registry by
18//!    `key_attestation.ctx_id`, matches `key_attestation.content_hash`,
19//!    is signed under its registry's DID, carries a valid receipt and
20//!    meets §5.3 (`attestation_invalid`);
21//! 2. the attesting registry is accepted (`untrusted_attester`);
22//! 3. `agent_id`, `key_id` and `algorithm` match it
23//!    (`attestation_mismatch`);
24//! 4. the body's receipt verifies, comes from the attester and names the
25//!    attested fingerprint (`unplaceable`) — the receipt is required
26//!    whatever [`ReceiptPolicy`](crate::ReceiptPolicy) says, because it
27//!    is the only time that places a keyless signature;
28//! 5. its `created_at` lies in the window (`outside_attestation_window`);
29//! 6. the signature verifies under the attested key
30//!    (`invalid_signature`);
31//! 7. the attestation is not retracted (`attestation_withdrawn`);
32//! 8. RFC-ACDP-0014 revocations of the attested fingerprint, gathered by
33//!    `agent_id` under [`RevocationPolicy`](crate::RevocationPolicy)
34//!    (`revoked`, or pre-compromise);
35//! 9. the controller's current DID document lists the identity
36//!    (currently authorized), does not (historically authorized), or
37//!    cannot be fetched (`controller_unreachable`, transient).
38//!
39//! The outcome is one of the three existing
40//! [`KeyAuthorization`](crate::KeyAuthorization) values or an error.
41//!
42//! **Known limitation.** Revocation discovery verifies the revocation
43//! bodies it finds through RFC-ACDP-0001 §5.11, so a revocation signed
44//! with a keyless key is never applied here, even one under an `org_wide`
45//! attestation (§9.3 makes those producer-signed). Revocations signed by
46//! a DID-document key of the controller, and registry-attested ones,
47//! apply as usual.
48
49use acdp_types::trust_list::PinState;
50use chrono::{DateTime, Utc};
51
52/// Which registries a consumer accepts as keyless attesters (§11).
53///
54/// A registry is accepted when its DID is in [`Self::attesters`], or when
55/// [`Self::trust_lists`] is set and the keyless-signed body evaluates
56/// `trusted` (RFC-ACDP-0020 §6.2) with the attesting registry as its
57/// receipting registry. The default accepts none, so every keyless-signed
58/// body is refused with `untrusted_attester` (`kl-006` A).
59#[derive(Debug, Clone, Default, PartialEq, Eq)]
60#[non_exhaustive]
61pub struct KeylessPolicy {
62    /// Attesting registries' `did:web` DIDs, compared byte for byte.
63    pub attesters: Vec<String>,
64    /// RFC-ACDP-0020 trust-list acceptance.
65    pub trust_lists: Option<KeylessTrust>,
66}
67
68impl KeylessPolicy {
69    /// Accept the registries `attesters` (their `did:web` DIDs).
70    pub fn attesters<I, S>(attesters: I) -> Self
71    where
72        I: IntoIterator<Item = S>,
73        S: Into<String>,
74    {
75        Self {
76            attesters: attesters.into_iter().map(Into::into).collect(),
77            trust_lists: None,
78        }
79    }
80
81    /// Also accept a registry for a body the trust lists `trust` trust.
82    pub fn with_trust_lists(mut self, trust: KeylessTrust) -> Self {
83        self.trust_lists = Some(trust);
84        self
85    }
86}
87
88/// The RFC-ACDP-0020 inputs for §11 trust-list acceptance: the pins'
89/// §5.4 state at `t` (from [`TrustLists::select`](crate::trust::TrustLists::select))
90/// and, when fetched, the attesting registry's advertised profiles (C).
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct KeylessTrust {
93    /// The pin states at `t`.
94    pub pins: Vec<PinState>,
95    /// The evaluation time.
96    pub t: DateTime<Utc>,
97    /// The attesting registry's capabilities `profiles`, if fetched.
98    pub capabilities: Option<Vec<String>>,
99}
100
101impl KeylessTrust {
102    /// The state of `lists` at `t`.
103    pub fn at(
104        lists: &crate::trust::TrustLists,
105        t: DateTime<Utc>,
106        capabilities: Option<Vec<String>>,
107    ) -> Self {
108        Self {
109            pins: lists.select(t),
110            t,
111            capabilities,
112        }
113    }
114}
115
116#[cfg(feature = "keyless")]
117pub(crate) use verify::verify;
118
119#[cfg(feature = "keyless")]
120mod verify {
121    use super::KeylessPolicy;
122    use crate::receipt::{verify_receipt_placed, PlacedReceipt};
123    use crate::registry::RegistryClient;
124    use crate::verified::{KeyAuthorization, KeyPhases, RevocationPolicy};
125    use acdp_did::web::WebResolver;
126    use acdp_primitives::error::AcdpError;
127    use acdp_types::body::{Body, FullContext};
128    use acdp_types::keyless::{
129        classify, decide_until_identity, IdentityNow, KeyAttestation, KeyAttestationRef,
130        KeylessFacts, KeylessReason, KeylessStatus, ListedIdentities, ReceiptFacts,
131    };
132    use acdp_types::primitives::CtxId;
133    use acdp_types::trust_list::{evaluate, ReceiptInput, TrustOutcome, TrustSubject};
134    use acdp_verify::Verifier;
135    use std::time::Instant;
136
137    /// A verified attestation (§8.1 step 1).
138    struct Attestation {
139        metadata: KeyAttestation,
140        /// The attesting registry's DID (the attestation's `agent_id`).
141        registry: String,
142        retracted: bool,
143    }
144
145    fn invalid(e: impl std::fmt::Display) -> AcdpError {
146        KeylessReason::AttestationInvalid.error(e)
147    }
148
149    /// §8.1 step 1. Transient failures propagate unchanged (retry later);
150    /// every other failure is `attestation_invalid`.
151    async fn attestation(
152        client: &RegistryClient,
153        resolver: &WebResolver,
154        reference: &KeyAttestationRef,
155        serving_authority: &str,
156        since: Instant,
157    ) -> Result<Attestation, AcdpError> {
158        // §6.3 step 2: a registry accepts only attestations it minted, so
159        // the attestation of a body it serves is its own.
160        if reference.ctx_id.authority() != serving_authority {
161            return Err(invalid(format!(
162                "key_attestation.ctx_id '{}' is not on the serving registry '{serving_authority}'",
163                reference.ctx_id
164            )));
165        }
166        let ctx = match client.retrieve(&reference.ctx_id).await {
167            Ok(ctx) => ctx,
168            Err(e) if e.is_transient() => return Err(e),
169            Err(e) => return Err(invalid(format!("fetching the attestation: {e}"))),
170        };
171        let checked = check_attestation(resolver, &ctx, reference, serving_authority, since).await;
172        match checked {
173            Ok(metadata) => Ok(Attestation {
174                metadata,
175                registry: ctx.body.agent_id.as_str().to_owned(),
176                retracted: ctx.registry_state.status.is_retracted(),
177            }),
178            Err(e) if e.is_transient() => Err(e),
179            Err(e) => Err(invalid(e)),
180        }
181    }
182
183    async fn check_attestation(
184        resolver: &WebResolver,
185        ctx: &FullContext,
186        reference: &KeyAttestationRef,
187        serving_authority: &str,
188        since: Instant,
189    ) -> Result<KeyAttestation, AcdpError> {
190        acdp_verify::verify_ctx_id_binding(ctx.body.ctx_id.as_str(), reference.ctx_id.as_str())?;
191        acdp_verify::verify_context_hash(ctx)?;
192        if ctx.body.content_hash != reference.content_hash {
193            return Err(AcdpError::HashMismatch {
194                stored: reference.content_hash.clone(),
195                recomputed: ctx.body.content_hash.clone(),
196            });
197        }
198        acdp_validation::validate_body(&ctx.body)?;
199        let metadata = KeyAttestation::from_body(&ctx.body)?;
200        let verifier = Verifier::new(resolver).fresh_since(since);
201        verifier.verify_body_signature(&ctx.body).await?;
202        let receipt = ctx.registry_receipt.as_ref().ok_or_else(|| {
203            AcdpError::InvalidReceipt("the attestation carries no receipt".into())
204        })?;
205        let registry_fp = acdp_crypto::fingerprint::fingerprint_for_key_id_since(
206            &ctx.body.signature.key_id,
207            &ctx.body.signature.algorithm,
208            resolver,
209            since,
210        )
211        .await?;
212        verify_receipt_placed(
213            receipt,
214            &reference.ctx_id,
215            &ctx.body,
216            &ctx.body.content_hash,
217            &registry_fp,
218            serving_authority,
219            resolver,
220            since,
221        )
222        .await?;
223        Ok(metadata)
224    }
225
226    /// §11: does the consumer accept `registry` as the attester of `body`?
227    fn accepted(
228        policy: &KeylessPolicy,
229        registry: &str,
230        body: &Body,
231        placed: Option<&PlacedReceipt>,
232    ) -> bool {
233        if policy.attesters.iter().any(|a| a == registry) {
234            return true;
235        }
236        let (Some(trust), Some(placed)) = (&policy.trust_lists, placed) else {
237            return false;
238        };
239        if placed.receipt.registry_did != registry {
240            return false;
241        }
242        let ty = serde_json::to_value(&body.context_type)
243            .ok()
244            .and_then(|v| v.as_str().map(str::to_owned))
245            .unwrap_or_default();
246        let receipt = ReceiptInput {
247            registry_did: placed.receipt.registry_did.clone(),
248            registry_key_fingerprint: placed.registry_key_fingerprint.clone(),
249        };
250        let node = evaluate(
251            &TrustSubject::of(body, &ty),
252            &trust.pins,
253            trust.t,
254            Some(&receipt),
255            trust.capabilities.as_deref(),
256        );
257        node.outcome == TrustOutcome::Trusted
258    }
259
260    /// §8.1 step 9 against a document fetched for this verification.
261    async fn identity_now(
262        resolver: &WebResolver,
263        attestation: &KeyAttestation,
264        since: Instant,
265    ) -> IdentityNow {
266        let listed = |doc: &acdp_did::DidDocument| {
267            ListedIdentities::of(doc)
268                .find(&attestation.identity)
269                .is_some()
270        };
271        match resolver
272            .resolve_until(&attestation.controller, since, listed)
273            .await
274        {
275            Ok(r) if listed(&r.document) => IdentityNow::Listed,
276            // Absence is final only against a fresh document.
277            Ok(r) if r.fresh => IdentityNow::NotListed,
278            Ok(_) | Err(_) => IdentityNow::Unreachable,
279        }
280    }
281
282    fn refusal(reason: KeylessReason, body: &Body, att: &KeyAttestation) -> AcdpError {
283        let key_id = &body.signature.key_id;
284        let detail = match reason {
285            KeylessReason::UntrustedAttester => format!(
286                "the attesting registry of '{key_id}' is not an accepted attester \
287                 (RFC-ACDP-0021 §11; configure VerificationPolicy.keyless)"
288            ),
289            KeylessReason::AttestationMismatch => format!(
290                "agent_id, key_id or algorithm differ from the attestation of '{}'",
291                att.key_id
292            ),
293            KeylessReason::Unplaceable => "no verified receipt from the attesting registry \
294                 names the attested key (RFC-ACDP-0021 §8.1 step 4)"
295                .to_string(),
296            KeylessReason::OutsideAttestationWindow => format!(
297                "the receipt places the signature outside [{}, {})",
298                acdp_primitives::time::fmt_rfc3339_ms(att.not_before),
299                acdp_primitives::time::fmt_rfc3339_ms(att.not_after)
300            ),
301            KeylessReason::InvalidSignature => {
302                format!("the signature does not verify under the key attested for '{key_id}'")
303            }
304            KeylessReason::AttestationWithdrawn => {
305                "the attesting registry retracted the attestation (RFC-ACDP-0021 §9.2)".to_string()
306            }
307            KeylessReason::ControllerUnreachable => format!(
308                "the DID document of '{}' could not be fetched fresh",
309                att.controller
310            ),
311            other => format!("keyless verification refused: {other}"),
312        };
313        reason.error(detail)
314    }
315
316    /// RFC-ACDP-0021 §8.1 for a keyless-signed body whose content hash
317    /// the caller has already recomputed.
318    #[allow(clippy::too_many_arguments)]
319    pub(crate) async fn verify(
320        client: &RegistryClient,
321        resolver: &WebResolver,
322        ctx: &FullContext,
323        expected_ctx_id: &CtxId,
324        serving_authority: &str,
325        policy: &KeylessPolicy,
326        revocations: &RevocationPolicy,
327        since: Instant,
328    ) -> Result<KeyPhases, AcdpError> {
329        let body = &ctx.body;
330        let reference = body.key_attestation()?.ok_or_else(|| {
331            KeylessReason::AttestationMissing.error(format!(
332                "signature.key_id '{}' is a keyless key id but the body has no key_attestation",
333                body.signature.key_id
334            ))
335        })?;
336
337        // Step 1 decides alone: nothing below can be evaluated without it.
338        let att = attestation(client, resolver, &reference, serving_authority, since).await?;
339        let meta = &att.metadata;
340
341        // Step 4's receipt is verified up front: step 2's trust-list path
342        // reads the receipting registry from it. The order of the
343        // decisions is still §8.1's (`classify`).
344        let placed = match &ctx.registry_receipt {
345            None => None,
346            Some(value) => Some(
347                verify_receipt_placed(
348                    value,
349                    expected_ctx_id,
350                    body,
351                    &body.content_hash,
352                    &meta.key_fingerprint,
353                    serving_authority,
354                    resolver,
355                    since,
356                )
357                .await,
358            ),
359        };
360        let placed_ok = placed.as_ref().and_then(|p| p.as_ref().ok());
361        let receipt = placed.as_ref().map(|p| match p {
362            Ok(p) => ReceiptFacts {
363                verified: true,
364                registry_is_attester: p.receipt.registry_did == att.registry,
365                fingerprint_matches: p.receipt.key_fingerprint == meta.key_fingerprint,
366                created_at: p.receipt.created_at,
367            },
368            Err(_) => ReceiptFacts {
369                verified: false,
370                registry_is_attester: false,
371                fingerprint_matches: false,
372                created_at: meta.not_before,
373            },
374        });
375
376        // Step 8's input: revocations of the attested fingerprint,
377        // discovered by agent_id (§9.3), placed by the receipt.
378        let gathered =
379            crate::revocation_verdict::gather(client, resolver, &body.agent_id, revocations).await;
380        let assessment = crate::revocation_verdict::decide(
381            gathered,
382            Some(&meta.key_fingerprint),
383            placed_ok.map(|p| (p.receipt.created_at, p.key_status)),
384            revocations.accept_historical_receipt_key_for_pre_t,
385        );
386        let compromised_since = match &assessment.verdict {
387            crate::RevocationVerdict::Revoked { boundary, .. } => vec![*boundary],
388            _ => Vec::new(),
389        };
390
391        let facts = KeylessFacts {
392            attestation_valid: true,
393            attester_accepted: accepted(policy, &att.registry, body, placed_ok),
394            bindings_ok: meta.binds(&body.agent_id, &body.signature),
395            receipt,
396            not_before: meta.not_before,
397            not_after: meta.not_after,
398            signature_valid: meta
399                .public_key_jwk
400                .verify(
401                    &body.signature.algorithm,
402                    &body.signature.value,
403                    body.content_hash.as_str(),
404                )
405                .is_ok(),
406            retracted: att.retracted,
407            compromised_since,
408        };
409
410        // Step 9 only when steps 1–8 leave it to decide.
411        let now = match decide_until_identity(&facts) {
412            Ok(None) => identity_now(resolver, meta, since).await,
413            _ => IdentityNow::Unreachable,
414        };
415        let status = match classify(&facts, now) {
416            Ok(status) => status,
417            // The revocation refusal keeps its typed form.
418            Err(KeylessReason::Revoked) => {
419                return Err(match assessment.enforce() {
420                    Err(e) => e,
421                    Ok(_) => refusal(KeylessReason::Revoked, body, meta),
422                })
423            }
424            Err(reason) => return Err(refusal(reason, body, meta)),
425        };
426        // An unfinished discovery fails closed under its policy, and the
427        // strict receipt-key profile can still refuse a pre-compromise
428        // placement.
429        let revocation = assessment.enforce()?;
430        let placed = placed_ok.expect("classify accepted only with a verified receipt");
431        Ok(KeyPhases {
432            key_status: match status {
433                KeylessStatus::CurrentlyAuthorized => KeyAuthorization::CurrentlyAuthorized,
434                KeylessStatus::HistoricallyAuthorized => KeyAuthorization::HistoricallyAuthorized,
435                KeylessStatus::HistoricallyAuthorizedPreCompromise => {
436                    KeyAuthorization::HistoricallyAuthorizedPreCompromise
437                }
438            },
439            verified_receipt: Some(placed.receipt.clone()),
440            receipt_key_status: Some(placed.key_status),
441            receipt_registry_key_fingerprint: Some(placed.registry_key_fingerprint.clone()),
442            receipt_verdict: Some(Ok(())),
443            revocation,
444        })
445    }
446}