Skip to content

Commit

Permalink
Change verifiable_credential to type Vec<CRED> in Presentation (#…
Browse files Browse the repository at this point in the history
…1231)

* replace OneOrMany with Vec

* remove useless conversion

* finetune serde, add unit tests

* fix WASM error

* add rustfmt::skip

* disallow empty verifiableCredential array

* replace bool::then with bool::then_some

* adjust EmptyVerifiableCredentialArray Error

* test explicit error

* use actual error variant in assertion
  • Loading branch information
nanderstabel authored Sep 11, 2023
1 parent 204087e commit 54a86cf
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 6 deletions.
3 changes: 1 addition & 2 deletions examples/0_basic/6_create_vp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Jwt> = &presentation.presentation.verifiable_credential;
let jwt_credentials: &Vec<Jwt> = &presentation.presentation.verifiable_credential;
let issuers: Vec<CoreDID> = jwt_credentials
.iter()
.map(JwtCredentialValidator::extract_issuer_from_jwt)
Expand Down
5 changes: 5 additions & 0 deletions identity_credential/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")]
Expand Down
2 changes: 1 addition & 1 deletion identity_credential/src/presentation/jwt_serialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ where
types: Cow<'presentation, OneOrMany<String>>,
/// Credential(s) expressing the claims of the `Presentation`.
#[serde(default = "Default::default", rename = "verifiableCredential")]
pub(crate) verifiable_credential: Cow<'presentation, OneOrMany<CRED>>,
pub(crate) verifiable_credential: Cow<'presentation, Vec<CRED>>,
/// 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<RefreshService>>,
Expand Down
111 changes: 108 additions & 3 deletions identity_credential/src/presentation/presentation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use core::fmt::Display;
use core::fmt::Formatter;

use serde::de;
use serde::Deserialize;
use serde::Serialize;

Expand Down Expand Up @@ -38,8 +39,9 @@ pub struct Presentation<CRED, T = Object> {
#[serde(rename = "type")]
pub types: OneOrMany<String>,
/// Credential(s) expressing the claims of the `Presentation`.
#[serde(default = "Default::default", rename = "verifiableCredential")]
pub verifiable_credential: OneOrMany<CRED>,
#[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<CRED>,
/// The entity that generated the `Presentation`.
pub holder: Url,
/// Service(s) used to refresh an expired [`Credential`] in the `Presentation`.
Expand All @@ -56,6 +58,18 @@ pub struct Presentation<CRED, T = Object> {
pub proof: Option<Proof>,
}

/// Deserializes a `Vec<T>` while ensuring that it is not empty.
fn deserialize_verifiable_credential<'de, T: Deserialize<'de>, D>(deserializer: D) -> Result<Vec<T>, D::Error>
where
D: de::Deserializer<'de>,
{
let verifiable_credentials = Vec::<T>::deserialize(deserializer)?;

(!verifiable_credentials.is_empty())
.then_some(verifiable_credentials)
.ok_or_else(|| de::Error::custom(Error::EmptyVerifiableCredentialArray))
}

impl<CRED, T> Presentation<CRED, T> {
/// Returns the base JSON-LD context for `Presentation`s.
pub fn base_context() -> &'static Context {
Expand All @@ -80,7 +94,7 @@ impl<CRED, T> Presentation<CRED, T> {
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(),
Expand Down Expand Up @@ -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::<Object>::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()
);
}
}
18 changes: 18 additions & 0 deletions identity_credential/src/presentation/presentation_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Jwt> = 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::<Object>::base_context()
);
assert_eq!(presentation.types.len(), 2);
assert_eq!(presentation.types.get(0).unwrap(), Presentation::<Object>::base_type());
assert_eq!(presentation.types.get(1).unwrap(), "ExamplePresentation");
assert_eq!(presentation.verifiable_credential.len(), 0);
}
}

0 comments on commit 54a86cf

Please sign in to comment.