Skip to content

Commit

Permalink
use vec instead of option, parse pattern filters for credential types
Browse files Browse the repository at this point in the history
Signed-off-by: Ryan Tate <[email protected]>
  • Loading branch information
Ryanmtate committed Sep 19, 2024
1 parent e46ec41 commit 521895e
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 8 deletions.
89 changes: 81 additions & 8 deletions src/core/input_descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,8 +596,16 @@ impl ConstraintsField {
.collect()
}

/// Return the Credential Type of the constraints field
pub fn credential_type(&self) -> Option<CredentialType> {
/// 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_type_hint(&self) -> Vec<CredentialType> {
// NOTE: There may be other ways to search for a valid the credential type
// that meets the input descriptor constraints.
//
Expand All @@ -608,13 +616,15 @@ impl ConstraintsField {
// 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: I am not sure if this is normative to add a `type` field to the path
// for a verifiable credential.
// 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`
Expand All @@ -625,7 +635,7 @@ impl ConstraintsField {
.and_then(serde_json::Value::as_str)
.map(CredentialType::from)
}) {
return Some(credential);
parsed_credentials.push(credential);
}

// The `type` field may be an array with a nested const value.
Expand All @@ -636,11 +646,35 @@ impl ConstraintsField {
.and_then(serde_json::Value::as_str)
.map(CredentialType::from)
}) {
return Some(credential);
parsed_credentials.push(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);
}
}

None
parsed_credentials
}
}

Expand Down Expand Up @@ -669,7 +703,7 @@ mod tests {
.input_descriptors()
.iter()
.flat_map(|descriptor| descriptor.constraints().fields())
.filter_map(|field| field.credential_type())
.flat_map(|field| field.credential_type_hint())
.collect::<Vec<CredentialType>>();

assert_eq!(
Expand All @@ -679,4 +713,43 @@ mod tests {

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
.input_descriptors()
.iter()
.flat_map(|descriptor| descriptor.constraints().fields())
.flat_map(|field| field.credential_type_hint())
.collect::<Vec<CredentialType>>();

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
.input_descriptors()
.iter()
.flat_map(|descriptor| descriptor.constraints().fields())
.flat_map(|field| field.credential_type_hint())
.collect::<Vec<CredentialType>>();

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

Ok(())
}
}
88 changes: 88 additions & 0 deletions tests/presentation-definition/multi-credential-array.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
{
"id": "32f54163-7166-48f1-93d8-ff217bdb0653",
"name": "Identity and Qualifications Verification",
"purpose": "We need to verify your identity and qualifications",
"format": {
"jwt_vp": {
"alg": ["EdDSA", "ES256K"]
},
"jwt_vc": {
"alg": ["EdDSA", "ES256K"]
},
"ldp_vc": {
"proof_type": ["Ed25519Signature2018", "EcdsaSecp256k1Signature2019"]
},
"ldp_vp": {
"proof_type": ["Ed25519Signature2018", "EcdsaSecp256k1Signature2019"]
}
},
"input_descriptors": [
{
"id": "combined_credential",
"name": "Identity and Education Credential",
"purpose": "Please provide a credential that includes both identity and educational information",
"constraints": {
"fields": [
{
"path": ["$.type", "$.vc.type"],
"filter": {
"type": "array",
"contains": {
"const": "IdentityCredential"
}
}
},
{
"path": ["$.type", "$.vc.type"],
"filter": {
"type": "array",
"contains": {
"const": "EducationalCredential"
}
}
},
{
"path": ["$.credentialSubject.firstName", "$.vc.credentialSubject.firstName"],
"purpose": "The credential must contain the holder's first name",
"filter": {
"type": "string",
"minLength": 1
}
},
{
"path": ["$.credentialSubject.lastName", "$.vc.credentialSubject.lastName"],
"purpose": "The credential must contain the holder's last name",
"filter": {
"type": "string",
"minLength": 1
}
},
{
"path": ["$.credentialSubject.dateOfBirth", "$.vc.credentialSubject.dateOfBirth"],
"purpose": "The credential must contain the holder's date of birth",
"filter": {
"type": "string",
"format": "date"
}
},
{
"path": ["$.credentialSubject.degree.name", "$.vc.credentialSubject.degree.name"],
"purpose": "The credential must contain the name of an educational degree",
"filter": {
"type": "string",
"minLength": 1
}
},
{
"path": ["$.credentialSubject.degree.institution", "$.vc.credentialSubject.degree.institution"],
"purpose": "The credential must contain the name of the educational institution",
"filter": {
"type": "string",
"minLength": 1
}
}
]
}
}
]
}
70 changes: 70 additions & 0 deletions tests/presentation-definition/multi-credential-pattern.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"id": "32f54163-7166-48f1-93d8-ff217bdb0653",
"name": "Identity Verification",
"purpose": "We need to verify your identity for account creation",
"format": {
"jwt_vp": {
"alg": ["EdDSA", "ES256K"]
},
"jwt_vc": {
"alg": ["EdDSA", "ES256K"]
},
"ldp_vc": {
"proof_type": ["Ed25519Signature2018", "EcdsaSecp256k1Signature2019"]
},
"ldp_vp": {
"proof_type": ["Ed25519Signature2018", "EcdsaSecp256k1Signature2019"]
}
},
"input_descriptors": [
{
"id": "identity_credential",
"name": "Identity Document",
"purpose": "Please provide a government-issued identity document",
"constraints": {
"fields": [
{
"path": ["$.type"],
"filter": {
"type": "string",
"pattern": "^(PassportCredential|DriversLicenseCredential|NationalIDCredential)$"
}
},
{
"path": ["$.credentialSubject.firstName"],
"purpose": "The credential must contain the holder's first name",
"filter": {
"type": "string",
"minLength": 1
}
},
{
"path": ["$.credentialSubject.lastName"],
"purpose": "The credential must contain the holder's last name",
"filter": {
"type": "string",
"minLength": 1
}
},
{
"path": ["$.credentialSubject.dateOfBirth"],
"purpose": "The credential must contain the holder's date of birth",
"filter": {
"type": "string",
"format": "date"
}
},
{
"path": ["$.issuanceDate"],
"purpose": "The credential must have been issued within the last 5 years",
"filter": {
"type": "string",
"format": "date",
"minimum": "2018-01-01"
}
}
]
}
}
]
}

0 comments on commit 521895e

Please sign in to comment.