use acdp_types::trust_list::PinState;
use chrono::{DateTime, Utc};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct KeylessPolicy {
pub attesters: Vec<String>,
pub trust_lists: Option<KeylessTrust>,
}
impl KeylessPolicy {
pub fn attesters<I, S>(attesters: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
attesters: attesters.into_iter().map(Into::into).collect(),
trust_lists: None,
}
}
pub fn with_trust_lists(mut self, trust: KeylessTrust) -> Self {
self.trust_lists = Some(trust);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeylessTrust {
pub pins: Vec<PinState>,
pub t: DateTime<Utc>,
pub capabilities: Option<Vec<String>>,
}
impl KeylessTrust {
pub fn at(
lists: &crate::trust::TrustLists,
t: DateTime<Utc>,
capabilities: Option<Vec<String>>,
) -> Self {
Self {
pins: lists.select(t),
t,
capabilities,
}
}
}
#[cfg(feature = "keyless")]
pub(crate) use verify::verify;
#[cfg(feature = "keyless")]
mod verify {
use super::KeylessPolicy;
use crate::receipt::{verify_receipt_placed, PlacedReceipt};
use crate::registry::RegistryClient;
use crate::verified::{KeyAuthorization, KeyPhases, RevocationPolicy};
use acdp_did::web::WebResolver;
use acdp_primitives::error::AcdpError;
use acdp_types::body::{Body, FullContext};
use acdp_types::keyless::{
classify, decide_until_identity, IdentityNow, KeyAttestation, KeyAttestationRef,
KeylessFacts, KeylessReason, KeylessStatus, ListedIdentities, ReceiptFacts,
};
use acdp_types::primitives::CtxId;
use acdp_types::trust_list::{evaluate, ReceiptInput, TrustOutcome, TrustSubject};
use acdp_verify::Verifier;
use std::time::Instant;
struct Attestation {
metadata: KeyAttestation,
registry: String,
retracted: bool,
}
fn invalid(e: impl std::fmt::Display) -> AcdpError {
KeylessReason::AttestationInvalid.error(e)
}
async fn attestation(
client: &RegistryClient,
resolver: &WebResolver,
reference: &KeyAttestationRef,
serving_authority: &str,
since: Instant,
) -> Result<Attestation, AcdpError> {
if reference.ctx_id.authority() != serving_authority {
return Err(invalid(format!(
"key_attestation.ctx_id '{}' is not on the serving registry '{serving_authority}'",
reference.ctx_id
)));
}
let ctx = match client.retrieve(&reference.ctx_id).await {
Ok(ctx) => ctx,
Err(e) if e.is_transient() => return Err(e),
Err(e) => return Err(invalid(format!("fetching the attestation: {e}"))),
};
let checked = check_attestation(resolver, &ctx, reference, serving_authority, since).await;
match checked {
Ok(metadata) => Ok(Attestation {
metadata,
registry: ctx.body.agent_id.as_str().to_owned(),
retracted: ctx.registry_state.status.is_retracted(),
}),
Err(e) if e.is_transient() => Err(e),
Err(e) => Err(invalid(e)),
}
}
async fn check_attestation(
resolver: &WebResolver,
ctx: &FullContext,
reference: &KeyAttestationRef,
serving_authority: &str,
since: Instant,
) -> Result<KeyAttestation, AcdpError> {
acdp_verify::verify_ctx_id_binding(ctx.body.ctx_id.as_str(), reference.ctx_id.as_str())?;
acdp_verify::verify_context_hash(ctx)?;
if ctx.body.content_hash != reference.content_hash {
return Err(AcdpError::HashMismatch {
stored: reference.content_hash.clone(),
recomputed: ctx.body.content_hash.clone(),
});
}
acdp_validation::validate_body(&ctx.body)?;
let metadata = KeyAttestation::from_body(&ctx.body)?;
let verifier = Verifier::new(resolver).fresh_since(since);
verifier.verify_body_signature(&ctx.body).await?;
let receipt = ctx.registry_receipt.as_ref().ok_or_else(|| {
AcdpError::InvalidReceipt("the attestation carries no receipt".into())
})?;
let registry_fp = acdp_crypto::fingerprint::fingerprint_for_key_id_since(
&ctx.body.signature.key_id,
&ctx.body.signature.algorithm,
resolver,
since,
)
.await?;
verify_receipt_placed(
receipt,
&reference.ctx_id,
&ctx.body,
&ctx.body.content_hash,
®istry_fp,
serving_authority,
resolver,
since,
)
.await?;
Ok(metadata)
}
fn accepted(
policy: &KeylessPolicy,
registry: &str,
body: &Body,
placed: Option<&PlacedReceipt>,
) -> bool {
if policy.attesters.iter().any(|a| a == registry) {
return true;
}
let (Some(trust), Some(placed)) = (&policy.trust_lists, placed) else {
return false;
};
if placed.receipt.registry_did != registry {
return false;
}
let ty = serde_json::to_value(&body.context_type)
.ok()
.and_then(|v| v.as_str().map(str::to_owned))
.unwrap_or_default();
let receipt = ReceiptInput {
registry_did: placed.receipt.registry_did.clone(),
registry_key_fingerprint: placed.registry_key_fingerprint.clone(),
};
let node = evaluate(
&TrustSubject::of(body, &ty),
&trust.pins,
trust.t,
Some(&receipt),
trust.capabilities.as_deref(),
);
node.outcome == TrustOutcome::Trusted
}
async fn identity_now(
resolver: &WebResolver,
attestation: &KeyAttestation,
since: Instant,
) -> IdentityNow {
let listed = |doc: &acdp_did::DidDocument| {
ListedIdentities::of(doc)
.find(&attestation.identity)
.is_some()
};
match resolver
.resolve_until(&attestation.controller, since, listed)
.await
{
Ok(r) if listed(&r.document) => IdentityNow::Listed,
Ok(r) if r.fresh => IdentityNow::NotListed,
Ok(_) | Err(_) => IdentityNow::Unreachable,
}
}
fn refusal(reason: KeylessReason, body: &Body, att: &KeyAttestation) -> AcdpError {
let key_id = &body.signature.key_id;
let detail = match reason {
KeylessReason::UntrustedAttester => format!(
"the attesting registry of '{key_id}' is not an accepted attester \
(RFC-ACDP-0021 §11; configure VerificationPolicy.keyless)"
),
KeylessReason::AttestationMismatch => format!(
"agent_id, key_id or algorithm differ from the attestation of '{}'",
att.key_id
),
KeylessReason::Unplaceable => "no verified receipt from the attesting registry \
names the attested key (RFC-ACDP-0021 §8.1 step 4)"
.to_string(),
KeylessReason::OutsideAttestationWindow => format!(
"the receipt places the signature outside [{}, {})",
acdp_primitives::time::fmt_rfc3339_ms(att.not_before),
acdp_primitives::time::fmt_rfc3339_ms(att.not_after)
),
KeylessReason::InvalidSignature => {
format!("the signature does not verify under the key attested for '{key_id}'")
}
KeylessReason::AttestationWithdrawn => {
"the attesting registry retracted the attestation (RFC-ACDP-0021 §9.2)".to_string()
}
KeylessReason::ControllerUnreachable => format!(
"the DID document of '{}' could not be fetched fresh",
att.controller
),
other => format!("keyless verification refused: {other}"),
};
reason.error(detail)
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn verify(
client: &RegistryClient,
resolver: &WebResolver,
ctx: &FullContext,
expected_ctx_id: &CtxId,
serving_authority: &str,
policy: &KeylessPolicy,
revocations: &RevocationPolicy,
since: Instant,
) -> Result<KeyPhases, AcdpError> {
let body = &ctx.body;
let reference = body.key_attestation()?.ok_or_else(|| {
KeylessReason::AttestationMissing.error(format!(
"signature.key_id '{}' is a keyless key id but the body has no key_attestation",
body.signature.key_id
))
})?;
let att = attestation(client, resolver, &reference, serving_authority, since).await?;
let meta = &att.metadata;
let placed = match &ctx.registry_receipt {
None => None,
Some(value) => Some(
verify_receipt_placed(
value,
expected_ctx_id,
body,
&body.content_hash,
&meta.key_fingerprint,
serving_authority,
resolver,
since,
)
.await,
),
};
let placed_ok = placed.as_ref().and_then(|p| p.as_ref().ok());
let receipt = placed.as_ref().map(|p| match p {
Ok(p) => ReceiptFacts {
verified: true,
registry_is_attester: p.receipt.registry_did == att.registry,
fingerprint_matches: p.receipt.key_fingerprint == meta.key_fingerprint,
created_at: p.receipt.created_at,
},
Err(_) => ReceiptFacts {
verified: false,
registry_is_attester: false,
fingerprint_matches: false,
created_at: meta.not_before,
},
});
let gathered =
crate::revocation_verdict::gather(client, resolver, &body.agent_id, revocations).await;
let assessment = crate::revocation_verdict::decide(
gathered,
Some(&meta.key_fingerprint),
placed_ok.map(|p| (p.receipt.created_at, p.key_status)),
revocations.accept_historical_receipt_key_for_pre_t,
);
let compromised_since = match &assessment.verdict {
crate::RevocationVerdict::Revoked { boundary, .. } => vec![*boundary],
_ => Vec::new(),
};
let facts = KeylessFacts {
attestation_valid: true,
attester_accepted: accepted(policy, &att.registry, body, placed_ok),
bindings_ok: meta.binds(&body.agent_id, &body.signature),
receipt,
not_before: meta.not_before,
not_after: meta.not_after,
signature_valid: meta
.public_key_jwk
.verify(
&body.signature.algorithm,
&body.signature.value,
body.content_hash.as_str(),
)
.is_ok(),
retracted: att.retracted,
compromised_since,
};
let now = match decide_until_identity(&facts) {
Ok(None) => identity_now(resolver, meta, since).await,
_ => IdentityNow::Unreachable,
};
let status = match classify(&facts, now) {
Ok(status) => status,
Err(KeylessReason::Revoked) => {
return Err(match assessment.enforce() {
Err(e) => e,
Ok(_) => refusal(KeylessReason::Revoked, body, meta),
})
}
Err(reason) => return Err(refusal(reason, body, meta)),
};
let revocation = assessment.enforce()?;
let placed = placed_ok.expect("classify accepted only with a verified receipt");
Ok(KeyPhases {
key_status: match status {
KeylessStatus::CurrentlyAuthorized => KeyAuthorization::CurrentlyAuthorized,
KeylessStatus::HistoricallyAuthorized => KeyAuthorization::HistoricallyAuthorized,
KeylessStatus::HistoricallyAuthorizedPreCompromise => {
KeyAuthorization::HistoricallyAuthorizedPreCompromise
}
},
verified_receipt: Some(placed.receipt.clone()),
receipt_key_status: Some(placed.key_status),
receipt_registry_key_fingerprint: Some(placed.registry_key_fingerprint.clone()),
receipt_verdict: Some(Ok(())),
revocation,
})
}
}