Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 71 additions & 10 deletions matcher-rs/src/openid4vp.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
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<Cow<'a, OpenId4VpData>, Box<dyn std::error::Error>> {
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
Expand All @@ -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 {
Expand Down Expand Up @@ -105,7 +132,10 @@ pub fn openid4vp_main(credman: &mut impl CredmanApi) -> Result<(), Box<dyn std::

for (i, pr) in protocol_requests.iter().enumerate() {
log::debug!("Processing request {}: protocol={}", i, pr.protocol);
if pr.protocol != "openid4vp-v1-unsigned" && pr.protocol != "openid4vp-v1-signed" {
if pr.protocol != "openid4vp-v1-unsigned"
&& pr.protocol != "openid4vp-v1-signed"
&& pr.protocol != "openid4vp-v1-multisigned"
{
log::warn!("Unsupported protocol: {}", pr.protocol);
continue;
}
Expand Down Expand Up @@ -135,7 +165,7 @@ mod tests {
request: "".to_string(),
};
let data = parse_protocol_request_data(&pr).unwrap();
assert_eq!(data.request, "test");
assert_eq!(data.request, JsonValue::String("test".to_string()));
}

#[test]
Expand All @@ -160,6 +190,36 @@ mod tests {
assert!(err.to_string().contains("Missing unsigned request data"));
}

#[test]
fn test_parse_protocol_request_data_multisigned_valid() {
let json = r#"{
"protocol": "openid4vp-v1-multisigned",
"data": {
"request": {
"payload": "eyJkY3FsX3F1ZXJ5Ijp7ImNyZWRlbnRpYWxzIjpbXX19",
"signatures": []
}
}
}"#;
let pr: ProtocolRequest = DeJson::deserialize_json(json).unwrap();
let data = parse_protocol_request_data(&pr).unwrap();
assert!(data.dcql_query.is_some());
}


#[test]
fn test_parse_protocol_request_data_multisigned_invalid_request_type() {
let json = r#"{
"protocol": "openid4vp-v1-multisigned",
"data": {
"request": "not a jws json object"
}
}"#;
let pr: ProtocolRequest = DeJson::deserialize_json(json).unwrap();
let err = parse_protocol_request_data(&pr).unwrap_err();
assert!(err.to_string().contains("must be a JWS JSON object"));
}

use crate::test_utils::*;

macro_rules! define_test {
Expand Down Expand Up @@ -208,6 +268,7 @@ mod tests {
);
define_test!(tc30_parse_v1_unsigned, "TC30_ParseV1Unsigned");
define_test!(tc31_parse_v1_signed, "TC31_ParseV1Signed");
define_test!(tc40_parse_v1_multisigned, "TC40_ParseV1Multisigned");
define_test!(tc32_extract_payment_sca1, "TC32_ExtractPaymentSca1");
define_test!(tc33_extract_payment_details, "TC33_ExtractPaymentDetails");
define_test!(tc34_extract_payment_generic, "TC34_ExtractPaymentGeneric");
Expand Down
5 changes: 3 additions & 2 deletions matcher-rs/src/openid4vp_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ pub struct OpenId4VpData {
pub dcql_query: Option<DcqlQuery>,
pub offer: Option<JsonValue>,
pub transaction_data: Vec<String>,
pub request: String,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to avoid using JsonValue as much as possible. It removes the clarity and guardrails provided by static typing.

We should make request an enum of either a string or a struct with the right fields, and implement a custom DeJson for it, just like how ProtocolRequestData is done.

In addition, the payload field seems to be a mistake. Judging from the test data, it is a subfield of request instead.

pub request: JsonValue,
pub payload: Option<String>,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -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]
Expand Down
139 changes: 139 additions & 0 deletions matcher-rs/testdata/TC40_ParseV1Multisigned_expected.json
Original file line number Diff line number Diff line change
@@ -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": ""
}
]
}
18 changes: 18 additions & 0 deletions matcher-rs/testdata/TC40_ParseV1Multisigned_request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"requests": [
{
"data": {
"request": {
"payload": "eyJkY3FsX3F1ZXJ5Ijp7ImNyZWRlbnRpYWxzIjpbeyJmb3JtYXQiOiJtc29fbWRvYyIsImlkIjoibWRsIiwibWV0YSI6eyJkb2N0eXBlX3ZhbHVlIjoib3JnLmlzby4xODAxMy41LjEubURMIn19XX19",
"signatures": [
{
"protected": "eyJhbGciOiJFUzI1NiJ9",
"signature": "c2lnbmF0dXJl"
}
]
}
},
"protocol": "openid4vp-v1-multisigned"
}
]
}
Loading