From f28a23d5fcfd8015e43e9337d44d4447b4d377b7 Mon Sep 17 00:00:00 2001 From: Brian Lee Date: Fri, 31 Jul 2026 04:08:07 +0000 Subject: [PATCH 1/3] Port multi-protocol preference-ordered matching to Rust matcher --- .../wasm/matcher-rs/src/openid4vp.rs | 91 ++++++++++-- .../wasm/matcher-rs/src/openid4vp_models.rs | 2 + ...edSupportedProtocolAvailable_expected.json | 139 ++++++++++++++++++ ...redSupportedProtocolAvailable_request.json | 32 ++++ ...equestsForSupportedProtocols_expected.json | 4 + ...RequestsForSupportedProtocols_request.json | 20 +++ .../wasm/matcher-rs/testdata/registry.json | 5 + 7 files changed, 283 insertions(+), 10 deletions(-) create mode 100644 CredentialProvider/wasm/matcher-rs/testdata/TC40_MatchMostPreferredSupportedProtocolAvailable_expected.json create mode 100644 CredentialProvider/wasm/matcher-rs/testdata/TC40_MatchMostPreferredSupportedProtocolAvailable_request.json create mode 100644 CredentialProvider/wasm/matcher-rs/testdata/TC41_NoMatchIfNoRequestsForSupportedProtocols_expected.json create mode 100644 CredentialProvider/wasm/matcher-rs/testdata/TC41_NoMatchIfNoRequestsForSupportedProtocols_request.json diff --git a/CredentialProvider/wasm/matcher-rs/src/openid4vp.rs b/CredentialProvider/wasm/matcher-rs/src/openid4vp.rs index d233bd4..06405f0 100644 --- a/CredentialProvider/wasm/matcher-rs/src/openid4vp.rs +++ b/CredentialProvider/wasm/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/CredentialProvider/wasm/matcher-rs/src/openid4vp_models.rs b/CredentialProvider/wasm/matcher-rs/src/openid4vp_models.rs index e7f1537..e822854 100644 --- a/CredentialProvider/wasm/matcher-rs/src/openid4vp_models.rs +++ b/CredentialProvider/wasm/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/CredentialProvider/wasm/matcher-rs/testdata/TC40_MatchMostPreferredSupportedProtocolAvailable_expected.json b/CredentialProvider/wasm/matcher-rs/testdata/TC40_MatchMostPreferredSupportedProtocolAvailable_expected.json new file mode 100644 index 0000000..3c43a78 --- /dev/null +++ b/CredentialProvider/wasm/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/CredentialProvider/wasm/matcher-rs/testdata/TC40_MatchMostPreferredSupportedProtocolAvailable_request.json b/CredentialProvider/wasm/matcher-rs/testdata/TC40_MatchMostPreferredSupportedProtocolAvailable_request.json new file mode 100644 index 0000000..15f8635 --- /dev/null +++ b/CredentialProvider/wasm/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/CredentialProvider/wasm/matcher-rs/testdata/TC41_NoMatchIfNoRequestsForSupportedProtocols_expected.json b/CredentialProvider/wasm/matcher-rs/testdata/TC41_NoMatchIfNoRequestsForSupportedProtocols_expected.json new file mode 100644 index 0000000..4a37a2a --- /dev/null +++ b/CredentialProvider/wasm/matcher-rs/testdata/TC41_NoMatchIfNoRequestsForSupportedProtocols_expected.json @@ -0,0 +1,4 @@ +{ + "entrySets": {}, + "standaloneEntries": [] +} diff --git a/CredentialProvider/wasm/matcher-rs/testdata/TC41_NoMatchIfNoRequestsForSupportedProtocols_request.json b/CredentialProvider/wasm/matcher-rs/testdata/TC41_NoMatchIfNoRequestsForSupportedProtocols_request.json new file mode 100644 index 0000000..ed8afd9 --- /dev/null +++ b/CredentialProvider/wasm/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" + } + ] +} diff --git a/CredentialProvider/wasm/matcher-rs/testdata/registry.json b/CredentialProvider/wasm/matcher-rs/testdata/registry.json index c219cc9..9034bbd 100644 --- a/CredentialProvider/wasm/matcher-rs/testdata/registry.json +++ b/CredentialProvider/wasm/matcher-rs/testdata/registry.json @@ -1,4 +1,9 @@ { + "supported_protocols": [ + "openid4vp-v1-signed", + "openid4vp-v1-unsigned", + "openid4vp-v1-multisigned" + ], "credentials": { "mso_mdoc": { "org.iso.18013.5.1.mDL": [ From 7d1e94f2231d7a90a1b1ec57b949ad05dabbe34d Mon Sep 17 00:00:00 2001 From: Helen Qin Date: Fri, 31 Jul 2026 04:08:09 +0000 Subject: [PATCH 2/3] Support OpenID4VP multi-signed request in Rust matcher --- .../wasm/matcher-rs/src/openid4vp.rs | 78 +++++++++- .../TC42_ParseV1Multisigned_expected.json | 139 ++++++++++++++++++ .../TC42_ParseV1Multisigned_request.json | 10 ++ 3 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 CredentialProvider/wasm/matcher-rs/testdata/TC42_ParseV1Multisigned_expected.json create mode 100644 CredentialProvider/wasm/matcher-rs/testdata/TC42_ParseV1Multisigned_request.json diff --git a/CredentialProvider/wasm/matcher-rs/src/openid4vp.rs b/CredentialProvider/wasm/matcher-rs/src/openid4vp.rs index 06405f0..7206336 100644 --- a/CredentialProvider/wasm/matcher-rs/src/openid4vp.rs +++ b/CredentialProvider/wasm/matcher-rs/src/openid4vp.rs @@ -1,10 +1,51 @@ use crate::base64url::decode_base64url; use crate::credman::CredmanApi; +use crate::json_value::JsonValue; pub use crate::openid4vp_models::*; use crate::reporter::report_match_result; use nanoserde::DeJson; use std::borrow::Cow; +fn extract_multisigned_payload<'a>( + pr: &'a ProtocolRequest, +) -> Result> { + let json_str: &str = if let Some(data) = &pr.data { + match data { + ProtocolRequestData::String(s) => s.as_str(), + ProtocolRequestData::Object(obj) => obj.request.as_str(), + } + } else if !pr.request.is_empty() { + pr.request.as_str() + } else { + return Err("Missing multisigned request data".into()); + }; + + let parsed: JsonValue = DeJson::deserialize_json(json_str)?; + + let payload = match &parsed { + JsonValue::Object(map) => { + if let Some(JsonValue::Object(req_map)) = map.get("request") { + if let Some(JsonValue::String(p)) = req_map.get("payload") { + p.clone() + } else { + return Err("Missing 'payload' in 'request' object".into()); + } + } else if let Some(JsonValue::String(p)) = map.get("payload") { + p.clone() + } else { + return Err("Missing 'payload' field in multisigned request".into()); + } + } + _ => return Err("Multisigned request must be a JSON object".into()), + }; + + if payload.is_empty() { + return Err("Empty payload in multisigned request".into()); + } + + Ok(payload) +} + fn parse_protocol_request_data<'a>( pr: &'a ProtocolRequest, ) -> Result, Box> { @@ -36,7 +77,14 @@ fn parse_protocol_request_data<'a>( return Ok(Cow::Owned(DeJson::deserialize_json(std::str::from_utf8( &decoded, )?)?)); - } + } else if pr.protocol == "openid4vp-v1-multisigned" { + log::debug!("Handling multisigned OpenID4VP request"); + let payload_str = extract_multisigned_payload(pr)?; + let decoded = decode_base64url(&payload_str)?; + return Ok(Cow::Owned(DeJson::deserialize_json(std::str::from_utf8( + &decoded, + )?)?)); + } log::debug!("Handling unsigned OpenID4VP request"); if let Some(data) = &pr.data { @@ -110,7 +158,10 @@ pub fn openid4vp_main(credman: &mut impl CredmanApi) -> Result<(), Box Date: Fri, 31 Jul 2026 04:08:10 +0000 Subject: [PATCH 3/3] address reviewer feedback --- .../wasm/matcher-rs/src/openid4vp.rs | 74 +++++++++---------- 1 file changed, 36 insertions(+), 38 deletions(-) diff --git a/CredentialProvider/wasm/matcher-rs/src/openid4vp.rs b/CredentialProvider/wasm/matcher-rs/src/openid4vp.rs index 7206336..b3ec3e8 100644 --- a/CredentialProvider/wasm/matcher-rs/src/openid4vp.rs +++ b/CredentialProvider/wasm/matcher-rs/src/openid4vp.rs @@ -6,37 +6,49 @@ use crate::reporter::report_match_result; use nanoserde::DeJson; use std::borrow::Cow; -fn extract_multisigned_payload<'a>( +fn extract_request_str<'a>( pr: &'a ProtocolRequest, -) -> Result> { - let json_str: &str = if let Some(data) = &pr.data { +) -> Result<&'a str, Box> { + if let Some(data) = &pr.data { match data { - ProtocolRequestData::String(s) => s.as_str(), - ProtocolRequestData::Object(obj) => obj.request.as_str(), + ProtocolRequestData::String(s) => Ok(s.as_str()), + ProtocolRequestData::Object(obj) => { + if obj.request.is_empty() { + return Err("Missing 'request' field in data object".into()); + } + Ok(obj.request.as_str()) + } } } else if !pr.request.is_empty() { - pr.request.as_str() + Ok(pr.request.as_str()) } else { - return Err("Missing multisigned request data".into()); - }; + Err("Missing request data".into()) + } +} +fn extract_multisigned_payload( + pr: &ProtocolRequest, +) -> Result> { + let json_str = extract_request_str(pr)?; let parsed: JsonValue = DeJson::deserialize_json(json_str)?; - let payload = match &parsed { - JsonValue::Object(map) => { - if let Some(JsonValue::Object(req_map)) = map.get("request") { - if let Some(JsonValue::String(p)) = req_map.get("payload") { - p.clone() - } else { - return Err("Missing 'payload' in 'request' object".into()); - } - } else if let Some(JsonValue::String(p)) = map.get("payload") { - p.clone() - } else { - return Err("Missing 'payload' field in multisigned request".into()); - } - } - _ => return Err("Multisigned request must be a JSON object".into()), + let JsonValue::Object(mut map) = parsed else { + return Err("Multisigned request must be a JSON object".into()); + }; + + let payload = if let Some(req_val) = map.shift_remove("request") { + let JsonValue::Object(mut req_map) = req_val else { + return Err("Missing 'payload' in 'request' object".into()); + }; + let Some(JsonValue::String(p)) = req_map.shift_remove("payload") else { + return Err("Missing 'payload' in 'request' object".into()); + }; + p + } else { + let Some(JsonValue::String(p)) = map.shift_remove("payload") else { + return Err("Missing 'payload' field in multisigned request".into()); + }; + p }; if payload.is_empty() { @@ -51,21 +63,7 @@ fn parse_protocol_request_data<'a>( ) -> Result, Box> { if pr.protocol == "openid4vp-v1-signed" { log::debug!("Handling signed OpenID4VP request"); - let jws: &'a str = if let Some(data) = &pr.data { - match data { - ProtocolRequestData::String(s) => s, - ProtocolRequestData::Object(obj) => { - if obj.request.is_empty() { - return Err("Missing 'request' field in signed data object".into()); - } - &obj.request - } - } - } else if !pr.request.is_empty() { - &pr.request - } else { - return Err("Missing signed request data".into()); - }; + let jws = extract_request_str(pr)?; let parts: Vec<&str> = jws.split('.').collect(); if parts.len() < 2 {