From 10fefde12a8672cea40da3a08a2f451d51bbfa39 Mon Sep 17 00:00:00 2001 From: Helen Qin Date: Tue, 7 Jul 2026 03:51:34 +0000 Subject: [PATCH] Support OpenID4VP multi-signed request in Rust matcher --- matcher-rs/src/openid4vp.rs | 81 ++++++++-- matcher-rs/src/openid4vp_models.rs | 5 +- .../TC40_ParseV1Multisigned_expected.json | 139 ++++++++++++++++++ .../TC40_ParseV1Multisigned_request.json | 18 +++ 4 files changed, 231 insertions(+), 12 deletions(-) create mode 100644 matcher-rs/testdata/TC40_ParseV1Multisigned_expected.json create mode 100644 matcher-rs/testdata/TC40_ParseV1Multisigned_request.json diff --git a/matcher-rs/src/openid4vp.rs b/matcher-rs/src/openid4vp.rs index d233bd4..2c0923b 100644 --- a/matcher-rs/src/openid4vp.rs +++ b/matcher-rs/src/openid4vp.rs @@ -1,24 +1,42 @@ 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<&'a str, Box> { + let jws = match &pr.data { + Some(ProtocolRequestData::Object(obj)) => &obj.request, + _ => return Err("Missing or invalid data object for multisigned request".into()), + }; + + match jws { + JsonValue::Object(map) => { + match map.get("payload") { + Some(JsonValue::String(payload)) if !payload.is_empty() => Ok(payload.as_str()), + _ => Err("Missing or invalid 'payload' field in multisigned request".into()), + } + } + _ => Err("Multisigned 'request' field must be a JWS JSON object".into()), + } +} + fn parse_protocol_request_data<'a>( pr: &'a ProtocolRequest, ) -> Result, Box> { if pr.protocol == "openid4vp-v1-signed" { log::debug!("Handling signed OpenID4VP request"); - let jws: &'a str = if let Some(data) = &pr.data { + let jws: &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 - } + ProtocolRequestData::String(s) => s.as_str(), + ProtocolRequestData::Object(obj) => match &obj.request { + JsonValue::String(s) if !s.is_empty() => s.as_str(), + _ => return Err("Missing 'request' field in signed data object".into()), + }, } } else if !pr.request.is_empty() { &pr.request @@ -38,6 +56,15 @@ fn parse_protocol_request_data<'a>( )?)?)); } + 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 { return match data { @@ -105,7 +132,10 @@ pub fn openid4vp_main(credman: &mut impl CredmanApi) -> Result<(), Box, pub offer: Option, pub transaction_data: Vec, - pub request: String, + pub request: JsonValue, + pub payload: Option, } #[derive(Debug, Clone)] @@ -394,7 +395,7 @@ mod tests { let json_obj = r#"{"request":"test_req"}"#; let data: ProtocolRequestData = DeJson::deserialize_json(json_obj).expect("Failed to parse object ProtocolRequestData"); - assert!(matches!(data, ProtocolRequestData::Object(obj) if obj.request == "test_req")); + assert!(matches!(data, ProtocolRequestData::Object(obj) if obj.request == JsonValue::String("test_req".to_string()))); } #[test] diff --git a/matcher-rs/testdata/TC40_ParseV1Multisigned_expected.json b/matcher-rs/testdata/TC40_ParseV1Multisigned_expected.json new file mode 100644 index 0000000..1744651 --- /dev/null +++ b/matcher-rs/testdata/TC40_ParseV1Multisigned_expected.json @@ -0,0 +1,139 @@ +{ + "entrySets": { + "req:0;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_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": "" + }, + "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": "" + } + } + }, + "setId": "req:0;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_ParseV1Multisigned_request.json b/matcher-rs/testdata/TC40_ParseV1Multisigned_request.json new file mode 100644 index 0000000..8a4d5bc --- /dev/null +++ b/matcher-rs/testdata/TC40_ParseV1Multisigned_request.json @@ -0,0 +1,18 @@ +{ + "requests": [ + { + "data": { + "request": { + "payload": "eyJkY3FsX3F1ZXJ5Ijp7ImNyZWRlbnRpYWxzIjpbeyJmb3JtYXQiOiJtc29fbWRvYyIsImlkIjoibWRsIiwibWV0YSI6eyJkb2N0eXBlX3ZhbHVlIjoib3JnLmlzby4xODAxMy41LjEubURMIn19XX19", + "signatures": [ + { + "protected": "eyJhbGciOiJFUzI1NiJ9", + "signature": "c2lnbmF0dXJl" + } + ] + } + }, + "protocol": "openid4vp-v1-multisigned" + } + ] +}