use std::sync::Arc;
use affinidi_data_integrity::{
DataIntegrityError, SignatureFailure, VerificationMethodResolver, VerifyOptions,
};
use async_trait::async_trait;
use serde::Serialize;
use trust_tasks_rs::{ProofVerifier, TrustTask, VerificationError};
pub struct TransportBoundVerifier {
resolver: Arc<dyn VerificationMethodResolver>,
options: VerifyOptions,
}
impl TransportBoundVerifier {
pub fn with_resolver(resolver: Arc<dyn VerificationMethodResolver>) -> Self {
Self {
resolver,
options: VerifyOptions::default(),
}
}
pub fn with_options(mut self, options: VerifyOptions) -> Self {
self.options = options;
self
}
}
#[async_trait]
impl ProofVerifier for TransportBoundVerifier {
async fn verify<P>(&self, doc: &TrustTask<P>) -> Result<(), VerificationError>
where
P: Serialize + Send + Sync,
{
let Some(proof) = &doc.proof else {
return Err(VerificationError::MalformedProof(
"document carries no proof member".to_string(),
));
};
let proof_value = serde_json::to_value(proof)
.map_err(|e| VerificationError::MalformedProof(format!("serialise proof: {e}")))?;
let parsed_proof = trust_tasks_proof::affinidi::parse_data_integrity_proof(&proof_value)?;
let mut doc_value = serde_json::to_value(doc).map_err(|e| {
VerificationError::Other(format!("serialise TrustTask for verification: {e}"))
})?;
if let Some(obj) = doc_value.as_object_mut() {
obj.remove("proof");
}
if let Some(issuer) = doc_value.get("issuer").and_then(|v| v.as_str()) {
let vm_did = proof
.verification_method
.split('#')
.next()
.unwrap_or(&proof.verification_method);
if vm_did != issuer {
return Err(VerificationError::IssuerMismatch(format!(
"verificationMethod is controlled by {vm_did}, not the document issuer {issuer}"
)));
}
}
parsed_proof
.verify(&doc_value, &*self.resolver, self.options.clone())
.await
.map_err(map_error)?;
Ok(())
}
}
fn map_error(err: DataIntegrityError) -> VerificationError {
match err {
DataIntegrityError::UnsupportedCryptoSuite { name } => {
VerificationError::UnsupportedCryptosuite(name)
}
DataIntegrityError::KeyTypeMismatch {
expected,
actual,
suite,
} => VerificationError::IssuerMismatch(format!(
"key type {actual:?} does not match cryptosuite {suite:?} (expected {expected:?})"
)),
DataIntegrityError::InvalidSignature { reason, .. } => match reason {
SignatureFailure::Malformed | SignatureFailure::Invalid => {
VerificationError::SignatureInvalid
}
_ => VerificationError::SignatureInvalid,
},
DataIntegrityError::InvalidPublicKey { reason, .. } => {
VerificationError::MalformedProof(format!("public key: {reason}"))
}
DataIntegrityError::Canonicalization(reason) => {
VerificationError::Other(format!("canonicalisation: {reason}"))
}
DataIntegrityError::MalformedProof(reason) => VerificationError::MalformedProof(reason),
other => VerificationError::Other(other.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use affinidi_data_integrity::{DataIntegrityProof, DidKeyResolver, SignOptions};
use affinidi_secrets_resolver::secrets::Secret;
use serde_json::{Value, json};
use trust_tasks_rs::TrustTask;
fn verifier() -> TransportBoundVerifier {
TransportBoundVerifier::with_resolver(Arc::new(DidKeyResolver))
}
fn base_body() -> Value {
json!({
"id": "urn:uuid:24f1b27f-36d9-49af-9a20-751def9000aa",
"type": "https://trusttasks.org/spec/acl/grant/0.1",
"recipient": "did:web:server.example",
"issuedAt": "2026-06-09T08:51:03Z",
"payload": { "entry": { "subject": "did:web:alice.example", "role": "owner" } },
})
}
async fn sign(body: Value) -> (TrustTask<Value>, String) {
let secret = Secret::generate_ed25519(None, Some(&[7u8; 32]));
let pk_mb = secret.get_public_keymultibase().expect("multibase pubkey");
let did_key = format!("did:key:{pk_mb}");
let mut signer = secret.clone();
signer.id = format!("{did_key}#{pk_mb}");
let doc_noproof: TrustTask<Value> =
serde_json::from_value(body).expect("body parses as TrustTask");
let signing_value = serde_json::to_value(&doc_noproof).expect("serialise doc");
let di_proof = DataIntegrityProof::sign(&signing_value, &signer, SignOptions::new())
.await
.expect("sign");
let mut full = signing_value;
full.as_object_mut().unwrap().insert(
"proof".to_string(),
serde_json::to_value(&di_proof).unwrap(),
);
let doc: TrustTask<Value> = serde_json::from_value(full).expect("proofed doc parses");
(doc, did_key)
}
#[tokio::test]
async fn issuer_absent_verifies_signature_only() {
let (doc, _did) = sign(base_body()).await;
assert!(doc.issuer.is_none(), "test fixture must omit issuer");
verifier()
.verify(&doc)
.await
.expect("issuer-absent proof verifies");
}
#[tokio::test]
async fn issuer_present_matching_verifies() {
let secret = Secret::generate_ed25519(None, Some(&[7u8; 32]));
let pk_mb = secret.get_public_keymultibase().unwrap();
let did_key = format!("did:key:{pk_mb}");
let mut body = base_body();
body.as_object_mut()
.unwrap()
.insert("issuer".to_string(), json!(did_key));
let (doc, signer_did) = sign(body).await;
assert_eq!(doc.issuer.as_deref(), Some(signer_did.as_str()));
verifier()
.verify(&doc)
.await
.expect("issuer-matched proof verifies");
}
#[tokio::test]
async fn issuer_present_mismatch_rejected() {
let mut body = base_body();
body.as_object_mut()
.unwrap()
.insert("issuer".to_string(), json!("did:web:evil.example"));
let (doc, signer_did) = sign(body).await;
assert_ne!(signer_did, "did:web:evil.example");
let err = verifier().verify(&doc).await.expect_err("must reject");
assert!(
matches!(err, VerificationError::IssuerMismatch(_)),
"expected IssuerMismatch, got {err:?}"
);
}
#[tokio::test]
async fn issuer_absent_tampered_payload_rejected() {
let (mut doc, _did) = sign(base_body()).await;
doc.payload = json!({ "entry": { "subject": "did:web:mallory.example", "role": "admin" } });
let err = verifier()
.verify(&doc)
.await
.expect_err("must reject tampered doc");
assert!(
matches!(err, VerificationError::SignatureInvalid),
"expected SignatureInvalid, got {err:?}"
);
}
#[tokio::test]
async fn missing_proof_rejected() {
let doc: TrustTask<Value> = serde_json::from_value(base_body()).expect("body parses");
let err = verifier().verify(&doc).await.expect_err("must reject");
assert!(
matches!(err, VerificationError::MalformedProof(_)),
"expected MalformedProof, got {err:?}"
);
}
const STEP_UP_FIXTURE: &str = include_str!("testdata/stepup-approval-fixture.json");
#[tokio::test]
async fn wallet_signed_step_up_approval_verifies() {
let fixture: Value = serde_json::from_str(STEP_UP_FIXTURE).expect("fixture parses");
let approved: TrustTask<Value> =
serde_json::from_value(fixture["approved"].clone()).expect("approved doc parses");
verifier()
.verify(&approved)
.await
.expect("wallet-signed approved response verifies");
let denied: TrustTask<Value> =
serde_json::from_value(fixture["denied"].clone()).expect("denied doc parses");
verifier()
.verify(&denied)
.await
.expect("wallet-signed denied response verifies");
}
#[tokio::test]
async fn wallet_signed_step_up_tampered_decision_rejected() {
let fixture: Value = serde_json::from_str(STEP_UP_FIXTURE).expect("fixture parses");
let mut doc: TrustTask<Value> =
serde_json::from_value(fixture["approved"].clone()).expect("approved doc parses");
doc.payload
.as_object_mut()
.expect("payload is an object")
.insert("decision".to_string(), json!("denied"));
let err = verifier()
.verify(&doc)
.await
.expect_err("tampered decision must be rejected");
assert!(
!matches!(err, VerificationError::IssuerMismatch(_)),
"expected a signature failure, got {err:?}"
);
}
}