Skip to content
This repository was archived by the owner on Mar 5, 2025. It is now read-only.

Commit 3f9af34

Browse files
authored
18013-7: Handle missing requested fields. (#106)
1 parent c0211d7 commit 3f9af34

File tree

3 files changed

+86
-41
lines changed

3 files changed

+86
-41
lines changed

src/oid4vp/iso_18013_7/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ impl InProgressRequest180137 {
198198
&self.request,
199199
credential,
200200
approved_fields,
201+
&request_match.missing_fields,
201202
field_map,
202203
mdoc_generated_nonce.clone(),
203204
)?;

src/oid4vp/iso_18013_7/prepare_response.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use isomdl::{
66
cbor,
77
cose::sign1::PreparedCoseSign1,
88
definitions::{
9+
device_response::DocumentErrorCode,
910
device_signed::{DeviceAuthentication, DeviceNamespaces},
1011
helpers::{ByteStr, NonEmptyMap, NonEmptyVec, Tag24},
1112
session::SessionTranscript as SessionTranscriptTrait,
@@ -117,6 +118,7 @@ pub fn prepare_response(
117118
request: &AuthorizationRequestObject,
118119
credential: &Mdoc,
119120
approved_fields: Vec<FieldId180137>,
121+
missing_fields: &BTreeMap<String, String>,
120122
mut field_map: FieldMap,
121123
mdoc_generated_nonce: String,
122124
) -> Result<DeviceResponse> {
@@ -203,14 +205,30 @@ pub fn prepare_response(
203205
device_auth,
204206
};
205207

208+
let mut errors: BTreeMap<String, NonEmptyMap<String, DocumentErrorCode>> = BTreeMap::new();
209+
for (namespace, element_identifier) in missing_fields {
210+
if let Some(elems) = errors.get_mut(namespace) {
211+
elems.insert(
212+
element_identifier.clone(),
213+
DocumentErrorCode::DataNotReturned,
214+
);
215+
} else {
216+
let element_map = NonEmptyMap::new(
217+
element_identifier.clone(),
218+
DocumentErrorCode::DataNotReturned,
219+
);
220+
errors.insert(namespace.clone(), element_map);
221+
}
222+
}
223+
206224
let document = Document {
207225
doc_type: mdoc.mso.doc_type.clone(),
208226
issuer_signed: IssuerSigned {
209227
issuer_auth: mdoc.issuer_auth.clone(),
210228
namespaces: Some(revealed_namespaces),
211229
},
212230
device_signed,
213-
errors: None,
231+
errors: NonEmptyMap::maybe_new(errors),
214232
};
215233

216234
let documents = NonEmptyVec::new(document);

src/oid4vp/iso_18013_7/requested_values.rs

Lines changed: 66 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub struct RequestMatch180137 {
2020
pub credential_id: Uuid,
2121
pub field_map: FieldMap,
2222
pub requested_fields: Vec<RequestedField180137>,
23+
pub missing_fields: BTreeMap<String, String>,
2324
}
2425

2526
uniffi::custom_newtype!(FieldId180137, String);
@@ -76,11 +77,7 @@ where
7677
credentials
7778
.filter_map(
7879
|credential| match find_match(input_descriptor, credential) {
79-
Ok((field_map, requested_fields)) => Some(Arc::new(RequestMatch180137 {
80-
field_map,
81-
requested_fields,
82-
credential_id: credential.id(),
83-
})),
80+
Ok(m) => Some(Arc::new(m)),
8481
Err(e) => {
8582
tracing::info!("credential did not match: {e}");
8683
None
@@ -90,10 +87,7 @@ where
9087
.collect()
9188
}
9289

93-
fn find_match(
94-
input_descriptor: &InputDescriptor,
95-
credential: &Mdoc,
96-
) -> Result<(FieldMap, Vec<RequestedField180137>)> {
90+
fn find_match(input_descriptor: &InputDescriptor, credential: &Mdoc) -> Result<RequestMatch180137> {
9791
let mdoc = credential.document();
9892

9993
if mdoc.mso.doc_type != input_descriptor.id {
@@ -152,6 +146,7 @@ fn find_match(
152146
);
153147

154148
let mut requested_fields = BTreeMap::new();
149+
let mut missing_fields = BTreeMap::new();
155150

156151
let elements_json_ref = &elements_json;
157152

@@ -210,32 +205,50 @@ fn find_match(
210205
},
211206
);
212207
}
213-
None if field.is_required() => bail!(
214-
"missing requested field: {}",
215-
field.path.as_ref()[0].to_string()
216-
),
217-
None => (),
208+
None => {
209+
let json_path = field.path.as_ref()[0].to_string();
210+
if let Some((namespace, element_identifier)) = split_json_path(&json_path) {
211+
missing_fields.insert(namespace, element_identifier);
212+
} else {
213+
tracing::warn!("invalid JSON path expression: {json_path}")
214+
}
215+
}
218216
}
219217
}
220218

221219
let mut seen_age_over_attestations = 0;
220+
let requested_fields = requested_fields
221+
.into_values()
222+
// According to the rules in ISO/IEC 18013-5 Section 7.2.5, don't respond with more
223+
// than 2 age over attestations.
224+
.filter(|field| {
225+
if field.displayable_name.starts_with("age_over_") {
226+
seen_age_over_attestations += 1;
227+
seen_age_over_attestations < 3
228+
} else {
229+
true
230+
}
231+
})
232+
.collect();
222233

223-
Ok((
234+
Ok(RequestMatch180137 {
235+
credential_id: credential.id(),
224236
field_map,
225-
requested_fields
226-
.into_values()
227-
// According to the rules in ISO/IEC 18013-5 Section 7.2.5, don't respond with more
228-
// than 2 age over attestations.
229-
.filter(|field| {
230-
if field.displayable_name.starts_with("age_over_") {
231-
seen_age_over_attestations += 1;
232-
seen_age_over_attestations < 3
233-
} else {
234-
true
235-
}
236-
})
237-
.collect(),
238-
))
237+
requested_fields,
238+
missing_fields,
239+
})
240+
}
241+
242+
fn split_json_path(json_path: &str) -> Option<(String, String)> {
243+
// Find the namespace between "$['" and "']['"".
244+
let (namespace, rest) = json_path.strip_prefix("$['")?.split_once("']['")?;
245+
// Find the element identifier up to "']".
246+
let (element_id, "") = rest.split_once("']")? else {
247+
// Unexpected trailing characters.
248+
return None;
249+
};
250+
251+
Some((namespace.to_string(), element_id.to_string()))
239252
}
240253

241254
fn cbor_to_string(cbor: &Cbor) -> Option<String> {
@@ -397,13 +410,13 @@ mod test {
397410
use super::{parse_request, reverse_mapping};
398411

399412
#[rstest]
400-
#[case::valid("tests/examples/18013_7_presentation_definition.json", true)]
401-
#[case::invalid(
402-
"tests/examples/18013_7_presentation_definition_age_over_25.json",
403-
false
404-
)]
413+
#[case::valid("tests/examples/18013_7_presentation_definition.json", 0)]
414+
#[case::missing("tests/examples/18013_7_presentation_definition_age_over_25.json", 1)]
405415
#[tokio::test]
406-
async fn mdl_matches_presentation_definition(#[case] filepath: &str, #[case] valid: bool) {
416+
async fn mdl_matches_presentation_definition(
417+
#[case] filepath: &str,
418+
#[case] missing_fields: usize,
419+
) {
407420
let key_manager = Arc::new(RustTestKeyManager::default());
408421
let key_alias = KeyAlias("".to_string());
409422

@@ -420,12 +433,11 @@ mod test {
420433

421434
let request = parse_request(&presentation_definition, credentials.iter());
422435

423-
assert_eq!(request.len() == 1, valid);
436+
assert_eq!(request.len(), 1);
424437

425-
if valid {
426-
let request = &request[0];
427-
assert_eq!(request.requested_fields.len(), 12)
428-
}
438+
let request = &request[0];
439+
assert_eq!(request.requested_fields.len(), 12 - missing_fields);
440+
assert_eq!(request.missing_fields.len(), missing_fields);
429441
}
430442

431443
#[test]
@@ -444,4 +456,18 @@ mod test {
444456
_ => panic!("unexpected value"),
445457
})
446458
}
459+
460+
#[rstest]
461+
#[case::valid("$['namespace']['element_id']", true)]
462+
#[case::invalid("$.namespace.element_id", false)]
463+
#[case::trailing("$['namespace']['element_id']['extra']", false)]
464+
fn json_path_splitting(#[case] path: &str, #[case] is_some: bool) {
465+
let Some((namespace, element_id)) = super::split_json_path(path) else {
466+
assert!(!is_some);
467+
return;
468+
};
469+
470+
assert_eq!(namespace, "namespace");
471+
assert_eq!(element_id, "element_id");
472+
}
447473
}

0 commit comments

Comments
 (0)