From d37386b1f74de2c6f53bacbb59a7f9926dcfc4b0 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Tue, 5 Sep 2023 10:55:24 +0200 Subject: [PATCH 01/10] replace OneOrMany with Vec --- examples/0_basic/6_create_vp.rs | 3 +-- identity_credential/src/presentation/jwt_serialization.rs | 2 +- identity_credential/src/presentation/presentation.rs | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/0_basic/6_create_vp.rs b/examples/0_basic/6_create_vp.rs index 3dfade3a5f..2597ade733 100644 --- a/examples/0_basic/6_create_vp.rs +++ b/examples/0_basic/6_create_vp.rs @@ -12,7 +12,6 @@ use std::collections::HashMap; use examples::create_did; use examples::MemStorage; use identity_iota::core::Object; -use identity_iota::core::OneOrMany; use identity_iota::credential::DecodedJwtCredential; use identity_iota::credential::DecodedJwtPresentation; use identity_iota::credential::Jwt; @@ -201,7 +200,7 @@ async fn main() -> anyhow::Result<()> { JwtPresentationValidator::new().validate(&presentation_jwt, &holder, &presentation_validation_options)?; // Concurrently resolve the issuers' documents. - let jwt_credentials: &OneOrMany = &presentation.presentation.verifiable_credential; + let jwt_credentials: &Vec = &presentation.presentation.verifiable_credential; let issuers: Vec = jwt_credentials .iter() .map(JwtCredentialValidator::extract_issuer_from_jwt) diff --git a/identity_credential/src/presentation/jwt_serialization.rs b/identity_credential/src/presentation/jwt_serialization.rs index 28625f0468..5122edc420 100644 --- a/identity_credential/src/presentation/jwt_serialization.rs +++ b/identity_credential/src/presentation/jwt_serialization.rs @@ -112,7 +112,7 @@ where types: Cow<'presentation, OneOrMany>, /// Credential(s) expressing the claims of the `Presentation`. #[serde(default = "Default::default", rename = "verifiableCredential")] - pub(crate) verifiable_credential: Cow<'presentation, OneOrMany>, + pub(crate) verifiable_credential: Cow<'presentation, Vec>, /// Service(s) used to refresh an expired [`Credential`] in the `Presentation`. #[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")] refresh_service: Cow<'presentation, OneOrMany>, diff --git a/identity_credential/src/presentation/presentation.rs b/identity_credential/src/presentation/presentation.rs index 2a4f1c4a59..a6ae9074bc 100644 --- a/identity_credential/src/presentation/presentation.rs +++ b/identity_credential/src/presentation/presentation.rs @@ -39,7 +39,7 @@ pub struct Presentation { pub types: OneOrMany, /// Credential(s) expressing the claims of the `Presentation`. #[serde(default = "Default::default", rename = "verifiableCredential")] - pub verifiable_credential: OneOrMany, + pub verifiable_credential: Vec, /// The entity that generated the `Presentation`. pub holder: Url, /// Service(s) used to refresh an expired [`Credential`] in the `Presentation`. From 0e20b766f2ade4e8f0fee49ba61084d533854fad Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Tue, 5 Sep 2023 14:58:34 +0200 Subject: [PATCH 02/10] remove useless conversion --- identity_credential/src/presentation/presentation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/identity_credential/src/presentation/presentation.rs b/identity_credential/src/presentation/presentation.rs index a6ae9074bc..10287f7162 100644 --- a/identity_credential/src/presentation/presentation.rs +++ b/identity_credential/src/presentation/presentation.rs @@ -80,7 +80,7 @@ impl Presentation { context: builder.context.into(), id: builder.id, types: builder.types.into(), - verifiable_credential: builder.credentials.into(), + verifiable_credential: builder.credentials, holder: builder.holder, refresh_service: builder.refresh_service.into(), terms_of_use: builder.terms_of_use.into(), From 617428f14ce61ee789258f7a048defb4695cde3a Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Tue, 5 Sep 2023 16:46:51 +0200 Subject: [PATCH 03/10] finetune serde, add unit tests --- .../src/presentation/presentation.rs | 68 ++++++++++++++++++- .../src/presentation/presentation_builder.rs | 18 +++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/identity_credential/src/presentation/presentation.rs b/identity_credential/src/presentation/presentation.rs index 10287f7162..d9a9f9dee7 100644 --- a/identity_credential/src/presentation/presentation.rs +++ b/identity_credential/src/presentation/presentation.rs @@ -38,7 +38,7 @@ pub struct Presentation { #[serde(rename = "type")] pub types: OneOrMany, /// Credential(s) expressing the claims of the `Presentation`. - #[serde(default = "Default::default", rename = "verifiableCredential")] + #[serde(default, rename = "verifiableCredential", skip_serializing_if = "Vec::is_empty")] pub verifiable_credential: Vec, /// The entity that generated the `Presentation`. pub holder: Url, @@ -143,3 +143,69 @@ where self.fmt_json(f) } } + +#[cfg(test)] +mod tests { + use crate::presentation::Presentation; + use identity_core::common::Object; + use serde_json::json; + + #[test] + fn test_presentation_deserialization() { + // Example verifiable presentation taken from: + // https://www.w3.org/TR/vc-data-model/#example-a-simple-example-of-a-verifiable-presentation + // with some minor adjustments (adding the `holder` property and shortening the 'jws' values). + assert!(serde_json::from_value::>(json!({ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "holder": "did:test:abc1", + "type": "VerifiablePresentation", + "verifiableCredential": [{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "id": "http://example.edu/credentials/1872", + "type": ["VerifiableCredential", "AlumniCredential"], + "issuer": "https://example.edu/issuers/565049", + "issuanceDate": "2010-01-01T19:23:24Z", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "alumniOf": { + "id": "did:example:c276e12ec21ebfeb1f712ebc6f1", + "name": [{ + "value": "Example University", + "lang": "en" + }, { + "value": "Exemple d'Université", + "lang": "fr" + }] + } + }, + "proof": { + "type": "RsaSignature2018", + "created": "2017-06-18T21:19:10Z", + "proofPurpose": "assertionMethod", + "verificationMethod": "https://example.edu/issuers/565049#key-1", + "jws": "eyJhb...dBBPM" + } + }], + })) + .is_ok()); + } + + #[test] + fn test_presentation_deserialization_without_credentials() { + assert!(serde_json::from_value::>(json!({ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "holder": "did:test:abc1", + "type": "VerifiablePresentation" + })) + .is_ok()); + } +} diff --git a/identity_credential/src/presentation/presentation_builder.rs b/identity_credential/src/presentation/presentation_builder.rs index e6c750b42d..3831ad472f 100644 --- a/identity_credential/src/presentation/presentation_builder.rs +++ b/identity_credential/src/presentation/presentation_builder.rs @@ -174,4 +174,22 @@ mod tests { assert_eq!(presentation.types.get(1).unwrap(), "ExamplePresentation"); assert_eq!(presentation.verifiable_credential.len(), 1); } + + #[test] + fn test_presentation_builder_valid_without_credentials() { + let presentation: Presentation = PresentationBuilder::new(Url::parse("did:test:abc1").unwrap(), Object::new()) + .type_("ExamplePresentation") + .build() + .unwrap(); + + assert_eq!(presentation.context.len(), 1); + assert_eq!( + presentation.context.get(0).unwrap(), + Presentation::::base_context() + ); + assert_eq!(presentation.types.len(), 2); + assert_eq!(presentation.types.get(0).unwrap(), Presentation::::base_type()); + assert_eq!(presentation.types.get(1).unwrap(), "ExamplePresentation"); + assert_eq!(presentation.verifiable_credential.len(), 0); + } } From 3fc9607d26578cea6a7609c555dc5dcb4c03c6ae Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Tue, 5 Sep 2023 17:39:30 +0200 Subject: [PATCH 04/10] fix WASM error --- identity_credential/src/presentation/presentation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/identity_credential/src/presentation/presentation.rs b/identity_credential/src/presentation/presentation.rs index d9a9f9dee7..3a0c5b26d6 100644 --- a/identity_credential/src/presentation/presentation.rs +++ b/identity_credential/src/presentation/presentation.rs @@ -38,7 +38,7 @@ pub struct Presentation { #[serde(rename = "type")] pub types: OneOrMany, /// Credential(s) expressing the claims of the `Presentation`. - #[serde(default, rename = "verifiableCredential", skip_serializing_if = "Vec::is_empty")] + #[serde(default = "Default::default", rename = "verifiableCredential", skip_serializing_if = "Vec::is_empty")] pub verifiable_credential: Vec, /// The entity that generated the `Presentation`. pub holder: Url, From df87e88b3ffbd8bfeaa9f576b81afc91058da2d5 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Tue, 5 Sep 2023 17:48:11 +0200 Subject: [PATCH 05/10] add rustfmt::skip --- identity_credential/src/presentation/presentation.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/identity_credential/src/presentation/presentation.rs b/identity_credential/src/presentation/presentation.rs index 3a0c5b26d6..80e32362b1 100644 --- a/identity_credential/src/presentation/presentation.rs +++ b/identity_credential/src/presentation/presentation.rs @@ -38,6 +38,7 @@ pub struct Presentation { #[serde(rename = "type")] pub types: OneOrMany, /// Credential(s) expressing the claims of the `Presentation`. + #[rustfmt::skip] #[serde(default = "Default::default", rename = "verifiableCredential", skip_serializing_if = "Vec::is_empty")] pub verifiable_credential: Vec, /// The entity that generated the `Presentation`. From 72f03ac0f3168b7f219d861360b7972634631e4a Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Wed, 6 Sep 2023 19:35:05 +0200 Subject: [PATCH 06/10] disallow empty verifiableCredential array --- identity_credential/src/error.rs | 5 +++ .../src/presentation/presentation.rs | 42 ++++++++++++++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs index daa93a5139..cb7dc42068 100644 --- a/identity_credential/src/error.rs +++ b/identity_credential/src/error.rs @@ -45,6 +45,11 @@ pub enum Error { #[error("could not convert JWT to the VC data model: {0}")] InconsistentCredentialJwtClaims(&'static str), + /// Caused when deserializing a Presentation with an empty array for the + /// `verifiableCredential` property. + #[error("empty verifiableCredential array")] + EmptyVerifiableCredentialsArray, + /// Caused when attempting to convert a JWT to a `Presentation` that has conflicting values /// between the registered claims and those in the `vp` object. #[error("could not convert JWT to the VP data model: {0}")] diff --git a/identity_credential/src/presentation/presentation.rs b/identity_credential/src/presentation/presentation.rs index 80e32362b1..a65ebf14d5 100644 --- a/identity_credential/src/presentation/presentation.rs +++ b/identity_credential/src/presentation/presentation.rs @@ -4,6 +4,7 @@ use core::fmt::Display; use core::fmt::Formatter; +use serde::de; use serde::Deserialize; use serde::Serialize; @@ -39,7 +40,7 @@ pub struct Presentation { pub types: OneOrMany, /// Credential(s) expressing the claims of the `Presentation`. #[rustfmt::skip] - #[serde(default = "Default::default", rename = "verifiableCredential", skip_serializing_if = "Vec::is_empty")] + #[serde(default = "Default::default", rename = "verifiableCredential", skip_serializing_if = "Vec::is_empty", deserialize_with = "deserialize_verifiable_credential", bound(deserialize = "CRED: serde::de::DeserializeOwned"))] pub verifiable_credential: Vec, /// The entity that generated the `Presentation`. pub holder: Url, @@ -57,6 +58,18 @@ pub struct Presentation { pub proof: Option, } +/// Deserializes a `Vec` while ensuring that it is not empty. +fn deserialize_verifiable_credential<'de, T: Deserialize<'de>, D>(deserializer: D) -> Result, D::Error> +where + D: de::Deserializer<'de>, +{ + let verifiable_credentials = Vec::::deserialize(deserializer)?; + + (!verifiable_credentials.is_empty()) + .then(|| verifiable_credentials) + .ok_or_else(|| de::Error::custom(Error::EmptyVerifiableCredentialsArray)) +} + impl Presentation { /// Returns the base JSON-LD context for `Presentation`s. pub fn base_context() -> &'static Context { @@ -147,16 +160,19 @@ where #[cfg(test)] mod tests { - use crate::presentation::Presentation; - use identity_core::common::Object; use serde_json::json; + use identity_core::common::Object; + use identity_core::convert::FromJson; + + use crate::presentation::Presentation; + #[test] fn test_presentation_deserialization() { // Example verifiable presentation taken from: // https://www.w3.org/TR/vc-data-model/#example-a-simple-example-of-a-verifiable-presentation // with some minor adjustments (adding the `holder` property and shortening the 'jws' values). - assert!(serde_json::from_value::>(json!({ + assert!(Presentation::::from_json_value(json!({ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1" @@ -199,7 +215,8 @@ mod tests { #[test] fn test_presentation_deserialization_without_credentials() { - assert!(serde_json::from_value::>(json!({ + // Deserializing a Presentation without `verifiableCredential' property is allowed. + assert!(Presentation::<()>::from_json_value(json!({ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1" @@ -209,4 +226,19 @@ mod tests { })) .is_ok()); } + + #[test] + fn test_presentation_deserialization_with_empty_credentials_array() { + // Deserializing a Presentation with an empty `verifiableCredential' property is not allowed. + assert!(Presentation::<()>::from_json_value(json!({ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "holder": "did:test:abc1", + "type": "VerifiablePresentation", + "verifiableCredential": [] + })) + .is_err()); + } } From 2b0287280742972bb38dd0ec6d134f1752bba1ec Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Wed, 6 Sep 2023 19:48:15 +0200 Subject: [PATCH 07/10] replace bool::then with bool::then_some --- identity_credential/src/presentation/presentation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/identity_credential/src/presentation/presentation.rs b/identity_credential/src/presentation/presentation.rs index a65ebf14d5..312fd9e365 100644 --- a/identity_credential/src/presentation/presentation.rs +++ b/identity_credential/src/presentation/presentation.rs @@ -66,7 +66,7 @@ where let verifiable_credentials = Vec::::deserialize(deserializer)?; (!verifiable_credentials.is_empty()) - .then(|| verifiable_credentials) + .then_some(verifiable_credentials) .ok_or_else(|| de::Error::custom(Error::EmptyVerifiableCredentialsArray)) } From becc44a4e2879b8a51cc62eaa557c4b410c44390 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Thu, 7 Sep 2023 11:08:02 +0200 Subject: [PATCH 08/10] adjust EmptyVerifiableCredentialArray Error --- identity_credential/src/error.rs | 4 ++-- identity_credential/src/presentation/presentation.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs index cb7dc42068..356d89d3d2 100644 --- a/identity_credential/src/error.rs +++ b/identity_credential/src/error.rs @@ -47,8 +47,8 @@ pub enum Error { /// Caused when deserializing a Presentation with an empty array for the /// `verifiableCredential` property. - #[error("empty verifiableCredential array")] - EmptyVerifiableCredentialsArray, + #[error("empty verifiableCredential array in presentation")] + EmptyVerifiableCredentialArray, /// Caused when attempting to convert a JWT to a `Presentation` that has conflicting values /// between the registered claims and those in the `vp` object. diff --git a/identity_credential/src/presentation/presentation.rs b/identity_credential/src/presentation/presentation.rs index 312fd9e365..188e1aa068 100644 --- a/identity_credential/src/presentation/presentation.rs +++ b/identity_credential/src/presentation/presentation.rs @@ -67,7 +67,7 @@ where (!verifiable_credentials.is_empty()) .then_some(verifiable_credentials) - .ok_or_else(|| de::Error::custom(Error::EmptyVerifiableCredentialsArray)) + .ok_or_else(|| de::Error::custom(Error::EmptyVerifiableCredentialArray)) } impl Presentation { From 5b1acc0f51f319faa0e86fb2c8d5f6275430d401 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Fri, 8 Sep 2023 11:51:06 +0200 Subject: [PATCH 09/10] test explicit error --- .../src/presentation/presentation.rs | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/identity_credential/src/presentation/presentation.rs b/identity_credential/src/presentation/presentation.rs index 188e1aa068..ea237ed92c 100644 --- a/identity_credential/src/presentation/presentation.rs +++ b/identity_credential/src/presentation/presentation.rs @@ -161,6 +161,7 @@ where #[cfg(test)] mod tests { use serde_json::json; + use std::error::Error; use identity_core::common::Object; use identity_core::convert::FromJson; @@ -228,17 +229,22 @@ mod tests { } #[test] - fn test_presentation_deserialization_with_empty_credentials_array() { - // Deserializing a Presentation with an empty `verifiableCredential' property is not allowed. - assert!(Presentation::<()>::from_json_value(json!({ - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1" - ], - "holder": "did:test:abc1", - "type": "VerifiablePresentation", - "verifiableCredential": [] - })) - .is_err()); + fn test_presentation_deserialization_with_empty_credential_array() { + assert_eq!( + Presentation::<()>::from_json_value(json!({ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "holder": "did:test:abc1", + "type": "VerifiablePresentation", + "verifiableCredential": [] + })) + .unwrap_err() + .source() + .unwrap() + .to_string(), + "empty verifiableCredential array in presentation" + ); } } From 2564010666bfe304176f56e6170a9128fecc7705 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Fri, 8 Sep 2023 15:37:42 +0200 Subject: [PATCH 10/10] use actual error variant in assertion --- identity_credential/src/presentation/presentation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/identity_credential/src/presentation/presentation.rs b/identity_credential/src/presentation/presentation.rs index ea237ed92c..1f1eb7d376 100644 --- a/identity_credential/src/presentation/presentation.rs +++ b/identity_credential/src/presentation/presentation.rs @@ -244,7 +244,7 @@ mod tests { .source() .unwrap() .to_string(), - "empty verifiableCredential array in presentation" + crate::error::Error::EmptyVerifiableCredentialArray.to_string() ); } }