use std::sync::Arc;
use affinidi_tdk::data_integrity::{DataIntegrityError, ResolvedKey};
use affinidi_tdk::did_common::Document;
use affinidi_tdk::did_common::verification_method::VerificationRelationship;
use affinidi_tdk::did_resolver::DIDCacheClient;
use async_trait::async_trait;
use serde_json::Value;
use trust_tasks_proof::affinidi::{
CachedDidResolver, ProofPurpose, ProofPurposeResolver, Verifier,
};
use trust_tasks_rs::{DynProofVerifier, RejectReason, TrustTask, erase_verifier};
pub const WRITE_PROOF_PURPOSE: &str = "authentication";
pub fn is_authentication_method(doc: &Document, vm: &str) -> bool {
let fragment = vm.find('#').map(|i| &vm[i..]);
let refers = |id: &str| id == vm || fragment.is_some_and(|f| id == f);
doc.authentication
.iter()
.any(|relationship| match relationship {
VerificationRelationship::Reference(id) => refers(id),
VerificationRelationship::VerificationMethod(method) => refers(method.id.as_str()),
_ => false,
})
}
pub struct AuthenticationKeyResolver {
keys: CachedDidResolver,
}
impl AuthenticationKeyResolver {
pub fn new(client: Arc<DIDCacheClient>) -> Self {
Self {
keys: CachedDidResolver::new(client),
}
}
}
#[async_trait]
impl ProofPurposeResolver for AuthenticationKeyResolver {
async fn resolve_vm_for_purpose(
&self,
vm: &str,
_purpose: ProofPurpose,
) -> Result<ResolvedKey, DataIntegrityError> {
self.keys
.resolve_vm_for_purpose(vm, ProofPurpose::Authentication)
.await
}
}
pub fn is_write_slug(slug: &str) -> bool {
matches!(
slug,
"registry/record/put"
| "registry/record/delete"
| "registry/did/rotate"
| "governance/capability/enable"
| "governance/capability/disable"
| "git-trust/grant"
| "git-trust/revoke"
)
}
pub fn requires_proof(slug: &str) -> bool {
is_write_slug(slug) || slug == "registry/record/query"
}
pub async fn build_verifier() -> Arc<dyn DynProofVerifier> {
use affinidi_tdk::did_resolver::{DIDCacheClient, config::DIDCacheConfigBuilder};
match DIDCacheClient::new(DIDCacheConfigBuilder::default().build()).await {
Ok(client) => {
let resolver = Arc::new(AuthenticationKeyResolver::new(Arc::new(client)));
erase_verifier(Verifier::with_resolver(resolver))
}
Err(e) => {
tracing::warn!(
"DID resolver cache unavailable ({e}); Trust Task proof verification limited to did:key issuers"
);
erase_verifier(Verifier::for_did_key())
}
}
}
pub async fn verify_write_proof(
verifier: &Arc<dyn DynProofVerifier>,
doc: &TrustTask<Value>,
) -> Result<(), RejectReason> {
if !requires_proof(doc.type_uri.slug()) {
return Ok(());
}
if doc.proof.is_none() {
return Err(RejectReason::ProofRequired);
}
if doc.issuer.is_none() {
return Err(RejectReason::MalformedRequest {
reason: "a write must name its issuer".to_string(),
});
}
verifier
.verify_json(doc)
.await
.map_err(|e| RejectReason::ProofInvalid {
reason: e.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn doc_with_dummy_proof(type_uri: &str) -> TrustTask<Value> {
let mut doc = TrustTask::new(
"id-1",
type_uri.parse().expect("valid type uri"),
serde_json::json!({}),
);
doc.issuer = Some("did:example:admin".to_string());
doc.proof = Some(
serde_json::from_value(serde_json::json!({
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-jcs-2022",
"created": "2026-07-09T00:00:00Z",
"proofPurpose": "assertionMethod",
"verificationMethod": "did:example:admin#key-1",
"proofValue": "z0000"
}))
.expect("valid proof fixture"),
);
doc
}
#[tokio::test]
async fn read_document_skips_verification() {
let verifier = erase_verifier(Verifier::for_did_key());
let doc = doc_with_dummy_proof("https://trusttasks.org/spec/registry/recognition/0.1");
assert!(verify_write_proof(&verifier, &doc).await.is_ok());
}
#[tokio::test]
async fn write_with_bogus_proof_is_rejected() {
let verifier = erase_verifier(Verifier::for_did_key());
let doc = doc_with_dummy_proof("https://trusttasks.org/spec/registry/record/put/0.1");
assert!(matches!(
verify_write_proof(&verifier, &doc).await,
Err(RejectReason::ProofInvalid { .. })
));
}
const RECORD_PUT: &str = "https://trusttasks.org/spec/registry/record/put/0.1";
#[tokio::test]
async fn write_without_proof_is_refused() {
let verifier = erase_verifier(Verifier::for_did_key());
let mut doc = doc_with_dummy_proof(RECORD_PUT);
doc.proof = None;
assert!(matches!(
verify_write_proof(&verifier, &doc).await,
Err(RejectReason::ProofRequired)
));
}
#[tokio::test]
async fn write_without_issuer_is_refused() {
let verifier = erase_verifier(Verifier::for_did_key());
let mut doc = doc_with_dummy_proof(RECORD_PUT);
doc.issuer = None;
assert!(matches!(
verify_write_proof(&verifier, &doc).await,
Err(RejectReason::MalformedRequest { .. })
));
}
fn document(authentication: Value) -> Document {
serde_json::from_value(serde_json::json!({
"id": "did:example:writer",
"verificationMethod": [{
"id": "did:example:writer#key-1",
"type": "Multikey",
"controller": "did:example:writer",
"publicKeyMultibase": "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
}],
"authentication": authentication,
"assertionMethod": ["did:example:writer#key-1"]
}))
.expect("valid DID document")
}
#[test]
fn a_key_listed_under_authentication_is_accepted() {
let doc = document(serde_json::json!(["#key-1"]));
assert!(is_authentication_method(&doc, "did:example:writer#key-1"));
}
#[test]
fn a_key_listed_only_under_assertion_method_is_refused() {
let doc = document(serde_json::json!([]));
assert!(!is_authentication_method(&doc, "did:example:writer#key-1"));
}
}