baseid_pctf/
policy.rs

1//! PCTF policy engine for validating credential operations against
2//! Pan-Canadian Trust Framework requirements.
3//!
4//! Combines assurance level checks, consent validation, and audit enforcement
5//! into a single policy evaluation.
6
7use baseid_core::types::AssuranceLevel;
8use serde::{Deserialize, Serialize};
9
10use crate::consent::ConsentManager;
11
12/// PCTF policy configuration.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct PctfPolicy {
15    /// Minimum assurance level required for credential acceptance.
16    pub min_assurance_level: AssuranceLevel,
17    /// Whether consent records are required for presentations.
18    pub require_consent: bool,
19    /// Whether audit logging is mandatory.
20    pub require_audit: bool,
21    /// Accepted credential types (empty = accept all).
22    pub accepted_types: Vec<String>,
23    /// Trusted issuer DIDs (empty = accept all).
24    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/// Result of a policy validation.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct PolicyResult {
42    /// Whether the operation satisfies the policy.
43    pub compliant: bool,
44    /// Individual check results.
45    pub checks: Vec<PolicyCheck>,
46}
47
48impl PolicyResult {
49    /// Get all failing checks.
50    pub fn violations(&self) -> Vec<&PolicyCheck> {
51        self.checks.iter().filter(|c| !c.passed).collect()
52    }
53}
54
55/// A single policy check result.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct PolicyCheck {
58    /// Name of the check.
59    pub name: String,
60    /// Whether the check passed.
61    pub passed: bool,
62    /// Description of the result.
63    pub message: String,
64}
65
66/// Context for evaluating a credential presentation against PCTF policy.
67#[derive(Debug, Clone)]
68pub struct PresentationContext {
69    /// Assurance level of the credential.
70    pub assurance_level: AssuranceLevel,
71    /// Credential type(s).
72    pub credential_types: Vec<String>,
73    /// Issuer DID.
74    pub issuer: String,
75    /// Verifier DID (recipient).
76    pub verifier: String,
77    /// Purpose of presentation.
78    pub purpose: String,
79    /// Data elements being shared.
80    pub data_elements: Vec<String>,
81    /// Current timestamp (RFC 3339).
82    pub now: String,
83}
84
85/// Validates credential operations against PCTF policy.
86pub struct PctfValidator;
87
88impl PctfValidator {
89    /// Validate a presentation context against a PCTF policy.
90    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        // Check 1: Assurance level
98        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        // Check 2: Credential type
120        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        // Check 3: Trusted issuer
139        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        // Check 4: Consent
155        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                // Check that at least one consent covers the data elements
160                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        // Check 5: Audit requirement (informational — we can't check if audit was logged
181        // since that happens after the presentation, but we flag the requirement).
182        checks.push(PolicyCheck {
183            name: "audit".to_string(),
184            passed: true, // Always passes — it's a reminder, not a gate
185            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(); // Substantial
245        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(); // CanadianDigitalID
259        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(); // issuer = did:web:gov.ca
275        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        // No consent manager
289        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        // Should have 3 violations: assurance, type, consent
335        assert_eq!(result.violations().len(), 3);
336    }
337}