Skip to main content

dpp_crypto/access/credential/
verify.rs

1use chrono::{DateTime, Utc};
2
3use super::revocation::{RevocationOutcome, check_revocation};
4use super::trust::TrustedIssuerRegistry;
5use super::types::{AccessTier, CredentialRole, DppAccessCredential};
6use crate::access::status_list::StatusList;
7
8// ─── Verification result ────────────────────────────────────────────────────
9
10/// Result of verifying a DPP access credential.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum VerificationResult {
13    /// Credential is valid — the granted access tier is returned.
14    Valid {
15        access_tier: AccessTier,
16        role: CredentialRole,
17        holder_did: String,
18    },
19    /// Credential has expired.
20    Expired { expired_at: DateTime<Utc> },
21    /// JWS signature is invalid or cannot be verified.
22    InvalidSignature(String),
23    /// Credential has been revoked.
24    Revoked,
25    /// Credential is structurally invalid (missing fields, wrong type).
26    MalformedCredential(String),
27    /// The credential's scope doesn't cover the requested resource.
28    OutOfScope { reason: String },
29    /// The credential's issuer DID is not in the operator's trust registry for
30    /// the tier it claims to grant (Gap 9: issuer trust anchor).
31    UntrustedIssuer { issuer_did: String },
32}
33
34impl VerificationResult {
35    pub fn is_valid(&self) -> bool {
36        matches!(self, Self::Valid { .. })
37    }
38}
39
40// ─── Verify functions ────────────────────────────────────────────────────────
41
42/// Verify structural validity and expiration of a credential (no signature check).
43///
44/// Signature verification requires the issuer's public key and is done
45/// separately via the JWS verifier. This function handles the credential-
46/// level checks: type, expiration, and scope.
47pub fn verify_credential_claims(
48    credential: &DppAccessCredential,
49    required_sector: Option<&str>,
50    now: DateTime<Utc>,
51) -> VerificationResult {
52    if !credential
53        .credential_type
54        .contains(&"VerifiableCredential".to_owned())
55    {
56        return VerificationResult::MalformedCredential(
57            "Missing 'VerifiableCredential' type".into(),
58        );
59    }
60
61    if now > credential.valid_until {
62        return VerificationResult::Expired {
63            expired_at: credential.valid_until,
64        };
65    }
66
67    if now < credential.valid_from {
68        return VerificationResult::MalformedCredential(
69            "Credential issuance date is in the future".into(),
70        );
71    }
72
73    if let Some(sector) = required_sector {
74        let subjects_sectors = &credential.credential_subject.sectors;
75        if !subjects_sectors.is_empty() && !subjects_sectors.iter().any(|s| s == sector) {
76            return VerificationResult::OutOfScope {
77                reason: format!(
78                    "Credential covers sectors {:?}, but '{}' was requested",
79                    subjects_sectors, sector
80                ),
81            };
82        }
83    }
84
85    let role = credential.credential_subject.role.clone();
86    let access_tier = role.access_tier();
87    VerificationResult::Valid {
88        access_tier,
89        role,
90        holder_did: credential.credential_subject.id.clone(),
91    }
92}
93
94/// Full credential verification **including revocation**, with a fail-closed
95/// policy (crypto Gap 5).
96///
97/// `status_list` is the result of fetching the credential's status list:
98/// `Some(list)` when fetched and verified, `None` when there is nothing to
99/// fetch **or** the fetch failed.
100///
101/// **Fail-closed:** a credential that *declares* a revocation status whose list
102/// is unavailable or unresolvable is treated as `Revoked`.
103pub fn verify_credential_with_revocation(
104    credential: &DppAccessCredential,
105    required_sector: Option<&str>,
106    now: DateTime<Utc>,
107    status_list: Option<&StatusList>,
108) -> VerificationResult {
109    let base = verify_credential_claims(credential, required_sector, now);
110    if !base.is_valid() {
111        return base;
112    }
113    if credential.credential_status.is_none() {
114        return base;
115    }
116    match status_list {
117        None => VerificationResult::Revoked,
118        Some(list) => match check_revocation(credential, list) {
119            RevocationOutcome::NotRevoked => base,
120            RevocationOutcome::Revoked | RevocationOutcome::Indeterminate => {
121                VerificationResult::Revoked
122            }
123        },
124    }
125}
126
127/// Verify structural validity, scope, and **issuer trust** of a credential
128/// (no signature check — that is the JWS verifier's responsibility).
129pub fn verify_credential_claims_with_trust(
130    credential: &DppAccessCredential,
131    required_sector: Option<&str>,
132    required_product_category: Option<&str>,
133    now: DateTime<Utc>,
134    trusted_issuers: &dyn TrustedIssuerRegistry,
135) -> VerificationResult {
136    let base = verify_credential_claims(credential, required_sector, now);
137    if !base.is_valid() {
138        return base;
139    }
140
141    if let Some(required_cat) = required_product_category {
142        let cats = &credential.credential_subject.product_categories;
143        if !cats.is_empty() && !cats.iter().any(|c| c == required_cat) {
144            return VerificationResult::OutOfScope {
145                reason: format!(
146                    "Credential covers product categories {:?}, but '{}' was requested",
147                    cats, required_cat
148                ),
149            };
150        }
151    }
152
153    let required_tier = credential.credential_subject.role.access_tier();
154    if !trusted_issuers.is_trusted_for_tier(&credential.issuer, required_tier) {
155        return VerificationResult::UntrustedIssuer {
156            issuer_did: credential.issuer.clone(),
157        };
158    }
159
160    base
161}
162
163/// Full credential verification including **revocation** and **issuer trust**,
164/// with a fail-closed policy.
165pub fn verify_credential_with_revocation_and_trust(
166    credential: &DppAccessCredential,
167    required_sector: Option<&str>,
168    required_product_category: Option<&str>,
169    now: DateTime<Utc>,
170    status_list: Option<&StatusList>,
171    trusted_issuers: &dyn TrustedIssuerRegistry,
172) -> VerificationResult {
173    let base = verify_credential_claims_with_trust(
174        credential,
175        required_sector,
176        required_product_category,
177        now,
178        trusted_issuers,
179    );
180    if !base.is_valid() {
181        return base;
182    }
183    if credential.credential_status.is_none() {
184        return base;
185    }
186    match status_list {
187        None => VerificationResult::Revoked,
188        Some(list) => match check_revocation(credential, list) {
189            RevocationOutcome::NotRevoked => base,
190            RevocationOutcome::Revoked | RevocationOutcome::Indeterminate => {
191                VerificationResult::Revoked
192            }
193        },
194    }
195}