use affinidi_data_integrity::{DataIntegrityError, ResolvedKey};
use affinidi_did_resolver_cache_sdk::DIDCacheClient;
use affinidi_secrets_resolver::secrets::KeyType;
use vti_common::auth::{ProofPurpose, PurposeVmResolver, TrustTaskVmResolver};
use vti_common::error::AppError;
pub struct DidVmResolver {
#[cfg_attr(not(feature = "bbs"), allow(dead_code))]
resolver: Option<DIDCacheClient>,
keys: TrustTaskVmResolver,
}
impl DidVmResolver {
pub fn new(resolver: Option<DIDCacheClient>) -> Self {
Self {
keys: TrustTaskVmResolver::from_optional(resolver.clone()),
resolver,
}
}
pub(crate) async fn resolve_ed25519(
&self,
vm: &str,
purpose: ProofPurpose,
) -> Result<Vec<u8>, AppError> {
let key = self
.keys
.resolve_vm_for_purpose(vm, purpose)
.await
.map_err(|e| AppError::Validation(format!("verification method refused: {e}")))?;
if key.key_type != KeyType::Ed25519 {
return Err(AppError::Validation(format!(
"the verification method is a {:?} key; this path verifies Ed25519 signatures",
key.key_type
)));
}
Ok(key.public_key_bytes)
}
pub(crate) async fn resolve_jws_key(
&self,
vm: &str,
purpose: ProofPurpose,
) -> Result<vta_sdk::jws::JwsKey, AppError> {
let key = self
.keys
.resolve_vm_for_purpose(vm, purpose)
.await
.map_err(|e| AppError::Validation(format!("verification method refused: {e}")))?;
vta_sdk::jws::JwsKey::from_resolved(&key)
.map_err(|e| AppError::Validation(format!("verification method refused: {e}")))
}
#[cfg(feature = "bbs")]
pub(crate) async fn resolve_bbs_g2(
&self,
vm: &str,
purpose: ProofPurpose,
) -> Result<[u8; 96], AppError> {
use serde_json::Value;
use vta_sdk::trust_task_proof::purpose::{
authorised_method, check_did_key_method, split_vm,
};
let refused = |e: DataIntegrityError| {
AppError::Validation(format!("verification method refused: {e}"))
};
let (base_did, _) = split_vm(vm).map_err(refused)?;
if base_did.starts_with("did:key:") {
check_did_key_method(vm).map_err(refused)?;
return affinidi_crypto::bls12381::did_key_to_g2_pub(base_did).map_err(|e| {
AppError::Validation(format!("the verification method is not a BBS did:key: {e}"))
});
}
let resolver = self.resolver.as_ref().ok_or_else(|| {
AppError::Validation(
"resolving this verification method needs a DID resolver to verify did:webvh / \
did:web BBS issuers"
.to_string(),
)
})?;
let resolved = vta_sdk::trust_task_proof::resolve_for_vm(resolver, base_did, vm)
.await
.map_err(|e| {
AppError::Validation(format!(
"the verification method's DID did not resolve: {e}"
))
})?;
let entry = authorised_method(&resolved.doc, base_did, vm, purpose).map_err(refused)?;
let multibase = entry
.property_set
.get("publicKeyMultibase")
.and_then(Value::as_str)
.ok_or_else(|| {
AppError::Validation(
"the verification method has no publicKeyMultibase (BLS12-381 G2 Multikey)"
.to_string(),
)
})?;
affinidi_crypto::bls12381::did_key_to_g2_pub(&format!("did:key:{multibase}")).map_err(|e| {
AppError::Validation(format!(
"the verification method is not a BLS12-381 G2 Multikey: {e}"
))
})
}
}
#[async_trait::async_trait]
impl PurposeVmResolver for DidVmResolver {
async fn resolve_vm_for_purpose(
&self,
vm: &str,
purpose: ProofPurpose,
) -> Result<ResolvedKey, DataIntegrityError> {
self.keys.resolve_vm_for_purpose(vm, purpose).await
}
}
pub(crate) use vti_common::auth::check_issuer_binding;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn issuer_binding_is_exact_and_needs_a_fragment() {
check_issuer_binding("did:web:a.example#key-0", "did:web:a.example").expect("bound");
assert!(check_issuer_binding("did:web:b.example#key-0", "did:web:a.example").is_err());
assert!(check_issuer_binding("did:web:a.example", "did:web:a.example").is_err());
assert!(check_issuer_binding("did:web:a.example#", "did:web:a.example").is_err());
let err = check_issuer_binding("did:web:b.example#key-0", "did:web:a.example")
.unwrap_err()
.to_string();
assert!(
!err.contains("a.example"),
"no identifiers in the refusal: {err}"
);
}
}