Skip to content

Commit

Permalink
wip: handle groups in presentation definition, input descriptor tests
Browse files Browse the repository at this point in the history
Signed-off-by: Ryan Tate <[email protected]>
  • Loading branch information
Ryanmtate committed Aug 28, 2024
1 parent 033fd5e commit d98d2a4
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 84 deletions.
36 changes: 27 additions & 9 deletions src/core/input_descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ pub struct InputDescriptor {
purpose: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
format: Option<ClaimFormatMap>,
#[serde(skip_serializing_if = "Option::is_none")]
group: Option<Vec<String>>,
}

impl InputDescriptor {
Expand Down Expand Up @@ -128,6 +130,24 @@ impl InputDescriptor {
self
}

/// Set the group of the constraints field.
pub fn set_group(mut self, group: Vec<String>) -> Self {
self.group = Some(group);
self
}

/// Return the group of the constraints field.
pub fn group(&self) -> Option<&Vec<String>> {
self.group.as_ref()
}

/// Return a mutable reference to the group of the constraints field.
pub fn add_to_group(mut self, member: String) -> Self {
self.group.get_or_insert_with(Vec::new).push(member);

self
}

/// Validate the input descriptor against the verifiable presentation and the descriptor map.
pub fn validate_verifiable_presentation(
&self,
Expand Down Expand Up @@ -300,14 +320,9 @@ impl Constraints {
/// Returns if the constraints fields contain non-optional
/// fields that must be satisfied.
pub fn is_required(&self) -> bool {
if let Some(fields) = self.fields() {
fields.iter().any(|field| field.is_required())
} else {
matches!(
self.limit_disclosure(),
Some(ConstraintsLimitDisclosure::Required)
)
}
self.fields()
.map(|fields| fields.iter().any(|field| field.is_required()))
.unwrap_or(false)
}
}

Expand Down Expand Up @@ -505,13 +520,16 @@ impl ConstraintsField {
}

/// Set the intent to retain the constraints field.
///
/// This value indicates the verifier's intent to retain the
/// field in the presentation, storing the value in the verifier's system.
pub fn set_retained(mut self, intent_to_retain: bool) -> Self {
self.intent_to_retain = Some(intent_to_retain);
self
}

/// Return the intent to retain the constraints field.
pub fn is_intended_to_retain(&self) -> bool {
pub fn intent_to_retain(&self) -> bool {
self.intent_to_retain.unwrap_or(false)
}

Expand Down
50 changes: 48 additions & 2 deletions src/core/presentation_definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::collections::HashMap;

use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use serde_json::Map;
use ssi_claims::jwt::VerifiablePresentation;

/// 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).
Expand All @@ -18,11 +19,13 @@ use ssi_claims::jwt::VerifiablePresentation;
/// in cases where different types of proofs may satisfy an input requirement.
///
/// For more information, see: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition](https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition)
#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct PresentationDefinition {
id: String,
input_descriptors: Vec<InputDescriptor>,
#[serde(skip_serializing_if = "Option::is_none")]
submission_requirements: Option<Vec<SubmissionRequirement>>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
purpose: Option<String>,
Expand Down Expand Up @@ -155,11 +158,15 @@ impl PresentationDefinition {
pub fn validate_definition_map(
&self,
verifiable_presentation: VerifiablePresentation,
descriptor_map: &HashMap<String, DescriptorMap>,
descriptor_map: &HashMap<String, &DescriptorMap>,
) -> Result<()> {
for input_descriptor in self.input_descriptors().iter() {
match descriptor_map.get(input_descriptor.id()) {
None => {
println!("Input Descriptor: {}", input_descriptor.id());

// TODO: check for groups in submission requirements

if input_descriptor.constraints().is_required() {
bail!("Required Input Descriptor ID not found in Descriptor Map.")
}
Expand All @@ -175,3 +182,42 @@ impl PresentationDefinition {
Ok(())
}
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SubmissionRequirementObject {
pub name: Option<String>,
pub purpose: Option<String>,
#[serde(flatten)]
pub property_set: Option<Map<String, serde_json::Value>>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum SubmissionRequirementBase {
From {
from: String,
#[serde(flatten)]
submission_requirement_base: SubmissionRequirementObject,
},
FromNested {
from_nested: Vec<SubmissionRequirement>,
#[serde(flatten)]
submission_requirement_base: SubmissionRequirementObject,
},
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "rule", rename_all = "snake_case")]
pub enum SubmissionRequirement {
All(SubmissionRequirementBase),
Pick(SubmissionRequirementPick),
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SubmissionRequirementPick {
#[serde(flatten)]
pub submission_requirement: SubmissionRequirementBase,
pub count: Option<u64>,
pub min: Option<u64>,
pub max: Option<u64>,
}
48 changes: 8 additions & 40 deletions src/core/presentation_submission.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use super::{credential_format::*, input_descriptor::*};

use serde::{Deserialize, Serialize};
use serde_json::Map;

/// Presentation Submissions are objects embedded within target
/// [Claim](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:claim) negotiation
Expand Down Expand Up @@ -55,6 +54,14 @@ impl PresentationSubmission {
pub fn descriptor_map_mut(&mut self) -> &mut Vec<DescriptorMap> {
&mut self.descriptor_map
}

/// Returns the descriptor map as a mapping of descriptor map id to descriptor map.
pub fn descriptor_map_by_id(&self) -> std::collections::HashMap<String, &DescriptorMap> {
self.descriptor_map
.iter()
.map(|descriptor_map| (descriptor_map.id.clone(), descriptor_map))
.collect()
}
}

/// Descriptor Maps are objects used to describe the information a [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder) provides to a [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier).
Expand Down Expand Up @@ -125,42 +132,3 @@ impl DescriptorMap {
self
}
}

#[derive(Deserialize)]
pub struct SubmissionRequirementBaseBase {
pub name: Option<String>,
pub purpose: Option<String>,
#[serde(flatten)]
pub property_set: Option<Map<String, serde_json::Value>>,
}

#[derive(Deserialize)]
#[serde(untagged)]
pub enum SubmissionRequirementBase {
From {
from: String, // TODO `group` string??
#[serde(flatten)]
submission_requirement_base: SubmissionRequirementBaseBase,
},
FromNested {
from_nested: Vec<SubmissionRequirement>,
#[serde(flatten)]
submission_requirement_base: SubmissionRequirementBaseBase,
},
}

#[derive(Deserialize)]
#[serde(tag = "rule", rename_all = "snake_case")]
pub enum SubmissionRequirement {
All(SubmissionRequirementBase),
Pick(SubmissionRequirementPick),
}

#[derive(Deserialize)]
pub struct SubmissionRequirementPick {
#[serde(flatten)]
pub submission_requirement: SubmissionRequirementBase,
pub count: Option<u64>,
pub min: Option<u64>,
pub max: Option<u64>,
}
10 changes: 2 additions & 8 deletions src/core/response/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use super::{
object::{ParsingErrorContext, UntypedObject},
presentation_definition::PresentationDefinition,
presentation_submission::DescriptorMap,
};

use std::collections::{BTreeMap, HashMap};
use std::collections::BTreeMap;

use anyhow::{bail, Context, Error, Result};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -75,12 +74,7 @@ impl AuthorizationResponse {
bail!("Presentation Definition ID does not match the Presentation Submission.")
}

// Parse the descriptor map into a HashMap for easier access
let descriptor_map: HashMap<String, DescriptorMap> = presentation_submission
.descriptor_map()
.iter()
.map(|descriptor_map| (descriptor_map.id().to_owned(), descriptor_map.clone()))
.collect();
let descriptor_map = presentation_submission.descriptor_map_by_id();

// Parse the VP Token according to the Spec, here:
// https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#section-6.1-2.2
Expand Down
77 changes: 63 additions & 14 deletions src/tests.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
use serde::Deserialize;

// use crate::core::response::AuthorizationResponse;
// pub use crate::utils::NonEmptyVec;

// use anyhow::{bail, Context, Result};
// use jsonschema::{JSONSchema, ValidationError};

// use serde_json::Map;
// use ssi_claims::jwt::VerifiablePresentation;
// use ssi_dids::ssi_json_ld::syntax::from_value;

use crate::core::{presentation_definition::PresentationDefinition, presentation_submission::*};
use crate::core::{
presentation_definition::{PresentationDefinition, SubmissionRequirement},
presentation_submission::*,
};

use serde_json::json;
use std::{
ffi::OsStr,
fs::{self, File},
};

use anyhow::Result;
use serde::Deserialize;
use serde_json::json;
use serde_json::Value;
use ssi_claims::jwt::VerifiablePresentation;

#[test]
fn request_example() {
let value = json!(
Expand Down Expand Up @@ -141,3 +137,56 @@ fn submission_requirements_suite() {
println!("✅")
}
}

#[test]
fn test_input_descriptor_validation() -> Result<()> {
// Include the `input_descriptors_example.json` file in the `examples` directory.
let input_descriptors = include_str!(
"../tests/presentation-exchange/test/presentation-definition/input_descriptors_example.json"
);

println!("Input Descriptors: {:?}", input_descriptors);
let mut value: Value = serde_json::from_str(input_descriptors)?;

let presentation_definition: PresentationDefinition = value
.as_object_mut()
.map(|obj| {
obj.remove("presentation_definition")
.map(|v| serde_json::from_value(v))
})
.flatten()
.expect("failed to parse presentation definition")?;

println!("Presentation Definition: {:?}", presentation_definition);

let presentation_submission = include_str!(
"../tests/presentation-exchange/test/presentation-submission/appendix_VP_example.json"
);

println!("Presentation Submission: {:?}", presentation_submission);

let value: Value = serde_json::from_str(presentation_submission)?;

let presentation_submission: PresentationSubmission = value
.as_object()
.map(|obj| {
obj.get("presentation_submission")
.map(|v| serde_json::from_value(v.clone()))
})
.flatten()
.expect("failed to parse presentation submission")?;

println!("Presentation Submission: {:?}", presentation_submission);

let descriptor_map = presentation_submission.descriptor_map_by_id();

let verifiable_presentation: VerifiablePresentation = serde_json::from_value(value)?;

println!("Verifiable Presentation: {verifiable_presentation:?}");

presentation_definition
.validate_definition_map(verifiable_presentation, &descriptor_map)
.expect("Failed to validate definition map");

Ok(())
}
13 changes: 4 additions & 9 deletions src/verifier/request_signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,7 @@ impl RequestSigner for P256Signer {
type Error = anyhow::Error;

fn alg(&self) -> Result<String, Self::Error> {
Ok(self
.jwk
.algorithm
.map(|alg| alg)
.unwrap_or(Algorithm::ES256)
.to_string())
Ok(self.jwk.algorithm.unwrap_or(Algorithm::ES256).to_string())
}

fn jwk(&self) -> Result<JWK, Self::Error> {
Expand All @@ -83,8 +78,8 @@ impl JWSSigner for P256Signer {
&self,
signing_bytes: &[u8],
) -> std::result::Result<Vec<u8>, ssi_claims::SignatureError> {
self.try_sign(signing_bytes).await.map_err(|e| {
ssi_claims::SignatureError::Other(format!("Failed to sign bytes: {}", e).into())
})
self.try_sign(signing_bytes)
.await
.map_err(|e| ssi_claims::SignatureError::Other(format!("Failed to sign bytes: {}", e)))
}
}
4 changes: 2 additions & 2 deletions tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ async fn w3c_vc_did_client_direct_post() {
.unwrap();

assert_eq!(
&presentation_definition,
parsed_presentation_definition.parsed()
presentation_definition.id(),
parsed_presentation_definition.parsed().id()
);

assert_eq!(&ResponseType::VpToken, request.response_type());
Expand Down

0 comments on commit d98d2a4

Please sign in to comment.