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