baseid_pctf/
assurance.rs

1//! Identity assurance level evaluation per PCTF Verified Person component.
2//!
3//! Maps identity-proofing evidence to PCTF Levels 1-3 using the DIACC
4//! evidence taxonomy. Also supports cross-framework mapping to eIDAS,
5//! NIST 800-63, and TDIF via `baseid_core::types::AssuranceLevel`.
6
7use baseid_core::types::AssuranceLevel;
8use serde::{Deserialize, Serialize};
9
10/// Types of identity-proofing evidence per PCTF evidence taxonomy.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum EvidenceType {
13    /// Self-asserted information (name, address, etc.).
14    SelfAsserted,
15    /// Knowledge-based verification (security questions, shared secrets).
16    KnowledgeBased,
17    /// Email or phone verification (OTP, magic link).
18    ChannelBinding,
19    /// Government-issued photo ID (passport, driver's licence, PR card).
20    GovernmentPhotoId,
21    /// Government-issued non-photo document (SIN letter, birth certificate).
22    GovernmentDocument,
23    /// Utility bill, bank statement, or similar address document.
24    AddressDocument,
25    /// Automated document verification (OCR + liveness + database check).
26    DocumentVerification,
27    /// Biometric capture (facial recognition, fingerprint).
28    Biometric,
29    /// In-person identity proofing at a trusted location.
30    InPerson,
31    /// Supervised remote identity proofing (video call with agent).
32    SupervisedRemote,
33    /// Credential from a trusted issuer (e.g., bank KYC, provincial ID program).
34    TrustedCredential,
35}
36
37/// Verification method used to validate evidence.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39pub enum VerificationMethod {
40    /// No verification beyond possession.
41    Unverified,
42    /// Checked against an authoritative database.
43    DatabaseCheck,
44    /// Visual inspection by a human operator.
45    VisualInspection,
46    /// Automated document authenticity check (OCR, hologram, MRZ).
47    AutomatedCheck,
48    /// Biometric comparison against reference (1:1 match).
49    BiometricMatch,
50    /// Cryptographic proof (digital signature verification).
51    CryptographicProof,
52}
53
54/// A single piece of identity-proofing evidence with metadata.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct Evidence {
57    /// Type of evidence.
58    pub evidence_type: EvidenceType,
59    /// How the evidence was verified.
60    pub verification: VerificationMethod,
61    /// Who issued or verified this evidence (DID or identifier).
62    pub issuer: String,
63    /// When the evidence was collected (RFC 3339).
64    pub timestamp: String,
65}
66
67/// A bundle of evidence submitted for assurance level evaluation.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct EvidenceBundle {
70    /// The subject being evaluated (holder DID).
71    pub subject: String,
72    /// Evidence items.
73    pub evidence: Vec<Evidence>,
74}
75
76/// Result of an assurance level evaluation.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct EvaluationResult {
79    /// The determined assurance level.
80    pub level: AssuranceLevel,
81    /// PCTF level name (e.g., "Level 2").
82    pub pctf_name: String,
83    /// Reasoning for the determined level.
84    pub reasoning: Vec<String>,
85    /// Whether the evidence meets requirements for the next level up.
86    pub upgrade_possible: bool,
87    /// What additional evidence would be needed to reach the next level.
88    pub upgrade_requirements: Vec<String>,
89}
90
91/// Evaluates identity assurance levels per PCTF Verified Person component.
92///
93/// # Scoring rules
94///
95/// - **Level 1** (Low): Self-asserted identity or unverified evidence only.
96/// - **Level 2** (Substantial): At least one verified government document
97///   plus one additional verification factor (channel binding, knowledge-based,
98///   or document verification).
99/// - **Level 3** (High): In-person or supervised remote proofing, plus
100///   biometric capture, plus verified government photo ID.
101pub struct AssuranceLevelEvaluator;
102
103impl AssuranceLevelEvaluator {
104    /// Evaluate assurance level from a legacy string-based evidence list.
105    ///
106    /// This is the original API. For richer evaluation, use `evaluate_bundle`.
107    pub fn evaluate(evidence: &[&str]) -> AssuranceLevel {
108        // Map string evidence to typed evidence for evaluation.
109        let has = |keyword: &str| evidence.iter().any(|e| e.contains(keyword));
110
111        let has_biometric = has("biometric") || has("facial") || has("fingerprint");
112        let has_in_person = has("in-person") || has("in_person") || has("supervised");
113        let has_govt_photo = has("passport")
114            || has("driver")
115            || has("photo_id")
116            || has("photo-id")
117            || has("government_photo");
118        let has_govt_doc = has_govt_photo || has("birth") || has("sin") || has("government");
119        let has_verification = has("verification")
120            || has("database")
121            || has("automated")
122            || has("ocr")
123            || has("verified");
124        let has_additional = has("email")
125            || has("phone")
126            || has("otp")
127            || has("knowledge")
128            || has("channel")
129            || has_verification;
130
131        // Level 3: in-person/supervised + biometric + government photo ID
132        if (has_in_person) && has_biometric && has_govt_photo {
133            return AssuranceLevel::High;
134        }
135
136        // Level 2: verified government document + additional factor
137        if has_govt_doc && has_additional {
138            return AssuranceLevel::Substantial;
139        }
140
141        // Level 1: everything else
142        AssuranceLevel::Low
143    }
144
145    /// Evaluate assurance level from a structured evidence bundle.
146    pub fn evaluate_bundle(bundle: &EvidenceBundle) -> EvaluationResult {
147        let evidence = &bundle.evidence;
148        let mut reasoning = Vec::new();
149
150        // Check for Level 3 requirements
151        let has_in_person = evidence.iter().any(|e| {
152            matches!(
153                e.evidence_type,
154                EvidenceType::InPerson | EvidenceType::SupervisedRemote
155            )
156        });
157        let has_biometric = evidence.iter().any(|e| {
158            e.evidence_type == EvidenceType::Biometric
159                && matches!(
160                    e.verification,
161                    VerificationMethod::BiometricMatch | VerificationMethod::AutomatedCheck
162                )
163        });
164        let has_govt_photo_verified = evidence.iter().any(|e| {
165            e.evidence_type == EvidenceType::GovernmentPhotoId
166                && !matches!(e.verification, VerificationMethod::Unverified)
167        });
168        let has_govt_doc = evidence.iter().any(|e| {
169            matches!(
170                e.evidence_type,
171                EvidenceType::GovernmentPhotoId | EvidenceType::GovernmentDocument
172            ) && !matches!(e.verification, VerificationMethod::Unverified)
173        });
174        let has_additional_factor = evidence.iter().any(|e| {
175            matches!(
176                e.evidence_type,
177                EvidenceType::ChannelBinding
178                    | EvidenceType::KnowledgeBased
179                    | EvidenceType::DocumentVerification
180                    | EvidenceType::TrustedCredential
181            )
182        });
183
184        // Level 3
185        if has_in_person && has_biometric && has_govt_photo_verified {
186            reasoning.push("In-person or supervised remote proofing confirmed".to_string());
187            reasoning.push("Biometric capture with match verification".to_string());
188            reasoning.push("Verified government photo ID presented".to_string());
189            return EvaluationResult {
190                level: AssuranceLevel::High,
191                pctf_name: AssuranceLevel::High.pctf_name().to_string(),
192                reasoning,
193                upgrade_possible: false,
194                upgrade_requirements: vec![],
195            };
196        }
197
198        // Level 2
199        if has_govt_doc && has_additional_factor {
200            reasoning.push("Verified government document confirmed".to_string());
201            reasoning.push("Additional verification factor present".to_string());
202
203            let mut upgrade_reqs = Vec::new();
204            if !has_in_person {
205                upgrade_reqs.push("In-person or supervised remote proofing".to_string());
206            }
207            if !has_biometric {
208                upgrade_reqs.push("Biometric capture with match verification".to_string());
209            }
210            if !has_govt_photo_verified {
211                upgrade_reqs.push("Verified government photo ID".to_string());
212            }
213
214            return EvaluationResult {
215                level: AssuranceLevel::Substantial,
216                pctf_name: AssuranceLevel::Substantial.pctf_name().to_string(),
217                reasoning,
218                upgrade_possible: true,
219                upgrade_requirements: upgrade_reqs,
220            };
221        }
222
223        // Level 1
224        if evidence.is_empty() {
225            reasoning.push("No evidence provided".to_string());
226        } else {
227            reasoning.push("Evidence does not meet Level 2 requirements".to_string());
228        }
229
230        let mut upgrade_reqs =
231            vec!["Verified government document (photo ID or official document)".to_string()];
232        if !has_additional_factor {
233            upgrade_reqs.push(
234                "Additional factor (channel binding, knowledge-based, or document verification)"
235                    .to_string(),
236            );
237        }
238
239        EvaluationResult {
240            level: AssuranceLevel::Low,
241            pctf_name: AssuranceLevel::Low.pctf_name().to_string(),
242            reasoning,
243            upgrade_possible: true,
244            upgrade_requirements: upgrade_reqs,
245        }
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn level1_no_evidence() {
255        assert_eq!(AssuranceLevelEvaluator::evaluate(&[]), AssuranceLevel::Low);
256    }
257
258    #[test]
259    fn level1_self_asserted_only() {
260        assert_eq!(
261            AssuranceLevelEvaluator::evaluate(&["self-asserted name", "self-asserted email"]),
262            AssuranceLevel::Low
263        );
264    }
265
266    #[test]
267    fn level2_govt_doc_plus_verification() {
268        assert_eq!(
269            AssuranceLevelEvaluator::evaluate(&["government_id", "email verified"]),
270            AssuranceLevel::Substantial
271        );
272    }
273
274    #[test]
275    fn level2_passport_plus_phone() {
276        assert_eq!(
277            AssuranceLevelEvaluator::evaluate(&["passport", "phone otp"]),
278            AssuranceLevel::Substantial
279        );
280    }
281
282    #[test]
283    fn level3_in_person_biometric_photo_id() {
284        assert_eq!(
285            AssuranceLevelEvaluator::evaluate(&["in-person", "biometric", "passport"]),
286            AssuranceLevel::High
287        );
288    }
289
290    #[test]
291    fn level3_supervised_remote() {
292        assert_eq!(
293            AssuranceLevelEvaluator::evaluate(&[
294                "supervised video",
295                "facial biometric",
296                "driver photo_id"
297            ]),
298            AssuranceLevel::High
299        );
300    }
301
302    #[test]
303    fn bundle_level1_empty() {
304        let bundle = EvidenceBundle {
305            subject: "did:key:z6MkTest".to_string(),
306            evidence: vec![],
307        };
308        let result = AssuranceLevelEvaluator::evaluate_bundle(&bundle);
309        assert_eq!(result.level, AssuranceLevel::Low);
310        assert_eq!(result.pctf_name, "Level 1");
311        assert!(result.upgrade_possible);
312        assert!(!result.upgrade_requirements.is_empty());
313    }
314
315    #[test]
316    fn bundle_level1_self_asserted() {
317        let bundle = EvidenceBundle {
318            subject: "did:key:z6MkTest".to_string(),
319            evidence: vec![Evidence {
320                evidence_type: EvidenceType::SelfAsserted,
321                verification: VerificationMethod::Unverified,
322                issuer: "self".to_string(),
323                timestamp: "2026-01-01T00:00:00Z".to_string(),
324            }],
325        };
326        let result = AssuranceLevelEvaluator::evaluate_bundle(&bundle);
327        assert_eq!(result.level, AssuranceLevel::Low);
328    }
329
330    #[test]
331    fn bundle_level2_govt_doc_plus_channel() {
332        let bundle = EvidenceBundle {
333            subject: "did:key:z6MkTest".to_string(),
334            evidence: vec![
335                Evidence {
336                    evidence_type: EvidenceType::GovernmentDocument,
337                    verification: VerificationMethod::DatabaseCheck,
338                    issuer: "did:web:gov.ca".to_string(),
339                    timestamp: "2026-01-01T00:00:00Z".to_string(),
340                },
341                Evidence {
342                    evidence_type: EvidenceType::ChannelBinding,
343                    verification: VerificationMethod::AutomatedCheck,
344                    issuer: "did:web:verify.ca".to_string(),
345                    timestamp: "2026-01-01T00:00:00Z".to_string(),
346                },
347            ],
348        };
349        let result = AssuranceLevelEvaluator::evaluate_bundle(&bundle);
350        assert_eq!(result.level, AssuranceLevel::Substantial);
351        assert_eq!(result.pctf_name, "Level 2");
352        assert!(result.upgrade_possible);
353    }
354
355    #[test]
356    fn bundle_level3_full_proofing() {
357        let bundle = EvidenceBundle {
358            subject: "did:key:z6MkTest".to_string(),
359            evidence: vec![
360                Evidence {
361                    evidence_type: EvidenceType::InPerson,
362                    verification: VerificationMethod::VisualInspection,
363                    issuer: "did:web:servicecanada.gc.ca".to_string(),
364                    timestamp: "2026-01-01T00:00:00Z".to_string(),
365                },
366                Evidence {
367                    evidence_type: EvidenceType::Biometric,
368                    verification: VerificationMethod::BiometricMatch,
369                    issuer: "did:web:servicecanada.gc.ca".to_string(),
370                    timestamp: "2026-01-01T00:00:00Z".to_string(),
371                },
372                Evidence {
373                    evidence_type: EvidenceType::GovernmentPhotoId,
374                    verification: VerificationMethod::DatabaseCheck,
375                    issuer: "did:web:servicecanada.gc.ca".to_string(),
376                    timestamp: "2026-01-01T00:00:00Z".to_string(),
377                },
378            ],
379        };
380        let result = AssuranceLevelEvaluator::evaluate_bundle(&bundle);
381        assert_eq!(result.level, AssuranceLevel::High);
382        assert_eq!(result.pctf_name, "Level 3");
383        assert!(!result.upgrade_possible);
384    }
385
386    #[test]
387    fn bundle_level2_upgrade_requirements() {
388        let bundle = EvidenceBundle {
389            subject: "did:key:z6MkTest".to_string(),
390            evidence: vec![
391                Evidence {
392                    evidence_type: EvidenceType::GovernmentPhotoId,
393                    verification: VerificationMethod::VisualInspection,
394                    issuer: "did:web:gov.ca".to_string(),
395                    timestamp: "2026-01-01T00:00:00Z".to_string(),
396                },
397                Evidence {
398                    evidence_type: EvidenceType::DocumentVerification,
399                    verification: VerificationMethod::AutomatedCheck,
400                    issuer: "did:web:verify.ca".to_string(),
401                    timestamp: "2026-01-01T00:00:00Z".to_string(),
402                },
403            ],
404        };
405        let result = AssuranceLevelEvaluator::evaluate_bundle(&bundle);
406        assert_eq!(result.level, AssuranceLevel::Substantial);
407        // Should need in-person and biometric for Level 3
408        assert!(result
409            .upgrade_requirements
410            .iter()
411            .any(|r| r.contains("In-person")));
412        assert!(result
413            .upgrade_requirements
414            .iter()
415            .any(|r| r.contains("Biometric")));
416    }
417
418    #[test]
419    fn evidence_type_serde_roundtrip() {
420        let evidence = Evidence {
421            evidence_type: EvidenceType::GovernmentPhotoId,
422            verification: VerificationMethod::DatabaseCheck,
423            issuer: "did:web:gov.ca".to_string(),
424            timestamp: "2026-01-01T00:00:00Z".to_string(),
425        };
426        let json = serde_json::to_string(&evidence).unwrap();
427        let back: Evidence = serde_json::from_str(&json).unwrap();
428        assert_eq!(back.evidence_type, EvidenceType::GovernmentPhotoId);
429        assert_eq!(back.verification, VerificationMethod::DatabaseCheck);
430    }
431}