use affinidi_data_integrity::{DataIntegrityError, DataIntegrityProof, VerifyOptions};
use super::purpose::{ProofPurpose, PurposeBound};
use super::vm_resolver::TrustTaskVmResolver;
use serde::Serialize;
use serde_json::Value;
use trust_tasks_rs::TrustTask;
#[derive(Debug)]
pub enum DiProofError {
NoProof,
NotDataIntegrity,
NoDid,
ResolverFailed(String),
VerifyFailed(String),
WrongPurpose {
expected: &'static str,
},
}
impl DiProofError {
#[must_use]
pub fn cause(&self) -> Option<&str> {
match self {
Self::ResolverFailed(e) | Self::VerifyFailed(e) => Some(e),
_ => None,
}
}
}
impl std::fmt::Display for DiProofError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoProof => write!(f, "document has no proof"),
Self::NotDataIntegrity => write!(f, "proof is not a Data Integrity proof"),
Self::NoDid => write!(f, "proof verificationMethod carries no DID"),
Self::ResolverFailed(_) | Self::VerifyFailed(_) => {
write!(f, "proof verification failed")
}
Self::WrongPurpose { expected } => {
write!(f, "proof must be made for `{expected}`")
}
}
}
}
fn classify(e: DataIntegrityError) -> DiProofError {
if super::vm_resolver::is_unretrievable(&e) {
return DiProofError::ResolverFailed(e.to_string());
}
DiProofError::VerifyFailed(e.to_string())
}
pub async fn verify_trust_task_proof(doc: &TrustTask<Value>) -> Result<String, DiProofError> {
verify_trust_task_proof_with(doc, &TrustTaskVmResolver::did_key_only()).await
}
pub async fn verify_trust_task_proof_with<P: Serialize + Clone + Sync>(
doc: &TrustTask<P>,
resolver: &TrustTaskVmResolver,
) -> Result<String, DiProofError> {
let proof = doc.proof.as_ref().ok_or(DiProofError::NoProof)?;
let di: DataIntegrityProof = serde_json::to_value(proof)
.ok()
.and_then(|v| serde_json::from_value(v).ok())
.ok_or(DiProofError::NotDataIntegrity)?;
let signer_did = di
.verification_method
.split('#')
.next()
.unwrap_or_default()
.to_string();
if signer_did.is_empty() {
return Err(DiProofError::NoDid);
}
let mut unsigned = doc.clone();
unsigned.proof = None;
let bound = PurposeBound::for_proof(resolver, &di).map_err(classify)?;
if let Err(first) = di.verify(&unsigned, &bound, VerifyOptions::new()).await {
if !resolver.refresh_if_cached(&signer_did).await {
return Err(classify(first));
}
di.verify(&unsigned, &bound, VerifyOptions::new())
.await
.map_err(classify)?;
}
Ok(signer_did)
}
pub const APPROVAL_PROOF_PURPOSE: &str = "assertionMethod";
pub async fn verify_approval_proof_with<P: Serialize + Clone + Sync>(
doc: &TrustTask<P>,
resolver: &TrustTaskVmResolver,
) -> Result<String, DiProofError> {
let proof = doc.proof.as_ref().ok_or(DiProofError::NoProof)?;
let di: DataIntegrityProof = serde_json::to_value(proof)
.ok()
.and_then(|v| serde_json::from_value(v).ok())
.ok_or(DiProofError::NotDataIntegrity)?;
if ProofPurpose::parse(&di.proof_purpose).ok() != Some(ProofPurpose::AssertionMethod) {
return Err(DiProofError::WrongPurpose {
expected: APPROVAL_PROOF_PURPOSE,
});
}
verify_trust_task_proof_with(doc, resolver).await
}
pub async fn verify_approval_proof(doc: &TrustTask<Value>) -> Result<String, DiProofError> {
verify_approval_proof_with(doc, &TrustTaskVmResolver::did_key_only()).await
}
#[cfg(test)]
mod approval_tests {
use super::*;
use affinidi_data_integrity::SignOptions;
use affinidi_secrets_resolver::secrets::Secret;
use serde_json::json;
fn peer(purpose_code: char, seed: u8) -> (String, Secret) {
let probe = Secret::generate_ed25519(None, Some(&[seed; 32]));
let mb = probe.get_public_keymultibase().expect("public key");
let did = format!("did:peer:2.{purpose_code}{mb}");
let secret = Secret::generate_ed25519(Some(&format!("{did}#key-1")), Some(&[seed; 32]));
(did, secret)
}
async fn decision(issuer: &str, secret: &Secret, purpose: &str) -> TrustTask<Value> {
let mut doc = json!({
"id": "urn:uuid:decision-1",
"type": "https://trusttasks.org/spec/task-consent/decision/0.1",
"issuer": issuer,
"recipient": "did:web:vta.example",
"issuedAt": "2026-09-25T10:00:00Z",
"payload": { "decision": "approve" },
});
let proof =
DataIntegrityProof::sign(&doc, secret, SignOptions::new().with_proof_purpose(purpose))
.await
.expect("sign");
doc["proof"] = serde_json::to_value(proof).unwrap();
serde_json::from_value(doc).unwrap()
}
#[tokio::test]
async fn an_approval_is_an_assertion_by_an_assertion_key() {
let (asserting, secret) = peer('A', 3);
let ok = decision(&asserting, &secret, "assertionMethod").await;
assert_eq!(verify_approval_proof(&ok).await.unwrap(), asserting);
let operational = decision(&asserting, &secret, "authentication").await;
assert!(matches!(
verify_approval_proof(&operational).await,
Err(DiProofError::WrongPurpose {
expected: "assertionMethod"
})
));
let (delegating, secret) = peer('D', 4);
let misfiled = decision(&delegating, &secret, "assertionMethod").await;
for err in [
verify_trust_task_proof(&misfiled).await.unwrap_err(),
verify_approval_proof(&misfiled).await.unwrap_err(),
] {
assert!(
err.cause().is_some_and(|c| c.contains("assertionMethod")),
"{err:?}"
);
}
let err = verify_trust_task_proof(&operational).await.unwrap_err();
assert!(
err.cause().is_some_and(|c| c.contains("authentication")),
"{err:?}"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolver_failed_and_verify_failed_render_identically() {
let resolver_failed = DiProofError::ResolverFailed("some resolver detail".to_string());
let verify_failed = DiProofError::VerifyFailed("some other detail".to_string());
assert_eq!(resolver_failed.to_string(), verify_failed.to_string());
assert_eq!(resolver_failed.to_string(), "proof verification failed");
}
#[test]
fn cause_surfaces_the_detail_for_both_variants() {
assert_eq!(
DiProofError::ResolverFailed("did not resolve".to_string()).cause(),
Some("did not resolve")
);
assert_eq!(
DiProofError::VerifyFailed("bad signature".to_string()).cause(),
Some("bad signature")
);
assert_eq!(DiProofError::NoProof.cause(), None);
}
#[tokio::test]
async fn a_resolver_failure_classifies_as_resolver_failed() {
let doc: TrustTask<Value> = serde_json::from_value(serde_json::json!({
"id": "urn:uuid:11111111-1111-4111-8111-111111111111",
"type": "https://trusttasks.org/spec/vta/contexts/create/1.0",
"issuer": "did:webvh:QmScid:example.com:glenn",
"recipient": "did:key:z6MkVta",
"payload": {},
"proof": {
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-jcs-2022",
"proofPurpose": "assertionMethod",
"verificationMethod": "did:webvh:QmScid:example.com:glenn#key-0",
"created": "2026-08-29T00:00:00Z",
"proofValue": "z2aBcD"
}
}))
.expect("a well-formed Trust Task");
let err = verify_trust_task_proof_with(&doc, &TrustTaskVmResolver::did_key_only())
.await
.expect_err("did:key-only cannot resolve a did:webvh key");
assert!(
matches!(err, DiProofError::ResolverFailed(_)),
"expected ResolverFailed, got {err:?}"
);
}
#[test]
fn an_authorisation_refusal_is_an_invalid_proof_not_a_retrieval_failure() {
for refusal in [
"verificationMethod is not listed under assertionMethod in its DID document",
"verificationMethod's controller is not the DID that names it",
"a did:key X25519 key is authorised for keyAgreement only",
] {
let err = classify(DataIntegrityError::Resolver(refusal.to_string()));
assert!(
matches!(err, DiProofError::VerifyFailed(_)),
"`{refusal}` must stay an invalid proof, got {err:?}"
);
}
}
#[tokio::test]
async fn a_did_key_method_mismatch_classifies_as_verify_failed() {
let doc: TrustTask<Value> = serde_json::from_value(serde_json::json!({
"id": "urn:uuid:22222222-2222-4222-8222-222222222222",
"type": "https://trusttasks.org/spec/vta/contexts/create/1.0",
"issuer": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"recipient": "did:key:z6MkVta",
"payload": {},
"proof": {
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-jcs-2022",
"proofPurpose": "assertionMethod",
"verificationMethod":
"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#not-the-key",
"created": "2026-08-29T00:00:00Z",
"proofValue": "z2aBcD"
}
}))
.expect("a well-formed Trust Task");
let err = verify_trust_task_proof_with(&doc, &TrustTaskVmResolver::did_key_only())
.await
.expect_err("the method is not the did:key's own key");
assert!(
matches!(err, DiProofError::VerifyFailed(_)),
"expected VerifyFailed, got {err:?}"
);
}
#[tokio::test]
async fn an_actual_bad_signature_classifies_as_verify_failed_with_identical_wire_text() {
use ed25519_dalek::SigningKey;
let sk = SigningKey::from_bytes(&[7u8; 32]);
let did = format!(
"did:key:{}",
crate::did_key::ed25519_multibase_pubkey(&sk.verifying_key().to_bytes())
);
let mut seed_secret = vec![0x80, 0x26];
seed_secret.extend_from_slice(&[7u8; 32]);
let secret_mb = multibase::encode(multibase::Base::Base58Btc, &seed_secret);
let signed = crate::trust_task_sign::build_signed(
"https://trusttasks.org/spec/vta/contexts/create/1.0",
serde_json::json!({}),
&did,
&secret_mb,
"did:key:z6MkVta",
)
.await
.expect("build a validly-signed document");
let mut doc: TrustTask<Value> = serde_json::from_str(&signed).expect("signed doc parses");
let proof = doc.proof.as_mut().expect("document is signed");
let last = proof.proof_value.pop().expect("non-empty proofValue");
proof.proof_value.push(if last == '1' { '2' } else { '1' });
let err = verify_trust_task_proof_with(&doc, &TrustTaskVmResolver::did_key_only())
.await
.expect_err("a corrupted signature must not verify");
assert!(
matches!(err, DiProofError::VerifyFailed(_)),
"expected VerifyFailed, got {err:?}"
);
assert_eq!(err.to_string(), "proof verification failed");
}
}