identity_credential/presentation/
presentation.rs

1// Copyright 2020-2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use core::fmt::Display;
5use core::fmt::Formatter;
6
7use serde::de;
8use serde::Deserialize;
9use serde::Serialize;
10
11use identity_core::common::Context;
12use identity_core::common::Object;
13use identity_core::common::OneOrMany;
14use identity_core::common::Url;
15use identity_core::convert::FmtJson;
16use identity_core::convert::ToJson;
17
18use crate::credential::Credential;
19use crate::credential::Policy;
20use crate::credential::Proof;
21use crate::credential::RefreshService;
22use crate::error::Error;
23use crate::error::Result;
24
25use super::jwt_serialization::PresentationJwtClaims;
26use super::JwtPresentationOptions;
27use super::PresentationBuilder;
28
29/// Represents a bundle of one or more [`Credential`]s.
30#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
31pub struct Presentation<CRED, T = Object> {
32  /// The JSON-LD context(s) applicable to the `Presentation`.
33  #[serde(rename = "@context")]
34  pub context: OneOrMany<Context>,
35  /// A unique `URI` that may be used to identify the `Presentation`.
36  #[serde(skip_serializing_if = "Option::is_none")]
37  pub id: Option<Url>,
38  /// One or more URIs defining the type of the `Presentation`.
39  #[serde(rename = "type")]
40  pub types: OneOrMany<String>,
41  /// Credential(s) expressing the claims of the `Presentation`.
42  #[rustfmt::skip]
43  #[serde(default = "Default::default", rename = "verifiableCredential", skip_serializing_if = "Vec::is_empty", deserialize_with = "deserialize_verifiable_credential", bound(deserialize = "CRED: serde::de::DeserializeOwned"))]
44  pub verifiable_credential: Vec<CRED>,
45  /// The entity that generated the `Presentation`.
46  pub holder: Url,
47  /// Service(s) used to refresh an expired [`Credential`] in the `Presentation`.
48  #[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")]
49  pub refresh_service: OneOrMany<RefreshService>,
50  /// Terms-of-use specified by the `Presentation` holder.
51  #[serde(default, rename = "termsOfUse", skip_serializing_if = "OneOrMany::is_empty")]
52  pub terms_of_use: OneOrMany<Policy>,
53  /// Miscellaneous properties.
54  #[serde(flatten)]
55  pub properties: T,
56  /// Optional cryptographic proof, unrelated to JWT.
57  #[serde(skip_serializing_if = "Option::is_none")]
58  pub proof: Option<Proof>,
59}
60
61/// Deserializes a `Vec<T>` while ensuring that it is not empty.
62fn deserialize_verifiable_credential<'de, T: Deserialize<'de>, D>(deserializer: D) -> Result<Vec<T>, D::Error>
63where
64  D: de::Deserializer<'de>,
65{
66  let verifiable_credentials = Vec::<T>::deserialize(deserializer)?;
67
68  (!verifiable_credentials.is_empty())
69    .then_some(verifiable_credentials)
70    .ok_or_else(|| de::Error::custom(Error::EmptyVerifiableCredentialArray))
71}
72
73impl<CRED, T> Presentation<CRED, T> {
74  /// Returns the base JSON-LD context for `Presentation`s.
75  pub fn base_context() -> &'static Context {
76    Credential::<Object>::base_context()
77  }
78
79  /// Returns the base type for `Presentation`s.
80  pub const fn base_type() -> &'static str {
81    "VerifiablePresentation"
82  }
83
84  /// Creates a `PresentationBuilder` to configure a new Presentation.
85  ///
86  /// This is the same as [PresentationBuilder::new].
87  pub fn builder(holder: Url, properties: T) -> PresentationBuilder<CRED, T> {
88    PresentationBuilder::new(holder, properties)
89  }
90
91  /// Returns a new `Presentation` based on the `PresentationBuilder` configuration.
92  pub fn from_builder(builder: PresentationBuilder<CRED, T>) -> Result<Self> {
93    let this: Self = Self {
94      context: builder.context.into(),
95      id: builder.id,
96      types: builder.types.into(),
97      verifiable_credential: builder.credentials,
98      holder: builder.holder,
99      refresh_service: builder.refresh_service.into(),
100      terms_of_use: builder.terms_of_use.into(),
101      properties: builder.properties,
102      proof: None,
103    };
104    this.check_structure()?;
105
106    Ok(this)
107  }
108
109  /// Validates the semantic structure of the `Presentation`.
110  ///
111  /// # Warning
112  ///
113  /// This does not check the semantic structure of the contained credentials. This needs to be done as part of
114  /// signature validation on the credentials as they are encoded as JWTs.
115  pub fn check_structure(&self) -> Result<()> {
116    // Ensure the base context is present and in the correct location
117    match self.context.get(0) {
118      Some(context) if context == Self::base_context() => {}
119      Some(_) | None => return Err(Error::MissingBaseContext),
120    }
121
122    // The set of types MUST contain the base type
123    if !self.types.iter().any(|type_| type_ == Self::base_type()) {
124      return Err(Error::MissingBaseType);
125    }
126    Ok(())
127  }
128
129  /// Serializes the [`Presentation`] as a JWT claims set
130  /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token).
131  ///
132  /// The resulting string can be used as the payload of a JWS when issuing the credential.  
133  pub fn serialize_jwt(&self, options: &JwtPresentationOptions) -> Result<String>
134  where
135    T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
136    CRED: ToOwned<Owned = CRED> + serde::Serialize + serde::de::DeserializeOwned + Clone,
137  {
138    let jwt_representation: PresentationJwtClaims<'_, CRED, T> = PresentationJwtClaims::new(self, options)?;
139    jwt_representation
140      .to_json()
141      .map_err(|err| Error::JwtClaimsSetSerializationError(err.into()))
142  }
143
144  /// Sets the value of the proof property.
145  ///
146  /// Note that this proof is not related to JWT.
147  pub fn set_proof(&mut self, proof: Option<Proof>) {
148    self.proof = proof;
149  }
150}
151
152impl<T> Display for Presentation<T>
153where
154  T: Serialize,
155{
156  fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
157    self.fmt_json(f)
158  }
159}
160
161#[cfg(test)]
162mod tests {
163  use serde_json::json;
164  use std::error::Error;
165
166  use identity_core::common::Object;
167  use identity_core::convert::FromJson;
168
169  use crate::presentation::Presentation;
170
171  #[test]
172  fn test_presentation_deserialization() {
173    // Example verifiable presentation taken from:
174    // https://www.w3.org/TR/vc-data-model/#example-a-simple-example-of-a-verifiable-presentation
175    // with some minor adjustments (adding the `holder` property and shortening the 'jws' values).
176    assert!(Presentation::<Object>::from_json_value(json!({
177      "@context": [
178        "https://www.w3.org/2018/credentials/v1",
179        "https://www.w3.org/2018/credentials/examples/v1"
180      ],
181      "holder": "did:test:abc1",
182      "type": "VerifiablePresentation",
183      "verifiableCredential": [{
184        "@context": [
185          "https://www.w3.org/2018/credentials/v1",
186          "https://www.w3.org/2018/credentials/examples/v1"
187        ],
188        "id": "http://example.edu/credentials/1872",
189        "type": ["VerifiableCredential", "AlumniCredential"],
190        "issuer": "https://example.edu/issuers/565049",
191        "issuanceDate": "2010-01-01T19:23:24Z",
192        "credentialSubject": {
193          "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
194          "alumniOf": {
195            "id": "did:example:c276e12ec21ebfeb1f712ebc6f1",
196            "name": [{
197              "value": "Example University",
198              "lang": "en"
199            }, {
200              "value": "Exemple d'Université",
201              "lang": "fr"
202            }]
203          }
204        },
205        "proof": {
206          "type": "RsaSignature2018",
207          "created": "2017-06-18T21:19:10Z",
208          "proofPurpose": "assertionMethod",
209          "verificationMethod": "https://example.edu/issuers/565049#key-1",
210          "jws": "eyJhb...dBBPM"
211        }
212      }],
213    }))
214    .is_ok());
215  }
216
217  #[test]
218  fn test_presentation_deserialization_without_credentials() {
219    // Deserializing a Presentation without `verifiableCredential' property is allowed.
220    assert!(Presentation::<()>::from_json_value(json!({
221      "@context": [
222        "https://www.w3.org/2018/credentials/v1",
223        "https://www.w3.org/2018/credentials/examples/v1"
224      ],
225      "holder": "did:test:abc1",
226      "type": "VerifiablePresentation"
227    }))
228    .is_ok());
229  }
230
231  #[test]
232  fn test_presentation_deserialization_with_empty_credential_array() {
233    assert_eq!(
234      Presentation::<()>::from_json_value(json!({
235        "@context": [
236          "https://www.w3.org/2018/credentials/v1",
237          "https://www.w3.org/2018/credentials/examples/v1"
238        ],
239        "holder": "did:test:abc1",
240        "type": "VerifiablePresentation",
241        "verifiableCredential": []
242      }))
243      .unwrap_err()
244      .source()
245      .unwrap()
246      .to_string(),
247      crate::error::Error::EmptyVerifiableCredentialArray.to_string()
248    );
249  }
250}