use affinidi_data_integrity::{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,
VerifyFailed(String),
WrongPurpose {
expected: &'static str,
},
}
impl DiProofError {
#[must_use]
pub fn cause(&self) -> Option<&str> {
match self {
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::VerifyFailed(_) => write!(f, "proof verification failed"),
Self::WrongPurpose { expected } => {
write!(f, "proof must be made for `{expected}`")
}
}
}
}
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(|e| DiProofError::VerifyFailed(e.to_string()))?;
if let Err(first) = di.verify(&unsigned, &bound, VerifyOptions::new()).await {
if !resolver.refresh_if_cached(&signer_did).await {
return Err(DiProofError::VerifyFailed(first.to_string()));
}
di.verify(&unsigned, &bound, VerifyOptions::new())
.await
.map_err(|e| DiProofError::VerifyFailed(e.to_string()))?;
}
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:?}"
);
}
}