pub mod card;
pub mod eligibility;
pub mod match_code;
pub mod requirements;
pub mod statement;
pub mod status;
pub mod ticket_uri;
use serde_json::Value;
use crate::trust_task_proof::TrustTaskVmResolver;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum VettingError {
#[error("malformed {what}")]
Malformed {
what: &'static str,
detail: String,
},
#[error("{0} does not match this session")]
Binding(&'static str),
#[error("{0} is outside its validity window")]
Expired(&'static str),
#[error("{what} is not signed by its {role}")]
WrongSigner {
what: &'static str,
role: &'static str,
},
#[error("{what} proof verification failed")]
Proof {
what: &'static str,
detail: String,
},
#[error("identity commitment does not recompute from the card")]
Commitment,
#[error("required claim `{0}` is missing")]
MissingClaim(String),
#[error("no role credential names this vetter in that role for this community")]
NoRoleCredential,
#[error("{what} version `{version}` is not supported")]
UnsupportedVersion {
what: &'static str,
version: String,
},
#[error("signing failed")]
Sign(String),
#[error("digest computation failed")]
Digest(String),
#[error("no randomness available")]
Random(String),
}
impl VettingError {
#[must_use]
pub fn cause(&self) -> Option<&str> {
match self {
Self::Malformed { detail, .. }
| Self::Proof { detail, .. }
| Self::Sign(detail)
| Self::Digest(detail)
| Self::Random(detail) => Some(detail),
_ => None,
}
}
}
pub(crate) fn did_of(vm: &str) -> &str {
vm.split('#').next().unwrap_or_default()
}
pub(crate) async fn verify_attached_proof(
what: &'static str,
signed: &Value,
expected_purpose: &str,
resolver: &TrustTaskVmResolver,
) -> Result<String, VettingError> {
use crate::trust_task_proof::{ProofPurpose, verify_proof_set};
let expected = ProofPurpose::parse(expected_purpose).map_err(|e| VettingError::Proof {
what,
detail: e.to_string(),
})?;
verify_proof_set(signed, expected, resolver)
.await
.map(|verified| verified.into_signer())
.map_err(|e| VettingError::Proof {
what,
detail: e.to_string(),
})
}
pub fn dtg_shape(
credential: &Value,
) -> Result<dtg_credentials::DTGCredential, dtg_credentials::DTGCredentialError> {
let mut proofless = credential.clone();
if let Some(map) = proofless.as_object_mut() {
map.remove("proof");
}
dtg_credentials::DTGCredential::try_from(proofless)
}
pub(crate) fn digest(value: &Value) -> Result<String, VettingError> {
dtg_credentials::digest_multibase_json(value).map_err(|e| VettingError::Digest(e.to_string()))
}
#[cfg(test)]
pub(crate) fn tests_support_json(dtg: &dtg_credentials::DTGCredential) -> Value {
serde_json::to_value(dtg).expect("catalog credential serialises")
}
#[cfg(test)]
pub(crate) mod test_support {
use affinidi_secrets_resolver::secrets::Secret;
pub fn secret(seed_byte: u8) -> Secret {
let seed = [seed_byte; 32];
let mut secret = Secret::generate_ed25519(None, Some(&seed));
let public = secret.get_public_keymultibase().unwrap();
secret.id = format!("did:key:{public}#{public}");
secret
}
pub fn did(secret: &Secret) -> String {
super::did_of(&secret.id).to_string()
}
}
#[cfg(test)]
mod proof_set_tests {
use super::test_support::{did, secret};
use super::*;
use affinidi_data_integrity::{DataIntegrityProof, SignOptions};
use affinidi_secrets_resolver::secrets::Secret;
use serde_json::json;
fn statement() -> Value {
json!({
"type": ["VerifiableCredential", "StatementCredential"],
"credentialSubject": { "id": "did:example:applicant" }
})
}
async fn proof_over(doc: &Value, key: &Secret, created: &str) -> Value {
let proof = DataIntegrityProof::sign(
doc,
key,
SignOptions::new()
.with_proof_purpose("assertionMethod")
.with_created(created.parse().unwrap()),
)
.await
.unwrap();
serde_json::to_value(proof).unwrap()
}
#[tokio::test]
async fn vti_44_a_proof_set_verifies_and_names_one_signer() {
let key = secret(0x51);
let doc = statement();
let mut signed = doc.clone();
signed["proof"] = json!([
proof_over(&doc, &key, "2026-09-17T15:00:00Z").await,
proof_over(&doc, &key, "2026-09-17T15:00:01Z").await,
]);
let signer = verify_attached_proof(
"statement",
&signed,
"assertionMethod",
&TrustTaskVmResolver::did_key_only(),
)
.await
.expect("a proof set verifies");
assert_eq!(signer, did(&key));
}
#[tokio::test]
async fn vti_44_one_tampered_proof_in_the_set_is_refused() {
let key = secret(0x52);
let doc = statement();
let mut signed = doc.clone();
let good = proof_over(&doc, &key, "2026-09-17T15:00:00Z").await;
let other = proof_over(&json!({"other": 1}), &key, "2026-09-17T15:00:00Z").await;
let mut bad = good.clone();
bad["proofValue"] = other["proofValue"].clone();
signed["proof"] = json!([good, bad]);
let err = verify_attached_proof(
"statement",
&signed,
"assertionMethod",
&TrustTaskVmResolver::did_key_only(),
)
.await
.expect_err("one bad proof refuses the set");
assert!(
err.cause().is_some_and(|c| c.contains("1 of 2 proofs")),
"{err:?}"
);
}
#[tokio::test]
async fn vti_44_a_proof_by_another_key_is_refused() {
let (key, other) = (secret(0x53), secret(0x54));
let doc = statement();
let mut signed = doc.clone();
signed["proof"] = json!([
proof_over(&doc, &key, "2026-09-17T15:00:00Z").await,
proof_over(&doc, &other, "2026-09-17T15:00:00Z").await,
]);
let err = verify_attached_proof(
"statement",
&signed,
"assertionMethod",
&TrustTaskVmResolver::did_key_only(),
)
.await
.expect_err("two signers");
assert!(
err.cause()
.is_some_and(|c| c.contains("two different issuers")),
"{err:?}"
);
}
#[tokio::test]
async fn vti_44_a_single_proof_object_still_verifies() {
let key = secret(0x55);
let doc = statement();
let mut signed = doc.clone();
signed["proof"] = proof_over(&doc, &key, "2026-09-17T15:00:00Z").await;
let resolver = TrustTaskVmResolver::did_key_only();
let signer = verify_attached_proof("statement", &signed, "assertionMethod", &resolver)
.await
.expect("verifies");
assert_eq!(signer, did(&key));
assert!(
verify_attached_proof("statement", &signed, "authentication", &resolver)
.await
.is_err()
);
}
}