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
78 changes: 68 additions & 10 deletions matcher-rs/src/openid4vp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,35 @@ 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 req_data = match &pr.data {
Some(ProtocolRequestData::Object(obj)) => &obj.request,
_ => return Err("Missing or invalid data object for multisigned request".into()),
};

match req_data {
OpenId4VpRequestData::Object(obj) if !obj.payload.is_empty() => Ok(obj.payload.as_str()),
OpenId4VpRequestData::Object(_) => {
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 {
OpenId4VpRequestData::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 +53,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 +129,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 +162,7 @@ mod tests {
request: "".to_string(),
};
let data = parse_protocol_request_data(&pr).unwrap();
assert_eq!(data.request, "test");
assert!(matches!(&data.request, OpenId4VpRequestData::String(s) if s == "test"));
}

#[test]
Expand All @@ -160,6 +187,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 +265,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
67 changes: 65 additions & 2 deletions matcher-rs/src/openid4vp_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,57 @@ 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: OpenId4VpRequestData,
}

#[derive(Debug, Clone)]
pub enum OpenId4VpRequestData {

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.

This struct is only for signed requests, right? Let's add "signed" to the name so it is clear to readers that unsigned requests have no use for this struct.

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 was referring to it as a struct, but it is actually an enum. Sorry for the mistake. The naming comment still stands.

String(String),
Object(OpenId4VpRequestObject),
}

impl Default for OpenId4VpRequestData {
fn default() -> Self {
OpenId4VpRequestData::String(String::new())
}
}

impl DeJson for OpenId4VpRequestData {
fn de_json(
state: &mut nanoserde::DeJsonState,
input: &mut std::str::Chars,
) -> Result<Self, nanoserde::DeJsonErr> {
match state.tok {
nanoserde::DeJsonTok::Str => {
let s = state.strbuf.clone();
state.next_tok(input)?;
Ok(OpenId4VpRequestData::String(s))
}
nanoserde::DeJsonTok::CurlyOpen => {
let data = DeJson::de_json(state, input)?;
Ok(OpenId4VpRequestData::Object(data))
}
_ => Err(state.err_exp("String or Object")),
}
}
}

#[derive(DeJson, Debug, Clone, Default)]
#[nserde(default)]
pub struct OpenId4VpRequestObject {

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.

Similarly, if this is only valid in the context multisigned, we should add multisigned to its name. If this struct is valid for both single-signed and multi-signed cases, we should add signed to its name instead.

pub payload: String,
pub signatures: Vec<OpenId4VpSignature>,
}

#[derive(DeJson, Debug, Clone, Default)]
#[nserde(default)]
pub struct OpenId4VpSignature {
pub protected: Option<String>,
pub header: Option<JsonValue>,
pub signature: String,
}


#[derive(Debug, Clone)]
pub enum ProtocolRequestData {
String(String),
Expand Down Expand Up @@ -394,7 +442,22 @@ 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 matches!(&obj.request, OpenId4VpRequestData::String(s) if s == "test_req")));

let json_multisigned_obj = r#"{"request":{"payload":"test_payload","signatures":[{"signature":"test_sig"}]}}"#;
let data: ProtocolRequestData =
DeJson::deserialize_json(json_multisigned_obj).expect("Failed to parse multisigned object ProtocolRequestData");
if let ProtocolRequestData::Object(obj) = data {
if let OpenId4VpRequestData::Object(req_obj) = &obj.request {
assert_eq!(req_obj.payload, "test_payload");
assert_eq!(req_obj.signatures.len(), 1);
assert_eq!(req_obj.signatures[0].signature, "test_sig");
} else {
panic!("Expected OpenId4VpRequestData::Object");
}
} else {
panic!("Expected ProtocolRequestData::Object");
}
}

#[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