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);
}
if base_did.starts_with("did:peer:") {
return resolve_did_peer(vm, base_did);
}
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(declared_key_type(entry, vm)?, bytes))
}
}
fn resolve_did_peer(vm: &str, base_did: &str) -> Result<ResolvedKey, DataIntegrityError> {
use affinidi_did_common::DID;
use affinidi_did_resolver_traits::{PeerResolver, Resolver};
let did = DID::try_from(base_did).map_err(|e| {
DataIntegrityError::Resolver(format!("`{base_did}` is not a well-formed DID: {e}"))
})?;
let doc = PeerResolver
.resolve(&did)
.ok_or_else(|| {
DataIntegrityError::Resolver(format!(
"`{base_did}` is not a did:peer this build resolves"
))
})?
.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 = 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(declared_key_type(entry, vm)?, bytes))
}
fn declared_key_type(
entry: &affinidi_did_common::verification_method::VerificationMethod,
vm: &str,
) -> Result<KeyType, DataIntegrityError> {
let multibase = entry
.property_set
.get("publicKeyMultibase")
.and_then(|v| v.as_str())
.ok_or_else(|| {
DataIntegrityError::Resolver(format!(
"verificationMethod `{vm}` has no `publicKeyMultibase`, so its algorithm cannot \
be read; a PQC key must be published as a Multikey (the JWK path does not \
express ML-DSA)"
))
})?;
let (_base, bytes) = multibase::decode(multibase).map_err(|e| {
DataIntegrityError::Resolver(format!(
"verificationMethod `{vm}` public key is not valid multibase: {e}"
))
})?;
for candidate in [
crate::keys::KeyType::Ed25519,
crate::keys::KeyType::X25519,
crate::keys::KeyType::P256,
crate::keys::KeyType::MlDsa44,
crate::keys::KeyType::MlDsa65,
] {
if bytes.starts_with(candidate.multicodec_public()) {
return Ok(match candidate {
crate::keys::KeyType::Ed25519 => KeyType::Ed25519,
crate::keys::KeyType::X25519 => KeyType::X25519,
crate::keys::KeyType::P256 => KeyType::P256,
crate::keys::KeyType::MlDsa44 => KeyType::MlDsa44,
crate::keys::KeyType::MlDsa65 => KeyType::MlDsa65,
});
}
}
Err(DataIntegrityError::Resolver(format!(
"verificationMethod `{vm}` carries a key whose multicodec prefix this build does not \
recognise, so its signature suite cannot be determined"
)))
}
#[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::declared_key_type;
use crate::keys::KeyType as LocalKeyType;
use affinidi_did_common::verification_method::VerificationMethod;
use affinidi_secrets_resolver::secrets::KeyType;
fn vm_with(key_type: LocalKeyType, payload_len: usize) -> VerificationMethod {
let mut bytes = key_type.multicodec_public().to_vec();
bytes.extend(std::iter::repeat_n(0x42u8, payload_len));
let mb = multibase::encode(multibase::Base::Base58Btc, &bytes);
let mut vm: VerificationMethod = serde_json::from_value(serde_json::json!({
"id": "did:example:alice#key-0",
"type": "Multikey",
"controller": "did:example:alice",
}))
.expect("a Multikey verification method");
vm.property_set
.insert("publicKeyMultibase".to_string(), serde_json::json!(mb));
vm
}
#[test]
fn an_ml_dsa_key_is_not_reported_as_ed25519() {
let vm44 = vm_with(LocalKeyType::MlDsa44, 1312);
assert_eq!(
declared_key_type(&vm44, "did:example:alice#key-0").expect("ML-DSA-44 is recognised"),
KeyType::MlDsa44,
);
let vm65 = vm_with(LocalKeyType::MlDsa65, 1952);
assert_eq!(
declared_key_type(&vm65, "did:example:alice#key-0").expect("ML-DSA-65 is recognised"),
KeyType::MlDsa65,
);
}
#[test]
fn an_ed25519_key_is_still_ed25519() {
let vm = vm_with(LocalKeyType::Ed25519, 32);
assert_eq!(
declared_key_type(&vm, "did:example:alice#key-0").expect("Ed25519 is recognised"),
KeyType::Ed25519,
);
}
#[test]
fn an_unknown_prefix_is_refused_rather_than_assumed() {
let mut vm = vm_with(LocalKeyType::Ed25519, 32);
let mb = multibase::encode(multibase::Base::Base58Btc, [0xff, 0xfe, 0x01, 0x02]);
vm.property_set
.insert("publicKeyMultibase".to_string(), serde_json::json!(mb));
let err = declared_key_type(&vm, "did:example:alice#key-0")
.expect_err("an unknown suite must not resolve");
let text = err.to_string();
assert!(
text.contains("suite cannot be determined") || text.contains("does not recognise"),
"the error must say the suite is undetermined, not blame the signature: {text}"
);
}
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 did_peer_resolves_with_no_resolver_configured() {
use affinidi_tdk::dids::{DID, KeyType};
let (did, secrets) = DID::generate_did_peer(
vec![
(
affinidi_tdk::dids::PeerKeyRole::Verification,
KeyType::Ed25519,
),
(affinidi_tdk::dids::PeerKeyRole::Encryption, KeyType::X25519),
],
None,
)
.expect("mint a did:peer");
let vm = format!("{did}#key-1");
let resolved = TrustTaskVmResolver::did_key_only()
.resolve_vm(&vm)
.await
.expect("a did:peer carries its keys in its identifier, so this needs no network");
let expected = secrets
.iter()
.find(|s| s.id.ends_with("#key-1"))
.expect("the verification secret");
assert_eq!(
resolved.public_key_bytes,
expected.get_public_bytes(),
"the resolved key must be the one the DID names"
);
}
#[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::trust_task_proof::verify::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}");
}
}