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//! **Keyless-signed revocations (§9.3).** Revocation discovery sets a
43//! candidate signed by a keyless key aside and verifies its signer by
44//! §8.1 steps 1–7 under this same [`KeylessPolicy`]. One under an
45//! `org_wide` attestation is producer-signed and applies; one under a
46//! non-org-wide attestation is only a hint (`kl-005` G). One that cannot
47//! be evaluated (no `keyless` feature, an unaccepted attester, an
48//! attestation that does not verify) makes the verdict for the key it
49//! names `unknown`, never not-revoked.
50
51use acdp_types::trust_list::PinState;
52use chrono::{DateTime, Utc};
53
54/// Which registries a consumer accepts as keyless attesters (§11).
55///
56/// A registry is accepted when its DID is in [`Self::attesters`], or when
57/// [`Self::trust_lists`] is set and the keyless-signed body evaluates
58/// `trusted` (RFC-ACDP-0020 §6.2) with the attesting registry as its
59/// receipting registry. The default accepts none, so every keyless-signed
60/// body is refused with `untrusted_attester` (`kl-006` A).
61#[derive(Debug, Clone, Default, PartialEq, Eq)]
62#[non_exhaustive]
63pub struct KeylessPolicy {
64    /// Attesting registries' `did:web` DIDs, compared byte for byte.
65    pub attesters: Vec<String>,
66    /// RFC-ACDP-0020 trust-list acceptance.
67    pub trust_lists: Option<KeylessTrust>,
68}
69
70impl KeylessPolicy {
71    /// Accept the registries `attesters` (their `did:web` DIDs).
72    pub fn attesters<I, S>(attesters: I) -> Self
73    where
74        I: IntoIterator<Item = S>,
75        S: Into<String>,
76    {
77        Self {
78            attesters: attesters.into_iter().map(Into::into).collect(),
79            trust_lists: None,
80        }
81    }
82
83    /// Also accept a registry for a body the trust lists `trust` trust.
84    pub fn with_trust_lists(mut self, trust: KeylessTrust) -> Self {
85        self.trust_lists = Some(trust);
86        self
87    }
88}
89
90/// The RFC-ACDP-0020 inputs for §11 trust-list acceptance: the pins'
91/// §5.4 state at `t` (from [`TrustLists::select`](crate::trust::TrustLists::select))
92/// and, when fetched, the attesting registry's advertised profiles (C).
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct KeylessTrust {
95    /// The pin states at `t`.
96    pub pins: Vec<PinState>,
97    /// The evaluation time.
98    pub t: DateTime<Utc>,
99    /// The attesting registry's capabilities `profiles`, if fetched.
100    pub capabilities: Option<Vec<String>>,
101}
102
103impl KeylessTrust {
104    /// The state of `lists` at `t`.
105    pub fn at(
106        lists: &crate::trust::TrustLists,
107        t: DateTime<Utc>,
108        capabilities: Option<Vec<String>>,
109    ) -> Self {
110        Self {
111            pins: lists.select(t),
112            t,
113            capabilities,
114        }
115    }
116}
117
118#[cfg(feature = "keyless")]
119pub(crate) use verify::{verify, verify_revocation_signer};
120
121/// A keyless-signed revocation candidate that could not be evaluated: no
122/// `keyless` feature, an unaccepted attester, or an attestation that does
123/// not verify. Naming the signing key it makes the verdict `unknown`
124/// (RFC-ACDP-0014 §7: unknown outranks not-revoked and pre-compromise).
125#[derive(Debug, Clone)]
126pub(crate) struct Unevaluated {
127    pub fingerprint: String,
128    pub detail: String,
129}
130
131/// What became of the keyless-signed candidates discovery set aside.
132#[derive(Debug, Default)]
133pub(crate) struct KeylessRevocations {
134    /// Signed under an `org_wide` attestation: producer-signed, applied
135    /// (§9.3).
136    pub applied: Vec<acdp_types::revocation::KeyRevocation>,
137    /// Signed under a non-org-wide attestation: reported, never applied
138    /// (`kl-005` G).
139    pub hints: Vec<acdp_types::revocation::KeyRevocation>,
140    pub unevaluated: Vec<Unevaluated>,
141}
142
143/// Evaluate keyless-signed revocation candidates for `agent_id`
144/// (RFC-ACDP-0021 §9.3). A candidate whose own shape is invalid, that
145/// does not speak for `agent_id`, or that revokes its own signing key is
146/// dropped, as for any revocation.
147pub(crate) async fn evaluate_revocations(
148    client: &crate::registry::RegistryClient,
149    resolver: &acdp_did::web::WebResolver,
150    agent_id: &acdp_types::primitives::AgentDid,
151    deferred: &[acdp_types::body::FullContext],
152    policy: &KeylessPolicy,
153) -> KeylessRevocations {
154    use acdp_types::revocation::{KeyRevocation, RevocationTrustClass};
155    let mut out = KeylessRevocations::default();
156    #[cfg(feature = "keyless")]
157    let since = std::time::Instant::now();
158    for ctx in deferred {
159        let Ok(rev) = KeyRevocation::from_body(&ctx.body) else {
160            continue;
161        };
162        if rev.publisher != *agent_id || rev.trust_class != RevocationTrustClass::ProducerSigned {
163            continue;
164        }
165        #[cfg(feature = "keyless")]
166        let outcome = verify_revocation_signer(client, resolver, ctx, policy, since).await;
167        #[cfg(not(feature = "keyless"))]
168        let outcome: Result<
169            acdp_types::keyless::KeyAttestation,
170            acdp_primitives::error::AcdpError,
171        > = {
172            let _ = (client, resolver, policy);
173            Err(acdp_primitives::error::AcdpError::KeyResolution(
174                "built without the `keyless` feature (RFC-ACDP-0021)".into(),
175            ))
176        };
177        match outcome {
178            Ok(att) => {
179                if rev.check_not_self_signed(&att.key_fingerprint).is_err() {
180                    continue;
181                }
182                if att.org_wide {
183                    out.applied.push(rev);
184                } else {
185                    out.hints.push(rev);
186                }
187            }
188            Err(e) => out.unevaluated.push(Unevaluated {
189                fingerprint: rev.revoked_key_fingerprint.clone(),
190                detail: format!(
191                    "the revocation {} of {} is signed by the keyless key {} and could not be \
192                     evaluated: {e}",
193                    ctx.body.ctx_id.as_str(),
194                    rev.revoked_key_fingerprint,
195                    ctx.body.signature.key_id
196                ),
197            }),
198        }
199    }
200    out
201}
202
203#[cfg(feature = "keyless")]
204mod verify {
205    use super::KeylessPolicy;
206    use crate::receipt::{verify_receipt_placed, PlacedReceipt};
207    use crate::registry::RegistryClient;
208    use crate::verified::{KeyAuthorization, KeyPhases, RevocationPolicy};
209    use acdp_did::web::WebResolver;
210    use acdp_primitives::error::AcdpError;
211    use acdp_types::body::{Body, FullContext};
212    use acdp_types::keyless::{
213        classify, decide_until_identity, IdentityNow, KeyAttestation, KeyAttestationRef,
214        KeylessFacts, KeylessReason, KeylessStatus, ListedIdentities, ReceiptFacts,
215    };
216    use acdp_types::primitives::CtxId;
217    use acdp_types::trust_list::{evaluate, ReceiptInput, TrustOutcome, TrustSubject};
218    use acdp_verify::Verifier;
219    use std::time::Instant;
220
221    /// A verified attestation (§8.1 step 1).
222    struct Attestation {
223        metadata: KeyAttestation,
224        /// The attesting registry's DID (the attestation's `agent_id`).
225        registry: String,
226        retracted: bool,
227    }
228
229    fn invalid(e: impl std::fmt::Display) -> AcdpError {
230        KeylessReason::AttestationInvalid.error(e)
231    }
232
233    /// §8.1 step 1. Transient failures propagate unchanged (retry later);
234    /// every other failure is `attestation_invalid`.
235    async fn attestation(
236        client: &RegistryClient,
237        resolver: &WebResolver,
238        reference: &KeyAttestationRef,
239        serving_authority: &str,
240        since: Instant,
241    ) -> Result<Attestation, AcdpError> {
242        // §6.3 step 2: a registry accepts only attestations it minted, so
243        // the attestation of a body it serves is its own.
244        if reference.ctx_id.authority() != serving_authority {
245            return Err(invalid(format!(
246                "key_attestation.ctx_id '{}' is not on the serving registry '{serving_authority}'",
247                reference.ctx_id
248            )));
249        }
250        let ctx = match client.retrieve(&reference.ctx_id).await {
251            Ok(ctx) => ctx,
252            Err(e) if e.is_transient() => return Err(e),
253            Err(e) => return Err(invalid(format!("fetching the attestation: {e}"))),
254        };
255        let checked = check_attestation(resolver, &ctx, reference, serving_authority, since).await;
256        match checked {
257            Ok(metadata) => Ok(Attestation {
258                metadata,
259                registry: ctx.body.agent_id.as_str().to_owned(),
260                retracted: ctx.registry_state.status.is_retracted(),
261            }),
262            Err(e) if e.is_transient() => Err(e),
263            Err(e) => Err(invalid(e)),
264        }
265    }
266
267    async fn check_attestation(
268        resolver: &WebResolver,
269        ctx: &FullContext,
270        reference: &KeyAttestationRef,
271        serving_authority: &str,
272        since: Instant,
273    ) -> Result<KeyAttestation, AcdpError> {
274        acdp_verify::verify_ctx_id_binding(ctx.body.ctx_id.as_str(), reference.ctx_id.as_str())?;
275        acdp_verify::verify_context_hash(ctx)?;
276        if ctx.body.content_hash != reference.content_hash {
277            return Err(AcdpError::HashMismatch {
278                stored: reference.content_hash.clone(),
279                recomputed: ctx.body.content_hash.clone(),
280            });
281        }
282        acdp_validation::validate_body(&ctx.body)?;
283        let metadata = KeyAttestation::from_body(&ctx.body)?;
284        let verifier = Verifier::new(resolver).fresh_since(since);
285        verifier.verify_body_signature(&ctx.body).await?;
286        let receipt = ctx.registry_receipt.as_ref().ok_or_else(|| {
287            AcdpError::InvalidReceipt("the attestation carries no receipt".into())
288        })?;
289        let registry_fp = acdp_crypto::fingerprint::fingerprint_for_key_id_since(
290            &ctx.body.signature.key_id,
291            &ctx.body.signature.algorithm,
292            resolver,
293            since,
294        )
295        .await?;
296        verify_receipt_placed(
297            receipt,
298            &reference.ctx_id,
299            &ctx.body,
300            &ctx.body.content_hash,
301            &registry_fp,
302            serving_authority,
303            resolver,
304            since,
305        )
306        .await?;
307        Ok(metadata)
308    }
309
310    /// §11: does the consumer accept `registry` as the attester of `body`?
311    fn accepted(
312        policy: &KeylessPolicy,
313        registry: &str,
314        body: &Body,
315        placed: Option<&PlacedReceipt>,
316    ) -> bool {
317        if policy.attesters.iter().any(|a| a == registry) {
318            return true;
319        }
320        let (Some(trust), Some(placed)) = (&policy.trust_lists, placed) else {
321            return false;
322        };
323        if placed.receipt.registry_did != registry {
324            return false;
325        }
326        let ty = serde_json::to_value(&body.context_type)
327            .ok()
328            .and_then(|v| v.as_str().map(str::to_owned))
329            .unwrap_or_default();
330        let receipt = ReceiptInput {
331            registry_did: placed.receipt.registry_did.clone(),
332            registry_key_fingerprint: placed.registry_key_fingerprint.clone(),
333        };
334        let node = evaluate(
335            &TrustSubject::of(body, &ty),
336            &trust.pins,
337            trust.t,
338            Some(&receipt),
339            trust.capabilities.as_deref(),
340        );
341        node.outcome == TrustOutcome::Trusted
342    }
343
344    /// §8.1 step 9 against a document fetched for this verification.
345    async fn identity_now(
346        resolver: &WebResolver,
347        attestation: &KeyAttestation,
348        since: Instant,
349    ) -> IdentityNow {
350        let listed = |doc: &acdp_did::DidDocument| {
351            ListedIdentities::of(doc)
352                .find(&attestation.identity)
353                .is_some()
354        };
355        match resolver
356            .resolve_until(&attestation.controller, since, listed)
357            .await
358        {
359            Ok(r) if listed(&r.document) => IdentityNow::Listed,
360            // Absence is final only against a fresh document.
361            Ok(r) if r.fresh => IdentityNow::NotListed,
362            Ok(_) | Err(_) => IdentityNow::Unreachable,
363        }
364    }
365
366    fn refusal(reason: KeylessReason, body: &Body, att: &KeyAttestation) -> AcdpError {
367        let key_id = &body.signature.key_id;
368        let detail = match reason {
369            KeylessReason::UntrustedAttester => format!(
370                "the attesting registry of '{key_id}' is not an accepted attester \
371                 (RFC-ACDP-0021 §11; configure VerificationPolicy.keyless)"
372            ),
373            KeylessReason::AttestationMismatch => format!(
374                "agent_id, key_id or algorithm differ from the attestation of '{}'",
375                att.key_id
376            ),
377            KeylessReason::Unplaceable => "no verified receipt from the attesting registry \
378                 names the attested key (RFC-ACDP-0021 §8.1 step 4)"
379                .to_string(),
380            KeylessReason::OutsideAttestationWindow => format!(
381                "the receipt places the signature outside [{}, {})",
382                acdp_primitives::time::fmt_rfc3339_ms(att.not_before),
383                acdp_primitives::time::fmt_rfc3339_ms(att.not_after)
384            ),
385            KeylessReason::InvalidSignature => {
386                format!("the signature does not verify under the key attested for '{key_id}'")
387            }
388            KeylessReason::AttestationWithdrawn => {
389                "the attesting registry retracted the attestation (RFC-ACDP-0021 §9.2)".to_string()
390            }
391            KeylessReason::ControllerUnreachable => format!(
392                "the DID document of '{}' could not be fetched fresh",
393                att.controller
394            ),
395            other => format!("keyless verification refused: {other}"),
396        };
397        reason.error(detail)
398    }
399
400    /// §8.1 steps 1–7 for a keyless-signed body: the attestation, the
401    /// placed receipt, and every fact but the revocations (step 8) and
402    /// the current identity (step 9).
403    struct Signer {
404        att: Attestation,
405        placed: Option<Result<PlacedReceipt, AcdpError>>,
406        facts: KeylessFacts,
407    }
408
409    #[allow(clippy::too_many_arguments)]
410    async fn signer(
411        client: &RegistryClient,
412        resolver: &WebResolver,
413        ctx: &FullContext,
414        expected_ctx_id: &CtxId,
415        serving_authority: &str,
416        policy: &KeylessPolicy,
417        since: Instant,
418    ) -> Result<Signer, AcdpError> {
419        let body = &ctx.body;
420        let reference = body.key_attestation()?.ok_or_else(|| {
421            KeylessReason::AttestationMissing.error(format!(
422                "signature.key_id '{}' is a keyless key id but the body has no key_attestation",
423                body.signature.key_id
424            ))
425        })?;
426
427        // Step 1 decides alone: nothing below can be evaluated without it.
428        let att = attestation(client, resolver, &reference, serving_authority, since).await?;
429        let meta = &att.metadata;
430
431        // Step 4's receipt is verified up front: step 2's trust-list path
432        // reads the receipting registry from it. The order of the
433        // decisions is still §8.1's (`classify`).
434        let placed = match &ctx.registry_receipt {
435            None => None,
436            Some(value) => Some(
437                verify_receipt_placed(
438                    value,
439                    expected_ctx_id,
440                    body,
441                    &body.content_hash,
442                    &meta.key_fingerprint,
443                    serving_authority,
444                    resolver,
445                    since,
446                )
447                .await,
448            ),
449        };
450        let placed_ok = placed.as_ref().and_then(|p| p.as_ref().ok());
451        let receipt = placed.as_ref().map(|p| match p {
452            Ok(p) => ReceiptFacts {
453                verified: true,
454                registry_is_attester: p.receipt.registry_did == att.registry,
455                fingerprint_matches: p.receipt.key_fingerprint == meta.key_fingerprint,
456                created_at: p.receipt.created_at,
457            },
458            Err(_) => ReceiptFacts {
459                verified: false,
460                registry_is_attester: false,
461                fingerprint_matches: false,
462                created_at: meta.not_before,
463            },
464        });
465        let facts = KeylessFacts {
466            attestation_valid: true,
467            attester_accepted: accepted(policy, &att.registry, body, placed_ok),
468            bindings_ok: meta.binds(&body.agent_id, &body.signature),
469            receipt,
470            not_before: meta.not_before,
471            not_after: meta.not_after,
472            signature_valid: meta
473                .public_key_jwk
474                .verify(
475                    &body.signature.algorithm,
476                    &body.signature.value,
477                    body.content_hash.as_str(),
478                )
479                .is_ok(),
480            retracted: att.retracted,
481            compromised_since: Vec::new(),
482        };
483        Ok(Signer { att, placed, facts })
484    }
485
486    /// §9.3: verify the keyless signer of a key-revocation candidate set
487    /// aside by discovery, through §8.1 steps 1–7 under the same attester
488    /// policy as bodies. Returns its attestation (the caller applies it
489    /// only when `org_wide`); revocations of the signing key and its
490    /// identity today are not consulted (removal does not reach back,
491    /// §9.1).
492    pub(crate) async fn verify_revocation_signer(
493        client: &RegistryClient,
494        resolver: &WebResolver,
495        ctx: &FullContext,
496        policy: &KeylessPolicy,
497        since: Instant,
498    ) -> Result<KeyAttestation, AcdpError> {
499        let body = &ctx.body;
500        acdp_verify::verify_context_hash(ctx)?;
501        acdp_validation::validate_body(body)?;
502        let serving_authority = client
503            .authority()
504            .unwrap_or_else(|| body.ctx_id.authority().to_string());
505        let s = signer(
506            client,
507            resolver,
508            ctx,
509            &body.ctx_id,
510            &serving_authority,
511            policy,
512            since,
513        )
514        .await?;
515        match decide_until_identity(&s.facts) {
516            Ok(_) => Ok(s.att.metadata),
517            Err(reason) => Err(refusal(reason, body, &s.att.metadata)),
518        }
519    }
520
521    /// RFC-ACDP-0021 §8.1 for a keyless-signed body whose content hash
522    /// the caller has already recomputed.
523    #[allow(clippy::too_many_arguments)]
524    pub(crate) async fn verify(
525        client: &RegistryClient,
526        resolver: &WebResolver,
527        ctx: &FullContext,
528        expected_ctx_id: &CtxId,
529        serving_authority: &str,
530        policy: &KeylessPolicy,
531        revocations: &RevocationPolicy,
532        since: Instant,
533    ) -> Result<KeyPhases, AcdpError> {
534        let body = &ctx.body;
535        let Signer {
536            att,
537            placed,
538            mut facts,
539        } = signer(
540            client,
541            resolver,
542            ctx,
543            expected_ctx_id,
544            serving_authority,
545            policy,
546            since,
547        )
548        .await?;
549        let meta = &att.metadata;
550        let placed_ok = placed.as_ref().and_then(|p| p.as_ref().ok());
551
552        // Step 8's input: revocations of the attested fingerprint,
553        // discovered by agent_id (§9.3), placed by the receipt.
554        let gathered = crate::revocation_verdict::gather(
555            client,
556            resolver,
557            &body.agent_id,
558            revocations,
559            policy,
560        )
561        .await;
562        let assessment = crate::revocation_verdict::decide(
563            gathered,
564            Some(&meta.key_fingerprint),
565            placed_ok.map(|p| (p.receipt.created_at, p.key_status)),
566            revocations.accept_historical_receipt_key_for_pre_t,
567        );
568        let compromised_since = match &assessment.verdict {
569            crate::RevocationVerdict::Revoked { boundary, .. } => vec![*boundary],
570            _ => Vec::new(),
571        };
572
573        facts.compromised_since = compromised_since;
574
575        // Step 9 only when steps 1–8 leave it to decide.
576        let now = match decide_until_identity(&facts) {
577            Ok(None) => identity_now(resolver, meta, since).await,
578            _ => IdentityNow::Unreachable,
579        };
580        let status = match classify(&facts, now) {
581            Ok(status) => status,
582            // The revocation refusal keeps its typed form.
583            Err(KeylessReason::Revoked) => {
584                return Err(match assessment.enforce() {
585                    Err(e) => e,
586                    Ok(_) => refusal(KeylessReason::Revoked, body, meta),
587                })
588            }
589            Err(reason) => return Err(refusal(reason, body, meta)),
590        };
591        // An unfinished discovery fails closed under its policy, and the
592        // strict receipt-key profile can still refuse a pre-compromise
593        // placement.
594        let revocation = assessment.enforce()?;
595        let placed = placed_ok.expect("classify accepted only with a verified receipt");
596        Ok(KeyPhases {
597            key_status: match status {
598                KeylessStatus::CurrentlyAuthorized => KeyAuthorization::CurrentlyAuthorized,
599                KeylessStatus::HistoricallyAuthorized => KeyAuthorization::HistoricallyAuthorized,
600                KeylessStatus::HistoricallyAuthorizedPreCompromise => {
601                    KeyAuthorization::HistoricallyAuthorizedPreCompromise
602                }
603            },
604            verified_receipt: Some(placed.receipt.clone()),
605            receipt_key_status: Some(placed.key_status),
606            receipt_registry_key_fingerprint: Some(placed.registry_key_fingerprint.clone()),
607            receipt_verdict: Some(Ok(())),
608            revocation,
609        })
610    }
611}