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