-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Credentials crate & PresentationDefinitionV2 struct definition #18
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
[package] | ||
name = "credentials" | ||
version = "0.1.0" | ||
edition = "2021" | ||
homepage.workspace = true | ||
repository.workspace = true | ||
license-file.workspace = true | ||
|
||
[dependencies] | ||
jsonschema = "0.17.1" | ||
serde = { version = "1.0.193", features = ["derive"] } | ||
serde_json = "1.0.108" | ||
serde_with = "3.4.0" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pub mod presentation_definition_v2; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
use jsonschema::{Draft, JSONSchema}; | ||
use serde::{Deserialize, Serialize}; | ||
use serde_json::Value as JsonValue; | ||
use serde_with::skip_serializing_none; | ||
use std::collections::HashMap; | ||
|
||
/// Presentation Exchange | ||
/// | ||
/// Presentation Exchange specification codifies a Presentation Definition data format Verifiers | ||
/// can use to articulate proof requirements, and a Presentation Submission data format Holders can | ||
/// use to describe proofs submitted in accordance with them. | ||
/// | ||
/// See [Presentation Definition](https://identity.foundation/presentation-exchange/#presentation-definition) | ||
/// for more information. | ||
#[skip_serializing_none] | ||
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Curious why the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
pub struct PresentationDefinitionV2 { | ||
pub id: String, | ||
pub name: Option<String>, | ||
pub purpose: Option<String>, | ||
pub format: Option<Format>, | ||
pub submission_requirements: Option<Vec<SubmissionRequirement>>, | ||
pub input_descriptors: Vec<InputDescriptorV2>, | ||
pub frame: Option<HashMap<String, JsonValue>>, | ||
} | ||
|
||
/// Represents an input descriptor in a presentation definition. | ||
/// | ||
/// See [Input Descriptor](https://identity.foundation/presentation-exchange/#input-descriptor-object) | ||
/// for more information. | ||
#[skip_serializing_none] | ||
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] | ||
pub struct InputDescriptorV2 { | ||
pub id: String, | ||
pub name: Option<String>, | ||
pub purpose: Option<String>, | ||
pub format: Option<Format>, | ||
pub constraints: ConstraintsV2, | ||
} | ||
|
||
/// Represents constraints for an input descriptor. | ||
/// | ||
/// See 'constraints object' defined in | ||
/// [Input Descriptor](https://identity.foundation/presentation-exchange/#input-descriptor-object) | ||
/// for more information. | ||
#[skip_serializing_none] | ||
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] | ||
pub struct ConstraintsV2 { | ||
amika-sq marked this conversation as resolved.
Show resolved
Hide resolved
|
||
pub fields: Option<Vec<FieldV2>>, | ||
pub limit_disclosure: Option<ConformantConsumerDisclosure>, | ||
} | ||
|
||
/// Represents a field in a presentation input descriptor. | ||
/// | ||
/// See 'fields object' as defined in | ||
/// [Input Descriptor](https://identity.foundation/presentation-exchange/#input-descriptor-object) | ||
/// for more information. | ||
#[skip_serializing_none] | ||
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] | ||
pub struct FieldV2 { | ||
pub id: Option<String>, | ||
pub path: Vec<String>, | ||
pub purpose: Option<String>, | ||
pub filter: Option<JsonValue>, | ||
pub predicate: Option<Optionality>, | ||
pub name: Option<String>, | ||
pub optional: Option<bool>, | ||
} | ||
|
||
impl FieldV2 { | ||
pub fn filter_schema(&self) -> Option<JSONSchema> { | ||
self.filter | ||
.as_ref() | ||
.map(|json| { | ||
JSONSchema::options() | ||
.with_draft(Draft::Draft7) | ||
.compile(json) | ||
.ok() | ||
}) | ||
.flatten() | ||
} | ||
} | ||
|
||
/// Enumeration representing consumer disclosure options. | ||
/// | ||
/// Represents the possible values of `limit_disclosure' property as defined in | ||
// [Input Descriptor](https://identity.foundation/presentation-exchange/#input-descriptor-object) | ||
#[derive(Debug, Deserialize, PartialEq, Serialize)] | ||
#[serde(rename_all = "snake_case")] | ||
pub enum ConformantConsumerDisclosure { | ||
Required, | ||
Preferred, | ||
} | ||
|
||
/// Represents the format of a presentation definition | ||
/// | ||
/// See `format` as defined in | ||
/// [Input Descriptor](https://identity.foundation/presentation-exchange/#input-descriptor-object) | ||
/// and [Registry](https://identity.foundation/claim-format-registry/#registry) | ||
#[skip_serializing_none] | ||
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] | ||
pub struct Format { | ||
pub jwt: Option<JwtObject>, | ||
pub jwt_vc: Option<JwtObject>, | ||
pub jwt_vp: Option<JwtObject>, | ||
} | ||
|
||
/// Represents a JWT object. | ||
#[derive(Debug, Deserialize, PartialEq, Serialize)] | ||
pub struct JwtObject { | ||
pub alg: Vec<String>, | ||
} | ||
|
||
/// Represents submission requirements for a presentation definition. | ||
#[skip_serializing_none] | ||
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] | ||
pub struct SubmissionRequirement { | ||
pub name: Option<String>, | ||
pub purpose: Option<String>, | ||
pub rule: Rule, | ||
pub count: Option<u32>, | ||
pub min: Option<u32>, | ||
pub max: Option<u32>, | ||
pub from: Option<String>, | ||
pub from_nested: Option<Vec<SubmissionRequirement>>, | ||
} | ||
|
||
/// Enumeration representing presentation rule options. | ||
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] | ||
#[serde(rename_all = "snake_case")] | ||
pub enum Rule { | ||
#[default] | ||
All, | ||
Pick, | ||
} | ||
|
||
/// Enumeration representing optionality. | ||
#[derive(Debug, Deserialize, PartialEq, Serialize)] | ||
#[serde(rename_all = "snake_case")] | ||
pub enum Optionality { | ||
Required, | ||
Preferred, | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use serde_json::json; | ||
use std::fs; | ||
use std::path::Path; | ||
|
||
#[test] | ||
fn can_serialize() { | ||
let pd = PresentationDefinitionV2 { | ||
id: "tests-pd-id".to_string(), | ||
name: "simple PD".to_string().into(), | ||
purpose: "pd for testing".to_string().into(), | ||
input_descriptors: vec![InputDescriptorV2 { | ||
id: "whatever".to_string(), | ||
purpose: "purpose".to_string().into(), | ||
constraints: ConstraintsV2 { | ||
fields: vec![FieldV2 { | ||
id: "field-id".to_string().into(), | ||
path: vec!["$.issuer".to_string()], | ||
purpose: "purpose".to_string().into(), | ||
filter: json!({"type": "string", "const": "123"}).into(), | ||
..Default::default() | ||
}] | ||
.into(), | ||
limit_disclosure: Some(ConformantConsumerDisclosure::Required), | ||
}, | ||
..Default::default() | ||
}], | ||
..Default::default() | ||
}; | ||
|
||
let serialized_pd = serde_json::to_string(&pd).unwrap(); | ||
|
||
assert!(serialized_pd.contains("input_descriptors")); | ||
assert!(serialized_pd.contains("123")); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not really convinced that this test fully tests serialization, but it's currently what we're doing in the web5-kt test suite. We could really use some test vectors here to have a more robust test of this functionality. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. I think most is already tested in the idempotency test. |
||
} | ||
|
||
#[test] | ||
fn can_deserialize() { | ||
let expected_id = "ec11a434-fe24-479b-aae0-511428b37e4f"; | ||
|
||
let expected_format = Format { | ||
jwt_vc: Some(JwtObject { | ||
alg: vec!["ES256K".to_string(), "EdDSA".to_string()], | ||
}), | ||
..Default::default() | ||
}; | ||
|
||
let expected_input_descriptors = vec![InputDescriptorV2 { | ||
id: "7b928839-f0b1-4237-893d-b27124b57952".to_string(), | ||
constraints: ConstraintsV2 { | ||
fields: Some(vec![ | ||
FieldV2 { | ||
path: vec!["$.iss".to_string(), "$.vc.issuer".to_string()], | ||
filter: Some(json!({"type": "string", "pattern": "^did:[^:]+:.+"})), | ||
..Default::default() | ||
}, | ||
FieldV2 { | ||
path: vec!["$.vc.type[*]".to_string(), "$.type[*]".to_string()], | ||
filter: Some(json!({"type": "string", "const": "SanctionsCredential"})), | ||
..Default::default() | ||
}, | ||
]), | ||
..Default::default() | ||
}, | ||
..Default::default() | ||
}]; | ||
|
||
let pd_string = load_json("tests/resources/pd_sanctions.json"); | ||
let deserialized_pd: PresentationDefinitionV2 = serde_json::from_str(&pd_string).unwrap(); | ||
|
||
assert_eq!(deserialized_pd.id, expected_id); | ||
assert_eq!(deserialized_pd.format, Some(expected_format)); | ||
assert_eq!( | ||
deserialized_pd.input_descriptors, | ||
expected_input_descriptors | ||
); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is the most powerful test. In kt, it checks that JSON is equal (see serialize / deserialize idempotency). Would it make sense to compare the the actual JSON outputs from the pre-serialization JSON vs the serialized and then deserialized JSON? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I couldn't find a canonicalizer written in Rust that behaves the same way as the Kotlin test. Whitespace differences caused issues when comparing the json strings. That said, I did vastly improve the deserialize test to ensure that the JSON is parsed into the expected format here, whereas Kotlin is just testing that it doesn't throw when deserializing. It should be testing the same thing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Curious if you took a look into https://docs.rs/serde_jcs/latest/serde_jcs/ ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Multiple times. Works well, but I couldn't get it to match white space between json of just the string vs deserialized jcs. I don't know why yet. In all honesty, I don't think it's worth fretting over yet. I'll make a task in the backlog to figure it out. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good! I'd love to take a look at that as a first stab at rust! |
||
} | ||
|
||
fn load_json(path: &str) -> String { | ||
let path = Path::new(path); | ||
let json = fs::read_to_string(path).expect("Unable to load json file"); | ||
json | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
{ | ||
"id": "ec11a434-fe24-479b-aae0-511428b37e4f", | ||
"format": { | ||
"jwt_vc": { | ||
"alg": [ | ||
"ES256K", | ||
"EdDSA" | ||
] | ||
} | ||
}, | ||
"input_descriptors": [ | ||
{ | ||
"id": "7b928839-f0b1-4237-893d-b27124b57952", | ||
"constraints": { | ||
"fields": [ | ||
{ | ||
"path": [ | ||
"$.iss", | ||
"$.vc.issuer" | ||
], | ||
"filter": { | ||
"type": "string", | ||
"pattern": "^did:[^:]+:.+" | ||
} | ||
}, | ||
{ | ||
"path": [ | ||
"$.vc.type[*]", | ||
"$.type[*]" | ||
], | ||
"filter": { | ||
"type": "string", | ||
"const": "SanctionsCredential" | ||
} | ||
} | ||
] | ||
} | ||
} | ||
] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious why 2021?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's editions. More info here:
#17 (comment)