1use acdp_types::trust_list::PinState;
50use chrono::{DateTime, Utc};
51
52#[derive(Debug, Clone, Default, PartialEq, Eq)]
60#[non_exhaustive]
61pub struct KeylessPolicy {
62 pub attesters: Vec<String>,
64 pub trust_lists: Option<KeylessTrust>,
66}
67
68impl KeylessPolicy {
69 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 pub fn with_trust_lists(mut self, trust: KeylessTrust) -> Self {
83 self.trust_lists = Some(trust);
84 self
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct KeylessTrust {
93 pub pins: Vec<PinState>,
95 pub t: DateTime<Utc>,
97 pub capabilities: Option<Vec<String>>,
99}
100
101impl KeylessTrust {
102 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 struct Attestation {
139 metadata: KeyAttestation,
140 registry: String,
142 retracted: bool,
143 }
144
145 fn invalid(e: impl std::fmt::Display) -> AcdpError {
146 KeylessReason::AttestationInvalid.error(e)
147 }
148
149 async fn attestation(
152 client: &RegistryClient,
153 resolver: &WebResolver,
154 reference: &KeyAttestationRef,
155 serving_authority: &str,
156 since: Instant,
157 ) -> Result<Attestation, AcdpError> {
158 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 ®istry_fp,
218 serving_authority,
219 resolver,
220 since,
221 )
222 .await?;
223 Ok(metadata)
224 }
225
226 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 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 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 #[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 let att = attestation(client, resolver, &reference, serving_authority, since).await?;
339 let meta = &att.metadata;
340
341 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 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 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 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 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}