use crate::did_refresh::{evict_for_fresh_resolve, resolve_for_vm};
use affinidi_data_integrity::did_vm::resolve_did_key;
use affinidi_data_integrity::{DataIntegrityError, ResolvedKey};
use super::purpose::{
ProofPurpose, PurposeVmResolver, authorised_method, check_did_key_method, split_vm,
};
use affinidi_did_resolver_cache_sdk::DIDCacheClient;
use affinidi_secrets_resolver::secrets::KeyType;
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
#[derive(Clone, Default)]
pub struct TrustTaskVmResolver {
resolver: Option<DIDCacheClient>,
served_from_cache: Arc<Mutex<HashSet<String>>>,
}
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),
served_from_cache: Arc::default(),
}
}
#[must_use]
pub fn did_key_only() -> Self {
Self::default()
}
#[must_use]
pub fn from_optional(resolver: Option<DIDCacheClient>) -> Self {
Self {
resolver,
served_from_cache: Arc::default(),
}
}
pub async fn refresh_if_cached(&self, did: &str) -> bool {
let Some(resolver) = self.resolver.as_ref() else {
return false;
};
let was_cached = self
.served_from_cache
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.remove(did);
was_cached && evict_for_fresh_resolve(resolver, did).await
}
#[must_use]
pub fn resolves_over_the_network(&self) -> bool {
self.resolver.is_some()
}
async fn resolve(
&self,
vm: &str,
purpose: ProofPurpose,
) -> Result<ResolvedKey, DataIntegrityError> {
let (base_did, _) = split_vm(vm)?;
if base_did.starts_with("did:key:") {
check_did_key_method(vm)?;
let key = resolve_did_key(vm)?;
if key.key_type == KeyType::X25519 {
return Err(DataIntegrityError::Resolver(
"a did:key X25519 key is authorised for keyAgreement only".to_string(),
));
}
return Ok(key);
}
if base_did.starts_with("did:peer:") {
return resolve_did_peer(vm, base_did, purpose);
}
let resolver = self.resolver.as_ref().ok_or_else(|| {
DataIntegrityError::Resolver(
"resolving this verificationMethod needs a DID resolver, but this verifier is \
configured for did:key only"
.to_string(),
)
})?;
let resolved = resolve_for_vm(resolver, base_did, vm).await.map_err(|e| {
DataIntegrityError::Resolver(format!(
"the verificationMethod's DID did not resolve: {e}"
))
})?;
if resolved.cache_hit {
self.served_from_cache
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.insert(base_did.to_string());
}
key_of(&resolved.doc, base_did, vm, purpose)
}
}
fn resolve_did_peer(
vm: &str,
base_did: &str,
purpose: ProofPurpose,
) -> 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!(
"the verificationMethod's DID is not well-formed: {e}"
))
})?;
let doc = PeerResolver
.resolve(&did)
.ok_or_else(|| {
DataIntegrityError::Resolver(
"the verificationMethod's did:peer is not one this build resolves".to_string(),
)
})?
.map_err(|e| {
DataIntegrityError::Resolver(format!(
"the verificationMethod's DID did not resolve: {e}"
))
})?;
key_of(&doc, base_did, vm, purpose)
}
pub(crate) fn key_of(
doc: &affinidi_did_common::Document,
did: &str,
vm: &str,
purpose: ProofPurpose,
) -> Result<ResolvedKey, DataIntegrityError> {
let entry = authorised_method(doc, did, vm, purpose)?;
let bytes = entry.get_public_key_bytes().map_err(|e| {
DataIntegrityError::Resolver(format!(
"the verificationMethod's 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(
"the verificationMethod 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)"
.to_string(),
)
})?;
let (_base, bytes) = multibase::decode(multibase).map_err(|e| {
DataIntegrityError::Resolver(format!(
"the verificationMethod's 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(
"the verificationMethod carries a key whose multicodec prefix this build does not \
recognise, so its signature suite cannot be determined"
.to_string(),
))
}
#[async_trait::async_trait]
impl PurposeVmResolver for TrustTaskVmResolver {
async fn resolve_vm_for_purpose(
&self,
vm: &str,
purpose: ProofPurpose,
) -> Result<ResolvedKey, DataIntegrityError> {
self.resolve(vm, purpose).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_for_purpose(&vm, ProofPurpose::AssertionMethod)
.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_for_purpose(&vm, ProofPurpose::AssertionMethod)
.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_for_purpose(
"did:webvh:QmScid:example.com:glenn#key-0",
ProofPurpose::AssertionMethod,
)
.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}"
);
}
fn ed25519_did_key(seed: u8) -> String {
let sk = ed25519_dalek::SigningKey::from_bytes(&[seed; 32]);
let mb = multibase::encode(
multibase::Base::Base58Btc,
[&[0xed, 0x01][..], &sk.verifying_key().to_bytes()[..]].concat(),
);
format!("did:key:{mb}")
}
#[tokio::test]
async fn a_did_key_resolves_for_every_signing_purpose_and_only_as_itself() {
let did = ed25519_did_key(0x21);
let id = did.strip_prefix("did:key:").unwrap();
let vm = format!("{did}#{id}");
let r = TrustTaskVmResolver::did_key_only();
for purpose in ProofPurpose::ALL {
r.resolve_vm_for_purpose(&vm, purpose)
.await
.unwrap_or_else(|e| panic!("did:key for {purpose}: {e}"));
}
let err = r
.resolve_vm_for_purpose(&format!("{did}#key-0"), ProofPurpose::Authentication)
.await
.expect_err("a did:key has no #key-0");
assert!(err.to_string().contains("fragment must repeat"), "{err}");
}
#[tokio::test]
async fn a_did_peer_key_resolves_only_for_its_relationships() {
use affinidi_tdk::dids::{DID, KeyType, PeerKeyRole};
let (did, _) = DID::generate_did_peer(
vec![
(PeerKeyRole::Verification, KeyType::Ed25519),
(PeerKeyRole::Encryption, KeyType::X25519),
],
None,
)
.expect("mint a did:peer");
let r = TrustTaskVmResolver::did_key_only();
let v = format!("{did}#key-1");
let e = format!("{did}#key-2");
for purpose in [ProofPurpose::Authentication, ProofPurpose::AssertionMethod] {
r.resolve_vm_for_purpose(&v, purpose)
.await
.unwrap_or_else(|err| panic!("V key for {purpose}: {err}"));
}
let err = r
.resolve_vm_for_purpose(&v, ProofPurpose::CapabilityInvocation)
.await
.expect_err("a V key is not a capabilityInvocation key");
assert!(
err.to_string()
.contains("not listed under capabilityInvocation"),
"{err}"
);
for purpose in ProofPurpose::ALL {
let err = r
.resolve_vm_for_purpose(&e, purpose)
.await
.expect_err("a key-agreement key signs for nothing");
assert!(err.to_string().contains("not listed under"), "{err}");
assert!(
!err.to_string().contains(&did),
"no DID in the refusal: {err}"
);
}
}
#[tokio::test]
async fn a_trust_task_proof_for_the_wrong_purpose_is_refused() {
use affinidi_data_integrity::{DataIntegrityProof, SignOptions};
use affinidi_tdk::dids::{DID, KeyType, PeerKeyRole};
use trust_tasks_rs::TrustTask;
let (did, secrets) =
DID::generate_did_peer(vec![(PeerKeyRole::Verification, KeyType::Ed25519)], None)
.expect("mint a did:peer");
let secret = secrets.into_iter().next().expect("the V key");
let unsigned = serde_json::json!({
"id": "urn:uuid:11111111-1111-4111-8111-111111111112",
"type": "https://trusttasks.org/spec/vta/contexts/create/1.0",
"issuer": did,
"recipient": "did:key:z6MkVta",
"payload": {},
});
let verify = |purpose: &'static str| {
let unsigned = unsigned.clone();
let secret = secret.clone();
async move {
let proof = DataIntegrityProof::sign(
&unsigned,
&secret,
SignOptions::new().with_proof_purpose(purpose),
)
.await
.expect("sign");
let mut doc = unsigned;
doc["proof"] = serde_json::to_value(proof).unwrap();
let doc: TrustTask<serde_json::Value> = serde_json::from_value(doc).unwrap();
crate::trust_task_proof::verify::verify_trust_task_proof_with(
&doc,
&TrustTaskVmResolver::did_key_only(),
)
.await
}
};
verify("authentication")
.await
.expect("listed under authentication");
verify("assertionMethod")
.await
.expect("listed under assertionMethod");
assert!(verify("capabilityInvocation").await.is_err());
assert!(verify("keyAgreement").await.is_err());
}
#[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}");
}
}