1use baseid_core::types::AssuranceLevel;
8use serde::{Deserialize, Serialize};
9
10use crate::consent::ConsentManager;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct PctfPolicy {
15 pub min_assurance_level: AssuranceLevel,
17 pub require_consent: bool,
19 pub require_audit: bool,
21 pub accepted_types: Vec<String>,
23 pub trusted_issuers: Vec<String>,
25}
26
27impl Default for PctfPolicy {
28 fn default() -> Self {
29 Self {
30 min_assurance_level: AssuranceLevel::Substantial,
31 require_consent: true,
32 require_audit: true,
33 accepted_types: vec![],
34 trusted_issuers: vec![],
35 }
36 }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct PolicyResult {
42 pub compliant: bool,
44 pub checks: Vec<PolicyCheck>,
46}
47
48impl PolicyResult {
49 pub fn violations(&self) -> Vec<&PolicyCheck> {
51 self.checks.iter().filter(|c| !c.passed).collect()
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct PolicyCheck {
58 pub name: String,
60 pub passed: bool,
62 pub message: String,
64}
65
66#[derive(Debug, Clone)]
68pub struct PresentationContext {
69 pub assurance_level: AssuranceLevel,
71 pub credential_types: Vec<String>,
73 pub issuer: String,
75 pub verifier: String,
77 pub purpose: String,
79 pub data_elements: Vec<String>,
81 pub now: String,
83}
84
85pub struct PctfValidator;
87
88impl PctfValidator {
89 pub fn validate_presentation(
91 policy: &PctfPolicy,
92 context: &PresentationContext,
93 consent_manager: Option<&ConsentManager>,
94 ) -> PolicyResult {
95 let mut checks = Vec::new();
96
97 let assurance_ok = context.assurance_level >= policy.min_assurance_level;
99 checks.push(PolicyCheck {
100 name: "assurance_level".to_string(),
101 passed: assurance_ok,
102 message: if assurance_ok {
103 format!(
104 "Assurance level {} meets minimum {}",
105 context.assurance_level.pctf_name(),
106 policy.min_assurance_level.pctf_name()
107 )
108 } else {
109 format!(
110 "Assurance level {} below minimum {} / Le niveau d'assurance {} est inférieur au minimum {}",
111 context.assurance_level.pctf_name(),
112 policy.min_assurance_level.pctf_name(),
113 context.assurance_level.pctf_name(),
114 policy.min_assurance_level.pctf_name()
115 )
116 },
117 });
118
119 let type_ok = policy.accepted_types.is_empty()
121 || context
122 .credential_types
123 .iter()
124 .any(|t| policy.accepted_types.contains(t));
125 checks.push(PolicyCheck {
126 name: "credential_type".to_string(),
127 passed: type_ok,
128 message: if type_ok {
129 "Credential type accepted".to_string()
130 } else {
131 format!(
132 "Credential type not accepted. Expected one of: {:?}",
133 policy.accepted_types
134 )
135 },
136 });
137
138 let issuer_ok =
140 policy.trusted_issuers.is_empty() || policy.trusted_issuers.contains(&context.issuer);
141 checks.push(PolicyCheck {
142 name: "trusted_issuer".to_string(),
143 passed: issuer_ok,
144 message: if issuer_ok {
145 "Issuer is trusted".to_string()
146 } else {
147 format!(
148 "Issuer {} not in trusted list / L'émetteur {} n'est pas dans la liste de confiance",
149 context.issuer, context.issuer
150 )
151 },
152 });
153
154 let consent_ok = if policy.require_consent {
156 if let Some(mgr) = consent_manager {
157 let valid =
158 mgr.find_valid_consents(&context.verifier, &context.purpose, &context.now);
159 let data_refs: Vec<&str> =
161 context.data_elements.iter().map(|s| s.as_str()).collect();
162 valid.iter().any(|c| c.covers(&data_refs, &context.purpose))
163 } else {
164 false
165 }
166 } else {
167 true
168 };
169 checks.push(PolicyCheck {
170 name: "consent".to_string(),
171 passed: consent_ok,
172 message: if consent_ok {
173 "Valid consent exists for this presentation".to_string()
174 } else {
175 "No valid consent found for this presentation / Aucun consentement valide trouvé"
176 .to_string()
177 },
178 });
179
180 checks.push(PolicyCheck {
183 name: "audit".to_string(),
184 passed: true, message: if policy.require_audit {
186 "Audit logging required for this operation".to_string()
187 } else {
188 "Audit logging not required".to_string()
189 },
190 });
191
192 let compliant = checks.iter().all(|c| c.passed);
193 PolicyResult { compliant, checks }
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use crate::consent::ConsentRecord;
201
202 fn default_context() -> PresentationContext {
203 PresentationContext {
204 assurance_level: AssuranceLevel::Substantial,
205 credential_types: vec!["VerifiableCredential".into(), "CanadianDigitalID".into()],
206 issuer: "did:web:gov.ca".to_string(),
207 verifier: "did:key:z6MkVerifier".to_string(),
208 purpose: "age-verification".to_string(),
209 data_elements: vec!["givenName".into(), "dateOfBirth".into()],
210 now: "2026-03-15T10:00:00Z".to_string(),
211 }
212 }
213
214 fn consent_manager_with_valid_consent() -> ConsentManager {
215 let mut mgr = ConsentManager::new();
216 mgr.record_consent(ConsentRecord::new(
217 "c-1",
218 "did:key:z6MkHolder",
219 "did:key:z6MkVerifier",
220 vec!["givenName".into(), "dateOfBirth".into()],
221 "age-verification",
222 "2026-03-01T00:00:00Z",
223 Some("2026-12-31T23:59:59Z".into()),
224 ));
225 mgr
226 }
227
228 #[test]
229 fn fully_compliant_presentation() {
230 let policy = PctfPolicy::default();
231 let ctx = default_context();
232 let mgr = consent_manager_with_valid_consent();
233 let result = PctfValidator::validate_presentation(&policy, &ctx, Some(&mgr));
234 assert!(result.compliant);
235 assert!(result.violations().is_empty());
236 }
237
238 #[test]
239 fn assurance_level_too_low() {
240 let policy = PctfPolicy {
241 min_assurance_level: AssuranceLevel::High,
242 ..Default::default()
243 };
244 let ctx = default_context(); let mgr = consent_manager_with_valid_consent();
246 let result = PctfValidator::validate_presentation(&policy, &ctx, Some(&mgr));
247 assert!(!result.compliant);
248 let violations = result.violations();
249 assert!(violations.iter().any(|v| v.name == "assurance_level"));
250 }
251
252 #[test]
253 fn wrong_credential_type() {
254 let policy = PctfPolicy {
255 accepted_types: vec!["DriverLicense".into()],
256 ..Default::default()
257 };
258 let ctx = default_context(); let mgr = consent_manager_with_valid_consent();
260 let result = PctfValidator::validate_presentation(&policy, &ctx, Some(&mgr));
261 assert!(!result.compliant);
262 assert!(result
263 .violations()
264 .iter()
265 .any(|v| v.name == "credential_type"));
266 }
267
268 #[test]
269 fn untrusted_issuer() {
270 let policy = PctfPolicy {
271 trusted_issuers: vec!["did:web:ontario.ca".into()],
272 ..Default::default()
273 };
274 let ctx = default_context(); let mgr = consent_manager_with_valid_consent();
276 let result = PctfValidator::validate_presentation(&policy, &ctx, Some(&mgr));
277 assert!(!result.compliant);
278 assert!(result
279 .violations()
280 .iter()
281 .any(|v| v.name == "trusted_issuer"));
282 }
283
284 #[test]
285 fn missing_consent() {
286 let policy = PctfPolicy::default();
287 let ctx = default_context();
288 let result = PctfValidator::validate_presentation(&policy, &ctx, None);
290 assert!(!result.compliant);
291 assert!(result.violations().iter().any(|v| v.name == "consent"));
292 }
293
294 #[test]
295 fn consent_not_required() {
296 let policy = PctfPolicy {
297 require_consent: false,
298 ..Default::default()
299 };
300 let ctx = default_context();
301 let result = PctfValidator::validate_presentation(&policy, &ctx, None);
302 assert!(result.compliant);
303 }
304
305 #[test]
306 fn empty_accepted_types_accepts_all() {
307 let policy = PctfPolicy {
308 accepted_types: vec![],
309 ..Default::default()
310 };
311 let ctx = default_context();
312 let mgr = consent_manager_with_valid_consent();
313 let result = PctfValidator::validate_presentation(&policy, &ctx, Some(&mgr));
314 assert!(
315 result
316 .checks
317 .iter()
318 .find(|c| c.name == "credential_type")
319 .unwrap()
320 .passed
321 );
322 }
323
324 #[test]
325 fn policy_result_violations() {
326 let policy = PctfPolicy {
327 min_assurance_level: AssuranceLevel::High,
328 accepted_types: vec!["Passport".into()],
329 ..Default::default()
330 };
331 let ctx = default_context();
332 let result = PctfValidator::validate_presentation(&policy, &ctx, None);
333 assert!(!result.compliant);
334 assert_eq!(result.violations().len(), 3);
336 }
337}