From 61df8b12ca3e7d6287627ac2c179da087e14d87a Mon Sep 17 00:00:00 2001 From: Brian Lee Date: Wed, 15 Jul 2026 14:11:14 -0400 Subject: [PATCH] Port multi-protocol preference-ordered matching to Rust matcher --- matcher-rs/src/openid4vp.rs | 91 ++++++++++-- matcher-rs/src/openid4vp_models.rs | 2 + ...edSupportedProtocolAvailable_expected.json | 139 ++++++++++++++++++ ...redSupportedProtocolAvailable_request.json | 32 ++++ ...equestsForSupportedProtocols_expected.json | 4 + ...RequestsForSupportedProtocols_request.json | 20 +++ 6 files changed, 278 insertions(+), 10 deletions(-) create mode 100644 matcher-rs/testdata/TC40_MatchMostPreferredSupportedProtocolAvailable_expected.json create mode 100644 matcher-rs/testdata/TC40_MatchMostPreferredSupportedProtocolAvailable_request.json create mode 100644 matcher-rs/testdata/TC41_NoMatchIfNoRequestsForSupportedProtocols_expected.json create mode 100644 matcher-rs/testdata/TC41_NoMatchIfNoRequestsForSupportedProtocols_request.json diff --git a/matcher-rs/src/openid4vp.rs b/matcher-rs/src/openid4vp.rs index d233bd4..06405f0 100644 --- a/matcher-rs/src/openid4vp.rs +++ b/matcher-rs/src/openid4vp.rs @@ -103,18 +103,31 @@ pub fn openid4vp_main(credman: &mut impl CredmanApi) -> Result<(), Box String { + let start_key = "\"supported_protocols\": ["; + if let (Some(start_idx), Some(end_rel)) = ( + registry_json.find(start_key), + registry_json.find(start_key).and_then(|idx| registry_json[idx..].find("],")), + ) { + let end_idx = start_idx + end_rel + 2; + let formatted_protocols = protocols + .iter() + .map(|p| format!(" \"{}\"", p)) + .collect::>() + .join(",\n"); + let new_section = format!("\"supported_protocols\": [\n{}\n ],", formatted_protocols); + let mut result = String::with_capacity(registry_json.len() + new_section.len()); + result.push_str(®istry_json[..start_idx]); + result.push_str(&new_section); + result.push_str(®istry_json[end_idx..]); + result + } else { + registry_json.to_string() + } + } + + #[test] + fn tc40_match_most_preferred_supported_protocol_available() { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let testdata_dir = std::path::PathBuf::from(manifest_dir).join("testdata"); + let registry_json = std::fs::read_to_string(testdata_dir.join("registry.json")).unwrap(); + + // Inject custom supported_protocols preference order: signed then unsigned + let modified_registry = replace_supported_protocols( + ®istry_json, + &["openid4vp-v1-signed", "openid4vp-v1-unsigned"], + ); + + run_openid4vp_test( + "TC40_MatchMostPreferredSupportedProtocolAvailable", + Some(&modified_registry), + ); + } + + #[test] + fn tc41_no_match_if_no_requests_for_supported_protocols() { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let testdata_dir = std::path::PathBuf::from(manifest_dir).join("testdata"); + let registry_json = std::fs::read_to_string(testdata_dir.join("registry.json")).unwrap(); + + // Inject custom supported_protocols containing only "openid4vp-v1-signed" + let modified_registry = replace_supported_protocols(®istry_json, &["openid4vp-v1-signed"]); + + run_openid4vp_test( + "TC41_NoMatchIfNoRequestsForSupportedProtocols", + Some(&modified_registry), + ); + } } diff --git a/matcher-rs/src/openid4vp_models.rs b/matcher-rs/src/openid4vp_models.rs index e7f1537..e822854 100644 --- a/matcher-rs/src/openid4vp_models.rs +++ b/matcher-rs/src/openid4vp_models.rs @@ -44,6 +44,8 @@ pub struct DcqlCredentialSet { #[nserde(default)] pub struct Registry { pub credentials: RegistryCredentials, + #[nserde(default)] + pub supported_protocols: Vec, } #[derive(DeJson, Debug, Clone, Default)] diff --git a/matcher-rs/testdata/TC40_MatchMostPreferredSupportedProtocolAvailable_expected.json b/matcher-rs/testdata/TC40_MatchMostPreferredSupportedProtocolAvailable_expected.json new file mode 100644 index 0000000..3c43a78 --- /dev/null +++ b/matcher-rs/testdata/TC40_MatchMostPreferredSupportedProtocolAvailable_expected.json @@ -0,0 +1,139 @@ +{ + "entrySets": { + "req:1;null": { + "entries": { + "0": { + "mdoc_cred_1": { + "additional_info": "", + "credId": "mdoc_cred_1", + "disclaimer": "", + "fields": [ + [ + "Family Name", + "Doe" + ], + [ + "Given Name", + "John" + ], + [ + "Age", + "" + ], + [ + "Over 21", + "Yes" + ] + ], + "merchant_name": "", + "metadata_display_text": "", + "subtitle": "", + "title": "John's Driving License", + "transaction_amount": "", + "type": "Verification", + "warning": "" + }, + "mdoc_cred_underage": { + "additional_info": "", + "credId": "mdoc_cred_underage", + "disclaimer": "", + "fields": [ + [ + "Age", + "" + ], + [ + "Over 21", + "Yes" + ] + ], + "merchant_name": "", + "metadata_display_text": "", + "subtitle": "", + "title": "Underage License", + "transaction_amount": "", + "type": "Verification", + "warning": "" + }, + "mdoc_cred_3": { + "additional_info": "", + "credId": "mdoc_cred_3", + "disclaimer": "", + "fields": [ + [ + "Family Name", + "" + ], + [ + "Given Name", + "" + ], + [ + "Age", + "" + ], + [ + "Over 21", + "" + ] + ], + "merchant_name": "", + "metadata_display_text": "", + "subtitle": "", + "title": "Alice's Driving License", + "transaction_amount": "", + "type": "Verification", + "warning": "" + }, + "mdoc_cred_4": { + "additional_info": "", + "credId": "mdoc_cred_4", + "disclaimer": "", + "fields": [ + [ + "Family Name", + "" + ], + [ + "Given Name", + "" + ], + [ + "Age", + "" + ], + [ + "Over 21", + "" + ] + ], + "merchant_name": "", + "metadata_display_text": "", + "subtitle": "", + "title": "Jane's Driving License", + "transaction_amount": "", + "type": "Verification", + "warning": "" + } + } + }, + "setId": "req:1;null", + "setLength": 1 + } + }, + "standaloneEntries": [ + { + "additional_info": "", + "credId": "issuance_mdl_1", + "disclaimer": "", + "fields": [], + "merchant_name": "", + "metadata_display_text": "", + "subtitle": "From your local DMV", + "title": "Get a New mDL", + "transaction_amount": "", + "type": "InlineIssuance", + "warning": "" + } + ] +} diff --git a/matcher-rs/testdata/TC40_MatchMostPreferredSupportedProtocolAvailable_request.json b/matcher-rs/testdata/TC40_MatchMostPreferredSupportedProtocolAvailable_request.json new file mode 100644 index 0000000..15f8635 --- /dev/null +++ b/matcher-rs/testdata/TC40_MatchMostPreferredSupportedProtocolAvailable_request.json @@ -0,0 +1,32 @@ +{ + "requests": [ + { + "data": { + "dcql_query": { + "credentials": [ + { + "format": "mso_mdoc", + "id": "mdl", + "meta": { + "doctype_value": "org.iso.18013.5.1.mDL" + } + } + ] + } + }, + "protocol": "openid4vp-v1-unsigned" + }, + { + "data": { + "request": "header.eyJkY3FsX3F1ZXJ5Ijp7ImNyZWRlbnRpYWxzIjpbeyJmb3JtYXQiOiJtc29fbWRvYyIsImlkIjoibWRsIiwibWV0YSI6eyJkb2N0eXBlX3ZhbHVlIjoib3JnLmlzby4xODAxMy41LjEubURMIn19XX19.signature" + }, + "protocol": "openid4vp-v1-signed" + }, + { + "data": { + "request": "header.eyJkY3FsX3F1ZXJ5Ijp7ImNyZWRlbnRpYWxzIjpbeyJmb3JtYXQiOiJtc29fbWRvYyIsImlkIjoibWRsIiwibWV0YSI6eyJkb2N0eXBlX3ZhbHVlIjoib3JnLmlzby4xODAxMy41LjEubURMIn19XX19.signature" + }, + "protocol": "openid4vp-v1-signed" + } + ] +} diff --git a/matcher-rs/testdata/TC41_NoMatchIfNoRequestsForSupportedProtocols_expected.json b/matcher-rs/testdata/TC41_NoMatchIfNoRequestsForSupportedProtocols_expected.json new file mode 100644 index 0000000..4a37a2a --- /dev/null +++ b/matcher-rs/testdata/TC41_NoMatchIfNoRequestsForSupportedProtocols_expected.json @@ -0,0 +1,4 @@ +{ + "entrySets": {}, + "standaloneEntries": [] +} diff --git a/matcher-rs/testdata/TC41_NoMatchIfNoRequestsForSupportedProtocols_request.json b/matcher-rs/testdata/TC41_NoMatchIfNoRequestsForSupportedProtocols_request.json new file mode 100644 index 0000000..ed8afd9 --- /dev/null +++ b/matcher-rs/testdata/TC41_NoMatchIfNoRequestsForSupportedProtocols_request.json @@ -0,0 +1,20 @@ +{ + "requests": [ + { + "data": { + "dcql_query": { + "credentials": [ + { + "format": "mso_mdoc", + "id": "mdl", + "meta": { + "doctype_value": "org.iso.18013.5.1.mDL" + } + } + ] + } + }, + "protocol": "openid4vp-v1-unsigned" + } + ] +}