use affinidi_data_integrity::did_vm::resolve_did_key;
use affinidi_data_integrity::{DataIntegrityError, ResolvedKey, VerificationMethodResolver};
use affinidi_did_resolver_cache_sdk::DIDCacheClient;
use affinidi_secrets_resolver::secrets::KeyType;
#[derive(Clone, Default)]
pub struct TrustTaskVmResolver {
resolver: Option<DIDCacheClient>,
}
impl std::fmt::Debug for TrustTaskVmResolver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TrustTaskVmResolver")
.field("network_resolution", &self.resolver.is_some())
.finish()
}
}
impl TrustTaskVmResolver {
#[must_use]
pub fn new(resolver: DIDCacheClient) -> Self {
Self {
resolver: Some(resolver),
}
}
#[must_use]
pub fn did_key_only() -> Self {
Self { resolver: None }
}
#[must_use]
pub fn from_optional(resolver: Option<DIDCacheClient>) -> Self {
Self { resolver }
}
#[must_use]
pub fn resolves_over_the_network(&self) -> bool {
self.resolver.is_some()
}
async fn resolve(&self, vm: &str) -> Result<ResolvedKey, DataIntegrityError> {
let base_did = vm.split('#').next().unwrap_or(vm);
if base_did.starts_with("did:key:") {
return resolve_did_key(vm);
}
let resolver = self.resolver.as_ref().ok_or_else(|| {
DataIntegrityError::Resolver(format!(
"resolving `{base_did}` needs a DID resolver, but this verifier is configured \
for did:key only"
))
})?;
let resolved = resolver.resolve(base_did).await.map_err(|e| {
DataIntegrityError::Resolver(format!("`{base_did}` did not resolve: {e}"))
})?;
let relative = vm
.split_once('#')
.map(|(_, fragment)| format!("#{fragment}"))
.unwrap_or_default();
let entry = resolved
.doc
.verification_method
.iter()
.find(|m| m.id.as_str() == vm || m.id.as_str() == relative)
.ok_or_else(|| {
DataIntegrityError::Resolver(format!(
"verificationMethod `{vm}` is not in the DID document for `{base_did}`"
))
})?;
let bytes = entry.get_public_key_bytes().map_err(|e| {
DataIntegrityError::Resolver(format!(
"verificationMethod `{vm}` public key could not be extracted: {e}"
))
})?;
Ok(ResolvedKey::new(KeyType::Ed25519, bytes))
}
}
#[async_trait::async_trait]
impl VerificationMethodResolver for TrustTaskVmResolver {
async fn resolve_vm(&self, vm: &str) -> Result<ResolvedKey, DataIntegrityError> {
self.resolve(vm).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn did_key_resolves_with_no_resolver_configured() {
let sk = ed25519_dalek::SigningKey::from_bytes(&[0x11; 32]);
let mb = multibase::encode(
multibase::Base::Base58Btc,
[&[0xed, 0x01][..], &sk.verifying_key().to_bytes()[..]].concat(),
);
let vm = format!("did:key:{mb}#{mb}");
let key = TrustTaskVmResolver::did_key_only()
.resolve_vm(&vm)
.await
.expect("did:key resolves with no cache client");
assert_eq!(key.public_key_bytes, sk.verifying_key().to_bytes().to_vec());
}
#[tokio::test]
async fn a_did_key_only_resolver_names_its_own_limit() {
let err = TrustTaskVmResolver::did_key_only()
.resolve_vm("did:webvh:QmScid:example.com:glenn#key-0")
.await
.expect_err("did:webvh needs a resolver");
let msg = err.to_string();
assert!(
msg.contains("did:key only"),
"the error must name the configuration, got: {msg}"
);
}
#[tokio::test]
async fn a_did_webvh_proof_is_refused_by_the_narrow_verifier_for_the_right_reason() {
use trust_tasks_rs::TrustTask;
let doc: TrustTask<serde_json::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 = crate::auth::di_proof::verify_trust_task_proof(&doc)
.await
.expect_err("did:key-only cannot resolve a did:webvh key");
let cause = err.cause().unwrap_or_default();
assert!(
cause.contains("did:key only"),
"the operator-facing cause must name the resolver's configuration, not the \
signature, got: {cause}"
);
}
#[test]
fn the_debug_rendering_says_whether_it_can_reach_the_network() {
let s = format!("{:?}", TrustTaskVmResolver::did_key_only());
assert!(s.contains("network_resolution: false"), "got {s}");
}
}