1use acdp_types::trust_list::PinState;
52use chrono::{DateTime, Utc};
53
54#[derive(Debug, Clone, Default, PartialEq, Eq)]
62#[non_exhaustive]
63pub struct KeylessPolicy {
64 pub attesters: Vec<String>,
66 pub trust_lists: Option<KeylessTrust>,
68}
69
70impl KeylessPolicy {
71 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 pub fn with_trust_lists(mut self, trust: KeylessTrust) -> Self {
85 self.trust_lists = Some(trust);
86 self
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct KeylessTrust {
95 pub pins: Vec<PinState>,
97 pub t: DateTime<Utc>,
99 pub capabilities: Option<Vec<String>>,
101}
102
103impl KeylessTrust {
104 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#[derive(Debug, Clone)]
126pub(crate) struct Unevaluated {
127 pub fingerprint: String,
128 pub detail: String,
129}
130
131#[derive(Debug, Default)]
133pub(crate) struct KeylessRevocations {
134 pub applied: Vec<acdp_types::revocation::KeyRevocation>,
137 pub hints: Vec<acdp_types::revocation::KeyRevocation>,
140 pub unevaluated: Vec<Unevaluated>,
141}
142
143pub(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 struct Attestation {
223 metadata: KeyAttestation,
224 registry: String,
226 retracted: bool,
227 }
228
229 fn invalid(e: impl std::fmt::Display) -> AcdpError {
230 KeylessReason::AttestationInvalid.error(e)
231 }
232
233 async fn attestation(
236 client: &RegistryClient,
237 resolver: &WebResolver,
238 reference: &KeyAttestationRef,
239 serving_authority: &str,
240 since: Instant,
241 ) -> Result<Attestation, AcdpError> {
242 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 ®istry_fp,
302 serving_authority,
303 resolver,
304 since,
305 )
306 .await?;
307 Ok(metadata)
308 }
309
310 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 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 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 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 let att = attestation(client, resolver, &reference, serving_authority, since).await?;
429 let meta = &att.metadata;
430
431 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 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 #[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 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 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 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 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}