Skip to content
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

Change verifiable_credential to type Vec<CRED> in Presentation #1231

Merged
merged 10 commits into from
Sep 11, 2023
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")]
EmptyVerifiableCredentialsArray,
PhilippGackstatter marked this conversation as resolved.
Show resolved Hide resolved

/// 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
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
105 changes: 102 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::EmptyVerifiableCredentialsArray))
}

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,88 @@ where
self.fmt_json(f)
}
}

#[cfg(test)]
mod tests {
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!(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_credentials_array() {
nanderstabel marked this conversation as resolved.
Show resolved Hide resolved
// 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());
}
}
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);
}
}
Loading