1use baseid_core::types::AssuranceLevel;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum EvidenceType {
13 SelfAsserted,
15 KnowledgeBased,
17 ChannelBinding,
19 GovernmentPhotoId,
21 GovernmentDocument,
23 AddressDocument,
25 DocumentVerification,
27 Biometric,
29 InPerson,
31 SupervisedRemote,
33 TrustedCredential,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39pub enum VerificationMethod {
40 Unverified,
42 DatabaseCheck,
44 VisualInspection,
46 AutomatedCheck,
48 BiometricMatch,
50 CryptographicProof,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct Evidence {
57 pub evidence_type: EvidenceType,
59 pub verification: VerificationMethod,
61 pub issuer: String,
63 pub timestamp: String,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct EvidenceBundle {
70 pub subject: String,
72 pub evidence: Vec<Evidence>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct EvaluationResult {
79 pub level: AssuranceLevel,
81 pub pctf_name: String,
83 pub reasoning: Vec<String>,
85 pub upgrade_possible: bool,
87 pub upgrade_requirements: Vec<String>,
89}
90
91pub struct AssuranceLevelEvaluator;
102
103impl AssuranceLevelEvaluator {
104 pub fn evaluate(evidence: &[&str]) -> AssuranceLevel {
108 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 if (has_in_person) && has_biometric && has_govt_photo {
133 return AssuranceLevel::High;
134 }
135
136 if has_govt_doc && has_additional {
138 return AssuranceLevel::Substantial;
139 }
140
141 AssuranceLevel::Low
143 }
144
145 pub fn evaluate_bundle(bundle: &EvidenceBundle) -> EvaluationResult {
147 let evidence = &bundle.evidence;
148 let mut reasoning = Vec::new();
149
150 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 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 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 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 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}