oxirs-did 0.4.0

W3C DID and Verifiable Credentials implementation with Signed RDF Graphs for OxiRS
Documentation
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>,
}