use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ProofPurposeKind {
Authentication,
AssertionMethod,
KeyAgreement,
CapabilityInvocation,
CapabilityDelegation,
Custom(String),
}
impl ProofPurposeKind {
pub fn as_str(&self) -> &str {
match self {
Self::Authentication => "authentication",
Self::AssertionMethod => "assertionMethod",
Self::KeyAgreement => "keyAgreement",
Self::CapabilityInvocation => "capabilityInvocation",
Self::CapabilityDelegation => "capabilityDelegation",
Self::Custom(uri) => uri.as_str(),
}
}
pub fn from_str_lossy(s: &str) -> Self {
match s {
"authentication" => Self::Authentication,
"assertionMethod" => Self::AssertionMethod,
"keyAgreement" => Self::KeyAgreement,
"capabilityInvocation" => Self::CapabilityInvocation,
"capabilityDelegation" => Self::CapabilityDelegation,
other => Self::Custom(other.to_string()),
}
}
pub fn is_standard(&self) -> bool {
!matches!(self, Self::Custom(_))
}
}
impl std::fmt::Display for ProofPurposeKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct DidRelationships {
pub controller: String,
relationships: HashMap<String, HashSet<String>>,
}
impl DidRelationships {
pub fn new(controller: impl Into<String>) -> Self {
Self {
controller: controller.into(),
relationships: HashMap::new(),
}
}
pub fn add_authentication(&mut self, vm_id: impl Into<String>) {
self.add(ProofPurposeKind::Authentication, vm_id);
}
pub fn add_assertion_method(&mut self, vm_id: impl Into<String>) {
self.add(ProofPurposeKind::AssertionMethod, vm_id);
}
pub fn add_key_agreement(&mut self, vm_id: impl Into<String>) {
self.add(ProofPurposeKind::KeyAgreement, vm_id);
}
pub fn add_capability_invocation(&mut self, vm_id: impl Into<String>) {
self.add(ProofPurposeKind::CapabilityInvocation, vm_id);
}
pub fn add_capability_delegation(&mut self, vm_id: impl Into<String>) {
self.add(ProofPurposeKind::CapabilityDelegation, vm_id);
}
pub fn add_custom(&mut self, purpose_uri: impl Into<String>, vm_id: impl Into<String>) {
let purpose = ProofPurposeKind::Custom(purpose_uri.into());
self.add(purpose, vm_id);
}
pub fn add(&mut self, purpose: ProofPurposeKind, vm_id: impl Into<String>) {
self.relationships
.entry(purpose.as_str().to_string())
.or_default()
.insert(vm_id.into());
}
pub fn is_authorised(&self, purpose: &ProofPurposeKind, vm_id: &str) -> bool {
self.relationships
.get(purpose.as_str())
.is_some_and(|set| set.contains(vm_id))
}
pub fn methods_for(&self, purpose: &ProofPurposeKind) -> Vec<String> {
self.relationships
.get(purpose.as_str())
.map_or_else(Vec::new, |set| {
let mut v: Vec<String> = set.iter().cloned().collect();
v.sort();
v
})
}
pub fn purposes(&self) -> Vec<String> {
let mut v: Vec<String> = self.relationships.keys().cloned().collect();
v.sort();
v
}
pub fn total_entries(&self) -> usize {
self.relationships.values().map(|s| s.len()).sum()
}
pub fn remove(&mut self, purpose: &ProofPurposeKind, vm_id: &str) -> bool {
if let Some(set) = self.relationships.get_mut(purpose.as_str()) {
let removed = set.remove(vm_id);
if set.is_empty() {
self.relationships.remove(purpose.as_str());
}
removed
} else {
false
}
}
}
#[derive(Debug, Clone)]
pub struct ProofEntry {
pub purpose: ProofPurposeKind,
pub verification_method: String,
pub created_ms: u64,
pub expires_ms: Option<u64>,
pub challenge: Option<String>,
pub domain: Option<String>,
pub nonce: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ValidationContext {
pub challenge: Option<String>,
pub domain: Option<String>,
pub current_time_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PurposeError {
Unauthorised {
purpose: String,
verification_method: String,
},
Expired { expires_ms: u64, current_ms: u64 },
FutureProof { created_ms: u64, current_ms: u64 },
ChallengeMismatch { expected: String, actual: String },
ChallengeMissing,
DomainMismatch { expected: String, actual: String },
UnknownCustomPurpose(String),
ChainError { index: usize, reason: String },
EmptyChain,
}
impl std::fmt::Display for PurposeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unauthorised {
purpose,
verification_method,
} => write!(
f,
"Verification method '{verification_method}' is not authorised for purpose '{purpose}'"
),
Self::Expired {
expires_ms,
current_ms,
} => write!(
f,
"Proof expired at {expires_ms} ms, current time is {current_ms} ms"
),
Self::FutureProof {
created_ms,
current_ms,
} => write!(
f,
"Proof created at {created_ms} ms is in the future (current: {current_ms} ms)"
),
Self::ChallengeMismatch { expected, actual } => {
write!(
f,
"Challenge mismatch: expected '{expected}', got '{actual}'"
)
}
Self::ChallengeMissing => write!(f, "Authentication proof requires a challenge"),
Self::DomainMismatch { expected, actual } => {
write!(f, "Domain mismatch: expected '{expected}', got '{actual}'")
}
Self::UnknownCustomPurpose(uri) => {
write!(f, "Unknown custom proof purpose: '{uri}'")
}
Self::ChainError { index, reason } => {
write!(f, "Proof chain error at index {index}: {reason}")
}
Self::EmptyChain => write!(f, "Proof chain must contain at least one entry"),
}
}
}
impl std::error::Error for PurposeError {}
pub trait CustomPurposeValidator: Send + Sync {
fn validate(
&self,
entry: &ProofEntry,
relationships: &DidRelationships,
context: &ValidationContext,
) -> Result<(), String>;
}
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub valid: bool,
pub purpose: String,
pub verification_method: String,
pub message: String,
}
#[derive(Debug, Clone)]
pub struct ChainValidationResult {
pub valid: bool,
pub entries: Vec<ValidationResult>,
pub chain_length: usize,
pub error: Option<String>,
}
pub struct ProofPurposeValidator {
custom_validators: HashMap<String, Box<dyn CustomPurposeValidator>>,
max_clock_skew_ms: u64,
}
impl Default for ProofPurposeValidator {
fn default() -> Self {
Self::new()
}
}
impl ProofPurposeValidator {
pub fn new() -> Self {
Self {
custom_validators: HashMap::new(),
max_clock_skew_ms: 300_000, }
}
pub fn with_max_clock_skew_ms(mut self, ms: u64) -> Self {
self.max_clock_skew_ms = ms;
self
}
pub fn register_custom(
&mut self,
purpose_uri: impl Into<String>,
validator: Box<dyn CustomPurposeValidator>,
) {
self.custom_validators.insert(purpose_uri.into(), validator);
}
pub fn registered_custom_purposes(&self) -> Vec<String> {
let mut v: Vec<String> = self.custom_validators.keys().cloned().collect();
v.sort();
v
}
pub fn validate(
&self,
entry: &ProofEntry,
relationships: &DidRelationships,
context: &ValidationContext,
) -> Result<ValidationResult, PurposeError> {
if !relationships.is_authorised(&entry.purpose, &entry.verification_method) {
return Err(PurposeError::Unauthorised {
purpose: entry.purpose.as_str().to_string(),
verification_method: entry.verification_method.clone(),
});
}
if let Some(expires_ms) = entry.expires_ms {
if context.current_time_ms > expires_ms {
return Err(PurposeError::Expired {
expires_ms,
current_ms: context.current_time_ms,
});
}
}
if entry.created_ms > context.current_time_ms + self.max_clock_skew_ms {
return Err(PurposeError::FutureProof {
created_ms: entry.created_ms,
current_ms: context.current_time_ms,
});
}
match &entry.purpose {
ProofPurposeKind::Authentication => {
self.validate_authentication(entry, context)?;
}
ProofPurposeKind::AssertionMethod => {
self.validate_assertion_method(entry, context)?;
}
ProofPurposeKind::KeyAgreement => {
self.validate_key_agreement(entry, context)?;
}
ProofPurposeKind::CapabilityInvocation => {
self.validate_capability_invocation(entry, context)?;
}
ProofPurposeKind::CapabilityDelegation => {
self.validate_capability_delegation(entry, context)?;
}
ProofPurposeKind::Custom(uri) => {
self.validate_custom(uri, entry, relationships, context)?;
}
}
Ok(ValidationResult {
valid: true,
purpose: entry.purpose.as_str().to_string(),
verification_method: entry.verification_method.clone(),
message: format!(
"Proof purpose '{}' validated successfully for method '{}'",
entry.purpose.as_str(),
entry.verification_method
),
})
}
fn validate_authentication(
&self,
entry: &ProofEntry,
context: &ValidationContext,
) -> Result<(), PurposeError> {
if let Some(ref expected) = context.challenge {
match &entry.challenge {
Some(actual) if actual == expected => {}
Some(actual) => {
return Err(PurposeError::ChallengeMismatch {
expected: expected.clone(),
actual: actual.clone(),
});
}
None => {
return Err(PurposeError::ChallengeMissing);
}
}
}
self.check_domain(entry, context)?;
Ok(())
}
fn validate_assertion_method(
&self,
entry: &ProofEntry,
context: &ValidationContext,
) -> Result<(), PurposeError> {
self.check_domain(entry, context)?;
Ok(())
}
fn validate_key_agreement(
&self,
entry: &ProofEntry,
context: &ValidationContext,
) -> Result<(), PurposeError> {
self.check_domain(entry, context)?;
Ok(())
}
fn validate_capability_invocation(
&self,
entry: &ProofEntry,
context: &ValidationContext,
) -> Result<(), PurposeError> {
self.check_domain(entry, context)?;
Ok(())
}
fn validate_capability_delegation(
&self,
entry: &ProofEntry,
context: &ValidationContext,
) -> Result<(), PurposeError> {
self.check_domain(entry, context)?;
Ok(())
}
fn validate_custom(
&self,
uri: &str,
entry: &ProofEntry,
relationships: &DidRelationships,
context: &ValidationContext,
) -> Result<(), PurposeError> {
if let Some(validator) = self.custom_validators.get(uri) {
validator
.validate(entry, relationships, context)
.map_err(|reason| PurposeError::ChainError { index: 0, reason })
} else {
Err(PurposeError::UnknownCustomPurpose(uri.to_string()))
}
}
fn check_domain(
&self,
entry: &ProofEntry,
context: &ValidationContext,
) -> Result<(), PurposeError> {
if let Some(ref expected) = context.domain {
if let Some(ref actual) = entry.domain {
if actual != expected {
return Err(PurposeError::DomainMismatch {
expected: expected.clone(),
actual: actual.clone(),
});
}
}
}
Ok(())
}
pub fn validate_chain(
&self,
chain: &[ProofEntry],
relationships: &DidRelationships,
context: &ValidationContext,
) -> Result<ChainValidationResult, PurposeError> {
if chain.is_empty() {
return Err(PurposeError::EmptyChain);
}
let mut results = Vec::with_capacity(chain.len());
let mut prev_created_ms: u64 = 0;
for (i, entry) in chain.iter().enumerate() {
if entry.created_ms < prev_created_ms {
return Err(PurposeError::ChainError {
index: i,
reason: format!(
"Proof at index {} has created_ms={} which is before previous entry's created_ms={}",
i, entry.created_ms, prev_created_ms
),
});
}
prev_created_ms = entry.created_ms;
match self.validate(entry, relationships, context) {
Ok(result) => results.push(result),
Err(e) => {
return Err(PurposeError::ChainError {
index: i,
reason: e.to_string(),
});
}
}
}
Ok(ChainValidationResult {
valid: true,
chain_length: chain.len(),
entries: results,
error: None,
})
}
pub fn requires_challenge(purpose: &ProofPurposeKind) -> bool {
matches!(purpose, ProofPurposeKind::Authentication)
}
pub fn describe_purpose(purpose: &ProofPurposeKind) -> &'static str {
match purpose {
ProofPurposeKind::Authentication => {
"Prove the identity of the DID subject via challenge-response"
}
ProofPurposeKind::AssertionMethod => {
"Make verifiable claims about a subject (e.g. issue credentials)"
}
ProofPurposeKind::KeyAgreement => {
"Establish a shared secret for encrypted communication"
}
ProofPurposeKind::CapabilityInvocation => {
"Exercise an authorization capability granted by a controller"
}
ProofPurposeKind::CapabilityDelegation => {
"Delegate an authorization capability to another party"
}
ProofPurposeKind::Custom(_) => "User-defined proof purpose",
}
}
pub fn relationship_name(purpose: &ProofPurposeKind) -> &str {
purpose.as_str()
}
}
#[derive(Debug, Clone)]
pub struct ProofPurposeRegistry {
purposes: HashMap<String, PurposeMetadata>,
}
#[derive(Debug, Clone)]
pub struct PurposeMetadata {
pub purpose_id: String,
pub label: String,
pub description: String,
pub requires_challenge: bool,
pub recommends_domain: bool,
}
impl Default for ProofPurposeRegistry {
fn default() -> Self {
Self::new()
}
}
impl ProofPurposeRegistry {
pub fn new() -> Self {
let mut registry = Self {
purposes: HashMap::new(),
};
registry.register(PurposeMetadata {
purpose_id: "authentication".to_string(),
label: "Authentication".to_string(),
description: "Prove the identity of the DID subject".to_string(),
requires_challenge: true,
recommends_domain: true,
});
registry.register(PurposeMetadata {
purpose_id: "assertionMethod".to_string(),
label: "Assertion Method".to_string(),
description: "Make verifiable claims about a subject".to_string(),
requires_challenge: false,
recommends_domain: false,
});
registry.register(PurposeMetadata {
purpose_id: "keyAgreement".to_string(),
label: "Key Agreement".to_string(),
description: "Establish a shared secret for encrypted communication".to_string(),
requires_challenge: false,
recommends_domain: false,
});
registry.register(PurposeMetadata {
purpose_id: "capabilityInvocation".to_string(),
label: "Capability Invocation".to_string(),
description: "Exercise an authorization capability".to_string(),
requires_challenge: false,
recommends_domain: true,
});
registry.register(PurposeMetadata {
purpose_id: "capabilityDelegation".to_string(),
label: "Capability Delegation".to_string(),
description: "Delegate an authorization capability".to_string(),
requires_challenge: false,
recommends_domain: true,
});
registry
}
pub fn register(&mut self, metadata: PurposeMetadata) {
self.purposes.insert(metadata.purpose_id.clone(), metadata);
}
pub fn get(&self, purpose_id: &str) -> Option<&PurposeMetadata> {
self.purposes.get(purpose_id)
}
pub fn is_registered(&self, purpose_id: &str) -> bool {
self.purposes.contains_key(purpose_id)
}
pub fn all_purpose_ids(&self) -> Vec<String> {
let mut v: Vec<String> = self.purposes.keys().cloned().collect();
v.sort();
v
}
pub fn count(&self) -> usize {
self.purposes.len()
}
pub fn unregister(&mut self, purpose_id: &str) -> bool {
self.purposes.remove(purpose_id).is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_relationships() -> DidRelationships {
let mut rels = DidRelationships::new("did:example:alice");
rels.add_authentication("did:example:alice#key-1");
rels.add_assertion_method("did:example:alice#key-2");
rels.add_key_agreement("did:example:alice#key-3");
rels.add_capability_invocation("did:example:alice#key-4");
rels.add_capability_delegation("did:example:alice#key-5");
rels
}
fn make_context() -> ValidationContext {
ValidationContext {
challenge: Some("challenge-123".to_string()),
domain: Some("example.com".to_string()),
current_time_ms: 1_700_000_000_000,
}
}
fn make_entry(purpose: ProofPurposeKind, vm: &str) -> ProofEntry {
ProofEntry {
purpose,
verification_method: vm.to_string(),
created_ms: 1_699_999_000_000,
expires_ms: Some(1_700_100_000_000),
challenge: Some("challenge-123".to_string()),
domain: Some("example.com".to_string()),
nonce: None,
}
}
#[test]
fn test_purpose_kind_as_str() {
assert_eq!(ProofPurposeKind::Authentication.as_str(), "authentication");
assert_eq!(
ProofPurposeKind::AssertionMethod.as_str(),
"assertionMethod"
);
assert_eq!(ProofPurposeKind::KeyAgreement.as_str(), "keyAgreement");
assert_eq!(
ProofPurposeKind::CapabilityInvocation.as_str(),
"capabilityInvocation"
);
assert_eq!(
ProofPurposeKind::CapabilityDelegation.as_str(),
"capabilityDelegation"
);
}
#[test]
fn test_purpose_kind_custom() {
let custom = ProofPurposeKind::Custom("https://example.com/myPurpose".to_string());
assert_eq!(custom.as_str(), "https://example.com/myPurpose");
assert!(!custom.is_standard());
}
#[test]
fn test_purpose_kind_is_standard() {
assert!(ProofPurposeKind::Authentication.is_standard());
assert!(ProofPurposeKind::AssertionMethod.is_standard());
assert!(ProofPurposeKind::KeyAgreement.is_standard());
assert!(ProofPurposeKind::CapabilityInvocation.is_standard());
assert!(ProofPurposeKind::CapabilityDelegation.is_standard());
}
#[test]
fn test_purpose_kind_from_str_lossy() {
assert_eq!(
ProofPurposeKind::from_str_lossy("authentication"),
ProofPurposeKind::Authentication
);
assert_eq!(
ProofPurposeKind::from_str_lossy("assertionMethod"),
ProofPurposeKind::AssertionMethod
);
assert_eq!(
ProofPurposeKind::from_str_lossy("keyAgreement"),
ProofPurposeKind::KeyAgreement
);
assert_eq!(
ProofPurposeKind::from_str_lossy("capabilityInvocation"),
ProofPurposeKind::CapabilityInvocation
);
assert_eq!(
ProofPurposeKind::from_str_lossy("capabilityDelegation"),
ProofPurposeKind::CapabilityDelegation
);
let c = ProofPurposeKind::from_str_lossy("custom:xyz");
assert_eq!(c, ProofPurposeKind::Custom("custom:xyz".to_string()));
}
#[test]
fn test_purpose_kind_display() {
assert_eq!(
format!("{}", ProofPurposeKind::Authentication),
"authentication"
);
assert_eq!(
format!("{}", ProofPurposeKind::Custom("urn:x".to_string())),
"urn:x"
);
}
#[test]
fn test_did_relationships_new() {
let rels = DidRelationships::new("did:example:bob");
assert_eq!(rels.controller, "did:example:bob");
assert_eq!(rels.total_entries(), 0);
}
#[test]
fn test_did_relationships_add_and_check() {
let rels = make_relationships();
assert!(rels.is_authorised(&ProofPurposeKind::Authentication, "did:example:alice#key-1"));
assert!(rels.is_authorised(
&ProofPurposeKind::AssertionMethod,
"did:example:alice#key-2"
));
assert!(rels.is_authorised(&ProofPurposeKind::KeyAgreement, "did:example:alice#key-3"));
assert!(rels.is_authorised(
&ProofPurposeKind::CapabilityInvocation,
"did:example:alice#key-4"
));
assert!(rels.is_authorised(
&ProofPurposeKind::CapabilityDelegation,
"did:example:alice#key-5"
));
}
#[test]
fn test_did_relationships_unauthorised() {
let rels = make_relationships();
assert!(!rels.is_authorised(&ProofPurposeKind::Authentication, "did:example:alice#key-2"));
assert!(!rels.is_authorised(
&ProofPurposeKind::AssertionMethod,
"did:example:alice#key-1"
));
}
#[test]
fn test_did_relationships_methods_for() {
let rels = make_relationships();
let auth_methods = rels.methods_for(&ProofPurposeKind::Authentication);
assert_eq!(auth_methods, vec!["did:example:alice#key-1"]);
}
#[test]
fn test_did_relationships_purposes() {
let rels = make_relationships();
let purposes = rels.purposes();
assert_eq!(purposes.len(), 5);
assert!(purposes.contains(&"authentication".to_string()));
assert!(purposes.contains(&"assertionMethod".to_string()));
}
#[test]
fn test_did_relationships_total_entries() {
let rels = make_relationships();
assert_eq!(rels.total_entries(), 5);
}
#[test]
fn test_did_relationships_remove() {
let mut rels = make_relationships();
assert!(rels.remove(&ProofPurposeKind::Authentication, "did:example:alice#key-1"));
assert!(!rels.is_authorised(&ProofPurposeKind::Authentication, "did:example:alice#key-1"));
assert_eq!(rels.total_entries(), 4);
}
#[test]
fn test_did_relationships_remove_nonexistent() {
let mut rels = make_relationships();
assert!(!rels.remove(
&ProofPurposeKind::Authentication,
"did:example:alice#key-99"
));
}
#[test]
fn test_did_relationships_custom_purpose() {
let mut rels = DidRelationships::new("did:example:carol");
rels.add_custom("https://custom.example/sign", "did:example:carol#key-c");
assert!(rels.is_authorised(
&ProofPurposeKind::Custom("https://custom.example/sign".to_string()),
"did:example:carol#key-c"
));
}
#[test]
fn test_did_relationships_multiple_methods_per_purpose() {
let mut rels = DidRelationships::new("did:example:dave");
rels.add_authentication("did:example:dave#key-a");
rels.add_authentication("did:example:dave#key-b");
let methods = rels.methods_for(&ProofPurposeKind::Authentication);
assert_eq!(methods.len(), 2);
}
#[test]
fn test_validate_authentication_success() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = make_context();
let entry = make_entry(ProofPurposeKind::Authentication, "did:example:alice#key-1");
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_ok());
let r = result.expect("should succeed");
assert!(r.valid);
assert_eq!(r.purpose, "authentication");
}
#[test]
fn test_validate_authentication_challenge_mismatch() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = make_context();
let mut entry = make_entry(ProofPurposeKind::Authentication, "did:example:alice#key-1");
entry.challenge = Some("wrong-challenge".to_string());
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_err());
let err = result.expect_err("should fail");
assert!(matches!(err, PurposeError::ChallengeMismatch { .. }));
}
#[test]
fn test_validate_authentication_challenge_missing() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = make_context();
let mut entry = make_entry(ProofPurposeKind::Authentication, "did:example:alice#key-1");
entry.challenge = None;
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_err());
let err = result.expect_err("should fail");
assert!(matches!(err, PurposeError::ChallengeMissing));
}
#[test]
fn test_validate_authentication_domain_mismatch() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = make_context();
let mut entry = make_entry(ProofPurposeKind::Authentication, "did:example:alice#key-1");
entry.domain = Some("evil.com".to_string());
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_err());
let err = result.expect_err("should fail");
assert!(matches!(err, PurposeError::DomainMismatch { .. }));
}
#[test]
fn test_validate_assertion_method_success() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = ValidationContext::default();
let mut entry = make_entry(ProofPurposeKind::AssertionMethod, "did:example:alice#key-2");
entry.created_ms = 0;
entry.expires_ms = None;
entry.challenge = None;
entry.domain = None;
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_ok());
}
#[test]
fn test_validate_assertion_method_unauthorised() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = ValidationContext::default();
let mut entry = make_entry(ProofPurposeKind::AssertionMethod, "did:example:alice#key-1");
entry.created_ms = 0;
entry.expires_ms = None;
entry.challenge = None;
entry.domain = None;
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_err());
let err = result.expect_err("should fail");
assert!(matches!(err, PurposeError::Unauthorised { .. }));
}
#[test]
fn test_validate_key_agreement_success() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = ValidationContext::default();
let mut entry = make_entry(ProofPurposeKind::KeyAgreement, "did:example:alice#key-3");
entry.created_ms = 0;
entry.expires_ms = None;
entry.challenge = None;
entry.domain = None;
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_ok());
}
#[test]
fn test_validate_key_agreement_unauthorised() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = ValidationContext::default();
let mut entry = make_entry(ProofPurposeKind::KeyAgreement, "did:example:alice#key-99");
entry.created_ms = 0;
entry.expires_ms = None;
entry.challenge = None;
entry.domain = None;
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_err());
}
#[test]
fn test_validate_capability_invocation_success() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = ValidationContext::default();
let mut entry = make_entry(
ProofPurposeKind::CapabilityInvocation,
"did:example:alice#key-4",
);
entry.created_ms = 0;
entry.expires_ms = None;
entry.challenge = None;
entry.domain = None;
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_ok());
}
#[test]
fn test_validate_capability_invocation_unauthorised() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = ValidationContext::default();
let mut entry = make_entry(
ProofPurposeKind::CapabilityInvocation,
"did:example:alice#key-1",
);
entry.created_ms = 0;
entry.expires_ms = None;
entry.challenge = None;
entry.domain = None;
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_err());
}
#[test]
fn test_validate_capability_delegation_success() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = ValidationContext::default();
let mut entry = make_entry(
ProofPurposeKind::CapabilityDelegation,
"did:example:alice#key-5",
);
entry.created_ms = 0;
entry.expires_ms = None;
entry.challenge = None;
entry.domain = None;
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_ok());
}
#[test]
fn test_validate_capability_delegation_unauthorised() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = ValidationContext::default();
let mut entry = make_entry(
ProofPurposeKind::CapabilityDelegation,
"did:example:alice#key-1",
);
entry.created_ms = 0;
entry.expires_ms = None;
entry.challenge = None;
entry.domain = None;
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_err());
}
#[test]
fn test_validate_expired_proof() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = ValidationContext {
current_time_ms: 1_800_000_000_000, ..Default::default()
};
let mut entry = make_entry(ProofPurposeKind::AssertionMethod, "did:example:alice#key-2");
entry.expires_ms = Some(1_700_000_000_000);
entry.challenge = None;
entry.domain = None;
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_err());
let err = result.expect_err("should fail");
assert!(matches!(err, PurposeError::Expired { .. }));
}
#[test]
fn test_validate_future_proof() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = ValidationContext {
current_time_ms: 1_000_000_000_000,
..Default::default()
};
let mut entry = make_entry(ProofPurposeKind::AssertionMethod, "did:example:alice#key-2");
entry.created_ms = 1_500_000_000_000; entry.expires_ms = None;
entry.challenge = None;
entry.domain = None;
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_err());
let err = result.expect_err("should fail");
assert!(matches!(err, PurposeError::FutureProof { .. }));
}
#[test]
fn test_validate_within_clock_skew() {
let validator = ProofPurposeValidator::new().with_max_clock_skew_ms(600_000);
let rels = make_relationships();
let ctx = ValidationContext {
current_time_ms: 1_700_000_000_000,
..Default::default()
};
let mut entry = make_entry(ProofPurposeKind::AssertionMethod, "did:example:alice#key-2");
entry.created_ms = 1_700_000_300_000;
entry.expires_ms = None;
entry.challenge = None;
entry.domain = None;
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_ok());
}
struct AlwaysAcceptValidator;
impl CustomPurposeValidator for AlwaysAcceptValidator {
fn validate(
&self,
_entry: &ProofEntry,
_relationships: &DidRelationships,
_context: &ValidationContext,
) -> Result<(), String> {
Ok(())
}
}
struct AlwaysRejectValidator;
impl CustomPurposeValidator for AlwaysRejectValidator {
fn validate(
&self,
_entry: &ProofEntry,
_relationships: &DidRelationships,
_context: &ValidationContext,
) -> Result<(), String> {
Err("custom rejection".to_string())
}
}
#[test]
fn test_custom_purpose_accepted() {
let mut validator = ProofPurposeValidator::new();
validator.register_custom(
"https://example.com/custom",
Box::new(AlwaysAcceptValidator),
);
let mut rels = DidRelationships::new("did:example:custom");
rels.add_custom("https://example.com/custom", "did:example:custom#key-c");
let entry = ProofEntry {
purpose: ProofPurposeKind::Custom("https://example.com/custom".to_string()),
verification_method: "did:example:custom#key-c".to_string(),
created_ms: 0,
expires_ms: None,
challenge: None,
domain: None,
nonce: None,
};
let ctx = ValidationContext::default();
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_ok());
}
#[test]
fn test_custom_purpose_rejected() {
let mut validator = ProofPurposeValidator::new();
validator.register_custom(
"https://example.com/custom",
Box::new(AlwaysRejectValidator),
);
let mut rels = DidRelationships::new("did:example:custom");
rels.add_custom("https://example.com/custom", "did:example:custom#key-c");
let entry = ProofEntry {
purpose: ProofPurposeKind::Custom("https://example.com/custom".to_string()),
verification_method: "did:example:custom#key-c".to_string(),
created_ms: 0,
expires_ms: None,
challenge: None,
domain: None,
nonce: None,
};
let ctx = ValidationContext::default();
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_err());
}
#[test]
fn test_unknown_custom_purpose() {
let validator = ProofPurposeValidator::new();
let mut rels = DidRelationships::new("did:example:unknown");
rels.add_custom("https://unknown.example/purpose", "did:example:unknown#key");
let entry = ProofEntry {
purpose: ProofPurposeKind::Custom("https://unknown.example/purpose".to_string()),
verification_method: "did:example:unknown#key".to_string(),
created_ms: 0,
expires_ms: None,
challenge: None,
domain: None,
nonce: None,
};
let ctx = ValidationContext::default();
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_err());
let err = result.expect_err("should fail");
assert!(matches!(err, PurposeError::UnknownCustomPurpose(_)));
}
#[test]
fn test_registered_custom_purposes() {
let mut validator = ProofPurposeValidator::new();
validator.register_custom("urn:alpha", Box::new(AlwaysAcceptValidator));
validator.register_custom("urn:beta", Box::new(AlwaysAcceptValidator));
let purposes = validator.registered_custom_purposes();
assert_eq!(purposes.len(), 2);
assert!(purposes.contains(&"urn:alpha".to_string()));
assert!(purposes.contains(&"urn:beta".to_string()));
}
#[test]
fn test_chain_empty() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = make_context();
let result = validator.validate_chain(&[], &rels, &ctx);
assert!(result.is_err());
let err = result.expect_err("should fail");
assert!(matches!(err, PurposeError::EmptyChain));
}
#[test]
fn test_chain_single_entry() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = make_context();
let entry = make_entry(ProofPurposeKind::Authentication, "did:example:alice#key-1");
let result = validator.validate_chain(&[entry], &rels, &ctx);
assert!(result.is_ok());
let r = result.expect("should succeed");
assert!(r.valid);
assert_eq!(r.chain_length, 1);
assert_eq!(r.entries.len(), 1);
}
#[test]
fn test_chain_multi_step() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = make_context();
let entry1 = ProofEntry {
purpose: ProofPurposeKind::Authentication,
verification_method: "did:example:alice#key-1".to_string(),
created_ms: 1_699_999_000_000,
expires_ms: Some(1_700_100_000_000),
challenge: Some("challenge-123".to_string()),
domain: Some("example.com".to_string()),
nonce: None,
};
let entry2 = ProofEntry {
purpose: ProofPurposeKind::AssertionMethod,
verification_method: "did:example:alice#key-2".to_string(),
created_ms: 1_699_999_500_000,
expires_ms: None,
challenge: None,
domain: Some("example.com".to_string()),
nonce: None,
};
let result = validator.validate_chain(&[entry1, entry2], &rels, &ctx);
assert!(result.is_ok());
let r = result.expect("should succeed");
assert_eq!(r.chain_length, 2);
}
#[test]
fn test_chain_out_of_order() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = make_context();
let entry1 = ProofEntry {
purpose: ProofPurposeKind::Authentication,
verification_method: "did:example:alice#key-1".to_string(),
created_ms: 1_700_000_000_000, expires_ms: Some(1_700_100_000_000),
challenge: Some("challenge-123".to_string()),
domain: Some("example.com".to_string()),
nonce: None,
};
let entry2 = ProofEntry {
purpose: ProofPurposeKind::AssertionMethod,
verification_method: "did:example:alice#key-2".to_string(),
created_ms: 1_699_998_000_000, expires_ms: None,
challenge: None,
domain: Some("example.com".to_string()),
nonce: None,
};
let result = validator.validate_chain(&[entry1, entry2], &rels, &ctx);
assert!(result.is_err());
let err = result.expect_err("should fail");
assert!(matches!(err, PurposeError::ChainError { index: 1, .. }));
}
#[test]
fn test_chain_with_invalid_entry() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = make_context();
let good = make_entry(ProofPurposeKind::Authentication, "did:example:alice#key-1");
let bad = ProofEntry {
purpose: ProofPurposeKind::AssertionMethod,
verification_method: "did:example:alice#key-WRONG".to_string(),
created_ms: 1_700_000_000_000,
expires_ms: None,
challenge: None,
domain: None,
nonce: None,
};
let result = validator.validate_chain(&[good, bad], &rels, &ctx);
assert!(result.is_err());
let err = result.expect_err("should fail");
assert!(matches!(err, PurposeError::ChainError { index: 1, .. }));
}
#[test]
fn test_registry_default_has_five_purposes() {
let reg = ProofPurposeRegistry::new();
assert_eq!(reg.count(), 5);
}
#[test]
fn test_registry_all_purpose_ids() {
let reg = ProofPurposeRegistry::new();
let ids = reg.all_purpose_ids();
assert!(ids.contains(&"authentication".to_string()));
assert!(ids.contains(&"assertionMethod".to_string()));
assert!(ids.contains(&"keyAgreement".to_string()));
assert!(ids.contains(&"capabilityInvocation".to_string()));
assert!(ids.contains(&"capabilityDelegation".to_string()));
}
#[test]
fn test_registry_get() {
let reg = ProofPurposeRegistry::new();
let auth = reg.get("authentication");
assert!(auth.is_some());
let meta = auth.expect("should exist");
assert_eq!(meta.label, "Authentication");
assert!(meta.requires_challenge);
assert!(meta.recommends_domain);
}
#[test]
fn test_registry_get_assertion() {
let reg = ProofPurposeRegistry::new();
let am = reg.get("assertionMethod").expect("should exist");
assert!(!am.requires_challenge);
assert!(!am.recommends_domain);
}
#[test]
fn test_registry_is_registered() {
let reg = ProofPurposeRegistry::new();
assert!(reg.is_registered("keyAgreement"));
assert!(!reg.is_registered("nonExistentPurpose"));
}
#[test]
fn test_registry_register_custom() {
let mut reg = ProofPurposeRegistry::new();
reg.register(PurposeMetadata {
purpose_id: "https://custom.example/purpose".to_string(),
label: "Custom Purpose".to_string(),
description: "A custom proof purpose".to_string(),
requires_challenge: true,
recommends_domain: false,
});
assert_eq!(reg.count(), 6);
assert!(reg.is_registered("https://custom.example/purpose"));
}
#[test]
fn test_registry_unregister() {
let mut reg = ProofPurposeRegistry::new();
assert!(reg.unregister("authentication"));
assert_eq!(reg.count(), 4);
assert!(!reg.is_registered("authentication"));
}
#[test]
fn test_registry_unregister_nonexistent() {
let mut reg = ProofPurposeRegistry::new();
assert!(!reg.unregister("nonexistent"));
assert_eq!(reg.count(), 5);
}
#[test]
fn test_requires_challenge() {
assert!(ProofPurposeValidator::requires_challenge(
&ProofPurposeKind::Authentication
));
assert!(!ProofPurposeValidator::requires_challenge(
&ProofPurposeKind::AssertionMethod
));
assert!(!ProofPurposeValidator::requires_challenge(
&ProofPurposeKind::KeyAgreement
));
}
#[test]
fn test_describe_purpose() {
let desc = ProofPurposeValidator::describe_purpose(&ProofPurposeKind::Authentication);
assert!(desc.contains("identity"));
let desc2 = ProofPurposeValidator::describe_purpose(&ProofPurposeKind::AssertionMethod);
assert!(desc2.contains("claims"));
}
#[test]
fn test_relationship_name() {
assert_eq!(
ProofPurposeValidator::relationship_name(&ProofPurposeKind::Authentication),
"authentication"
);
assert_eq!(
ProofPurposeValidator::relationship_name(&ProofPurposeKind::CapabilityDelegation),
"capabilityDelegation"
);
}
#[test]
fn test_error_display_unauthorised() {
let err = PurposeError::Unauthorised {
purpose: "authentication".to_string(),
verification_method: "did:x#k".to_string(),
};
let msg = format!("{err}");
assert!(msg.contains("not authorised"));
}
#[test]
fn test_error_display_expired() {
let err = PurposeError::Expired {
expires_ms: 100,
current_ms: 200,
};
let msg = format!("{err}");
assert!(msg.contains("expired"));
}
#[test]
fn test_error_display_future() {
let err = PurposeError::FutureProof {
created_ms: 300,
current_ms: 100,
};
let msg = format!("{err}");
assert!(msg.contains("future"));
}
#[test]
fn test_error_display_challenge_mismatch() {
let err = PurposeError::ChallengeMismatch {
expected: "a".to_string(),
actual: "b".to_string(),
};
let msg = format!("{err}");
assert!(msg.contains("mismatch"));
}
#[test]
fn test_error_display_chain_error() {
let err = PurposeError::ChainError {
index: 2,
reason: "bad".to_string(),
};
let msg = format!("{err}");
assert!(msg.contains("index 2"));
}
#[test]
fn test_error_display_empty_chain() {
let err = PurposeError::EmptyChain;
let msg = format!("{err}");
assert!(msg.contains("at least one"));
}
#[test]
fn test_validate_no_expiration() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = ValidationContext::default();
let mut entry = make_entry(ProofPurposeKind::AssertionMethod, "did:example:alice#key-2");
entry.created_ms = 0;
entry.expires_ms = None;
entry.challenge = None;
entry.domain = None;
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_ok());
}
#[test]
fn test_validate_authentication_no_expected_challenge() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = ValidationContext {
challenge: None,
domain: None,
current_time_ms: 1_700_000_000_000,
};
let mut entry = make_entry(ProofPurposeKind::Authentication, "did:example:alice#key-1");
entry.challenge = Some("any-challenge".to_string());
entry.domain = None;
let result = validator.validate(&entry, &rels, &ctx);
assert!(result.is_ok());
}
#[test]
fn test_validator_default_construction() {
let v = ProofPurposeValidator::default();
let rels = make_relationships();
let ctx = ValidationContext::default();
let mut entry = make_entry(ProofPurposeKind::AssertionMethod, "did:example:alice#key-2");
entry.created_ms = 0;
entry.expires_ms = None;
entry.challenge = None;
entry.domain = None;
assert!(v.validate(&entry, &rels, &ctx).is_ok());
}
#[test]
fn test_validation_result_fields() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = ValidationContext::default();
let mut entry = make_entry(
ProofPurposeKind::CapabilityInvocation,
"did:example:alice#key-4",
);
entry.created_ms = 0;
entry.expires_ms = None;
entry.challenge = None;
entry.domain = None;
let r = validator
.validate(&entry, &rels, &ctx)
.expect("should succeed");
assert!(r.valid);
assert_eq!(r.purpose, "capabilityInvocation");
assert_eq!(r.verification_method, "did:example:alice#key-4");
assert!(r.message.contains("validated successfully"));
}
#[test]
fn test_chain_validation_result_fields() {
let validator = ProofPurposeValidator::new();
let rels = make_relationships();
let ctx = make_context();
let entry = make_entry(ProofPurposeKind::Authentication, "did:example:alice#key-1");
let r = validator
.validate_chain(&[entry], &rels, &ctx)
.expect("should succeed");
assert!(r.valid);
assert_eq!(r.chain_length, 1);
assert!(r.error.is_none());
assert_eq!(r.entries.len(), 1);
}
}