1use crate::error::WalletError;
8use crate::presenter::CredentialMatch;
9use crate::{AnyCredential, CredentialRecord};
10use baseid_core::types::CredentialFormat;
11
12#[derive(Debug, Clone)]
14pub enum PresentationRequest {
15 PresentationExchange(baseid_oid4vp::PresentationDefinition),
17 Dcql(baseid_oid4vp::DcqlQuery),
19}
20
21impl PresentationRequest {
22 pub fn from_value(value: &serde_json::Value) -> baseid_core::Result<Self> {
28 if value.get("input_descriptors").is_some() {
29 let pd: baseid_oid4vp::PresentationDefinition = serde_json::from_value(value.clone())
30 .map_err(|e| {
31 WalletError::MatchingFailed(format!("failed to parse PresentationDefinition: {e}"))
32 })?;
33 Ok(PresentationRequest::PresentationExchange(pd))
34 } else if value.get("credentials").is_some() {
35 let query: baseid_oid4vp::DcqlQuery =
36 serde_json::from_value(value.clone()).map_err(|e| {
37 WalletError::MatchingFailed(format!("failed to parse DcqlQuery: {e}"))
38 })?;
39 Ok(PresentationRequest::Dcql(query))
40 } else {
41 Err(WalletError::MatchingFailed("unknown request format".to_string()).into())
42 }
43 }
44}
45
46fn format_matches_pe(format_value: &serde_json::Value, cred_format: CredentialFormat) -> bool {
51 let obj = match format_value.as_object() {
52 Some(o) => o,
53 None => return false,
54 };
55 for key in obj.keys() {
56 let compatible = match key.as_str() {
57 "jwt_vc" | "jwt_vc_json" | "ldp_vc" => cred_format == CredentialFormat::W3cVc,
58 "mso_mdoc" => cred_format == CredentialFormat::Mdl,
59 "dc+sd-jwt" | "vc+sd-jwt" => cred_format == CredentialFormat::SdJwtVc,
60 _ => false,
61 };
62 if compatible {
63 return true;
64 }
65 }
66 false
67}
68
69fn path_last_segment(path: &str) -> &str {
74 path.rsplit('.').next().unwrap_or(path)
75}
76
77fn field_matches_w3c_vc(vc: &baseid_vc::VerifiableCredential, path: &str) -> bool {
83 let lower = path.to_lowercase();
84
85 if lower.contains("type") {
87 return !vc.r#type.is_empty();
88 }
89
90 if lower.contains("credentialsubject") {
92 let segment = path_last_segment(path);
93 if segment.eq_ignore_ascii_case("credentialSubject") {
94 return true;
96 }
97 if let Some(obj) = vc.credential_subject.as_object() {
99 return obj.contains_key(segment);
100 }
101 return false;
102 }
103
104 true
107}
108
109pub fn match_credentials_pe(
115 credentials: &[CredentialRecord],
116 definition: &baseid_oid4vp::PresentationDefinition,
117) -> Vec<CredentialMatch> {
118 let mut matches = Vec::new();
119
120 for descriptor in &definition.input_descriptors {
121 for record in credentials {
122 if let Some(ref fmt) = descriptor.format {
124 if !format_matches_pe(fmt, record.credential.credential_format()) {
125 continue;
126 }
127 }
128
129 let mut matching_fields = Vec::new();
131 let mut any_field_matched = descriptor.constraints.fields.is_empty();
132
133 for field in &descriptor.constraints.fields {
134 let field_matched = field.path.iter().any(|path| match &record.credential {
135 AnyCredential::W3cVc(vc) => field_matches_w3c_vc(vc, path),
136 AnyCredential::Mdoc(_) => {
137 true
139 }
140 AnyCredential::SdJwtVc(_) => {
141 true
143 }
144 AnyCredential::Bbs(_) => {
145 true
147 }
148 AnyCredential::AnonCreds(cred) => {
149 let segment = path_last_segment(path);
151 cred.values.contains_key(segment)
152 }
153 });
154
155 if field_matched {
156 any_field_matched = true;
157 for path in &field.path {
159 matching_fields.push(path.clone());
160 }
161 }
162 }
163
164 if any_field_matched {
165 matches.push(CredentialMatch {
166 credential_id: record.id.clone(),
167 format: record.credential.credential_format(),
168 matching_fields,
169 });
170 }
171 }
172 }
173
174 matches
175}
176
177fn format_matches_dcql(dcql_format: &str, cred_format: CredentialFormat) -> bool {
179 match dcql_format {
180 "dc+sd-jwt" | "vc+sd-jwt" => cred_format == CredentialFormat::SdJwtVc,
181 "mso_mdoc" => cred_format == CredentialFormat::Mdl,
182 "jwt_vc_json" | "ldp_vc" => cred_format == CredentialFormat::W3cVc,
183 _ => false,
184 }
185}
186
187fn vc_type_matches(vc: &baseid_vc::VerifiableCredential, type_values: &[Vec<String>]) -> bool {
192 type_values.iter().any(|required_types| {
193 required_types
194 .iter()
195 .all(|required| vc.r#type.iter().any(|t| t == required))
196 })
197}
198
199pub fn match_credentials_dcql(
205 credentials: &[CredentialRecord],
206 query: &baseid_oid4vp::DcqlQuery,
207) -> Vec<CredentialMatch> {
208 let mut matches = Vec::new();
209
210 for dcql_cred in &query.credentials {
211 for record in credentials {
212 if !format_matches_dcql(&dcql_cred.format, record.credential.credential_format()) {
214 continue;
215 }
216
217 if let Some(ref meta) = dcql_cred.meta {
219 match &record.credential {
220 AnyCredential::W3cVc(vc) => {
221 if let Some(ref type_values) = meta.type_values {
223 if !vc_type_matches(vc, type_values) {
224 continue;
225 }
226 }
227 }
228 AnyCredential::Mdoc(_mdoc) => {
229 }
232 AnyCredential::SdJwtVc(_) => {
233 }
236 AnyCredential::Bbs(_) => {
237 }
240 AnyCredential::AnonCreds(_) => {
241 }
244 }
245 }
246
247 let matching_fields: Vec<String> = dcql_cred
249 .claims
250 .iter()
251 .map(|claim| claim.path.join("."))
252 .collect();
253
254 matches.push(CredentialMatch {
255 credential_id: record.id.clone(),
256 format: record.credential.credential_format(),
257 matching_fields,
258 });
259 }
260 }
261
262 matches
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use baseid_core::types::CredentialId;
269 use baseid_vc::credential::Issuer;
270 use baseid_vc::VerifiableCredential;
271
272 fn make_vc(types: &[&str], subject: serde_json::Value) -> CredentialRecord {
274 let vc = VerifiableCredential {
275 context: vec!["https://www.w3.org/ns/credentials/v2".to_string()],
276 id: None,
277 r#type: types.iter().map(|s| s.to_string()).collect(),
278 issuer: Issuer::Uri("did:key:z6Mk-test-issuer".to_string()),
279 valid_from: Some("2024-01-01T00:00:00Z".to_string()),
280 valid_until: None,
281 credential_subject: subject,
282 credential_status: None,
283 proof: None,
284 };
285 CredentialRecord {
286 id: CredentialId(format!("vc-{}", types.last().unwrap_or(&"unknown"))),
287 credential: AnyCredential::W3cVc(vc),
288 raw: None,
289 }
290 }
291
292 fn make_sd_jwt(id: &str) -> CredentialRecord {
294 let sd_jwt = baseid_sd_jwt::SdJwt {
295 jwt: "eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJkaWQ6a2V5OnRlc3QifQ.sig"
296 .to_string(),
297 disclosures: vec![],
298 key_binding_jwt: None,
299 };
300 CredentialRecord {
301 id: CredentialId(id.to_string()),
302 credential: AnyCredential::SdJwtVc(sd_jwt),
303 raw: None,
304 }
305 }
306
307 fn make_mdoc(id: &str) -> CredentialRecord {
309 let mdoc = baseid_mdl::MobileDocument {
310 doc_type: "org.iso.18013.5.1.mDL".to_string(),
311 namespaces: std::collections::BTreeMap::new(),
312 };
313 CredentialRecord {
314 id: CredentialId(id.to_string()),
315 credential: AnyCredential::Mdoc(mdoc),
316 raw: None,
317 }
318 }
319
320 #[test]
325 fn parse_pe_request() {
326 let value = serde_json::json!({
327 "id": "pd-1",
328 "input_descriptors": [{
329 "id": "desc-1",
330 "constraints": { "fields": [] }
331 }]
332 });
333 let req = PresentationRequest::from_value(&value).unwrap();
334 assert!(matches!(req, PresentationRequest::PresentationExchange(_)));
335 }
336
337 #[test]
338 fn parse_dcql_request() {
339 let value = serde_json::json!({
340 "credentials": [{
341 "id": "cred-1",
342 "format": "dc+sd-jwt"
343 }]
344 });
345 let req = PresentationRequest::from_value(&value).unwrap();
346 assert!(matches!(req, PresentationRequest::Dcql(_)));
347 }
348
349 #[test]
350 fn parse_unknown_request() {
351 let value = serde_json::json!({ "foo": "bar" });
352 let result = PresentationRequest::from_value(&value);
353 assert!(result.is_err());
354 }
355
356 #[test]
361 fn pe_match_by_format() {
362 let creds = vec![make_vc(
363 &["VerifiableCredential", "UniversityDegreeCredential"],
364 serde_json::json!({"id": "did:key:holder"}),
365 )];
366 let pd: baseid_oid4vp::PresentationDefinition = serde_json::from_value(serde_json::json!({
367 "id": "pd-1",
368 "input_descriptors": [{
369 "id": "desc-1",
370 "constraints": { "fields": [] },
371 "format": { "jwt_vc_json": {} }
372 }]
373 }))
374 .unwrap();
375
376 let results = match_credentials_pe(&creds, &pd);
377 assert_eq!(results.len(), 1);
378 assert_eq!(results[0].format, CredentialFormat::W3cVc);
379 }
380
381 #[test]
382 fn pe_match_by_field_path() {
383 let creds = vec![make_vc(
384 &["VerifiableCredential", "UniversityDegreeCredential"],
385 serde_json::json!({"degree": {"type": "BachelorDegree"}}),
386 )];
387 let pd: baseid_oid4vp::PresentationDefinition = serde_json::from_value(serde_json::json!({
388 "id": "pd-1",
389 "input_descriptors": [{
390 "id": "desc-1",
391 "constraints": {
392 "fields": [{
393 "path": ["$.credentialSubject.degree"]
394 }]
395 }
396 }]
397 }))
398 .unwrap();
399
400 let results = match_credentials_pe(&creds, &pd);
401 assert_eq!(results.len(), 1);
402 assert!(results[0]
403 .matching_fields
404 .contains(&"$.credentialSubject.degree".to_string()));
405 }
406
407 #[test]
408 fn pe_no_match_wrong_format() {
409 let creds = vec![make_vc(
410 &["VerifiableCredential"],
411 serde_json::json!({"id": "did:key:holder"}),
412 )];
413 let pd: baseid_oid4vp::PresentationDefinition = serde_json::from_value(serde_json::json!({
414 "id": "pd-1",
415 "input_descriptors": [{
416 "id": "desc-1",
417 "constraints": { "fields": [] },
418 "format": { "mso_mdoc": {} }
419 }]
420 }))
421 .unwrap();
422
423 let results = match_credentials_pe(&creds, &pd);
424 assert!(results.is_empty());
425 }
426
427 #[test]
428 fn pe_multiple_descriptors() {
429 let creds = vec![
430 make_vc(
431 &["VerifiableCredential", "UniversityDegreeCredential"],
432 serde_json::json!({"degree": "BSc"}),
433 ),
434 make_sd_jwt("sd-jwt-1"),
435 ];
436 let pd: baseid_oid4vp::PresentationDefinition = serde_json::from_value(serde_json::json!({
437 "id": "pd-1",
438 "input_descriptors": [
439 {
440 "id": "desc-vc",
441 "constraints": { "fields": [] },
442 "format": { "jwt_vc_json": {} }
443 },
444 {
445 "id": "desc-sd",
446 "constraints": { "fields": [] },
447 "format": { "dc+sd-jwt": {} }
448 }
449 ]
450 }))
451 .unwrap();
452
453 let results = match_credentials_pe(&creds, &pd);
454 assert_eq!(results.len(), 2);
455
456 let formats: Vec<CredentialFormat> = results.iter().map(|m| m.format).collect();
457 assert!(formats.contains(&CredentialFormat::W3cVc));
458 assert!(formats.contains(&CredentialFormat::SdJwtVc));
459 }
460
461 #[test]
466 fn dcql_match_by_format() {
467 let creds = vec![make_vc(
468 &["VerifiableCredential", "IDCredential"],
469 serde_json::json!({"id": "did:key:holder"}),
470 )];
471 let query: baseid_oid4vp::DcqlQuery = serde_json::from_value(serde_json::json!({
472 "credentials": [{
473 "id": "req-1",
474 "format": "jwt_vc_json"
475 }]
476 }))
477 .unwrap();
478
479 let results = match_credentials_dcql(&creds, &query);
480 assert_eq!(results.len(), 1);
481 assert_eq!(results[0].format, CredentialFormat::W3cVc);
482 }
483
484 #[test]
485 fn dcql_match_sd_jwt_format() {
486 let creds = vec![make_sd_jwt("sd-1")];
487 let query: baseid_oid4vp::DcqlQuery = serde_json::from_value(serde_json::json!({
488 "credentials": [{
489 "id": "req-1",
490 "format": "dc+sd-jwt",
491 "claims": [
492 {"path": ["family_name"]},
493 {"path": ["given_name"]}
494 ]
495 }]
496 }))
497 .unwrap();
498
499 let results = match_credentials_dcql(&creds, &query);
500 assert_eq!(results.len(), 1);
501 assert_eq!(results[0].format, CredentialFormat::SdJwtVc);
502 assert_eq!(
503 results[0].matching_fields,
504 vec!["family_name", "given_name"]
505 );
506 }
507
508 #[test]
509 fn dcql_no_match() {
510 let creds = vec![make_vc(&["VerifiableCredential"], serde_json::json!({}))];
511 let query: baseid_oid4vp::DcqlQuery = serde_json::from_value(serde_json::json!({
512 "credentials": [{
513 "id": "req-1",
514 "format": "mso_mdoc"
515 }]
516 }))
517 .unwrap();
518
519 let results = match_credentials_dcql(&creds, &query);
520 assert!(results.is_empty());
521 }
522
523 #[test]
524 fn dcql_match_vc_type() {
525 let creds = vec![make_vc(
526 &["VerifiableCredential", "IDCredential"],
527 serde_json::json!({"name": "Alice"}),
528 )];
529 let query: baseid_oid4vp::DcqlQuery = serde_json::from_value(serde_json::json!({
530 "credentials": [{
531 "id": "req-1",
532 "format": "jwt_vc_json",
533 "meta": {
534 "type_values": [["IDCredential"]]
535 },
536 "claims": [
537 {"path": ["name"]}
538 ]
539 }]
540 }))
541 .unwrap();
542
543 let results = match_credentials_dcql(&creds, &query);
544 assert_eq!(results.len(), 1);
545 assert_eq!(results[0].matching_fields, vec!["name"]);
546 }
547
548 #[test]
549 fn dcql_no_match_wrong_vc_type() {
550 let creds = vec![make_vc(
551 &["VerifiableCredential", "UniversityDegreeCredential"],
552 serde_json::json!({}),
553 )];
554 let query: baseid_oid4vp::DcqlQuery = serde_json::from_value(serde_json::json!({
555 "credentials": [{
556 "id": "req-1",
557 "format": "jwt_vc_json",
558 "meta": {
559 "type_values": [["IDCredential"]]
560 }
561 }]
562 }))
563 .unwrap();
564
565 let results = match_credentials_dcql(&creds, &query);
566 assert!(results.is_empty());
567 }
568
569 #[test]
570 fn dcql_match_mdoc_format() {
571 let creds = vec![make_mdoc("mdoc-1")];
572 let query: baseid_oid4vp::DcqlQuery = serde_json::from_value(serde_json::json!({
573 "credentials": [{
574 "id": "req-1",
575 "format": "mso_mdoc",
576 "meta": {
577 "doctype_value": "org.iso.18013.5.1.mDL"
578 },
579 "claims": [
580 {"path": ["org.iso.18013.5.1", "given_name"]}
581 ]
582 }]
583 }))
584 .unwrap();
585
586 let results = match_credentials_dcql(&creds, &query);
587 assert_eq!(results.len(), 1);
588 assert_eq!(results[0].format, CredentialFormat::Mdl);
589 assert_eq!(
590 results[0].matching_fields,
591 vec!["org.iso.18013.5.1.given_name"]
592 );
593 }
594
595 #[test]
596 fn pe_field_path_credential_subject_missing_key() {
597 let creds = vec![make_vc(
598 &["VerifiableCredential"],
599 serde_json::json!({"name": "Alice"}),
600 )];
601 let pd: baseid_oid4vp::PresentationDefinition = serde_json::from_value(serde_json::json!({
602 "id": "pd-1",
603 "input_descriptors": [{
604 "id": "desc-1",
605 "constraints": {
606 "fields": [{
607 "path": ["$.credentialSubject.degree"]
608 }]
609 }
610 }]
611 }))
612 .unwrap();
613
614 let results = match_credentials_pe(&creds, &pd);
616 assert!(results.is_empty());
617 }
618
619 #[test]
620 fn pe_field_path_type_matches() {
621 let creds = vec![make_vc(
622 &["VerifiableCredential", "IDCredential"],
623 serde_json::json!({}),
624 )];
625 let pd: baseid_oid4vp::PresentationDefinition = serde_json::from_value(serde_json::json!({
626 "id": "pd-1",
627 "input_descriptors": [{
628 "id": "desc-1",
629 "constraints": {
630 "fields": [{
631 "path": ["$.vc.type"]
632 }]
633 }
634 }]
635 }))
636 .unwrap();
637
638 let results = match_credentials_pe(&creds, &pd);
639 assert_eq!(results.len(), 1);
640 assert!(results[0]
641 .matching_fields
642 .contains(&"$.vc.type".to_string()));
643 }
644}