dpp_vc/credential/
verify.rs1use chrono::{DateTime, Utc};
2
3use super::revocation::{RevocationOutcome, check_revocation};
4use super::trust::TrustedIssuerRegistry;
5use super::types::{Audience, CredentialRole, DppAccessCredential};
6use crate::status_list::StatusList;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum VerificationResult {
13 Valid {
15 audience: Audience,
16 role: CredentialRole,
17 holder_did: String,
18 },
19 Expired { expired_at: DateTime<Utc> },
21 InvalidSignature(String),
23 Revoked,
25 MalformedCredential(String),
27 OutOfScope { reason: String },
29 UntrustedIssuer { issuer_did: String },
32}
33
34impl VerificationResult {
35 pub fn is_valid(&self) -> bool {
36 matches!(self, Self::Valid { .. })
37 }
38}
39
40pub 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 audience = role.audience();
87 VerificationResult::Valid {
88 audience,
89 role,
90 holder_did: credential.credential_subject.id.clone(),
91 }
92}
93
94pub 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 apply_revocation_check(base, credential, status_list)
114}
115
116fn apply_revocation_check(
123 base: VerificationResult,
124 credential: &DppAccessCredential,
125 status_list: Option<&StatusList>,
126) -> VerificationResult {
127 if credential.credential_status.is_none() {
128 return base;
129 }
130 match status_list {
131 None => VerificationResult::Revoked,
132 Some(list) => match check_revocation(credential, list) {
133 RevocationOutcome::NotRevoked => base,
134 RevocationOutcome::Revoked | RevocationOutcome::Indeterminate => {
135 VerificationResult::Revoked
136 }
137 },
138 }
139}
140
141pub fn verify_credential_claims_with_trust(
144 credential: &DppAccessCredential,
145 required_sector: Option<&str>,
146 required_product_category: Option<&str>,
147 now: DateTime<Utc>,
148 trusted_issuers: &dyn TrustedIssuerRegistry,
149) -> VerificationResult {
150 let base = verify_credential_claims(credential, required_sector, now);
151 if !base.is_valid() {
152 return base;
153 }
154
155 if let Some(required_cat) = required_product_category {
156 let cats = &credential.credential_subject.product_categories;
157 if !cats.is_empty() && !cats.iter().any(|c| c == required_cat) {
158 return VerificationResult::OutOfScope {
159 reason: format!(
160 "Credential covers product categories {:?}, but '{}' was requested",
161 cats, required_cat
162 ),
163 };
164 }
165 }
166
167 let required_audience = credential.credential_subject.role.audience();
168 if !trusted_issuers.is_trusted_for_audience(&credential.issuer, required_audience) {
169 return VerificationResult::UntrustedIssuer {
170 issuer_did: credential.issuer.clone(),
171 };
172 }
173
174 base
175}
176
177pub fn verify_credential_with_revocation_and_trust(
180 credential: &DppAccessCredential,
181 required_sector: Option<&str>,
182 required_product_category: Option<&str>,
183 now: DateTime<Utc>,
184 status_list: Option<&StatusList>,
185 trusted_issuers: &dyn TrustedIssuerRegistry,
186) -> VerificationResult {
187 let base = verify_credential_claims_with_trust(
188 credential,
189 required_sector,
190 required_product_category,
191 now,
192 trusted_issuers,
193 );
194 if !base.is_valid() {
195 return base;
196 }
197 apply_revocation_check(base, credential, status_list)
198}