identity_credential/credential/
credential.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 identity_core::convert::ToJson;
8#[cfg(feature = "jpt-bbs-plus")]
9use jsonprooftoken::jpt::claims::JptClaims;
10use once_cell::sync::Lazy;
11use serde::Deserialize;
12use serde::Serialize;
13
14use identity_core::common::Context;
15use identity_core::common::Object;
16use identity_core::common::OneOrMany;
17use identity_core::common::Timestamp;
18use identity_core::common::Url;
19use identity_core::convert::FmtJson;
20
21use crate::credential::CredentialBuilder;
22use crate::credential::CredentialSealed;
23use crate::credential::CredentialT;
24use crate::credential::Evidence;
25use crate::credential::Issuer;
26use crate::credential::Policy;
27use crate::credential::RefreshService;
28use crate::credential::Schema;
29use crate::credential::Status;
30use crate::credential::Subject;
31use crate::error::Error;
32use crate::error::Result;
33
34use super::jwt_serialization::CredentialJwtClaims;
35use super::Proof;
36
37static BASE_CONTEXT: Lazy<Context> =
38  Lazy::new(|| Context::Url(Url::parse("https://www.w3.org/2018/credentials/v1").unwrap()));
39
40/// Represents a set of claims describing an entity.
41#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
42pub struct Credential<T = Object> {
43  /// The JSON-LD context(s) applicable to the `Credential`.
44  #[serde(rename = "@context")]
45  pub context: OneOrMany<Context>,
46  /// A unique `URI` that may be used to identify the `Credential`.
47  #[serde(skip_serializing_if = "Option::is_none")]
48  pub id: Option<Url>,
49  /// One or more URIs defining the type of the `Credential`.
50  #[serde(rename = "type")]
51  pub types: OneOrMany<String>,
52  /// One or more `Object`s representing the `Credential` subject(s).
53  #[serde(rename = "credentialSubject")]
54  pub credential_subject: OneOrMany<Subject>,
55  /// A reference to the issuer of the `Credential`.
56  pub issuer: Issuer,
57  /// A timestamp of when the `Credential` becomes valid.
58  #[serde(rename = "issuanceDate")]
59  pub issuance_date: Timestamp,
60  /// A timestamp of when the `Credential` should no longer be considered valid.
61  #[serde(rename = "expirationDate", skip_serializing_if = "Option::is_none")]
62  pub expiration_date: Option<Timestamp>,
63  /// Information used to determine the current status of the `Credential`.
64  #[serde(default, rename = "credentialStatus", skip_serializing_if = "Option::is_none")]
65  pub credential_status: Option<Status>,
66  /// Information used to assist in the enforcement of a specific `Credential` structure.
67  #[serde(default, rename = "credentialSchema", skip_serializing_if = "OneOrMany::is_empty")]
68  pub credential_schema: OneOrMany<Schema>,
69  /// Service(s) used to refresh an expired `Credential`.
70  #[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")]
71  pub refresh_service: OneOrMany<RefreshService>,
72  /// Terms-of-use specified by the `Credential` issuer.
73  #[serde(default, rename = "termsOfUse", skip_serializing_if = "OneOrMany::is_empty")]
74  pub terms_of_use: OneOrMany<Policy>,
75  /// Human-readable evidence used to support the claims within the `Credential`.
76  #[serde(default, skip_serializing_if = "OneOrMany::is_empty")]
77  pub evidence: OneOrMany<Evidence>,
78  /// Indicates that the `Credential` must only be contained within a
79  /// [`Presentation`][crate::presentation::Presentation] with a proof issued from the `Credential` subject.
80  #[serde(rename = "nonTransferable", skip_serializing_if = "Option::is_none")]
81  pub non_transferable: Option<bool>,
82  /// Miscellaneous properties.
83  #[serde(flatten)]
84  pub properties: T,
85  /// Optional cryptographic proof, unrelated to JWT.
86  #[serde(skip_serializing_if = "Option::is_none")]
87  pub proof: Option<Proof>,
88}
89
90impl<T> Credential<T> {
91  /// Returns the base JSON-LD context.
92  pub fn base_context() -> &'static Context {
93    &BASE_CONTEXT
94  }
95
96  /// Returns the base type.
97  pub const fn base_type() -> &'static str {
98    "VerifiableCredential"
99  }
100
101  /// Creates a new `CredentialBuilder` to configure a `Credential`.
102  ///
103  /// This is the same as [CredentialBuilder::new].
104  pub fn builder(properties: T) -> CredentialBuilder<T> {
105    CredentialBuilder::new(properties)
106  }
107
108  /// Returns a new `Credential` based on the `CredentialBuilder` configuration.
109  pub fn from_builder(mut builder: CredentialBuilder<T>) -> Result<Self> {
110    if builder.context.first() != Some(Self::base_context()) {
111      builder.context.insert(0, Self::base_context().clone());
112    }
113
114    if builder.types.first().map(String::as_str) != Some(Self::base_type()) {
115      builder.types.insert(0, Self::base_type().to_owned());
116    }
117
118    let this: Self = Self {
119      context: OneOrMany::Many(builder.context),
120      id: builder.id,
121      types: builder.types.into(),
122      credential_subject: builder.subject.into(),
123      issuer: builder.issuer.ok_or(Error::MissingIssuer)?,
124      issuance_date: builder.issuance_date.unwrap_or_default(),
125      expiration_date: builder.expiration_date,
126      credential_status: builder.status,
127      credential_schema: builder.schema.into(),
128      refresh_service: builder.refresh_service.into(),
129      terms_of_use: builder.terms_of_use.into(),
130      evidence: builder.evidence.into(),
131      non_transferable: builder.non_transferable,
132      properties: builder.properties,
133      proof: builder.proof,
134    };
135
136    this.check_structure()?;
137
138    Ok(this)
139  }
140
141  /// Validates the semantic structure of the `Credential`.
142  pub fn check_structure(&self) -> Result<()> {
143    // Ensure the base context is present and in the correct location
144    match self.context.get(0) {
145      Some(context) if context == Self::base_context() => {}
146      Some(_) | None => return Err(Error::MissingBaseContext),
147    }
148
149    // The set of types MUST contain the base type
150    if !self.types.iter().any(|type_| type_ == Self::base_type()) {
151      return Err(Error::MissingBaseType);
152    }
153
154    // Credentials MUST have at least one subject
155    if self.credential_subject.is_empty() {
156      return Err(Error::MissingSubject);
157    }
158
159    // Each subject is defined as one or more properties - no empty objects
160    for subject in self.credential_subject.iter() {
161      if subject.id.is_none() && subject.properties.is_empty() {
162        return Err(Error::InvalidSubject);
163      }
164    }
165
166    Ok(())
167  }
168
169  /// Sets the proof property of the `Credential`.
170  ///
171  /// Note that this proof is not related to JWT.
172  pub fn set_proof(&mut self, proof: Option<Proof>) {
173    self.proof = proof;
174  }
175
176  /// Serializes the [`Credential`] as a JWT claims set
177  /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token).
178  ///
179  /// The resulting string can be used as the payload of a JWS when issuing the credential.  
180  pub fn serialize_jwt(&self, custom_claims: Option<Object>) -> Result<String>
181  where
182    T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
183  {
184    let jwt_representation: CredentialJwtClaims<'_, T> = CredentialJwtClaims::new(self, custom_claims)?;
185    jwt_representation
186      .to_json()
187      .map_err(|err| Error::JwtClaimsSetSerializationError(err.into()))
188  }
189
190  /// Converts the [`Credential`] into a JWT claims set in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token).
191  pub fn to_jwt_claims(&self, custom_claims: Option<Object>) -> Result<serde_json::Map<String, serde_json::Value>>
192  where
193    T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
194  {
195    let jwt_representation: CredentialJwtClaims<'_, T> = CredentialJwtClaims::new(self, custom_claims)?;
196    let serde_json::Value::Object(jwt_claims) = jwt_representation
197      .to_json_value()
198      .map_err(|err| Error::JwtClaimsSetSerializationError(err.into()))?
199    else {
200      unreachable!("CredentialJwtClaims always serializes to a JSON object");
201    };
202
203    Ok(jwt_claims)
204  }
205
206  ///Serializes the [`Credential`] as a JPT claims set
207  #[cfg(feature = "jpt-bbs-plus")]
208  pub fn serialize_jpt(&self, custom_claims: Option<Object>) -> Result<JptClaims>
209  where
210    T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
211  {
212    let jwt_representation: CredentialJwtClaims<'_, T> = CredentialJwtClaims::new(self, custom_claims)?;
213    Ok(jwt_representation.into())
214  }
215}
216
217impl<T> Display for Credential<T>
218where
219  T: Serialize,
220{
221  fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
222    self.fmt_json(f)
223  }
224}
225
226impl<T> CredentialSealed for Credential<T> {}
227
228impl<T> CredentialT for Credential<T>
229where
230  T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
231{
232  type Properties = T;
233
234  fn base_context(&self) -> &'static Context {
235    Self::base_context()
236  }
237
238  fn type_(&self) -> &OneOrMany<String> {
239    &self.types
240  }
241
242  fn context(&self) -> &OneOrMany<Context> {
243    &self.context
244  }
245
246  fn subject(&self) -> &OneOrMany<Subject> {
247    &self.credential_subject
248  }
249
250  fn issuer(&self) -> &Issuer {
251    &self.issuer
252  }
253
254  fn valid_from(&self) -> Timestamp {
255    self.issuance_date
256  }
257
258  fn valid_until(&self) -> Option<Timestamp> {
259    self.expiration_date
260  }
261
262  fn properties(&self) -> &Self::Properties {
263    &self.properties
264  }
265
266  fn status(&self) -> Option<&Status> {
267    self.credential_status.as_ref()
268  }
269
270  fn non_transferable(&self) -> bool {
271    self.non_transferable.unwrap_or_default()
272  }
273
274  fn serialize_jwt(&self, custom_claims: Option<Object>) -> Result<String> {
275    self.serialize_jwt(custom_claims)
276  }
277}
278
279#[cfg(test)]
280mod tests {
281  use identity_core::common::OneOrMany;
282  use identity_core::common::Url;
283  use identity_core::convert::FromJson;
284
285  use crate::credential::credential::BASE_CONTEXT;
286  use crate::credential::Credential;
287  use crate::credential::Subject;
288
289  const JSON1: &str = include_str!("../../tests/fixtures/credential-1.json");
290  const JSON2: &str = include_str!("../../tests/fixtures/credential-2.json");
291  const JSON3: &str = include_str!("../../tests/fixtures/credential-3.json");
292  const JSON4: &str = include_str!("../../tests/fixtures/credential-4.json");
293  const JSON5: &str = include_str!("../../tests/fixtures/credential-5.json");
294  const JSON6: &str = include_str!("../../tests/fixtures/credential-6.json");
295  const JSON7: &str = include_str!("../../tests/fixtures/credential-7.json");
296  const JSON8: &str = include_str!("../../tests/fixtures/credential-8.json");
297  const JSON9: &str = include_str!("../../tests/fixtures/credential-9.json");
298  const JSON10: &str = include_str!("../../tests/fixtures/credential-10.json");
299  const JSON11: &str = include_str!("../../tests/fixtures/credential-11.json");
300  const JSON12: &str = include_str!("../../tests/fixtures/credential-12.json");
301
302  #[test]
303  fn test_from_json() {
304    let _credential: Credential = Credential::from_json(JSON1).unwrap();
305    let _credential: Credential = Credential::from_json(JSON2).unwrap();
306    let _credential: Credential = Credential::from_json(JSON3).unwrap();
307    let _credential: Credential = Credential::from_json(JSON4).unwrap();
308    let _credential: Credential = Credential::from_json(JSON5).unwrap();
309    let _credential: Credential = Credential::from_json(JSON6).unwrap();
310    let _credential: Credential = Credential::from_json(JSON7).unwrap();
311    let _credential: Credential = Credential::from_json(JSON8).unwrap();
312    let _credential: Credential = Credential::from_json(JSON9).unwrap();
313    let _credential: Credential = Credential::from_json(JSON10).unwrap();
314    let _credential: Credential = Credential::from_json(JSON11).unwrap();
315    let _credential: Credential = Credential::from_json(JSON12).unwrap();
316  }
317
318  #[test]
319  fn credential_with_single_context_is_list_of_contexts_with_single_item() {
320    let mut credential = Credential::builder(serde_json::Value::default())
321      .id(Url::parse("https://example.com/credentials/123").unwrap())
322      .issuer(Url::parse("https://example.com").unwrap())
323      .subject(Subject::with_id(Url::parse("https://example.com/users/123").unwrap()))
324      .build()
325      .unwrap();
326
327    assert!(matches!(credential.context, OneOrMany::Many(_)));
328    assert_eq!(credential.context.len(), 1);
329    assert!(credential.check_structure().is_ok());
330
331    // Check backward compatibility with previously created credentials.
332    credential.context = OneOrMany::One(BASE_CONTEXT.clone());
333    assert!(credential.check_structure().is_ok());
334  }
335}