use affinidi_data_integrity::{
DataIntegrityError, DataIntegrityProof, ResolvedKey, VerificationMethodResolver,
};
use affinidi_did_common::Document;
use affinidi_did_common::verification_method::{VerificationMethod, VerificationRelationship};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ProofPurpose {
AssertionMethod,
Authentication,
CapabilityInvocation,
CapabilityDelegation,
}
impl ProofPurpose {
pub const ALL: [ProofPurpose; 4] = [
ProofPurpose::AssertionMethod,
ProofPurpose::Authentication,
ProofPurpose::CapabilityInvocation,
ProofPurpose::CapabilityDelegation,
];
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
ProofPurpose::AssertionMethod => "assertionMethod",
ProofPurpose::Authentication => "authentication",
ProofPurpose::CapabilityInvocation => "capabilityInvocation",
ProofPurpose::CapabilityDelegation => "capabilityDelegation",
}
}
pub fn parse(value: &str) -> Result<Self, DataIntegrityError> {
match value {
"assertionMethod" => Ok(ProofPurpose::AssertionMethod),
"authentication" => Ok(ProofPurpose::Authentication),
"capabilityInvocation" => Ok(ProofPurpose::CapabilityInvocation),
"capabilityDelegation" => Ok(ProofPurpose::CapabilityDelegation),
"keyAgreement" => Err(DataIntegrityError::MalformedProof(
"proofPurpose keyAgreement never authorises a signature".to_string(),
)),
_ => Err(DataIntegrityError::MalformedProof(
"proofPurpose names no signing verification relationship".to_string(),
)),
}
}
}
impl std::fmt::Display for ProofPurpose {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[async_trait::async_trait]
pub trait PurposeVmResolver: Send + Sync {
async fn resolve_vm_for_purpose(
&self,
vm: &str,
purpose: ProofPurpose,
) -> Result<ResolvedKey, DataIntegrityError>;
}
#[async_trait::async_trait]
impl<R: PurposeVmResolver + ?Sized> PurposeVmResolver for &R {
async fn resolve_vm_for_purpose(
&self,
vm: &str,
purpose: ProofPurpose,
) -> Result<ResolvedKey, DataIntegrityError> {
(**self).resolve_vm_for_purpose(vm, purpose).await
}
}
#[async_trait::async_trait]
impl<R: PurposeVmResolver + ?Sized> PurposeVmResolver for std::sync::Arc<R> {
async fn resolve_vm_for_purpose(
&self,
vm: &str,
purpose: ProofPurpose,
) -> Result<ResolvedKey, DataIntegrityError> {
(**self).resolve_vm_for_purpose(vm, purpose).await
}
}
pub struct PurposeBound<R> {
resolver: R,
purpose: ProofPurpose,
}
impl<R> PurposeBound<R> {
pub fn new(resolver: R, purpose: ProofPurpose) -> Self {
Self { resolver, purpose }
}
pub fn for_proof(resolver: R, proof: &DataIntegrityProof) -> Result<Self, DataIntegrityError> {
Ok(Self::new(
resolver,
ProofPurpose::parse(&proof.proof_purpose)?,
))
}
pub fn purpose(&self) -> ProofPurpose {
self.purpose
}
}
#[async_trait::async_trait]
impl<R: PurposeVmResolver> VerificationMethodResolver for PurposeBound<R> {
async fn resolve_vm(&self, vm: &str) -> Result<ResolvedKey, DataIntegrityError> {
self.resolver.resolve_vm_for_purpose(vm, self.purpose).await
}
}
pub fn split_vm(vm: &str) -> Result<(&str, &str), DataIntegrityError> {
match vm.split_once('#') {
Some((did, fragment)) if did.starts_with("did:") && !fragment.is_empty() => {
Ok((did, fragment))
}
_ => Err(DataIntegrityError::Resolver(
"verificationMethod must be an absolute DID URL with a fragment".to_string(),
)),
}
}
pub fn check_did_key_method(vm: &str) -> Result<(), DataIntegrityError> {
let (did, fragment) = split_vm(vm)?;
let id = did.strip_prefix("did:key:").ok_or_else(|| {
DataIntegrityError::Resolver("verificationMethod is not a did:key".to_string())
})?;
if fragment != id {
return Err(DataIntegrityError::Resolver(
"verificationMethod is not the did:key's own key (fragment must repeat the key id)"
.to_string(),
));
}
Ok(())
}
fn relationship(doc: &Document, purpose: ProofPurpose) -> &[VerificationRelationship] {
match purpose {
ProofPurpose::AssertionMethod => &doc.assertion_method,
ProofPurpose::Authentication => &doc.authentication,
ProofPurpose::CapabilityInvocation => &doc.capability_invocation,
ProofPurpose::CapabilityDelegation => &doc.capability_delegation,
}
}
pub fn authorised_method(
doc: &Document,
did: &str,
vm: &str,
purpose: ProofPurpose,
) -> Result<VerificationMethod, DataIntegrityError> {
if doc.id.as_str() != did {
return Err(DataIntegrityError::Resolver(
"the resolved DID document's id is not the verificationMethod's DID".to_string(),
));
}
let names_vm = |id: &str| match id.strip_prefix('#') {
Some(fragment) => {
vm.strip_prefix(did).and_then(|rest| rest.strip_prefix('#')) == Some(fragment)
}
None => id == vm,
};
let embedded = |r: &VerificationRelationship| match r {
VerificationRelationship::VerificationMethod(m) if names_vm(m.id.as_str()) => {
Some((**m).clone())
}
_ => None,
};
let listed = relationship(doc, purpose).iter().find_map(|r| match r {
VerificationRelationship::Reference(id) if names_vm(id) => Some(None),
other => embedded(other).map(Some),
});
let method = match listed {
None => {
return Err(DataIntegrityError::Resolver(format!(
"verificationMethod is not listed under {purpose} in its DID document"
)));
}
Some(Some(method)) => method,
Some(None) => doc
.verification_method
.iter()
.find(|m| names_vm(m.id.as_str()))
.cloned()
.ok_or_else(|| {
DataIntegrityError::Resolver(
"verificationMethod is referenced but not defined under verificationMethod \
in its DID document"
.to_string(),
)
})?,
};
if method.controller.as_str() != did {
return Err(DataIntegrityError::Resolver(
"verificationMethod's controller is not the DID that names it".to_string(),
));
}
Ok(method)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
const DID: &str = "did:web:issuer.example";
fn doc(v: serde_json::Value) -> Document {
serde_json::from_value(v).expect("DID document")
}
fn method(id: &str, controller: &str) -> serde_json::Value {
json!({ "id": id, "type": "Multikey", "controller": controller,
"publicKeyMultibase": "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" })
}
fn vm() -> String {
format!("{DID}#key-1")
}
#[test]
fn a_method_in_the_wrong_relationship_is_refused() {
let d = doc(
json!({ "id": DID, "verificationMethod": [method(&vm(), DID)],
"authentication": [vm()] }),
);
let err = authorised_method(&d, DID, &vm(), ProofPurpose::AssertionMethod).unwrap_err();
assert!(
err.to_string().contains("not listed under assertionMethod"),
"{err}"
);
authorised_method(&d, DID, &vm(), ProofPurpose::Authentication).expect("listed");
}
#[test]
fn a_key_agreement_key_is_never_a_signing_key() {
let d = doc(
json!({ "id": DID, "verificationMethod": [method(&vm(), DID)],
"keyAgreement": [vm()] }),
);
for p in ProofPurpose::ALL {
assert!(authorised_method(&d, DID, &vm(), p).is_err(), "{p}");
}
assert!(ProofPurpose::parse("keyAgreement").is_err());
assert!(ProofPurpose::parse("").is_err());
assert!(ProofPurpose::parse("assertion").is_err());
}
#[test]
fn a_method_of_another_did_is_refused() {
let d =
doc(json!({ "id": DID, "assertionMethod": [method(&vm(), "did:web:other.example")] }));
let err = authorised_method(&d, DID, &vm(), ProofPurpose::AssertionMethod).unwrap_err();
assert!(err.to_string().contains("controller"), "{err}");
let d = doc(json!({ "id": "did:web:other.example",
"verificationMethod": [method(&vm(), DID)],
"assertionMethod": [vm()] }));
let err = authorised_method(&d, DID, &vm(), ProofPurpose::AssertionMethod).unwrap_err();
assert!(err.to_string().contains("document's id"), "{err}");
let other = "did:web:other.example#key-1";
let d = doc(
json!({ "id": DID, "verificationMethod": [method(other, "did:web:other.example")],
"assertionMethod": [other] }),
);
assert!(authorised_method(&d, DID, other, ProofPurpose::AssertionMethod).is_err());
}
#[test]
fn a_relative_reference_resolves_against_the_document_id() {
let d = doc(
json!({ "id": DID, "verificationMethod": [method(&vm(), DID)],
"assertionMethod": ["#key-1"] }),
);
authorised_method(&d, DID, &vm(), ProofPurpose::AssertionMethod).expect("#key-1");
let d = doc(
json!({ "id": DID, "verificationMethod": [method(&vm(), DID)],
"assertionMethod": ["#key-2"] }),
);
assert!(authorised_method(&d, DID, &vm(), ProofPurpose::AssertionMethod).is_err());
}
#[test]
fn an_embedded_method_counts_for_its_own_relationship() {
let d = doc(json!({ "id": DID, "authentication": [method(&vm(), DID)] }));
authorised_method(&d, DID, &vm(), ProofPurpose::Authentication).expect("embedded");
assert!(authorised_method(&d, DID, &vm(), ProofPurpose::AssertionMethod).is_err());
}
#[test]
fn a_did_key_names_only_its_own_key() {
let id = "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
check_did_key_method(&format!("did:key:{id}#{id}")).expect("its own key");
assert!(check_did_key_method(&format!("did:key:{id}#key-0")).is_err());
assert!(check_did_key_method(&format!("did:key:{id}")).is_err());
}
#[test]
fn refusals_carry_no_identifier() {
let d = doc(json!({ "id": DID, "verificationMethod": [method(&vm(), DID)] }));
let err = authorised_method(&d, DID, &vm(), ProofPurpose::AssertionMethod).unwrap_err();
assert!(!err.to_string().contains(DID), "{err}");
}
}