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/error.rs b/identity_credential/src/error.rs index daa93a5139..356d89d3d2 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 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. #[error("could not convert JWT to the VP data model: {0}")] 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..1f1eb7d376 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; @@ -38,8 +39,9 @@ pub struct Presentation { #[serde(rename = "type")] pub types: OneOrMany, /// Credential(s) expressing the claims of the `Presentation`. - #[serde(default = "Default::default", rename = "verifiableCredential")] - pub verifiable_credential: OneOrMany, + #[rustfmt::skip] + #[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, /// Service(s) used to refresh an expired [`Credential`] in the `Presentation`. @@ -56,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_some(verifiable_credentials) + .ok_or_else(|| de::Error::custom(Error::EmptyVerifiableCredentialArray)) +} + impl Presentation { /// Returns the base JSON-LD context for `Presentation`s. pub fn base_context() -> &'static Context { @@ -80,7 +94,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(), @@ -143,3 +157,94 @@ where self.fmt_json(f) } } + +#[cfg(test)] +mod tests { + use serde_json::json; + use std::error::Error; + + 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!(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": [{ + "@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() { + // 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" + ], + "holder": "did:test:abc1", + "type": "VerifiablePresentation" + })) + .is_ok()); + } + + #[test] + 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(), + crate::error::Error::EmptyVerifiableCredentialArray.to_string() + ); + } +} 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); + } }