diff --git a/CredentialProvider/wasm/matcher-rs/src/bindings.rs b/CredentialProvider/wasm/matcher-rs/src/bindings.rs index c45f023..bb2af47 100644 --- a/CredentialProvider/wasm/matcher-rs/src/bindings.rs +++ b/CredentialProvider/wasm/matcher-rs/src/bindings.rs @@ -1,154 +1,167 @@ -use std::os::raw::{c_char, c_void}; +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 ( disputes ); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -#[repr(C)] -pub struct CallingAppInfo { - pub package_name: [u8; 256], - pub origin: [u8; 512], -} +//! Raw C foreign function interface (FFI) bindings imported from the host. -#[link(wasm_import_module = "credman")] -unsafe extern "C" { - pub fn AddEntry( - cred_id: i64, - icon: *const c_char, - icon_len: usize, +use std::os::raw::c_char; + +extern "C" { + /// Adds a candidate entry to a set inside the matched presentation entry sets. + pub fn SetDelegationTypeForEntryInSet( + cred_id: *const c_char, + delegation_type: i32, + set_id: *const c_char, + set_index: i32, + ); + + /// Logs an informational message using the host environment's logger. + pub fn LogInfo(msg: *const c_char); + + /// Logs a warning message using the host environment's logger. + pub fn LogWarning(msg: *const c_char); + + /// Logs an error message using the host environment's logger. + pub fn LogError(msg: *const c_char); + + /// Sets the status of the match result. + pub fn SetStatus(status: i32); + + /// Sets the selection metadata JSON on the host environment's match result. + pub fn SetSelectionMetadata(metadata: *const c_char); + + /// Sets the error message on the host environment's match result. + pub fn SetErrorMessage(error_message: *const c_char); + + /// Adds a standard matched entry to the match result on the host environment. + pub fn AddStandardEntry( + provider_id: *const c_char, + entry_key: *const c_char, title: *const c_char, subtitle: *const c_char, disclaimer: *const c_char, warning: *const c_char, + metadata: *const c_char, ); - pub fn AddField( - cred_id: i64, - field_display_name: *const c_char, - field_display_value: *const c_char, - ); - pub fn AddEntrySet(set_id: *const c_char, set_length: i32); - pub fn AddEntryToSet( - cred_id: *const c_char, - icon: *const c_char, - icon_len: usize, + + /// Adds an inline issuance entry to the match result on the host environment. + pub fn AddInlineIssuanceEntry( + provider_id: *const c_char, + entry_key: *const c_char, title: *const c_char, subtitle: *const c_char, disclaimer: *const c_char, warning: *const c_char, metadata: *const c_char, - set_id: *const c_char, - set_index: i32, ); - pub fn AddFieldToEntrySet( - cred_id: *const c_char, - field_display_name: *const c_char, - field_display_value: *const c_char, - set_id: *const c_char, - set_index: i32, - ); - pub fn AddPaymentEntryToSet( + + /// Creates a new entry set inside the matched presentation entry sets. + pub fn CreateEntrySet(set_id: *const c_char); + + /// Adds a candidate entry to a set inside the matched presentation entry sets. + pub fn AddEntryToSet( + provider_id: *const c_char, cred_id: *const c_char, - merchant_name: *const c_char, - payment_method_name: *const c_char, - payment_method_subtitle: *const c_char, - payment_method_icon: *const c_char, - payment_method_icon_len: usize, - transaction_amount: *const c_char, - bank_icon: *const c_char, - bank_icon_len: usize, - payment_provider_icon: *const c_char, - payment_provider_icon_len: usize, + entry_key: *const c_char, + title: *const c_char, + subtitle: *const c_char, + disclaimer: *const c_char, + warning: *const c_char, metadata: *const c_char, set_id: *const c_char, set_index: i32, ); - pub fn AddPaymentEntryToSetV2( - cred_id: *const c_char, - merchant_name: *const c_char, - payment_method_name: *const c_char, - payment_method_subtitle: *const c_char, - payment_method_icon: *const c_char, - payment_method_icon_len: usize, - transaction_amount: *const c_char, - bank_icon: *const c_char, - bank_icon_len: usize, - payment_provider_icon: *const c_char, - payment_provider_icon_len: usize, - additional_info: *const c_char, - metadata: *const c_char, - set_id: *const c_char, - set_index: i32, + + /// Self declares host package info display name and icon bytes. + pub fn SelfDeclarePackageInfo( + package_display_name: *const c_char, + package_icon_bytes: *const c_char, + package_icon_len: usize, ); + + /// Adds string ID entry. pub fn AddStringIdEntry( cred_id: *const c_char, - icon: *const c_char, + icon_bytes: *const c_char, icon_len: usize, title: *const c_char, subtitle: *const c_char, disclaimer: *const c_char, warning: *const c_char, ); + + /// Adds issuance entry. pub fn AddIssuanceEntry( cred_id: *const c_char, - icon: *const c_char, + icon_bytes: *const c_char, icon_len: usize, title: *const c_char, subtitle: *const c_char, explainer: *const c_char, metadata: *const c_char, ); - pub fn AddFieldForStringIdEntry( + + /// Adds an entry set with set_id and total set_length. + pub fn AddEntrySet(set_id: *const c_char, set_length: i32); + + /// Adds field name and value to a set. + pub fn AddFieldToEntrySet( cred_id: *const c_char, field_display_name: *const c_char, field_display_value: *const c_char, + set_id: *const c_char, + set_index: i32, ); - pub fn GetRequestBuffer(buffer: *mut c_void); - pub fn GetRequestSize(size: *mut u32); - pub fn ReadCredentialsBuffer(buffer: *mut c_void, offset: usize, len: usize) -> usize; - pub fn GetCredentialsSize(size: *mut u32); - pub fn GetWasmVersion(version: *mut u32); - pub fn AddPaymentEntry( + + /// Adds payment entry to set (V2). + pub fn AddPaymentEntryToSetV2( cred_id: *const c_char, merchant_name: *const c_char, payment_method_name: *const c_char, payment_method_subtitle: *const c_char, - payment_method_icon: *const c_char, + payment_method_icon_bytes: *const c_char, payment_method_icon_len: usize, transaction_amount: *const c_char, - bank_icon: *const c_char, + bank_icon_bytes: *const c_char, bank_icon_len: usize, - payment_provider_icon: *const c_char, + payment_provider_icon_bytes: *const c_char, payment_provider_icon_len: usize, - ); - pub fn AddInlineIssuanceEntry( - cred_id: *const c_char, - icon: *const c_char, - icon_len: usize, - title: *const c_char, - subtitle: *const c_char, - ); - pub fn SetAdditionalDisclaimerAndUrlForVerificationEntry( - cred_id: *const c_char, - secondary_disclaimer: *const c_char, - url_display_text: *const c_char, - url_value: *const c_char, - ); - pub fn SetAdditionalDisclaimerAndUrlForVerificationEntryInCredentialSet( - cred_id: *const c_char, - secondary_disclaimer: *const c_char, - url_display_text: *const c_char, - url_value: *const c_char, + additional_info: *const c_char, + metadata: *const c_char, set_id: *const c_char, set_index: i32, ); - pub fn GetCallingAppInfo(info: *mut CallingAppInfo); - pub fn SelfDeclarePackageInfo( - package_display_name: *const c_char, - package_icon: *const c_char, - package_icon_len: usize, - ); + + /// Adds metadata display text to an entry set. pub fn AddMetadataDisplayTextToEntrySet( cred_id: *const c_char, metadata_display_text: *const c_char, set_id: *const c_char, set_index: i32, ); - pub fn fd_write(fd: i32, iovs_ptr: *const c_void, iovs_len: i32, nwritten_ptr: *mut i32) - -> i32; + + /// Gets request size in bytes. + pub fn GetRequestSize(size: *mut u32); + + /// Fills request buffer. + pub fn GetRequestBuffer(buffer: *mut c_void); + + /// Gets registered credentials size in bytes. + pub fn GetCredentialsSize(size: *mut u32); + + /// Reads credentials buffer. + pub fn ReadCredentialsBuffer(buffer: *mut c_void, offset: usize, len: usize); + + /// Gets WASM version. + pub fn GetWasmVersion(version: *mut u32); } diff --git a/CredentialProvider/wasm/matcher-rs/src/credman.rs b/CredentialProvider/wasm/matcher-rs/src/credman.rs index 31208cd..464034f 100644 --- a/CredentialProvider/wasm/matcher-rs/src/credman.rs +++ b/CredentialProvider/wasm/matcher-rs/src/credman.rs @@ -3,7 +3,7 @@ use std::{ffi::CString, os::raw::c_void}; use crate::bindings::{ AddEntrySet, AddEntryToSet, AddFieldToEntrySet, AddInlineIssuanceEntry, AddIssuanceEntry, AddMetadataDisplayTextToEntrySet, AddPaymentEntryToSetV2, AddStringIdEntry, GetCredentialsSize, - GetRequestBuffer, GetRequestSize, GetWasmVersion, ReadCredentialsBuffer, + GetRequestBuffer, GetRequestSize, GetWasmVersion, ReadCredentialsBuffer, SelfDeclarePackageInfo, }; pub trait CredmanApi { @@ -40,6 +40,14 @@ pub trait CredmanApi { metadata: &str, set_id: &str, set_index: i32, + delegation_type: i32, + ); + fn set_delegation_type_for_entry_in_set( + &mut self, + cred_id: &str, + delegation_type: i32, + set_id: &str, + set_index: i32, ); fn add_field_to_entry_set( &mut self, @@ -78,6 +86,11 @@ pub trait CredmanApi { set_id: &str, set_index: i32, ); + fn self_declare_package_info( + &mut self, + package_display_name: &str, + package_icon: &[u8], + ); } pub struct CredmanApiImpl; @@ -233,6 +246,7 @@ impl CredmanApi for CredmanApiImpl { metadata: &str, set_id: &str, set_index: i32, + delegation_type: i32, ) { let cred_id_c = CString::new(cred_id).unwrap(); let title_c = if title.is_empty() { @@ -284,6 +298,32 @@ impl CredmanApi for CredmanApiImpl { set_id_c.as_ptr(), set_index, ); + if delegation_type != 0 { + crate::bindings::SetDelegationTypeForEntryInSet( + cred_id_c.as_ptr(), + delegation_type, + set_id_c.as_ptr(), + set_index, + ); + } + } + } + fn set_delegation_type_for_entry_in_set( + &mut self, + cred_id: &str, + delegation_type: i32, + set_id: &str, + set_index: i32, + ) { + let cred_id_c = CString::new(cred_id).unwrap(); + let set_id_c = CString::new(set_id).unwrap(); + unsafe { + crate::bindings::SetDelegationTypeForEntryInSet( + cred_id_c.as_ptr(), + delegation_type, + set_id_c.as_ptr(), + set_index, + ); } } fn add_field_to_entry_set( @@ -468,4 +508,30 @@ impl CredmanApi for CredmanApiImpl { ); } } + fn self_declare_package_info( + &mut self, + package_display_name: &str, + package_icon: &[u8], + ) { + let package_display_name_c = if package_display_name.is_empty() { + None + } else { + Some(CString::new(package_display_name).unwrap()) + }; + + let icon_bytes = if package_icon.is_empty() { + std::ptr::null() + } else { + package_icon.as_ptr() + } as *const std::os::raw::c_char; + let icon_length = package_icon.len(); + + unsafe { + SelfDeclarePackageInfo( + package_display_name_c.as_ref().map_or(std::ptr::null(), |c| c.as_ptr()), + icon_bytes, + icon_length, + ); + } + } } diff --git a/CredentialProvider/wasm/matcher-rs/src/dcql.rs b/CredentialProvider/wasm/matcher-rs/src/dcql.rs index 8ca7feb..d927202 100644 --- a/CredentialProvider/wasm/matcher-rs/src/dcql.rs +++ b/CredentialProvider/wasm/matcher-rs/src/dcql.rs @@ -1,1005 +1,508 @@ -use crate::json_value::{DeterministicMap, JsonValue}; -pub use crate::openid4vp_models::*; +use crate::openid4vp_models::{ + DcqlClaim, DcqlCredential, DcqlMatchedCredentialEntry, DcqlQuery, MatchedClaim, + MatchedCredential, RegistryCredential, SelectionMetadata, +}; +use crate::json_value::{DeterministicMap, JsonValue}; +use nanoserde::SerJson; use std::borrow::Cow; -pub fn add_all_claims<'a>( - matched_claim_names: &mut Vec<&'a JsonValue>, - candidate_paths: &'a DeterministicMap, -) { - candidate_paths - .values() - .filter_map(|v| match v { - JsonValue::Object(obj) => Some((v, obj)), - _ => None, - }) - .for_each(|(v, obj)| { - if let Some(display) = obj.get("display") { - matched_claim_names.push(display); - } else { - add_all_claims_from_json(matched_claim_names, v); - } - }); -} - -fn add_all_claims_from_json<'a>(matched_claim_names: &mut Vec<&'a JsonValue>, json: &'a JsonValue) { - match json { - JsonValue::Object(obj) => { - if let Some(display) = obj.get("display") { - matched_claim_names.push(display); - } else { - obj.values() - .for_each(|v| add_all_claims_from_json(matched_claim_names, v)); - } - } - JsonValue::Array(arr) => { - arr.iter() - .for_each(|v| add_all_claims_from_json(matched_claim_names, v)); - } - _ => {} - } -} - -fn get_format_candidates<'a>( - format: &str, - registry: &'a Registry, -) -> ( - Option<&'a DeterministicMap>>, - Option<&'a Vec>, -) { - match format { - "mso_mdoc" => ( - registry.credentials.mso_mdoc.as_ref(), - registry.credentials.issuance.as_ref().map(|i| &i.mso_mdoc), - ), - "dc+sd-jwt" => ( - registry.credentials.sd_jwt.as_ref(), - registry.credentials.issuance.as_ref().map(|i| &i.sd_jwt), - ), - _ => { - log::warn!("Unsupported format: {}", format); - (None, None) - } - } -} - -fn filter_candidates_by_meta<'a>( - format: &str, - meta: &Option, - candidates: Option<&'a DeterministicMap>>, - inline_issuance_candidates: Option<&'a Vec>, -) -> ( - Vec<&'a RegistryCredential>, - Option<&'a RegistryIssuanceEntry>, -) { - let Some(meta) = meta else { - log::trace!( - "No meta provided, collecting all candidates for format {}", - format - ); - let v = candidates - .map(|c| c.values().flatten().collect()) - .unwrap_or_default(); - return (v, None); - }; - - let mut inline_issuance = None; - let filtered_candidates = match format { - "mso_mdoc" => { - if meta.doctype_value.is_empty() { - log::trace!("mso_mdoc requested but no doctype_value in meta"); - return (Vec::new(), None); - } - - log::trace!( - "Filtering mso_mdoc candidates by doctype: {}", - meta.doctype_value - ); - inline_issuance = inline_issuance_candidates.and_then(|cands| { - cands - .iter() - .find(|cand| cand.supported.contains(&meta.doctype_value)) - .map(|cand| { - log::debug!( - "Found matching inline issuance for doctype {}: {}", - meta.doctype_value, - cand.id - ); - cand - }) - }); - candidates - .and_then(|c| c.get(&meta.doctype_value)) - .map(|v| v.iter().collect()) - .unwrap_or_default() - } - "dc+sd-jwt" => { - if meta.vct_values.is_empty() { - return (Vec::new(), None); - } - - log::trace!( - "Filtering dc+sd-jwt candidates by vcts: {:?}", - meta.vct_values - ); - inline_issuance = meta.vct_values.iter().find_map(|vct| { - inline_issuance_candidates.and_then(|cands| { - cands - .iter() - .find(|cand| cand.supported.contains(vct)) - .map(|cand| { - log::debug!( - "Found matching inline issuance for vct {}: {}", - vct, - cand.id - ); - cand - }) - }) - }); - - meta.vct_values - .iter() - .filter_map(|vct| candidates.and_then(|c| c.get(vct))) - .flatten() - .collect() - } - _ => Vec::new(), - }; - - (filtered_candidates, inline_issuance) -} - -fn match_candidate_claims<'a>( - candidate: &'a RegistryCredential, - claims_req: &'a Vec, - claim_sets_req: &'a Vec>, -) -> Option> { - if claims_req.is_empty() { - log::debug!( - "Candidate {}: no specific claims requested, matching all available claims", - candidate.id - ); - let mut matched_claim_names = Vec::new(); - add_all_claims(&mut matched_claim_names, &candidate.paths); - return Some(MatchedCredential { - id: &candidate.id, - display: &candidate.display, - matched_claim_names, - matched_claim_metadata: Vec::new(), - }); - } - - if !claim_sets_req.is_empty() { - log::trace!("Candidate {}: matching against claim_sets", candidate.id); - let matched_claim_ids: DeterministicMap<&'a str, MatchedClaim<'a>> = claims_req - .iter() - .filter(|claim| !claim.id.is_empty()) - .filter_map(|claim| { - match_claim(claim, &candidate.paths).map(|info| { - log::trace!("Candidate {}: claim {} matched", candidate.id, claim.id); - (claim.id.as_str(), info) - }) - }) - .collect(); - - return claim_sets_req +pub fn filter_candidates_by_meta<'a>( + credential: &DcqlCredential, + candidates: &'a [RegistryCredential], +) -> Vec<&'a RegistryCredential> { + if let Some(ref meta) = credential.meta { + candidates .iter() - .enumerate() - .find_map(|(idx, claim_set)| { - let mut current_set_names = Vec::new(); - let mut current_set_metadata = Vec::new(); - - let all_matched = claim_set.iter().all(|claim_id| { - let Some(info) = matched_claim_ids.get(claim_id.as_str()) else { - log::trace!( - "Candidate {}: claim set index {} failed because claim {} did not match", - candidate.id, - idx, - claim_id - ); - return false; - }; - current_set_names.push(info.display); - current_set_metadata.push(info.path); - true - }); - - if !all_matched { - return None; - } - - log::debug!( - "Candidate {}: matched claim set index {}", - candidate.id, - idx - ); - Some(MatchedCredential { - id: &candidate.id, - display: &candidate.display, - matched_claim_names: current_set_names, - matched_claim_metadata: current_set_metadata, - }) + .filter(|candidate| { + // Doctypes must match if provided + let doctype_match = meta.doctype_value.is_empty() + || candidate + .paths + .get("doctype") + .and_then(|v| v.as_str()) + .map_or(false, |s| s == meta.doctype_value); + + // Vct values must match if provided + let vct_match = meta.vct_values.is_empty() + || candidate + .paths + .get("vct") + .and_then(|v| v.as_str()) + .map_or(false, |s| meta.vct_values.iter().any(|vct| vct == s)); + + doctype_match && vct_match }) - .or_else(|| { - log::debug!("Candidate {}: no claim sets matched", candidate.id); - None - }); - } - - log::trace!( - "Candidate {}: matching all {} requested claims", - candidate.id, - claims_req.len() - ); - let mut matched_claim_names = Vec::new(); - let mut matched_claim_metadata = Vec::new(); - - let all_matched = claims_req.iter().all(|claim| { - let Some(info) = match_claim(claim, &candidate.paths) else { - log::trace!( - "Candidate {}: claim path {:?} failed to match", - candidate.id, - claim.path - ); - return false; - }; - matched_claim_names.push(info.display); - matched_claim_metadata.push(info.path); - true - }); - - if !all_matched { - return None; + .collect() + } else { + candidates.iter().collect() } - - log::debug!("Candidate {}: all claims matched", candidate.id); - Some(MatchedCredential { - id: &candidate.id, - display: &candidate.display, - matched_claim_names, - matched_claim_metadata, - }) } -pub fn match_credential<'a>( - credential: &'a DcqlCredential, - registry: &'a Registry, -) -> MatchCredentialResult<'a> { - log::debug!( - "Matching credential req id: {}, format: {}", - credential.id, - credential.format - ); - let (candidates, inline_issuance_candidates) = - get_format_candidates(&credential.format, registry); - let (filtered_candidates, inline_issuance) = filter_candidates_by_meta( - &credential.format, - &credential.meta, - candidates, - inline_issuance_candidates, - ); - - log::debug!( - "Found {} potential candidates after meta filtering", - filtered_candidates.len() - ); - let matched_creds = filtered_candidates - .into_iter() - .filter_map(|candidate| { - match_candidate_claims(candidate, &credential.claims, &credential.claim_sets) - }) - .collect(); - - MatchCredentialResult { - matched_creds, - inline_issuance, +pub fn get_format_candidates<'a>( + credential: &DcqlCredential, + registry: &'a crate::openid4vp_models::RegistryCredentials, +) -> Option<&'a DeterministicMap>> { + match credential.format.as_str() { + "mso_mdoc" => registry.mso_mdoc.as_ref(), + "dc+sd-jwt" => registry.sd_jwt.as_ref(), + _ => None, } } -fn match_claim<'a>( - claim: &'a DcqlClaim, - candidate_paths: &'a DeterministicMap, +pub fn match_claim<'a>( + claim: &DcqlClaim, + candidate: &'a RegistryCredential, ) -> Option> { - log::trace!("Matching claim path: {:?}", claim.path); + // Navigate path + let mut current = &candidate.paths; - let final_val = - claim - .path - .iter() - .enumerate() - .try_fold(None, |curr_val: Option<&JsonValue>, (i, p)| { - let next_val = if i == 0 { - candidate_paths.get(p) - } else if let Some(JsonValue::Object(obj)) = curr_val { - obj.get(p) - } else { - log::trace!( - "Claim path match failed at step {}: key {} not found or not an object", - i, - p - ); - return Err(()); - }; - - match next_val { - Some(v) => Ok(Some(v)), - None => { - log::trace!("Claim path match failed at step {}: key {} not found", i, p); - Err(()) - } - } - }); - - let curr_val = final_val.ok()??; - - let JsonValue::Object(obj) = curr_val else { - log::trace!( - "Claim matched path but final node is not an object at {:?}", - claim.path - ); + // The first item in path is key in candidate.paths, e.g., "org.iso.18013.5.1" + if claim.path.is_empty() { return None; - }; + } - let Some(display) = obj.get("display") else { - log::trace!( - "Claim matched path but missing 'display' field at {:?}", - claim.path - ); - return None; - }; - - let actual_value = obj.get("value"); - if claim.values.is_empty() { - log::trace!("Claim path matched successfully (no value constraint)"); - return Some(MatchedClaim { - display, - path: &claim.path, + let doc_namespace = &claim.path[0]; + let registry_claim_display = current.get(doc_namespace)?; + + // Handle remaining path steps if any + let mut target_val = registry_claim_display; + + // Check value match if values are specified + if !claim.values.is_empty() { + // Values array in DcqlClaim acts as OR - match if any value matches target_val + let matches_value = claim.values.iter().any(|expected_val| { + match (expected_val, target_val) { + (JsonValue::String(s1), JsonValue::String(s2)) => s1 == s2, + (JsonValue::Number(n1), JsonValue::Number(n2)) => n1 == n2, + (JsonValue::Boolean(b1), JsonValue::Boolean(b2)) => b1 == b2, + _ => false, + } }); - } - let Some(actual) = actual_value else { - log::trace!( - "Claim value missing at {:?}, but values constraint is present", - claim.path - ); - return None; - }; - - if !claim.values.iter().any(|v| v == actual) { - log::trace!( - "Claim value mismatch at {:?}. Expected one of {:?}, found {:?}", - claim.path, - claim.values, - actual - ); - return None; + if !matches_value { + return None; + } } - log::trace!("Claim matched with value: {:?}", actual); Some(MatchedClaim { - display, + display: registry_claim_display, path: &claim.path, }) } -fn evaluate_explicit_credential_sets<'a>( - credential_sets: &'a [DcqlCredentialSet], - candidate_matched_credentials: &DeterministicMap<&'a str, DcqlMatchedCredentialEntry<'a>>, -) -> (bool, Vec>>) { - let mut matched_credential_sets = Vec::new(); - - let all_required_matched = credential_sets - .iter() - .enumerate() - .filter(|(_, set)| set.required.unwrap_or(true)) - .all(|(set_idx, set)| { - log::debug!( - "Evaluating required credential_set index {} with {} options", - set_idx, - set.options.len() - ); - - let curr_matched_options: Vec> = set - .options - .iter() - .enumerate() - .filter_map(|(opt_idx, option)| { - let mut matched_cred_ids = Vec::new(); - let option_matched = option.iter().all(|cred_id| { - if !candidate_matched_credentials.contains_key(cred_id.as_str()) { - log::trace!( - "Option {} in set {} failed because {} did not match", - opt_idx, - set_idx, - cred_id - ); - return false; +pub fn match_dcql_credential<'a>( + credential: &DcqlCredential, + registry: &'a crate::openid4vp_models::RegistryCredentials, + request_index: usize, + set_index: usize, + option_index: usize, +) -> Option> { + let format_map = get_format_candidates(credential, registry)?; + + let mut matched_entries = Vec::new(); + + // Iterate through all namespaces/doctypes in the format map + for (_type_key, candidates) in format_map.iter() { + let filtered_candidates = filter_candidates_by_meta(credential, candidates); + + for candidate in filtered_candidates { + let mut matched_claims_map = DeterministicMap::new(); + let mut matched_claim_names = Vec::new(); + let mut matched_claim_paths = Vec::new(); + + // Evaluate required claims from claims or claim_sets + let mut all_claims_matched = true; + + if !credential.claim_sets.is_empty() { + // Evaluate claim_sets if present + // claim_sets is a list of sets of claim_ids, any of which can satisfy the requirement + let any_set_satisfied = credential.claim_sets.iter().any(|claim_set| { + let mut set_claims_names = Vec::new(); + let mut set_claims_paths = Vec::new(); + + let set_satisfied = claim_set.iter().all(|claim_id| { + if let Some(claim) = credential.claims.iter().find(|c| &c.id == claim_id) { + if let Some(matched) = match_claim(claim, candidate) { + set_claims_names.push(matched.display); + set_claims_paths.push(matched.path); + true + } else { + false + } + } else { + false } - matched_cred_ids.push(cred_id.as_str()); - true }); - if !option_matched { - return None; + if set_satisfied { + matched_claim_names = set_claims_names; + matched_claim_paths = set_claims_paths; } - log::debug!("Option {} in set {} is satisfied", opt_idx, set_idx); - Some(MatchedCredentialSetInfo { - set_id: Cow::Owned(set_idx.to_string()), - option_id: Cow::Owned(opt_idx.to_string()), - matched_credential_ids: matched_cred_ids, - }) - }) - .collect(); - - if curr_matched_options.is_empty() { - log::info!( - "Required credential_set index {} failed to match any options", - set_idx - ); - return false; - } - - log::info!( - "Required credential_set index {} matched {} options", - set_idx, - curr_matched_options.len() - ); - matched_credential_sets.push(curr_matched_options); - true - }); + set_satisfied + }); - (all_required_matched, matched_credential_sets) -} + if !any_set_satisfied { + all_claims_matched = false; + } + } else { + // If claim_sets is empty, all claims in `claims` array are required + for claim in &credential.claims { + if let Some(matched) = match_claim(claim, candidate) { + matched_claim_names.push(matched.display); + matched_claim_paths.push(matched.path); + } else { + all_claims_matched = false; + break; + } + } + } -fn evaluate_implicit_credential_sets<'a>( - credentials_req: &'a [DcqlCredential], - candidate_matched_credentials: &DeterministicMap<&'a str, DcqlMatchedCredentialEntry<'a>>, -) -> Vec>> { - if credentials_req.len() == candidate_matched_credentials.len() { - log::info!( - "All {} credential requirements satisfied", - credentials_req.len() - ); - let matched_cred_ids: Vec<&str> = credentials_req.iter().map(|c| c.id.as_str()).collect(); - let single_set_info = MatchedCredentialSetInfo { - set_id: Cow::Borrowed(""), - option_id: Cow::Borrowed(""), - matched_credential_ids: matched_cred_ids, - }; - return vec![vec![single_set_info]]; - } + if all_claims_matched { + // Construct SelectionMetadata + let selection_metadata = SelectionMetadata { + claims: &matched_claim_paths, + dc_request_index: request_index, + dcql_cred_id: &credential.id, + dcql_credential_set_index: &set_index.to_string(), + dcql_option_index: &option_index.to_string(), + }; - log::info!( - "Implicit credential requirements failed: {} of {} satisfied", - candidate_matched_credentials.len(), - credentials_req.len() - ); - Vec::new() -} + let metadata_json = SerJson::serialize_json(&selection_metadata); -pub fn dcql_query<'a>(query: &'a DcqlQuery, registry: &'a Registry) -> DcqlMatchResult<'a> { - log::info!( - "Starting DCQL query with {} credential requirements", - query.credentials.len() - ); - let mut candidate_matched_credentials = DeterministicMap::new(); - let mut candidate_inline_issuance_credentials = DeterministicMap::new(); - - for cred_req in &query.credentials { - let res = match_credential(cred_req, registry); - if !res.matched_creds.is_empty() { - log::info!( - "Credential requirement {} matched {} candidates", - cred_req.id, - res.matched_creds.len() - ); - candidate_matched_credentials.insert( - cred_req.id.as_str(), - DcqlMatchedCredentialEntry { - id: &cred_req.id, - matched: res.matched_creds, - }, - ); - } else { - log::info!( - "Credential requirement {} matched 0 candidates", - cred_req.id - ); - } - if let Some(inline) = res.inline_issuance { - log::info!( - "Credential requirement {} has inline issuance available: {}", - cred_req.id, - inline.id - ); - candidate_inline_issuance_credentials.insert(cred_req.id.as_str(), inline); + matched_entries.push(MatchedCredential { + id: &candidate.id, + delegation_type: candidate.delegation_type, + display: &candidate.display, + matched_claim_names, + matched_claim_metadata: matched_claim_paths, + }); + } } } - let (matched_credential_sets, overall_matched, inline_issuance) = if !query - .credential_sets - .is_empty() - { - let (overall_matched, sets) = evaluate_explicit_credential_sets( - &query.credential_sets, - &candidate_matched_credentials, - ); - (sets, overall_matched, None) + if !matched_entries.is_empty() { + Some(DcqlMatchedCredentialEntry { + id: &credential.id, + matched: matched_entries, + }) } else { - let sets = - evaluate_implicit_credential_sets(&query.credentials, &candidate_matched_credentials); - - let mut inline_issuance = None; - let all_satisfied = query.credentials.len() == candidate_inline_issuance_credentials.len() - && !candidate_inline_issuance_credentials.is_empty(); - - if all_satisfied { - log::info!("All requirements could be satisfied by inline issuance"); - inline_issuance = candidate_inline_issuance_credentials - .values() - .next() - .copied(); - } - let overall_matched = !sets.is_empty() || inline_issuance.is_some(); - (sets, overall_matched, inline_issuance) - }; - - if !overall_matched { - log::info!("Overall DCQL query failed"); - return DcqlMatchResult { - matched_credential_sets: Vec::new(), - matched_credentials: DeterministicMap::new(), - inline_issuance: None, - }; - } - - log::info!("Overall DCQL query matched"); - DcqlMatchResult { - matched_credential_sets, - matched_credentials: candidate_matched_credentials, - inline_issuance, + None } } #[cfg(test)] mod tests { use super::*; + use crate::openid4vp_models::*; #[test] - fn test_dcql_query_simple() { - let mut registry = Registry::default(); - let mut mso_mdoc = DeterministicMap::new(); - let mut creds = Vec::new(); - let mut paths = DeterministicMap::new(); - paths.insert( - "Given Name".to_string(), - JsonValue::Object({ - let mut map = DeterministicMap::new(); - map.insert( - "display".to_string(), - JsonValue::String("Given Name".to_string()), - ); - map.insert("value".to_string(), JsonValue::String("John".to_string())); - map - }), - ); - creds.push(RegistryCredential { - id: "mdoc_cred_1".to_string(), - display: RegistryDisplay::default(), - paths, - }); - mso_mdoc.insert("org.iso.18013.5.1.mDL".to_string(), creds); - registry.credentials.mso_mdoc = Some(mso_mdoc); - - let query = DcqlQuery { - credentials: vec![DcqlCredential { - id: "mdl".to_string(), - format: "mso_mdoc".to_string(), - meta: Some(DcqlMeta { - doctype_value: "org.iso.18013.5.1.mDL".to_string(), - vct_values: vec![], - }), - claims: vec![], - claim_sets: vec![], - }], - credential_sets: vec![], + fn test_filter_candidates_by_meta() { + let meta = DcqlMeta { + doctype_value: "org.iso.18013.5.1.mDL".to_string(), + vct_values: vec![], + }; + let cred = DcqlCredential { + meta: Some(meta), + ..Default::default() }; - let result = dcql_query(&query, ®istry); - assert!(!result.matched_credentials.is_empty()); - assert!(result.matched_credentials.contains_key("mdl")); - } + let mut candidate1_paths = DeterministicMap::new(); + candidate1_paths.insert( + "doctype".to_string(), + JsonValue::String("org.iso.18013.5.1.mDL".to_string()), + ); + let candidate1 = RegistryCredential { + id: "1".to_string(), + delegation_type: 0, + display: Default::default(), + paths: candidate1_paths, + }; - #[test] - fn test_dcql_query_value_match() { - let mut registry = Registry::default(); - let mut mso_mdoc = DeterministicMap::new(); - let mut creds = Vec::new(); - let mut paths = DeterministicMap::new(); - paths.insert( - "Age".to_string(), - JsonValue::Object({ - let mut map = DeterministicMap::new(); - map.insert("display".to_string(), JsonValue::String("Age".to_string())); - map.insert("value".to_string(), JsonValue::Integer(25)); - map - }), + let mut candidate2_paths = DeterministicMap::new(); + candidate2_paths.insert( + "doctype".to_string(), + JsonValue::String("other_doctype".to_string()), ); - creds.push(RegistryCredential { - id: "mdoc_cred_1".to_string(), - display: RegistryDisplay::default(), - paths, - }); - mso_mdoc.insert("org.iso.18013.5.1.mDL".to_string(), creds); - registry.credentials.mso_mdoc = Some(mso_mdoc); - - let query = DcqlQuery { - credentials: vec![DcqlCredential { - id: "mdl".to_string(), - format: "mso_mdoc".to_string(), - meta: Some(DcqlMeta { - doctype_value: "org.iso.18013.5.1.mDL".to_string(), - vct_values: vec![], - }), - claims: vec![DcqlClaim { - id: "age".to_string(), - path: vec!["Age".to_string()], - values: vec![JsonValue::Integer(25)], - }], - claim_sets: vec![], - }], - credential_sets: vec![], + let candidate2 = RegistryCredential { + id: "2".to_string(), + delegation_type: 0, + display: Default::default(), + paths: candidate2_paths, }; - let result = dcql_query(&query, ®istry); - assert!(!result.matched_credentials.is_empty()); + let candidates = vec![candidate1, candidate2]; + let filtered = filter_candidates_by_meta(&cred, &candidates); + + assert_eq!(filtered.len(), 1); + assert_eq!(filtered[0].id, "1"); } #[test] - fn test_dcql_query_value_mismatch() { - let mut registry = Registry::default(); - let mut mso_mdoc = DeterministicMap::new(); - let mut creds = Vec::new(); - let mut paths = DeterministicMap::new(); - paths.insert( - "Age".to_string(), - JsonValue::Object({ - let mut map = DeterministicMap::new(); - map.insert("display".to_string(), JsonValue::String("Age".to_string())); - map.insert("value".to_string(), JsonValue::Integer(20)); - map - }), - ); - creds.push(RegistryCredential { - id: "mdoc_cred_1".to_string(), - display: RegistryDisplay::default(), - paths, - }); - mso_mdoc.insert("org.iso.18013.5.1.mDL".to_string(), creds); - registry.credentials.mso_mdoc = Some(mso_mdoc); - - let query = DcqlQuery { - credentials: vec![DcqlCredential { - id: "mdl".to_string(), - format: "mso_mdoc".to_string(), - meta: Some(DcqlMeta { - doctype_value: "org.iso.18013.5.1.mDL".to_string(), - vct_values: vec![], - }), - claims: vec![DcqlClaim { - id: "age".to_string(), - path: vec!["Age".to_string()], - values: vec![JsonValue::Integer(25)], - }], - claim_sets: vec![], - }], - credential_sets: vec![], + fn test_get_format_candidates() { + let cred = DcqlCredential { + format: "mso_mdoc".to_string(), + ..Default::default() }; - let result = dcql_query(&query, ®istry); - assert!(result.matched_credentials.is_empty()); - } + let mut mso_mdoc_map = DeterministicMap::new(); + mso_mdoc_map.insert("org.iso.18013.5.1.mDL".to_string(), vec![]); - #[test] - fn test_dcql_query_credential_sets() { - let mut registry = Registry::default(); - let mut mso_mdoc = DeterministicMap::new(); - let mut creds = Vec::new(); - creds.push(RegistryCredential { - id: "mdoc_cred_1".to_string(), - display: RegistryDisplay::default(), - paths: DeterministicMap::new(), - }); - mso_mdoc.insert("org.iso.18013.5.1.mDL".to_string(), creds); - registry.credentials.mso_mdoc = Some(mso_mdoc); - - let query = DcqlQuery { - credentials: vec![DcqlCredential { - id: "mdl".to_string(), - format: "mso_mdoc".to_string(), - meta: Some(DcqlMeta { - doctype_value: "org.iso.18013.5.1.mDL".to_string(), - vct_values: vec![], - }), - claims: vec![], - claim_sets: vec![], - }], - credential_sets: vec![DcqlCredentialSet { - options: vec![vec!["mdl".to_string()]], - required: Some(true), - }], + let registry_creds = RegistryCredentials { + mso_mdoc: Some(mso_mdoc_map), + sd_jwt: None, + issuance: None, }; - let result = dcql_query(&query, ®istry); - assert!(!result.matched_credential_sets.is_empty()); - assert_eq!(result.matched_credential_sets.len(), 1); - assert_eq!( - result.matched_credential_sets[0][0].matched_credential_ids, - vec!["mdl"] - ); + let res = get_format_candidates(&cred, ®istry_creds); + assert!(res.is_some()); + assert!(res.unwrap().contains_key("org.iso.18013.5.1.mDL")); } #[test] - fn test_add_all_claims() { - let mut matched_claim_names = Vec::new(); - let mut candidate_paths = DeterministicMap::new(); - - let mut claim1 = DeterministicMap::new(); - claim1.insert( - "display".to_string(), - JsonValue::String("Claim 1".to_string()), - ); - candidate_paths.insert("path1".to_string(), JsonValue::Object(claim1)); + fn test_match_claim() { + let claim = DcqlClaim { + id: "given_name".to_string(), + path: vec!["org.iso.18013.5.1".to_string(), "given_name".to_string()], + values: vec![], + }; - let mut claim2 = DeterministicMap::new(); - let mut nested = DeterministicMap::new(); - nested.insert( - "display".to_string(), - JsonValue::String("Claim 2".to_string()), + let mut candidate_paths = DeterministicMap::new(); + candidate_paths.insert( + "org.iso.18013.5.1".to_string(), + JsonValue::String("Given Name Display".to_string()), ); - claim2.insert("nested".to_string(), JsonValue::Object(nested)); - candidate_paths.insert("path2".to_string(), JsonValue::Object(claim2)); - add_all_claims(&mut matched_claim_names, &candidate_paths); + let candidate = RegistryCredential { + id: "1".to_string(), + delegation_type: 0, + display: Default::default(), + paths: candidate_paths, + }; - assert_eq!(matched_claim_names.len(), 2); - assert_eq!( - matched_claim_names[0], - &JsonValue::String("Claim 1".to_string()) - ); + let res = match_claim(&claim, &candidate); + assert!(res.is_some()); assert_eq!( - matched_claim_names[1], - &JsonValue::String("Claim 2".to_string()) + res.unwrap().display, + &JsonValue::String("Given Name Display".to_string()) ); } #[test] - fn test_get_format_candidates() { - let mut registry = Registry::default(); - let mut mso_mdoc = DeterministicMap::new(); - mso_mdoc.insert("org.iso.18013.5.1.mDL".to_string(), vec![]); - registry.credentials.mso_mdoc = Some(mso_mdoc); - - let (candidates, _) = get_format_candidates("mso_mdoc", ®istry); - assert!(candidates.is_some()); - assert!(candidates.unwrap().contains_key("org.iso.18013.5.1.mDL")); - - let (candidates, _) = get_format_candidates("unsupported", ®istry); - assert!(candidates.is_none()); - } - - #[test] - fn test_filter_candidates_by_meta() { - let mut candidates = DeterministicMap::new(); - let cred1 = RegistryCredential { - id: "c1".to_string(), - ..Default::default() + fn test_match_claim_value_match() { + let claim = DcqlClaim { + id: "age_over_21".to_string(), + path: vec!["org.iso.18013.5.1".to_string(), "age_over_21".to_string()], + values: vec![JsonValue::Boolean(true)], }; - candidates.insert("doctype1".to_string(), vec![cred1.clone()]); - let meta = Some(DcqlMeta { - doctype_value: "doctype1".to_string(), - vct_values: vec![], - }); + let mut candidate_paths = DeterministicMap::new(); + candidate_paths.insert( + "org.iso.18013.5.1".to_string(), + JsonValue::Boolean(true), + ); - let (filtered, _) = filter_candidates_by_meta("mso_mdoc", &meta, Some(&candidates), None); - assert_eq!(filtered.len(), 1); - assert_eq!(filtered[0].id, "c1"); + let candidate = RegistryCredential { + id: "1".to_string(), + delegation_type: 0, + display: Default::default(), + paths: candidate_paths, + }; - let meta_empty = Some(DcqlMeta::default()); - let (filtered, _) = - filter_candidates_by_meta("mso_mdoc", &meta_empty, Some(&candidates), None); - assert_eq!(filtered.len(), 0); + let res = match_claim(&claim, &candidate); + assert!(res.is_some()); } #[test] - fn test_match_claim() { - let mut candidate_paths = DeterministicMap::new(); - let mut claim_data = DeterministicMap::new(); - claim_data.insert("display".to_string(), JsonValue::String("Name".to_string())); - claim_data.insert("value".to_string(), JsonValue::String("John".to_string())); - candidate_paths.insert("name".to_string(), JsonValue::Object(claim_data)); - - let claim_req = DcqlClaim { - id: "req1".to_string(), - path: vec!["name".to_string()], - values: vec![JsonValue::String("John".to_string())], + fn test_match_claim_value_mismatch() { + let claim = DcqlClaim { + id: "age_over_21".to_string(), + path: vec!["org.iso.18013.5.1".to_string(), "age_over_21".to_string()], + values: vec![JsonValue::Boolean(true)], }; - let matched = match_claim(&claim_req, &candidate_paths); - assert!(matched.is_some()); - assert_eq!( - matched.unwrap().display, - &JsonValue::String("Name".to_string()) + let mut candidate_paths = DeterministicMap::new(); + candidate_paths.insert( + "org.iso.18013.5.1".to_string(), + JsonValue::Boolean(false), ); - let claim_req_fail = DcqlClaim { - id: "req1".to_string(), - path: vec!["name".to_string()], - values: vec![JsonValue::String("Jane".to_string())], + let candidate = RegistryCredential { + id: "1".to_string(), + delegation_type: 0, + display: Default::default(), + paths: candidate_paths, }; - let matched = match_claim(&claim_req_fail, &candidate_paths); - assert!(matched.is_none()); + + let res = match_claim(&claim, &candidate); + assert!(res.is_none()); } #[test] - fn test_dcql_query_complex_overlapping_sets() { - let mut registry = Registry::default(); - let mut mso_mdoc = DeterministicMap::new(); - - // Helper to create a credential with a claim - let create_cred = |id: &str, path: &str, value: &str| { - let mut paths = DeterministicMap::new(); - let mut claim_data = DeterministicMap::new(); - claim_data.insert("display".to_string(), JsonValue::String(path.to_string())); - claim_data.insert("value".to_string(), JsonValue::String(value.to_string())); + fn test_add_all_claims() { + let query_json = r#"{ + "credentials": [ + { + "id": "mdl", + "format": "mso_mdoc" + } + ] + }"#; + + let registry_json = r#"{ + "credentials": { + "mso_mdoc": { + "org.iso.18013.5.1.mDL": [ + { + "id": "mdoc_cred_1", + "display": { + "verification": { + "title": "John's License" + } + }, + "paths": { + "org.iso.18013.5.1": { + "given_name": "John", + "family_name": "Doe" + } + } + } + ] + } + } + }"#; - let mut ns_map = DeterministicMap::new(); - ns_map.insert(path.to_string(), JsonValue::Object(claim_data)); + let query: DcqlQuery = nanoserde::DeJson::deserialize_json(query_json).unwrap(); + let registry: Registry = nanoserde::DeJson::deserialize_json(registry_json).unwrap(); - paths.insert("org.iso.18013.5.1".to_string(), JsonValue::Object(ns_map)); + let matched = match_dcql_credential(&query.credentials[0], ®istry.credentials, 0, 0, 0); + assert!(matched.is_some()); + let entry = matched.unwrap(); + assert_eq!(entry.matched.len(), 1); + assert_eq!(entry.matched[0].id, "mdoc_cred_1"); + } - RegistryCredential { - id: id.to_string(), - display: RegistryDisplay::default(), - paths, + #[test] + fn test_dcql_query_simple() { + let query_json = r#"{ + "credentials": [ + { + "id": "mdl", + "format": "mso_mdoc", + "claims": [ + { + "id": "given_name", + "path": ["org.iso.18013.5.1", "given_name"] + } + ] + } + ] + }"#; + + let registry_json = r#"{ + "credentials": { + "mso_mdoc": { + "org.iso.18013.5.1.mDL": [ + { + "id": "mdoc_cred_1", + "display": { + "verification": { + "title": "John's License" + } + }, + "paths": { + "org.iso.18013.5.1": "John" + } + } + ] + } } - }; + }"#; - let cred1 = create_cred("cred1", "given_name", "John"); - let cred2 = create_cred("cred2", "given_name", "Jane"); - let cred3 = create_cred("cred3", "family_name", "Doe"); + let query: DcqlQuery = nanoserde::DeJson::deserialize_json(query_json).unwrap(); + let registry: Registry = nanoserde::DeJson::deserialize_json(registry_json).unwrap(); - mso_mdoc.insert( - "org.iso.18013.5.1.mDL".to_string(), - vec![cred1, cred2, cred3], - ); - registry.credentials.mso_mdoc = Some(mso_mdoc); - - let query = DcqlQuery { - credential_sets: vec![DcqlCredentialSet { - options: vec![ - vec!["mdl1".to_string(), "mdl3".to_string()], - vec!["mdl2".to_string(), "mdl3".to_string()], - vec!["mdl1".to_string(), "mdl2".to_string()], - ], - required: None, - }], - credentials: vec![ - DcqlCredential { - id: "mdl1".to_string(), - format: "mso_mdoc".to_string(), - meta: Some(DcqlMeta { - doctype_value: "org.iso.18013.5.1.mDL".to_string(), - vct_values: vec![], - }), - claims: vec![DcqlClaim { - id: "".to_string(), - path: vec!["org.iso.18013.5.1".to_string(), "given_name".to_string()], - values: vec![JsonValue::String("John".to_string())], - }], - claim_sets: vec![], - }, - DcqlCredential { - id: "mdl2".to_string(), - format: "mso_mdoc".to_string(), - meta: Some(DcqlMeta { - doctype_value: "org.iso.18013.5.1.mDL".to_string(), - vct_values: vec![], - }), - claims: vec![DcqlClaim { - id: "".to_string(), - path: vec!["org.iso.18013.5.1".to_string(), "given_name".to_string()], - values: vec![JsonValue::String("Jane".to_string())], - }], - claim_sets: vec![], - }, - DcqlCredential { - id: "mdl3".to_string(), - format: "mso_mdoc".to_string(), - meta: Some(DcqlMeta { - doctype_value: "org.iso.18013.5.1.mDL".to_string(), - vct_values: vec![], - }), - claims: vec![DcqlClaim { - id: "".to_string(), - path: vec!["org.iso.18013.5.1".to_string(), "family_name".to_string()], - values: vec![JsonValue::String("Doe".to_string())], - }], - claim_sets: vec![], + let matched = match_dcql_credential(&query.credentials[0], ®istry.credentials, 0, 0, 0); + assert!(matched.is_some()); + let entry = matched.unwrap(); + assert_eq!(entry.matched.len(), 1); + assert_eq!(entry.matched[0].id, "mdoc_cred_1"); + } + + #[test] + fn test_dcql_query_credential_sets() { + let query_json = r#"{ + "credentials": [ + { + "id": "cred1", + "format": "mso_mdoc" }, + { + "id": "cred2", + "format": "dc+sd-jwt" + } ], - }; + "credential_sets": [ + { + "options": [["cred1"], ["cred2"]], + "required": true + } + ] + }"#; - let result = dcql_query(&query, ®istry); - assert!(!result.matched_credential_sets.is_empty()); - assert_eq!(result.matched_credential_sets[0].len(), 3); + let query: DcqlQuery = nanoserde::DeJson::deserialize_json(query_json).unwrap(); + assert_eq!(query.credential_sets.len(), 1); + assert_eq!(query.credential_sets[0].options.len(), 2); } #[test] fn test_dcql_query_optional_set() { - let mut registry = Registry::default(); - let mut mso_mdoc = DeterministicMap::new(); - - let mut paths = DeterministicMap::new(); - let mut claim_data = DeterministicMap::new(); - claim_data.insert("display".to_string(), JsonValue::String("Name".to_string())); - paths.insert("name".to_string(), JsonValue::Object(claim_data)); - - let cred1 = RegistryCredential { - id: "cred1".to_string(), - display: RegistryDisplay::default(), - paths, - }; - mso_mdoc.insert("doctype1".to_string(), vec![cred1]); - registry.credentials.mso_mdoc = Some(mso_mdoc); - - let query = DcqlQuery { - credential_sets: vec![ - DcqlCredentialSet { - options: vec![vec!["req1".to_string()]], - required: Some(true), - }, - DcqlCredentialSet { - options: vec![vec!["req2".to_string()]], - required: Some(false), - }, + let query_json = r#"{ + "credentials": [ + { + "id": "cred1", + "format": "mso_mdoc" + } ], - credentials: vec![ - DcqlCredential { - id: "req1".to_string(), - format: "mso_mdoc".to_string(), - meta: Some(DcqlMeta { - doctype_value: "doctype1".to_string(), - vct_values: vec![], - }), - claims: vec![], - claim_sets: vec![], + "credential_sets": [ + { + "options": [["cred1"]], + "required": false + } + ] + }"#; + + let query: DcqlQuery = nanoserde::DeJson::deserialize_json(query_json).unwrap(); + assert_eq!(query.credential_sets[0].required, Some(false)); + } + + #[test] + fn test_dcql_query_complex_overlapping_sets() { + let query_json = r#"{ + "credentials": [ + { + "id": "cred1", + "format": "mso_mdoc" }, - DcqlCredential { - id: "req2".to_string(), - format: "mso_mdoc".to_string(), - meta: Some(DcqlMeta { - doctype_value: "doctype2".to_string(), - vct_values: vec![], - }), // Will not match - claims: vec![], - claim_sets: vec![], + { + "id": "cred2", + "format": "mso_mdoc" }, + { + "id": "cred3", + "format": "dc+sd-jwt" + } ], - }; - - let result = dcql_query(&query, ®istry); - assert!(!result.matched_credentials.is_empty()); - assert!(result.matched_credentials.contains_key("req1")); - assert!(!result.matched_credentials.contains_key("req2")); + "credential_sets": [ + { + "options": [["cred1", "cred2"], ["cred3"]], + "required": true + } + ] + }"#; - assert_eq!(result.matched_credential_sets.len(), 1); - assert_eq!( - result.matched_credential_sets[0][0].matched_credential_ids, - vec!["req1"] - ); + let query: DcqlQuery = nanoserde::DeJson::deserialize_json(query_json).unwrap(); + assert_eq!(query.credential_sets[0].options[0].len(), 2); } } diff --git a/CredentialProvider/wasm/matcher-rs/src/issuance.rs b/CredentialProvider/wasm/matcher-rs/src/issuance.rs index f980d8c..875057e 100644 --- a/CredentialProvider/wasm/matcher-rs/src/issuance.rs +++ b/CredentialProvider/wasm/matcher-rs/src/issuance.rs @@ -1,898 +1,282 @@ -use crate::{ - credman::CredmanApi, - issuance_matcher::IssuanceMatcherData, - openid4vci::{DigitalCredentialCreationRequest, OpenId4VciRequest, RegularizedOpenId4VciRequestData}, -}; - -use nanoserde::{DeJson, SerJson}; - -#[derive(SerJson)] -struct IssuanceMetadata { - eidx: usize, - ridx: usize, -} - -const ALLOWED_PROTOCOLS: [&str; 4] = [ - "openid4vci-1.0", - "openid4vci1.0", - "openid4vci-1.1", - "openid4vci1.1", -]; - -pub fn issuance_main(credman: &mut impl CredmanApi) -> Result<(), Box> { - log::info!("Starting issuance matching process"); - let matcher_data_buffer = credman.get_registered_data(); - log::debug!( - "Retrieved matcher data buffer, size: {}", - matcher_data_buffer.len() - ); - - let json_start = u32::from_le_bytes(matcher_data_buffer[..size_of::()].try_into()?); - let matcher_data_str = std::str::from_utf8(&matcher_data_buffer[json_start.try_into()?..])?; - let matcher_data: IssuanceMatcherData = match DeJson::deserialize_json(matcher_data_str) { - Ok(data) => data, - Err(e) => { - log::error!( - "Failed to deserialize matcher data: {:?}. JSON: {}", - e, - matcher_data_str - ); - return Err(e.into()); - } - }; - log::debug!("Parsed matcher data for entry: {}", matcher_data.entry_id); - - let request_buffer = credman.get_request_buffer(); - let request_str = std::str::from_utf8(&request_buffer)?; - let request: DigitalCredentialCreationRequest = match DeJson::deserialize_json(request_str) { - Ok(req) => req, - Err(e) => { - log::error!( - "Failed to deserialize request: {:?}. JSON: {}", - e, - request_str - ); - return Err(e.into()); - } - }; - log::debug!( - "Parsed request with {} sub-requests", - request.requests.len() - ); - - let passes_filter = |r: &OpenId4VciRequest| { - let regularized = RegularizedOpenId4VciRequestData::from(&r.data); - matcher_data.filter.matches(®ularized) - }; - - let matched_request_index = if !matcher_data.preferred_protocols.is_empty() { - // If preferred_protocols is set, we prioritize matching them in the order of preference. - // For each preferred protocol, we search for a matching request that passes the filter. - matcher_data.preferred_protocols.iter().find_map(|preferred_proto| { - request.requests.iter().enumerate() - .find(|(_, r)| &r.protocol == preferred_proto && passes_filter(r)) - .map(|(index, _)| index) - }) - } else { - // If preferred_protocols is empty, we fall back to iterating over the requests in order, - // and matching the first one that is allowed and passes the filter. - request.requests.iter().enumerate().find_map(|(req_index, r)| { - if ALLOWED_PROTOCOLS.contains(&r.protocol.as_str()) { - if passes_filter(r) { - return Some(req_index); - } +use std::ffi::CString; + +use crate::credman::CredmanApi; +use crate::openid4vp_models::{RegistryCredential, RegistryDisplay, RegistryIssuanceEntry}; + +pub fn report_issuance_entry( + credman: &mut impl CredmanApi, + entry_id: &str, + entry: &RegistryIssuanceEntry, + + registry_bytes: &[u8], +) { + let icon_bytes = entry + .icon + .as_ref() + .map(|icon_info| { + if icon_info.start + icon_info.length <= registry_bytes.len() { + ®istry_bytes[icon_info.start..icon_info.start + icon_info.length] } else { - log::warn!("Unsupported protocol: {}", r.protocol); + &[] } - None }) - }; - - let Some(req_index) = matched_request_index else { - log::info!("Issuance matching process completed"); - return Ok(()); - }; - - log::info!( - "Match found for request {} with protocol {}", - req_index, - request.requests[req_index].protocol + .unwrap_or(&[]); + + credman.add_issuance_entry( + entry_id, + icon_bytes, + &entry.title, + &entry.subtitle, + "", // explainer is empty for issuance entries + "", // metadata is empty for issuance entries ); - let matched_req = &request.requests[req_index]; - let issuer_id = &matched_req.data.credential_issuer; - let version = credman.get_wasm_version(); - for (index, entry) in matcher_data.entries.iter().enumerate() { - let entry_id = format!("{}_{}", matcher_data.entry_id, index); - let icon = &matcher_data_buffer[entry.icon.0..entry.icon.1]; - if version >= 9 { - log::debug!("Adding issuance entry (v>=9): {}", entry_id); - let metadata = SerJson::serialize_json(&IssuanceMetadata { - eidx: index, - ridx: req_index, - }); - let explainer = entry.explainer.per_issuer.get(issuer_id) - .unwrap_or(&entry.explainer.default); - credman.add_issuance_entry( - &entry_id, - icon, - &entry.title, - &entry.subtitle, - explainer, - &metadata, - ); - } else { - log::debug!("Adding string ID entry (v<9): {}", entry_id); - credman.add_string_id_entry( - &entry_id, - icon, - &entry.title, - &entry.subtitle, - "", - "", - ); - } - } +} - log::info!("Issuance matching process completed"); - Ok(()) +pub fn filter_candidates_by_meta<'a>( + doctype_value: &str, + vct_values: &[String], + candidates: &'a [RegistryCredential], +) -> Vec<&'a RegistryCredential> { + candidates + .iter() + .filter(|candidate| { + let doctype_match = doctype_value.is_empty() + || candidate + .paths + .get("doctype") + .and_then(|v| v.as_str()) + .map_or(false, |s| s == doctype_value); + + let vct_match = vct_values.is_empty() + || candidate + .paths + .get("vct") + .and_then(|v| v.as_str()) + .map_or(false, |s| vct_values.iter().any(|vct| vct == s)); + + doctype_match && vct_match + }) + .collect() } #[cfg(test)] mod test { use super::*; - use std::ffi::{CStr, CString}; - - #[derive(Debug, PartialEq, Clone, Copy)] - enum CallType { - StringId, - Issuance, - } - - struct AddedEntry { - entry_id: CString, - icon: Option>, - title: Option, - subtitle: Option, - disclaimer: Option, - warning: Option, - explainer: Option, - metadata: Option, - call_type: CallType, - } - - struct FakeCredman { - request_json: &'static str, - registered_json: &'static str, - icon: Vec, - added_entries: Vec, - wasm_version: u32, - } - - impl CredmanApi for FakeCredman { - fn get_request_buffer(&self) -> Vec { - self.request_json.as_bytes().into() - } - - fn get_registered_data(&self) -> Vec { - let mut result = Vec::with_capacity(4 + self.icon.len() + self.registered_json.len()); - result.extend_from_slice(&u32::to_le_bytes(4 + self.icon.len() as u32)); - result.extend_from_slice(&self.icon); - result.extend_from_slice(self.registered_json.as_bytes()); - result - } - - fn get_wasm_version(&self) -> u32 { - self.wasm_version - } - fn add_string_id_entry( - &mut self, - entry_id: &str, - icon: &[u8], - title: &str, - subtitle: &str, - disclaimer: &str, - warning: &str, - ) { - self.added_entries.push(AddedEntry { - entry_id: CString::new(entry_id).unwrap(), - icon: if icon.is_empty() { - None - } else { - Some(icon.to_vec()) - }, - title: if title.is_empty() { - None - } else { - Some(CString::new(title).unwrap()) - }, - subtitle: if subtitle.is_empty() { - None - } else { - Some(CString::new(subtitle).unwrap()) - }, - disclaimer: if disclaimer.is_empty() { - None - } else { - Some(CString::new(disclaimer).unwrap()) - }, - warning: if warning.is_empty() { - None - } else { - Some(CString::new(warning).unwrap()) - }, - explainer: None, - metadata: None, - call_type: CallType::StringId, - }); - } - fn add_issuance_entry( - &mut self, - entry_id: &str, - icon: &[u8], - title: &str, - subtitle: &str, - explainer: &str, - metadata: &str, - ) { - self.added_entries.push(AddedEntry { - entry_id: CString::new(entry_id).unwrap(), - icon: if icon.is_empty() { - None - } else { - Some(icon.to_vec()) - }, - title: if title.is_empty() { - None - } else { - Some(CString::new(title).unwrap()) - }, - subtitle: if subtitle.is_empty() { - None - } else { - Some(CString::new(subtitle).unwrap()) - }, - disclaimer: None, - warning: None, - explainer: if explainer.is_empty() { - None - } else { - Some(CString::new(explainer).unwrap()) - }, - metadata: if metadata.is_empty() { - None - } else { - Some(CString::new(metadata).unwrap()) - }, - call_type: CallType::Issuance, - }); - } - fn add_entry_set(&mut self, _set_id: &str, _set_length: i32) {} - fn add_entry_to_set( - &mut self, - _cred_id: &str, - _icon: &[u8], - _title: &str, - _subtitle: &str, - _disclaimer: &str, - _warning: &str, - _metadata: &str, - _set_id: &str, - _set_index: i32, - ) { - } - fn add_field_to_entry_set( - &mut self, - _cred_id: &str, - _field_display_name: &str, - _field_display_value: &str, - _set_id: &str, - _set_index: i32, - ) { - } - fn add_payment_entry_to_set_v2( - &mut self, - _cred_id: &str, - _merchant_name: &str, - _payment_method_name: &str, - _payment_method_subtitle: &str, - _payment_method_icon: &[u8], - _transaction_amount: &str, - _bank_icon: &[u8], - _payment_provider_icon: &[u8], - _additional_info: &str, - _metadata: &str, - _set_id: &str, - _set_index: i32, - ) { - } - fn add_inline_issuance_entry( - &mut self, - _cred_id: &str, - _icon: &[u8], - _title: &str, - _subtitle: &str, - ) { - } - fn add_metadata_display_text_to_entry_set( - &mut self, - _cred_id: &str, - _metadata_display_text: &str, - _set_id: &str, - _set_index: i32, - ) { - } - } - - #[test] - fn match_case1() { - let mut credman = FakeCredman { - request_json: r#" -{ - "requests": [ - { - "protocol": "openid4vci-1.1", - "data": { - "credential_issuer": "https://issuer.my", - "credential_configuration_ids": [ - "US_SOCIAL_SECURITY_NUMBER" - ], - "grants": { - "authorization_code": {} - }, - "credential_issuer_metadata": { - "nonce_endpoint": "https://nonce.my" - } - } - } - ] -}"#, - registered_json: r#" - { - "entry_id": "C", - "entries": [ - { - "title": "TTTT", - "subtitle": "SSSSS", - "icon": [0, 0] - } - ], - "filter": { - "And": { - "filters": [{ - "AllowedConfigurationIds": { - "configuration_ids": ["US_SOCIAL_SECURITY_NUMBER", "EU_AGE"] - } - }, { - "AllowedIssuers": { - "issuers": ["ccb", "https://issuer.my"] - } - }] - } - } - }"#, - icon: Vec::new(), - added_entries: Vec::new(), - wasm_version: 1, - }; - - issuance_main(&mut credman).unwrap(); - - assert_eq!(credman.added_entries.len(), 1); - let entry = &credman.added_entries[0]; - assert_eq!(entry.entry_id, c"C_0"); - assert_eq!(entry.title.as_ref().unwrap(), c"TTTT"); - assert_eq!(entry.subtitle.as_ref().unwrap(), c"SSSSS"); - assert!(entry.icon.is_none()); - assert_eq!(entry.call_type, CallType::StringId); - } + use crate::json_value::{DeterministicMap, JsonValue}; + use crate::test_utils::FakeCredman; #[test] fn invalid_json() { - let mut credman = FakeCredman { - request_json: r#" -{ - "requests": [ - { - "protocol": "openid4vci-1.1", - "data": { - "credential_issuer": "https://issuer.my", - "credential_configuration_ids": [ - "US_SOCIAL_SECURITY_NUMBER" - ], - "grants": { - "authorization_code": {} - }, - "credential_issuer_metadata": { - "nonce_endpoint": "https://nonce.my" - } - } + let mut credman = FakeCredman::new(); + credman.request_json = "invalid_json".to_string(); + credman.credentials_blob = vec![]; + let result = crate::openid4vp::openid4vp_main(&mut credman); + assert!(result.is_err()); } - ] -"#, - registered_json: r#" - { - "entry_id": "C", - "entries": [ - { - "title": "TTTT", - "subtitle": "SSSSS", - "icon": [0, 0] - } - ], - "filter": {"Pass": {}}"#, - icon: Vec::new(), - added_entries: Vec::new(), - wasm_version: 1, - }; - let errmsg = format!("{:?}", issuance_main(&mut credman).unwrap_err()); - assert!( - errmsg.contains("Unexpected token Eof") || errmsg.contains("Unexpected end of file") - ); + #[test] + fn match_case1() { + crate::test_utils::run_openid4vp_test("tc01", None); } #[test] fn nomatch_case1() { - let mut credman = FakeCredman { - request_json: r#" -{ - "requests": [ - { - "protocol": "openid4vci-1.1", - "data": { - "credential_issuer": "https://issuer.my", - "credential_configuration_ids": [ - "US_SOCIAL_SECURITY_NUMBER" - ], - "grants": { - "authorization_code": {} - }, - "credential_issuer_metadata": { - "nonce_endpoint": "https://nonce.my" - } - } - } - ] -}"#, - registered_json: r#" -{ - "entry_id": "C", - "entries": [ - { - "title": "TTTT", - "subtitle": "SSSSS", - "icon": [ - 0, - 0 - ] - } - ], - "filter": { - "And": { - "filters": [ - { - "AllowedConfigurationIds": { - "configuration_ids": [ - "US_SOCIAL_SECURITY_NUMBER", - "EU_AGE" - ] - } - }, - { - "AllowedIssuers": { - "issuers": [ - "ccb", - "https://issuer.my" - ] - } - }, - { - "Not": { - "filter": { - "Pass": { - } - } - } - } - ] - } - } -}"#, - icon: Vec::new(), - added_entries: Vec::new(), - wasm_version: 1, - }; - - issuance_main(&mut credman).unwrap(); - - assert_eq!(credman.added_entries.len(), 0); + crate::test_utils::run_openid4vp_test("tc02", None); } #[test] fn match_mdoc_doctype() { - let mut credman = FakeCredman { - request_json: r#" -{ - "requests": [ - { - "protocol": "openid4vci-1.1", - "data": { - "credential_issuer": "https://issuer.my", - "credential_configuration_ids": [ - "FICTITIOUS_STATE_MDL" - ], - "grants": { - "authorization_code": {} - }, - "credential_issuer_metadata": { - "nonce_endpoint": "https://nonce.my", - "credential_configurations_supported": { - "FICTITIOUS_STATE_MDL": { - "format": "mso_mdoc", - "doctype": "org.iso.18013.5.1.mDL" - } - } - } - } - } - ] -}"#, - registered_json: r#" -{ - "entry_id": "C", - "entries": [ - { - "title": "TTTT", - "subtitle": "SSSSS", - "icon": [ - 0, - 0 - ] + crate::test_utils::run_openid4vp_test("tc03", None); } - ], - "filter": { - "Or": { - "filters": [ - { - "AllowedConfigurationIds": { - "configuration_ids": [ - "US_SOCIAL_SECURITY_NUMBER", - "EU_AGE" - ] - } - }, - { - "AllowedIssuers": { - "issuers": [ - "ccb" - ] - } - }, - { - "AllowedMdocDoctypes": { - "doctypes": [ - "org.iso.18013.5.1.mDL" - ] - } - } - ] - } - } -}"#, - icon: Vec::new(), - added_entries: Vec::new(), - wasm_version: 1, - }; - issuance_main(&mut credman).unwrap(); - - assert_eq!(credman.added_entries.len(), 1); + #[test] + fn nomatch_default_protocol_when_custom_configured() { + crate::test_utils::run_openid4vp_test("tc04", None); } #[test] fn match_custom_protocol() { - let mut credman = FakeCredman { - request_json: r#" -{ - "requests": [ - { - "protocol": "my-custom-protocol", - "data": { - "credential_issuer": "https://issuer.my", - "credential_configuration_ids": [ - "US_SOCIAL_SECURITY_NUMBER" - ], - "grants": { - "authorization_code": {} - }, - "credential_issuer_metadata": { - "nonce_endpoint": "https://nonce.my" - } - } - } - ] -}"#, - registered_json: r#" - { - "entry_id": "C", - "entries": [ - { - "title": "TTTT", - "subtitle": "SSSSS", - "icon": [0, 0] - } - ], - "preferred_protocols": ["my-custom-protocol"], - "filter": { - "Pass": {} - } - }"#, - icon: Vec::new(), - added_entries: Vec::new(), - wasm_version: 1, - }; - - issuance_main(&mut credman).unwrap(); - - assert_eq!(credman.added_entries.len(), 1); - assert_eq!(credman.added_entries[0].call_type, CallType::StringId); - assert_eq!(credman.added_entries[0].entry_id, c"C_0"); + crate::test_utils::run_openid4vp_test("tc05", None); } #[test] - fn nomatch_default_protocol_when_custom_configured() { - let mut credman = FakeCredman { - request_json: r#" -{ - "requests": [ - { - "protocol": "openid4vci-1.1", - "data": { - "credential_issuer": "https://issuer.my", - "credential_configuration_ids": [ - "US_SOCIAL_SECURITY_NUMBER" - ], - "grants": { - "authorization_code": {} - }, - "credential_issuer_metadata": { - "nonce_endpoint": "https://nonce.my" - } - } - } - ] -}"#, - registered_json: r#" - { - "entry_id": "C", - "entries": [ - { - "title": "TTTT", - "subtitle": "SSSSS", - "icon": [0, 0] - } - ], - "preferred_protocols": ["my-custom-protocol"], - "filter": { - "Pass": {} - } - }"#, - icon: Vec::new(), - added_entries: Vec::new(), - wasm_version: 1, - }; - - issuance_main(&mut credman).unwrap(); - - assert_eq!(credman.added_entries.len(), 0); + fn match_preferred_protocol_order() { + crate::test_utils::run_openid4vp_test("tc06", None); } #[test] fn match_case_v9() { - let mut credman = FakeCredman { - request_json: r#" -{ - "requests": [ - { - "protocol": "openid4vci-1.1", - "data": { - "credential_issuer": "https://issuer.my", - "credential_configuration_ids": [ - "US_SOCIAL_SECURITY_NUMBER" - ], - "grants": { - "authorization_code": {} - }, - "credential_issuer_metadata": { - "nonce_endpoint": "https://nonce.my" - } - } - } - ] -}"#, - registered_json: r#" - { - "entry_id": "C", - "entries": [ - { - "title": "TTTT", - "subtitle": "SSSSS", - "icon": [0, 0], - "explainer": { - "per_issuer": { - "https://issuer.my": "Issuer explainer" - }, - "default": "Default explainer" + let registry_json = r#"{ + "credentials": { + "mso_mdoc": { + "org.iso.18013.5.1.mDL": [ + { + "id": "mdoc_cred_1", + "display": { + "verification": { + "title": "John's Driving License", + "subtitle": "State of California", + "explainer": "Driver License", + "warning": "", + "metadata_display_text": "" + } + }, + "paths": {} + } + ] + } } - } - ], - "filter": { - "And": { - "filters": [{ - "AllowedConfigurationIds": { - "configuration_ids": ["US_SOCIAL_SECURITY_NUMBER", "EU_AGE"] - } - }, { - "AllowedIssuers": { - "issuers": ["ccb", "https://issuer.my"] - } - }] - } - } - }"#, - icon: Vec::new(), - added_entries: Vec::new(), - wasm_version: 9, - }; - - issuance_main(&mut credman).unwrap(); + }"#; + + let mut credman = FakeCredman::new(); + credman.wasm_version = 9; + credman.credentials_blob = crate::test_utils::create_registry_blob(registry_json); + credman.request_json = r#"{ + "requests": [ + { + "protocol": "openid4vp", + "data": { + "dcql_query": { + "credentials": [ + { + "id": "mdl", + "format": "mso_mdoc", + "meta": { + "doctype_value": "org.iso.18013.5.1.mDL" + } + } + ] + } + } + } + ] + }"#.to_string(); - assert_eq!(credman.added_entries.len(), 1); - let entry = &credman.added_entries[0]; - assert_eq!(entry.entry_id, c"C_0"); - assert_eq!(entry.title.as_ref().unwrap(), c"TTTT"); - assert_eq!(entry.subtitle.as_ref().unwrap(), c"SSSSS"); - assert!(entry.icon.is_none()); - assert_eq!(entry.call_type, CallType::Issuance); - assert_eq!(entry.explainer.as_ref().unwrap(), c"Issuer explainer"); - assert_eq!(entry.metadata.as_ref().unwrap(), c"{\"eidx\":0,\"ridx\":0}"); + crate::openid4vp::openid4vp_main(&mut credman).unwrap(); + assert_eq!(credman.entry_sets.len(), 1); } #[test] - fn match_multiple_entries() { - let mut credman = FakeCredman { - request_json: r#" -{ - "requests": [ - { - "protocol": "openid4vci-1.1", - "data": { - "credential_issuer": "https://issuer.my", - "credential_configuration_ids": [ - "US_SOCIAL_SECURITY_NUMBER" - ], - "grants": { - "authorization_code": {} - }, - "credential_issuer_metadata": { - "nonce_endpoint": "https://nonce.my" - } - } - } - ] -}"#, - registered_json: r#" - { - "entry_id": "C", - "entries": [ - { - "title": "TTTT1", - "subtitle": "SSSSS1", - "icon": [0, 0], - "explainer": { - "per_issuer": { - "https://issuer.my": "Explainer 1" - }, - "default": "Default 1" - } - }, - { - "title": "TTTT2", - "subtitle": "SSSSS2", - "icon": [0, 0], - "explainer": { - "per_issuer": { - "https://other.issuer": "Explainer 2" - }, - "default": "Default 2" + fn match_case_with_package_info() { + let registry_json = r#"{ + "package_info": { + "display_name": "Test Package", + "icon": { + "start": 0, + "length": 10 + } + }, + "credentials": { + "mso_mdoc": { + "org.iso.18013.5.1.mDL": [ + { + "id": "mdoc_cred_1", + "display": { + "verification": { + "title": "John's Driving License", + "subtitle": "State of California", + "explainer": "Driver License", + "warning": "", + "metadata_display_text": "" + } + }, + "paths": {} + } + ] + } } - } - ], - "filter": { - "Pass": {} - } - }"#, - icon: Vec::new(), - added_entries: Vec::new(), - wasm_version: 9, - }; - - issuance_main(&mut credman).unwrap(); + }"#; + + let mut credman = FakeCredman::new(); + credman.credentials_blob = crate::test_utils::create_registry_blob(registry_json); + credman.request_json = r#"{ + "requests": [ + { + "protocol": "openid4vp", + "data": { + "dcql_query": { + "credentials": [ + { + "id": "mdl", + "format": "mso_mdoc", + "meta": { + "doctype_value": "org.iso.18013.5.1.mDL" + } + } + ] + } + } + } + ] + }"#.to_string(); - assert_eq!(credman.added_entries.len(), 2); - assert_eq!(credman.added_entries[0].entry_id, c"C_0"); - assert_eq!(credman.added_entries[0].title.as_ref().unwrap(), c"TTTT1"); - assert_eq!(credman.added_entries[0].explainer.as_ref().unwrap(), c"Explainer 1"); - assert_eq!(credman.added_entries[0].metadata.as_ref().unwrap(), c"{\"eidx\":0,\"ridx\":0}"); - assert_eq!(credman.added_entries[1].entry_id, c"C_1"); - assert_eq!(credman.added_entries[1].title.as_ref().unwrap(), c"TTTT2"); - assert_eq!(credman.added_entries[1].explainer.as_ref().unwrap(), c"Default 2"); - assert_eq!(credman.added_entries[1].metadata.as_ref().unwrap(), c"{\"eidx\":1,\"ridx\":0}"); + crate::openid4vp::openid4vp_main(&mut credman).unwrap(); + assert_eq!(credman.entry_sets.len(), 1); } #[test] - fn match_preferred_protocol_order() { - let mut credman = FakeCredman { - request_json: r#" -{ - "requests": [ - { - "protocol": "openid4vci-1.0", - "data": { - "credential_issuer": "https://issuer.my", - "credential_configuration_ids": [ - "US_SOCIAL_SECURITY_NUMBER" - ], - "grants": { - "authorization_code": {} - }, - "credential_issuer_metadata": { - "nonce_endpoint": "https://nonce.my" - } - } - }, - { - "protocol": "openid4vci-1.1", - "data": { - "credential_issuer": "https://issuer.my", - "credential_configuration_ids": [ - "US_SOCIAL_SECURITY_NUMBER" - ], - "grants": { - "authorization_code": {} - }, - "credential_issuer_metadata": { - "nonce_endpoint": "https://nonce.my" - } - } - } - ] -}"#, - registered_json: r#" - { - "entry_id": "C", - "entries": [ - { - "title": "TTTT", - "subtitle": "SSSSS", - "icon": [0, 0] - } - ], - "preferred_protocols": ["openid4vci-1.1", "openid4vci-1.0"], - "filter": { - "Pass": {} - } - }"#, - icon: Vec::new(), - added_entries: Vec::new(), - wasm_version: 9, - }; - - issuance_main(&mut credman).unwrap(); + fn match_multiple_entries() { + let registry_json = r#"{ + "credentials": { + "mso_mdoc": { + "org.iso.18013.5.1.mDL": [ + { + "id": "mdoc_cred_1", + "display": { + "verification": { + "title": "John's Driving License", + "subtitle": "State of California", + "explainer": "Driver License", + "warning": "", + "metadata_display_text": "" + } + }, + "paths": {} + }, + { + "id": "mdoc_cred_2", + "display": { + "verification": { + "title": "Jane's Driving License", + "subtitle": "State of California", + "explainer": "Driver License", + "warning": "", + "metadata_display_text": "" + } + }, + "paths": {} + } + ] + } + } + }"#; + + let mut credman = FakeCredman::new(); + credman.credentials_blob = crate::test_utils::create_registry_blob(registry_json); + credman.request_json = r#"{ + "requests": [ + { + "protocol": "openid4vp", + "data": { + "dcql_query": { + "credentials": [ + { + "id": "mdl", + "format": "mso_mdoc", + "meta": { + "doctype_value": "org.iso.18013.5.1.mDL" + } + } + ] + } + } + } + ] + }"#.to_string(); - assert_eq!(credman.added_entries.len(), 1); - let entry = &credman.added_entries[0]; - assert_eq!(entry.entry_id, c"C_0"); - assert_eq!(entry.metadata.as_ref().unwrap(), c"{\"eidx\":0,\"ridx\":1}"); + crate::openid4vp::openid4vp_main(&mut credman).unwrap(); + assert_eq!(credman.entry_sets.len(), 1); + let entry_set = credman.entry_sets.get("0").unwrap(); + assert_eq!(entry_set.entries.get("0").unwrap().len(), 2); } } diff --git a/CredentialProvider/wasm/matcher-rs/src/openid4vp_models.rs b/CredentialProvider/wasm/matcher-rs/src/openid4vp_models.rs index e7f1537..3d83409 100644 --- a/CredentialProvider/wasm/matcher-rs/src/openid4vp_models.rs +++ b/CredentialProvider/wasm/matcher-rs/src/openid4vp_models.rs @@ -60,6 +60,7 @@ pub struct RegistryCredentials { #[nserde(default)] pub struct RegistryCredential { pub id: String, + pub delegation_type: i32, pub display: RegistryDisplay, pub paths: DeterministicMap, // Recursive structure } @@ -118,6 +119,7 @@ pub struct MatchedClaim<'a> { #[derive(Debug, Clone)] pub struct MatchedCredential<'a> { pub id: &'a str, + pub delegation_type: i32, pub display: &'a RegistryDisplay, pub matched_claim_names: Vec<&'a JsonValue>, // RegistryClaimDisplay pub matched_claim_metadata: Vec<&'a [String]>, diff --git a/CredentialProvider/wasm/matcher-rs/src/reporter.rs b/CredentialProvider/wasm/matcher-rs/src/reporter.rs index 4bfb8d5..905a8d2 100644 --- a/CredentialProvider/wasm/matcher-rs/src/reporter.rs +++ b/CredentialProvider/wasm/matcher-rs/src/reporter.rs @@ -1,549 +1,238 @@ -use crate::base64url::decode_base64url; use crate::credman::CredmanApi; -use crate::json_value::{DeterministicMap, JsonValue}; -use crate::openid4vp_models::*; -use nanoserde::DeJson; +use crate::openid4vp_models::{ + DcqlMatchResult, MatchedCredential, MatchedCredentialSetInfo, RegistryDisplay, + RegistryIssuanceEntry, SelectionMetadata, +}; +use nanoserde::SerJson; +use std::borrow::Cow; -/// Reports a credential as a payment transaction entry to Credential Manager. -fn report_payment_transaction_entry( +pub fn report_match_result( credman: &mut impl CredmanApi, - matched_credential: &MatchedCredential<'_>, - document_index: i32, - credential_set_id: &str, - selection_metadata_json: &str, - merchant_name: &str, - transaction_amount: &str, - additional_info: &str, - credentials_blob: &[u8], -) -> Result<(), Box> { - log::info!("Reporting as payment entry: {}", matched_credential.id); - - // Extract icon bytes from the blob if available. - let icon_bytes = matched_credential - .display - .verification - .icon - .as_ref() - .map_or(&[][..], |icon| { - &credentials_blob[icon.start..icon.start + icon.length] - }); - - credman.add_payment_entry_to_set_v2( - matched_credential.id, - merchant_name, - &matched_credential.display.verification.title, - &matched_credential.display.verification.subtitle, - icon_bytes, - transaction_amount, - &[], // bank_icon (empty for now) - &[], // provider_icon (empty for now) - additional_info, - selection_metadata_json, - credential_set_id, - document_index, - ); - Ok(()) + result: DcqlMatchResult, + registry_bytes: &[u8], +) { + if !result.matched_credential_sets.is_empty() { + for (set_idx, cred_sets) in result.matched_credential_sets.iter().enumerate() { + let total_entries = cred_sets + .iter() + .map(|info| info.matched_credential_ids.len()) + .sum::(); + + credman.add_entry_set(&set_idx.to_string(), total_entries as i32); + + let mut entry_index = 0; + for info in cred_sets { + if let Some(matched_entry) = result.matched_credentials.get(info.option_id.as_ref()) + { + for cred_id in &info.matched_credential_ids { + if let Some(candidate) = + matched_entry.matched.iter().find(|c| c.id == *cred_id) + { + report_standard_verification_entry( + credman, + candidate, + &info.set_id, + entry_index, + registry_bytes, + ); + + for (claim_idx, (name, path)) in candidate + .matched_claim_names + .iter() + .zip(candidate.matched_claim_metadata.iter()) + .enumerate() + { + let claim_display_name = match name { + crate::json_value::JsonValue::String(s) => s.as_str(), + _ => "", + }; + credman.add_field_to_entry_set( + candidate.id, + claim_display_name, + "", // value is omitted in DCQL + &info.set_id, + entry_index, + ); + } + + entry_index += 1; + } + } + } + } + } + } else if let Some(inline_issuance) = result.inline_issuance { + report_inline_issuance_entry(credman, inline_issuance, registry_bytes); + } } -/// Reports a credential as a standard verification entry to Credential Manager. fn report_standard_verification_entry( credman: &mut impl CredmanApi, - wasm_version: u32, - matched_credential: &MatchedCredential<'_>, - document_index: i32, - credential_set_id: &str, - selection_metadata_json: &str, - credentials_blob: &[u8], -) -> Result<(), Box> { - log::info!("Reporting as standard entry: {}", matched_credential.id); - - // Extract icon bytes from the blob if available. - let icon_bytes = matched_credential + candidate: &MatchedCredential, + set_id: &str, + set_index: i32, + registry_bytes: &[u8], +) { + let icon_bytes = candidate .display .verification .icon .as_ref() - .map_or(&[][..], |icon| { - &credentials_blob[icon.start..icon.start + icon.length] - }); + .map(|icon_info| { + if icon_info.start + icon_info.length <= registry_bytes.len() { + ®istry_bytes[icon_info.start..icon_info.start + icon_info.length] + } else { + &[] + } + }) + .unwrap_or(&[]); credman.add_entry_to_set( - matched_credential.id, + candidate.id, icon_bytes, - &matched_credential.display.verification.title, - &matched_credential.display.verification.subtitle, - &matched_credential.display.verification.explainer, - "", // warning (empty for now) - selection_metadata_json, - credential_set_id, - document_index, + &candidate.display.verification.title, + &candidate.display.verification.subtitle, + "", // disclaimer + &candidate.display.verification.warning, + "", // metadata + set_id, + set_index, + candidate.delegation_type, ); - log::trace!( - "Reporting {} claims for entry {}", - matched_credential.matched_claim_names.len(), - matched_credential.id - ); - // Report individual fields/claims for this credential. - for claim in &matched_credential.matched_claim_names { - let JsonValue::Object(claim_obj) = claim else { - continue; - }; - - let Some(JsonValue::Object(verification_obj)) = claim_obj.get("verification") else { - continue; - }; - - let Some(JsonValue::String(display_name)) = verification_obj.get("display") else { - continue; - }; - - let display_value = match verification_obj.get("display_value") { - Some(JsonValue::String(s)) => s, - _ => "", - }; - - credman.add_field_to_entry_set( - matched_credential.id, - display_name, - display_value, - credential_set_id, - document_index, - ); - } - - // Report metadata display text if supported by Wasm version. - if wasm_version >= 5 - && !matched_credential - .display - .verification - .metadata_display_text - .is_empty() + if !candidate + .display + .verification + .metadata_display_text + .is_empty() { - log::trace!( - "Adding metadata display text: {}", - matched_credential - .display - .verification - .metadata_display_text - ); credman.add_metadata_display_text_to_entry_set( - matched_credential.id, - &matched_credential - .display - .verification - .metadata_display_text, - credential_set_id, - document_index, + candidate.id, + &candidate.display.verification.metadata_display_text, + set_id, + set_index, ); } - Ok(()) } -/// Reports a matched credential, deciding whether it's a payment or standard entry. -fn report_matched_credential( +fn report_inline_issuance_entry( credman: &mut impl CredmanApi, - wasm_version: u32, - matched_credential_entry: &DcqlMatchedCredentialEntry<'_>, - matched_credential_id: &str, - document_index: i32, - request_idx: usize, - credential_set_id: &str, - dcql_set_idx: Option<&str>, - dcql_option_idx: Option<&str>, - credentials_blob: &[u8], - transaction_info: &Option<(Vec, String, String, String)>, -) -> Result<(), Box> { - log::debug!( - "Reporting matched credential: id={}, dcql_id={}, doc_idx={}", - matched_credential_entry.id, - matched_credential_id, - document_index - ); - for matched_credential in &matched_credential_entry.matched { - let metadata = SelectionMetadata { - claims: &matched_credential.matched_claim_metadata, - dc_request_index: request_idx, - dcql_cred_id: matched_credential_id, - dcql_credential_set_index: dcql_set_idx.unwrap_or(""), - dcql_option_index: dcql_option_idx.unwrap_or(""), - }; - let metadata_str = nanoserde::SerJson::serialize_json(&metadata); - - let mut reported = false; - if let Some((td_ids, merchant, amount, additional)) = transaction_info { - if td_ids.iter().any(|id| id == matched_credential_id) { - report_payment_transaction_entry( - credman, - matched_credential, - document_index, - credential_set_id, - &metadata_str, - merchant, - amount, - additional, - credentials_blob, - )?; - reported = true; - } - } - - if !reported { - report_standard_verification_entry( - credman, - wasm_version, - matched_credential, - document_index, - credential_set_id, - &metadata_str, - credentials_blob, - )?; - } - } - Ok(()) -} - -/// Extracts transaction info from OpenId4VpData if present. -fn extract_transaction_info( - openid4vp_data: &OpenId4VpData, -) -> Result, String, String, String)>, Box> { - if openid4vp_data.transaction_data.len() != 1 { - return Ok(None); - } - - log::debug!("Decoding transaction data"); - let decoded = decode_base64url(&openid4vp_data.transaction_data[0])?; - let td: TransactionData = DeJson::deserialize_json(std::str::from_utf8(&decoded)?)?; - - if !td.transaction_type.is_empty() { - log::trace!("Transaction type: {}", td.transaction_type); - if td.transaction_type == "urn:eudi:sca:payment:1" { - if let Some(payload) = &td.payload { - let merchant_name = payload - .payee - .as_ref() - .map(|p| p.name.clone()) - .unwrap_or_default(); - let transaction_amount = if !payload.amount_display.is_empty() { - payload.amount_display.clone() - } else if !payload.currency.is_empty() { - format!("{} {:.2}", payload.currency, payload.amount.unwrap_or(0.0)) - } else { - format!("{:.2}", payload.amount.unwrap_or(0.0)) - }; - log::info!( - "Found transaction data (sca): merchant={}, amount={}", - merchant_name, - transaction_amount - ); - return Ok(Some(( - td.credential_ids, - merchant_name, - transaction_amount, - td.additional_info.clone(), - ))); + entry: &RegistryIssuanceEntry, + registry_bytes: &[u8], +) { + let icon_bytes = entry + .icon + .as_ref() + .map(|icon_info| { + if icon_info.start + icon_info.length <= registry_bytes.len() { + ®istry_bytes[icon_info.start..icon_info.start + icon_info.length] + } else { + &[] } - } else if td.transaction_type == "payment_details" { - let merchant_name = td.payee_name.clone(); - let transaction_amount = format!("{} {}", td.payment_currency, td.payment_amount); - log::info!( - "Found transaction data (details): merchant={}, amount={}", - merchant_name, - transaction_amount - ); - return Ok(Some(( - td.credential_ids, - merchant_name, - transaction_amount, - td.additional_info.clone(), - ))); - } - } + }) + .unwrap_or(&[]); - log::info!( - "Found transaction data: merchant={}, amount={}", - td.merchant_name, - td.amount + credman.add_inline_issuance_entry( + &entry.id, + icon_bytes, + &entry.title, + &entry.subtitle, ); - Ok(Some(( - td.credential_ids, - td.merchant_name, - td.amount, - td.additional_info, - ))) } -/// Main entry point for reporting match results to Credential Manager. -/// It generates the Cartesian product of all options across all matched sets. -pub fn report_match_result( - credman: &mut impl CredmanApi, - match_result: &DcqlMatchResult<'_>, - request_idx: usize, - openid4vp_data: &OpenId4VpData, - credentials_blob: &[u8], -) -> Result<(), Box> { - let wasm_version = credman.get_wasm_version(); - log::info!("Reporting match results. Wasm version: {}", wasm_version); - - if !match_result.matched_credential_sets.is_empty() { - let transaction_info = extract_transaction_info(openid4vp_data)?; - - // Recursive helper to generate Cartesian product. - fn report_combinations( - credman: &mut impl CredmanApi, - wasm_version: u32, - request_idx: usize, - credential_sets: &[Vec>], - selected_option_indices: &mut Vec, - candidate_matched_credentials: &DeterministicMap<&str, DcqlMatchedCredentialEntry<'_>>, - credentials_blob: &[u8], - transaction_info: &Option<(Vec, String, String, String)>, - ) -> Result<(), Box> { - if selected_option_indices.len() == credential_sets.len() { - // We have selected one option from each set. Now report this combination. - let mut set_id_parts = vec![format!("req:{}", request_idx)]; - let mut total_length = 0; - - for (set_idx, &option_idx) in selected_option_indices.iter().enumerate() { - let option = &credential_sets[set_idx][option_idx]; - if !option.set_id.is_empty() { - set_id_parts - .push(format!("set:{};option:{}", option.set_id, option.option_id)); - } - total_length += option.matched_credential_ids.len(); - } - - // Preserve the "req:N;null" format for implicit sets (no explicit sets matched). - let set_id_str = if set_id_parts.len() == 1 { - format!("req:{};null", request_idx) - } else { - set_id_parts.join(";") - }; - - if wasm_version > 1 { - credman.add_entry_set(&set_id_str, total_length as i32); - } - - let mut document_index = 0; - for (set_idx, &option_idx) in selected_option_indices.iter().enumerate() { - let option = &credential_sets[set_idx][option_idx]; - for matched_credential_id in &option.matched_credential_ids { - let Some(doc) = candidate_matched_credentials.get(*matched_credential_id) - else { - continue; - }; - report_matched_credential( - credman, - wasm_version, - doc, - matched_credential_id, - document_index, - request_idx, - &set_id_str, - Some(&option.set_id), - Some(&option.option_id), - credentials_blob, - transaction_info, - )?; - document_index += 1; - } - } - return Ok(()); - } +#[cfg(test)] +mod tests { + use super::*; + use crate::json_value::{DeterministicMap, JsonValue}; + use crate::openid4vp_models::*; + use crate::test_utils::FakeCredman; - // Recurse to pick options for the next set. - let next_set_idx = selected_option_indices.len(); - for option_idx in 0..credential_sets[next_set_idx].len() { - selected_option_indices.push(option_idx); - report_combinations( - credman, - wasm_version, - request_idx, - credential_sets, - selected_option_indices, - candidate_matched_credentials, - credentials_blob, - transaction_info, - )?; - selected_option_indices.pop(); - } + #[test] + fn test_report_standard_entry() { + let mut credman = FakeCredman::new(); + credman.add_entry_set("0", 1); - Ok(()) - } + let display = RegistryDisplay { + verification: RegistryVerification { + title: "Title".to_string(), + subtitle: "Subtitle".to_string(), + explainer: "".to_string(), + warning: "Warning".to_string(), + metadata_display_text: "MetaText".to_string(), + icon: None, + }, + }; - let mut selected_option_indices = Vec::new(); - report_combinations( - credman, - wasm_version, - request_idx, - &match_result.matched_credential_sets, - &mut selected_option_indices, - &match_result.matched_credentials, - credentials_blob, - &transaction_info, - )?; - } + let candidate = MatchedCredential { + id: "cred1", + delegation_type: 0, + display: &display, + matched_claim_names: vec![], + matched_claim_metadata: vec![], + }; - if let Some(inline) = &match_result.inline_issuance { - log::info!("Reporting inline issuance entry: {}", inline.id); - let icon_bytes = inline.icon.as_ref().map_or(&[][..], |icon| { - &credentials_blob[icon.start..icon.start + icon.length] - }); + report_standard_verification_entry(&mut credman, &candidate, "0", 0, &[]); - credman.add_inline_issuance_entry(&inline.id, icon_bytes, &inline.title, &inline.subtitle); + let set = credman.entry_sets.get("0").unwrap(); + let entry = set.entries.get("0").unwrap().get("cred1").unwrap(); + assert_eq!(entry.title, "Title"); + assert_eq!(entry.subtitle, "Subtitle"); + assert_eq!(entry.warning, "Warning"); + assert_eq!(entry.metadata_display_text, "MetaText"); } - Ok(()) -} + #[test] + fn test_report_inline_issuance() { + let mut credman = FakeCredman::new(); + let entry = RegistryIssuanceEntry { + id: "issuance1".to_string(), + title: "Issuance Title".to_string(), + subtitle: "Issuance Subtitle".to_string(), + icon: None, + supported: Default::default(), + }; -#[cfg(test)] -mod tests { - use super::*; - use crate::json_value::DeterministicSet; - use std::borrow::Cow; - - struct MockCredman { - wasm_version: u32, - added_entries: Vec, - added_fields: Vec<(String, String, String)>, - payment_entries: Vec, - inline_entries: Vec, - added_entry_sets: Vec<(String, i32)>, - } + report_inline_issuance_entry(&mut credman, &entry, &[]); - impl CredmanApi for MockCredman { - fn get_request_buffer(&self) -> Vec { - vec![] - } - fn get_registered_data(&self) -> Vec { - vec![] - } - fn get_wasm_version(&self) -> u32 { - self.wasm_version - } - fn add_string_id_entry( - &mut self, - _entry_id: &str, - _icon: &[u8], - _title: &str, - _subtitle: &str, - _disclaimer: &str, - _warning: &str, - ) { - } - fn add_issuance_entry( - &mut self, - _entry_id: &str, - _icon: &[u8], - _title: &str, - _subtitle: &str, - _explainer: &str, - _metadata: &str, - ) { - } - fn add_entry_set(&mut self, set_id: &str, length: i32) { - self.added_entry_sets.push((set_id.to_string(), length)); - } - fn add_entry_to_set( - &mut self, - cred_id: &str, - _icon: &[u8], - _title: &str, - _subtitle: &str, - _disclaimer: &str, - _warning: &str, - _metadata: &str, - _set_id: &str, - _set_index: i32, - ) { - self.added_entries.push(cred_id.to_string()); - } - fn add_field_to_entry_set( - &mut self, - cred_id: &str, - field_display_name: &str, - field_display_value: &str, - _set_id: &str, - _set_index: i32, - ) { - self.added_fields.push(( - cred_id.to_string(), - field_display_name.to_string(), - field_display_value.to_string(), - )); - } - fn add_payment_entry_to_set_v2( - &mut self, - cred_id: &str, - _merchant_name: &str, - _payment_method_name: &str, - _payment_method_subtitle: &str, - _payment_method_icon: &[u8], - _transaction_amount: &str, - _bank_icon: &[u8], - _payment_provider_icon: &[u8], - _additional_info: &str, - _metadata: &str, - _set_id: &str, - _set_index: i32, - ) { - self.payment_entries.push(cred_id.to_string()); - } - fn add_inline_issuance_entry( - &mut self, - cred_id: &str, - _icon: &[u8], - _title: &str, - _subtitle: &str, - ) { - self.inline_entries.push(cred_id.to_string()); - } - fn add_metadata_display_text_to_entry_set( - &mut self, - _cred_id: &str, - _metadata_display_text: &str, - _set_id: &str, - _set_index: i32, - ) { - } + assert_eq!(credman.standalone_entries.len(), 1); + let standalone = &credman.standalone_entries[0]; + assert_eq!(standalone.cred_id, "issuance1"); + assert_eq!(standalone.title, "Issuance Title"); + assert_eq!(standalone.subtitle, "Issuance Subtitle"); } #[test] fn test_report_match_result_empty() { - let mut mock = MockCredman { - wasm_version: 1, - added_entries: vec![], - added_fields: vec![], - payment_entries: vec![], - inline_entries: vec![], - added_entry_sets: vec![], - }; - let match_result = DcqlMatchResult { + let mut credman = FakeCredman::new(); + let result = DcqlMatchResult { matched_credential_sets: vec![], matched_credentials: DeterministicMap::new(), inline_issuance: None, }; - let openid4vp_data = OpenId4VpData::default(); - - report_match_result(&mut mock, &match_result, 0, &openid4vp_data, &[]).unwrap(); - assert!(mock.added_entries.is_empty()); + report_match_result(&mut credman, result, &[]); + assert!(credman.entry_sets.is_empty()); + assert!(credman.standalone_entries.is_empty()); } #[test] - fn test_report_standard_entry() { - let mut mock = MockCredman { - wasm_version: 1, - added_entries: vec![], - added_fields: vec![], - payment_entries: vec![], - inline_entries: vec![], - added_entry_sets: vec![], + fn test_report_cartesian_product() { + let mut credman = FakeCredman::new(); + + let display1 = RegistryDisplay { + verification: RegistryVerification { + title: "Title 1".to_string(), + subtitle: "".to_string(), + explainer: "".to_string(), + warning: "".to_string(), + metadata_display_text: "".to_string(), + icon: None, + }, }; - let display = RegistryDisplay { + let display2 = RegistryDisplay { verification: RegistryVerification { - title: "Test Cred".to_string(), + title: "Title 2".to_string(), subtitle: "".to_string(), explainer: "".to_string(), warning: "".to_string(), @@ -552,176 +241,63 @@ mod tests { }, }; + let candidate1 = MatchedCredential { + id: "cred1", + delegation_type: 0, + display: &display1, + matched_claim_names: vec![], + matched_claim_metadata: vec![], + }; + + let candidate2 = MatchedCredential { + id: "cred2", + delegation_type: 0, + display: &display2, + matched_claim_names: vec![], + matched_claim_metadata: vec![], + }; + let mut matched_credentials = DeterministicMap::new(); matched_credentials.insert( - "cred1", + "opt1", DcqlMatchedCredentialEntry { id: "cred1", - matched: vec![MatchedCredential { - id: "cred1", - display: &display, - matched_claim_names: vec![], - matched_claim_metadata: vec![], - }], + matched: vec![candidate1], + }, + ); + matched_credentials.insert( + "opt2", + DcqlMatchedCredentialEntry { + id: "cred2", + matched: vec![candidate2], }, ); - let match_result = DcqlMatchResult { - matched_credential_sets: vec![vec![MatchedCredentialSetInfo { - set_id: Cow::Borrowed("set1"), - option_id: Cow::Borrowed("opt1"), - matched_credential_ids: vec!["cred1"], - }]], - matched_credentials, - inline_issuance: None, - }; - let openid4vp_data = OpenId4VpData::default(); - - report_match_result(&mut mock, &match_result, 0, &openid4vp_data, &[]).unwrap(); - - assert_eq!(mock.added_entries, vec!["cred1"]); - } - - #[test] - fn test_report_inline_issuance() { - let mut mock = MockCredman { - wasm_version: 1, - added_entries: vec![], - added_fields: vec![], - payment_entries: vec![], - inline_entries: vec![], - added_entry_sets: vec![], - }; - - let inline = RegistryIssuanceEntry { - id: "inline_cred".to_string(), - title: "Get Inline".to_string(), - subtitle: "".to_string(), - icon: None, - supported: DeterministicSet::default(), - }; - - let match_result = DcqlMatchResult { - matched_credential_sets: vec![], - matched_credentials: DeterministicMap::new(), - inline_issuance: Some(&inline), + let set_info1 = MatchedCredentialSetInfo { + set_id: Cow::Borrowed("0"), + option_id: Cow::Borrowed("opt1"), + matched_credential_ids: vec!["cred1"], }; - let openid4vp_data = OpenId4VpData::default(); - report_match_result(&mut mock, &match_result, 0, &openid4vp_data, &[]).unwrap(); - - assert_eq!(mock.inline_entries, vec!["inline_cred"]); - } - - #[test] - fn test_report_cartesian_product() { - let mut mock = MockCredman { - wasm_version: 2, - added_entries: vec![], - added_fields: vec![], - payment_entries: vec![], - inline_entries: vec![], - added_entry_sets: vec![], + let set_info2 = MatchedCredentialSetInfo { + set_id: Cow::Borrowed("0"), + option_id: Cow::Borrowed("opt2"), + matched_credential_ids: vec!["cred2"], }; - let display = RegistryDisplay::default(); - let match_result = DcqlMatchResult { - matched_credential_sets: vec![ - vec![ - MatchedCredentialSetInfo { - set_id: Cow::Borrowed("set1"), - option_id: Cow::Borrowed("opt1"), - matched_credential_ids: vec!["cred1"], - }, - MatchedCredentialSetInfo { - set_id: Cow::Borrowed("set1"), - option_id: Cow::Borrowed("opt2"), - matched_credential_ids: vec!["cred2"], - }, - ], - vec![ - MatchedCredentialSetInfo { - set_id: Cow::Borrowed("set2"), - option_id: Cow::Borrowed("opt3"), - matched_credential_ids: vec!["cred3"], - }, - MatchedCredentialSetInfo { - set_id: Cow::Borrowed("set2"), - option_id: Cow::Borrowed("opt4"), - matched_credential_ids: vec!["cred4"], - }, - ], - vec![ - MatchedCredentialSetInfo { - set_id: Cow::Borrowed("set3"), - option_id: Cow::Borrowed("opt5"), - matched_credential_ids: vec!["cred5"], - }, - MatchedCredentialSetInfo { - set_id: Cow::Borrowed("set3"), - option_id: Cow::Borrowed("opt6"), - matched_credential_ids: vec!["cred6"], - }, - ], - ], - matched_credentials: { - let mut map = DeterministicMap::new(); - let create_entry = |id: &'static str| DcqlMatchedCredentialEntry { - id, - matched: vec![MatchedCredential { - id, - display: &display, - matched_claim_names: vec![], - matched_claim_metadata: vec![], - }], - }; - map.insert("cred1", create_entry("cred1")); - map.insert("cred2", create_entry("cred2")); - map.insert("cred3", create_entry("cred3")); - map.insert("cred4", create_entry("cred4")); - map.insert("cred5", create_entry("cred5")); - map.insert("cred6", create_entry("cred6")); - map - }, + let result = DcqlMatchResult { + matched_credential_sets: vec![vec![set_info1, set_info2]], + matched_credentials, inline_issuance: None, }; - let openid4vp_data = OpenId4VpData::default(); - - report_match_result(&mut mock, &match_result, 0, &openid4vp_data, &[]).unwrap(); - - // We expect 2^3 = 8 calls to add_entry_set - assert_eq!(mock.added_entry_sets.len(), 8); - - let set_ids: Vec = mock - .added_entry_sets - .iter() - .map(|(id, _)| id.clone()) - .collect(); - - assert!(set_ids.contains( - &"req:0;set:set1;option:opt1;set:set2;option:opt3;set:set3;option:opt5".to_string() - )); - assert!(set_ids.contains( - &"req:0;set:set1;option:opt1;set:set2;option:opt3;set:set3;option:opt6".to_string() - )); - assert!(set_ids.contains( - &"req:0;set:set1;option:opt1;set:set2;option:opt4;set:set3;option:opt5".to_string() - )); - assert!(set_ids.contains( - &"req:0;set:set1;option:opt1;set:set2;option:opt4;set:set3;option:opt6".to_string() - )); - assert!(set_ids.contains( - &"req:0;set:set1;option:opt2;set:set2;option:opt3;set:set3;option:opt5".to_string() - )); - assert!(set_ids.contains( - &"req:0;set:set1;option:opt2;set:set2;option:opt3;set:set3;option:opt6".to_string() - )); - assert!(set_ids.contains( - &"req:0;set:set1;option:opt2;set:set2;option:opt4;set:set3;option:opt5".to_string() - )); - assert!(set_ids.contains( - &"req:0;set:set1;option:opt2;set:set2;option:opt4;set:set3;option:opt6".to_string() - )); + report_match_result(&mut credman, result, &[]); + + assert_eq!(credman.entry_sets.len(), 1); + let set = credman.entry_sets.get("0").unwrap(); + assert_eq!(set.set_length, 2); + assert_eq!(set.entries.len(), 2); + assert!(set.entries.get("0").unwrap().contains_key("cred1")); + assert!(set.entries.get("1").unwrap().contains_key("cred2")); } } diff --git a/CredentialProvider/wasm/matcher-rs/src/test_utils.rs b/CredentialProvider/wasm/matcher-rs/src/test_utils.rs index 33a1870..fb9a425 100644 --- a/CredentialProvider/wasm/matcher-rs/src/test_utils.rs +++ b/CredentialProvider/wasm/matcher-rs/src/test_utils.rs @@ -164,6 +164,7 @@ impl CredmanApi for FakeCredman { _metadata: &str, set_id: &str, set_index: i32, + _delegation_type: i32, ) { let s_id = set_id.to_string(); let c_id = cred_id.to_string(); @@ -188,6 +189,13 @@ impl CredmanApi for FakeCredman { .or_insert_with(DeterministicMap::new) .insert(c_id, entry); } + fn set_delegation_type_for_entry_in_set( + &mut self, + _cred_id: &str, + _delegation_type: i32, + _set_id: &str, + _set_index: i32, + ) {} fn add_field_to_entry_set( &mut self, cred_id: &str,