use chrono::{DateTime, Utc};
use super::revocation::{RevocationOutcome, check_revocation};
use super::trust::TrustedIssuerRegistry;
use super::types::{Audience, CredentialRole, DppAccessCredential};
use crate::status_list::StatusList;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerificationResult {
Valid {
audience: Audience,
role: CredentialRole,
holder_did: String,
},
Expired { expired_at: DateTime<Utc> },
InvalidSignature(String),
Revoked,
MalformedCredential(String),
OutOfScope { reason: String },
UntrustedIssuer { issuer_did: String },
}
impl VerificationResult {
pub fn is_valid(&self) -> bool {
matches!(self, Self::Valid { .. })
}
}
pub fn verify_credential_claims(
credential: &DppAccessCredential,
required_sector: Option<&str>,
now: DateTime<Utc>,
) -> VerificationResult {
if !credential
.credential_type
.contains(&"VerifiableCredential".to_owned())
{
return VerificationResult::MalformedCredential(
"Missing 'VerifiableCredential' type".into(),
);
}
if now > credential.valid_until {
return VerificationResult::Expired {
expired_at: credential.valid_until,
};
}
if now < credential.valid_from {
return VerificationResult::MalformedCredential(
"Credential issuance date is in the future".into(),
);
}
if let Some(sector) = required_sector {
let subjects_sectors = &credential.credential_subject.sectors;
if !subjects_sectors.is_empty() && !subjects_sectors.iter().any(|s| s == sector) {
return VerificationResult::OutOfScope {
reason: format!(
"Credential covers sectors {:?}, but '{}' was requested",
subjects_sectors, sector
),
};
}
}
let role = credential.credential_subject.role.clone();
let audience = role.audience();
VerificationResult::Valid {
audience,
role,
holder_did: credential.credential_subject.id.clone(),
}
}
pub fn verify_credential_with_revocation(
credential: &DppAccessCredential,
required_sector: Option<&str>,
now: DateTime<Utc>,
status_list: Option<&StatusList>,
) -> VerificationResult {
let base = verify_credential_claims(credential, required_sector, now);
if !base.is_valid() {
return base;
}
apply_revocation_check(base, credential, status_list)
}
fn apply_revocation_check(
base: VerificationResult,
credential: &DppAccessCredential,
status_list: Option<&StatusList>,
) -> VerificationResult {
if credential.credential_status.is_none() {
return base;
}
match status_list {
None => VerificationResult::Revoked,
Some(list) => match check_revocation(credential, list) {
RevocationOutcome::NotRevoked => base,
RevocationOutcome::Revoked | RevocationOutcome::Indeterminate => {
VerificationResult::Revoked
}
},
}
}
pub fn verify_credential_claims_with_trust(
credential: &DppAccessCredential,
required_sector: Option<&str>,
required_product_category: Option<&str>,
now: DateTime<Utc>,
trusted_issuers: &dyn TrustedIssuerRegistry,
) -> VerificationResult {
let base = verify_credential_claims(credential, required_sector, now);
if !base.is_valid() {
return base;
}
if let Some(required_cat) = required_product_category {
let cats = &credential.credential_subject.product_categories;
if !cats.is_empty() && !cats.iter().any(|c| c == required_cat) {
return VerificationResult::OutOfScope {
reason: format!(
"Credential covers product categories {:?}, but '{}' was requested",
cats, required_cat
),
};
}
}
let required_audience = credential.credential_subject.role.audience();
if !trusted_issuers.is_trusted_for_audience(&credential.issuer, required_audience) {
return VerificationResult::UntrustedIssuer {
issuer_did: credential.issuer.clone(),
};
}
base
}
pub fn verify_credential_with_revocation_and_trust(
credential: &DppAccessCredential,
required_sector: Option<&str>,
required_product_category: Option<&str>,
now: DateTime<Utc>,
status_list: Option<&StatusList>,
trusted_issuers: &dyn TrustedIssuerRegistry,
) -> VerificationResult {
let base = verify_credential_claims_with_trust(
credential,
required_sector,
required_product_category,
now,
trusted_issuers,
);
if !base.is_valid() {
return base;
}
apply_revocation_check(base, credential, status_list)
}