Skip to content

Commit

Permalink
credential type hint method for constraint field
Browse files Browse the repository at this point in the history
  • Loading branch information
Ryanmtate committed Sep 25, 2024
1 parent 4c6e2ba commit c24b4bf
Show file tree
Hide file tree
Showing 6 changed files with 699 additions and 15 deletions.
187 changes: 179 additions & 8 deletions src/core/input_descriptor.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashSet;

use super::{credential_format::*, presentation_submission::*};
use crate::utils::NonEmptyVec;

Expand Down Expand Up @@ -135,6 +137,23 @@ impl InputDescriptor {
self
}

/// Return the format of the input descriptor.
///
/// The Input Descriptor Object MAY contain a format property. If present,
/// its value MUST be an object with one or more properties matching the registered
/// Claim Format Designations (e.g., jwt, jwt_vc, jwt_vp, etc.).
///
/// This format property is identical in value signature to the top-level format object,
/// but can be used to specifically constrain submission of a single input to a subset of formats or algorithms.
pub fn format(&self) -> &ClaimFormatMap {
&self.format
}

/// Return the format designations of the input descriptor as a hash set.
pub fn format_designations(&self) -> HashSet<&ClaimFormatDesignation> {
self.format.keys().collect()
}

/// Set the group of the constraints field.
pub fn set_group(mut self, group: Vec<GroupId>) -> Self {
self.group = group;
Expand Down Expand Up @@ -254,16 +273,70 @@ impl InputDescriptor {
Ok(())
}

/// Return the format of the input descriptor.
/// Return the humanly readable requested fields of the input descriptor.
pub fn requested_fields(&self) -> Vec<String> {
self.constraints()
.fields()
.iter()
.flat_map(|field| field.requested_fields())
.collect()
}

/// Return the credential types of the input descriptor, if any.
pub fn credential_types_hint(&self) -> Vec<CredentialType> {
self.constraints()
.fields()
.iter()
.flat_map(|field| field.credential_types_hint())
.collect()
}

/// Return the requested fields and the associated credential type(s) of the input descriptor.
pub fn requested_fields_with_credential_types(&self) -> CredentialTypesRequestedFields {
CredentialTypesRequestedFields {
input_descriptor_id: self.id.clone(),
credential_type_hint: self.credential_types_hint(),
requested_fields: self.requested_fields(),
}
}
}

/// A parsed object containing the credential type(s) and their
/// respective requested fields, parsed from the input descriptor contraints fields.
///
/// NOTE: This object is not part of the OID4VP specification, but is used to simplify the
/// extraction of the requested fields and credential types from the input descriptor.
///
/// If the credential types hint is non-empty, then the holder MUST select from the list of
/// credentials that satisfies the requested fields. Otherwise, if the list is empty, the holder
/// may choose to select any credential that satisfies the requested fields.
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct CredentialTypesRequestedFields {
input_descriptor_id: String,
credential_type_hint: Vec<CredentialType>,
requested_fields: Vec<String>,
}

impl CredentialTypesRequestedFields {
/// Return the input descriptor ID.
pub fn input_descriptor_id(&self) -> &str {
&self.input_descriptor_id
}

/// Return the credential type hint(s).
///
/// The Input Descriptor Object MAY contain a format property. If present,
/// its value MUST be an object with one or more properties matching the registered
/// Claim Format Designations (e.g., jwt, jwt_vc, jwt_vp, etc.).
/// NOTE: If credential types hint is non-empty, then the holder MUST
/// select from the list of credentials that satisfies the requested fields.
///
/// This format property is identical in value signature to the top-level format object,
/// but can be used to specifically constrain submission of a single input to a subset of formats or algorithms.
pub fn format(&self) -> &ClaimFormatMap {
&self.format
/// Otherwise, if the list is empty, the holder may choose to select any
/// credential that satisfies the requested fields.
pub fn credential_type_hint(&self) -> &[CredentialType] {
&self.credential_type_hint
}

/// Return the requested fields.
pub fn requested_fields(&self) -> &[String] {
&self.requested_fields
}
}

Expand Down Expand Up @@ -588,6 +661,104 @@ impl ConstraintsField {
})
.collect()
}

/// Returns the Credential Type(s) found in the constraints field.
///
/// Note: This is a `hint` in that it is not guaranteed that the credential type
/// can be parsed from the input descriptor.
///
/// This will return an empty vector if the credential type cannot be parsed.
///
/// Multiple credentials can be returned if the input descriptor contains a pattern
/// filter that matches multiple credentials.
pub fn credential_types_hint(&self) -> Vec<CredentialType> {
// NOTE: There may be other ways to search for a valid the credential type
// that meets the input descriptor constraints.
//
// A more exhaustive search may require parsing each credential to
// check if it contains a certain field, e.g. `firstName`, `familyName`, etc.,
// and see if it will satisfy the constraints.
//
// For now, we explicity check the type of the credential if it is present
// in the credential `type` field.

let mut parsed_credentials = Vec::new();

if self
.path
.as_ref()
.iter()
// Check if any of the paths contain a reference to type.
// NOTE: It may not be guaranteed or normative that a `type` field to the path
// for a verifiable credential is present.
.any(|path| path.contains(&"type".to_string()))
{
// Check the filter field to determine the `const`
// value for the credential type, e.g. `iso.org.18013.5.1.mDL`, etc.
if let Some(credential) = self.filter.as_ref().and_then(|filter| {
filter
.get("const")
.and_then(serde_json::Value::as_str)
.map(CredentialType::from)
}) {
parsed_credentials.push(credential);
}

// The `type` field may be an array with a nested `const` value.
if let Some(credential) = self.filter.as_ref().and_then(|filter| {
filter
.get("contains")
.and_then(|value| value.get("const"))
.and_then(serde_json::Value::as_str)
.map(CredentialType::from)
}) {
parsed_credentials.push(credential);
}

// The `type` field may be an array with a nested `enum` value.
if let Some(credential) = self.filter.as_ref().and_then(|filter| {
filter
.get("contains")
.and_then(|value| value.get("enum"))
.and_then(serde_json::Value::as_array)
.map(|values| {
values
.iter()
.filter_map(serde_json::Value::as_str)
.map(CredentialType::from)
.collect::<Vec<String>>()
})
}) {
parsed_credentials.extend(credential);
}

// Check a pattern for the filter that may include multiple credentials
// that may satisfy the constraints.
if let Some(credentials) = self.filter.as_ref().and_then(|filter| {
filter
.get("pattern")
.and_then(serde_json::Value::as_str)
.map(|pattern| {
// Remove the start (^) and end ($) anchors
let trimmed = pattern.trim_start_matches('^').trim_end_matches('$');

// Remove the outer parentheses
let inner = trimmed.trim_start_matches('(').trim_end_matches(')');

// Split by the '|' character
inner
.split('|')
.map(|s| s.to_string())
.collect::<Vec<CredentialType>>()
})
}) {
// Found multiple credentials that may satisfy the constraints.
parsed_credentials.extend(credentials);
}
}

parsed_credentials
}
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
Expand Down
147 changes: 140 additions & 7 deletions src/core/presentation_definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ use serde::{Deserialize, Serialize};
use serde_json::Map;
use ssi::claims::jwt::VerifiablePresentation;

/// A non-normative mappings of credential type(s) to requested fields.
///
/// This is used for parsing human-readable requested fields and their associated credential types,
/// if any are provided.
///
/// This type is not part of the OID4VP specification, but is provided as a part of helper method for presenting
/// information about the presentation definition to the holder.
pub type CredentialTypesRequestedMap = HashMap<String, CredentialTypesRequestedFields>;

/// A presentation definition is a JSON object that describes the information a [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier) requires of a [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder).
///
/// > Presentation Definitions are objects that articulate what proofs a [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier) requires.
Expand Down Expand Up @@ -190,14 +199,30 @@ impl PresentationDefinition {
pub fn requested_fields(&self) -> Vec<String> {
self.input_descriptors
.iter()
.flat_map(|input_descriptor| {
input_descriptor
.constraints()
.fields()
.iter()
.map(|constraint| constraint.requested_fields())
.flat_map(|descriptor| descriptor.requested_fields())
.collect()
}

/// Return the credential types requested in the presentation definition,
/// if any.
pub fn credential_types_hint(&self) -> Vec<CredentialType> {
self.input_descriptors
.iter()
.flat_map(|descriptor| descriptor.credential_types_hint())
.collect()
}

/// Returns a map of the input descriptor ID to the credential type(s)
/// and their requested fields.
pub fn requested_credential_types_map(&self) -> CredentialTypesRequestedMap {
self.input_descriptors
.iter()
.map(|descriptor| {
(
descriptor.id().to_string(),
descriptor.requested_fields_with_credential_types(),
)
})
.flatten()
.collect()
}

Expand Down Expand Up @@ -371,3 +396,111 @@ pub struct SubmissionRequirementPick {
pub min: Option<usize>,
pub max: Option<usize>,
}

#[cfg(test)]
mod tests {
use super::*;

use anyhow::Result;

#[test]
fn test_input_descriptor_credential_type() -> Result<()> {
let definition: PresentationDefinition = serde_json::from_str(include_str!(
"../../tests/presentation-definition/iso.org.18013.5.1.mdl.json"
))?;

let credentials = definition.credential_types_hint();

assert_eq!(
credentials.first(),
Some(&"iso.org.18013.5.1.mDL".to_string())
);

Ok(())
}

#[test]
fn test_input_descriptor_multi_credential_types_pattern() -> Result<()> {
let definition: PresentationDefinition = serde_json::from_str(include_str!(
"../../tests/presentation-definition/multi-credential-pattern.json"
))?;

let credentials = definition.credential_types_hint();

assert!(credentials.contains(&"PassportCredential".into()));
assert!(credentials.contains(&"DriversLicenseCredential".into()));
assert!(credentials.contains(&"NationalIDCredential".into()));

Ok(())
}

#[test]
fn test_input_descriptor_multi_credential_types_array() -> Result<()> {
let definition: PresentationDefinition = serde_json::from_str(include_str!(
"../../tests/presentation-definition/multi-credential-array.json"
))?;

let credentials = definition.credential_types_hint();

assert!(credentials.contains(&"IdentityCredential".into()));
assert!(credentials.contains(&"EducationalCredential".into()));

Ok(())
}

#[test]
fn test_input_descriptor_multi_input_credential_types_enum() -> Result<()> {
let definition: PresentationDefinition = serde_json::from_str(include_str!(
"../../tests/presentation-definition/multi-input-credential-enum.json"
))?;

let input_descriptor_id_1 = "identity_credential";
let input_descriptor_id_2 = "educational_credential";
let input_descriptor_id_3 = "professional_credential";

let requested_credentials = definition.requested_credential_types_map();

assert_eq!(requested_credentials.len(), 3);

let request_1 = requested_credentials
.get(input_descriptor_id_1)
.expect("failed to find input descriptor id");

assert!(request_1
.credential_type_hint()
.contains(&"PassportCredential".into()));
assert!(request_1
.credential_type_hint()
.contains(&"DriversLicenseCredential".into()));
assert!(request_1
.credential_type_hint()
.contains(&"NationalIDCredential".into()));

let request_2 = requested_credentials
.get(input_descriptor_id_2)
.expect("failed to find input descriptor id");

assert!(request_2
.credential_type_hint()
.contains(&"BachelorDegreeCredential".into()));
assert!(request_2
.credential_type_hint()
.contains(&"MasterDegreeCredential".into()));
assert!(request_2
.credential_type_hint()
.contains(&"DoctoralDegreeCredential".into()));

let request_3 = requested_credentials
.get(input_descriptor_id_3)
.expect("failed to find input descriptor id");

assert!(request_3
.credential_type_hint()
.contains(&"ProfessionalLicenseCredential".into()));
assert!(request_3
.credential_type_hint()
.contains(&"CertificationCredential".into()));

Ok(())
}
}
Loading

0 comments on commit c24b4bf

Please sign in to comment.