identity_credential/credential/
credential_v2.rs

1// Copyright 2020-2025 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::fmt::Display;
5
6use identity_core::common::Context;
7use identity_core::common::Object;
8use identity_core::common::OneOrMany;
9use identity_core::common::Timestamp;
10use identity_core::common::Url;
11use identity_core::convert::FmtJson as _;
12use identity_core::convert::ToJson as _;
13use once_cell::sync::Lazy;
14use serde::de::DeserializeOwned;
15use serde::de::Error as _;
16use serde::Deserialize;
17use serde::Deserializer;
18use serde::Serialize;
19
20use crate::credential::CredentialBuilder;
21use crate::credential::CredentialSealed;
22use crate::credential::CredentialT;
23use crate::credential::Evidence;
24use crate::credential::Issuer;
25use crate::credential::Policy;
26use crate::credential::Proof;
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
34pub(crate) static BASE_CONTEXT: Lazy<Context> =
35  Lazy::new(|| Context::Url(Url::parse("https://www.w3.org/ns/credentials/v2").unwrap()));
36
37pub(crate) fn deserialize_vc2_0_context<'de, D>(deserializer: D) -> Result<OneOrMany<Context>, D::Error>
38where
39  D: Deserializer<'de>,
40{
41  let ctx = OneOrMany::<Context>::deserialize(deserializer)?;
42  if ctx.contains(&BASE_CONTEXT) {
43    Ok(ctx)
44  } else {
45    Err(D::Error::custom("Missing base context"))
46  }
47}
48
49/// A [VC Data Model](https://www.w3.org/TR/vc-data-model-2.0/) 2.0 Verifiable Credential.
50#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
51pub struct Credential<T = Object> {
52  /// The JSON-LD context(s) applicable to the `Credential`.
53  #[serde(rename = "@context", deserialize_with = "deserialize_vc2_0_context")]
54  pub context: OneOrMany<Context>,
55  /// A unique `URI` that may be used to identify the `Credential`.
56  #[serde(skip_serializing_if = "Option::is_none")]
57  pub id: Option<Url>,
58  /// One or more URIs defining the type of the `Credential`.
59  #[serde(rename = "type")]
60  pub types: OneOrMany<String>,
61  /// One or more `Object`s representing the `Credential` subject(s).
62  #[serde(rename = "credentialSubject")]
63  pub credential_subject: OneOrMany<Subject>,
64  /// A reference to the issuer of the `Credential`.
65  pub issuer: Issuer,
66  /// A timestamp of when the `Credential` becomes valid.
67  #[serde(rename = "validFrom")]
68  pub valid_from: Timestamp,
69  /// The latest point in time at which the `Credential` should be considered valid.
70  #[serde(rename = "validUntil", skip_serializing_if = "Option::is_none")]
71  pub valid_until: Option<Timestamp>,
72  /// Information used to determine the current status of the `Credential`.
73  #[serde(default, rename = "credentialStatus", skip_serializing_if = "Option::is_none")]
74  pub credential_status: Option<Status>,
75  /// Information used to assist in the enforcement of a specific `Credential` structure.
76  #[serde(default, rename = "credentialSchema", skip_serializing_if = "OneOrMany::is_empty")]
77  pub credential_schema: OneOrMany<Schema>,
78  /// Service(s) used to refresh an expired `Credential`.
79  #[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")]
80  pub refresh_service: OneOrMany<RefreshService>,
81  /// Terms-of-use specified by the `Credential` issuer.
82  #[serde(default, rename = "termsOfUse", skip_serializing_if = "OneOrMany::is_empty")]
83  pub terms_of_use: OneOrMany<Policy>,
84  /// Human-readable evidence used to support the claims within the `Credential`.
85  #[serde(default, skip_serializing_if = "OneOrMany::is_empty")]
86  pub evidence: OneOrMany<Evidence>,
87  /// Indicates that the `Credential` must only be contained within a
88  /// [`Presentation`][crate::presentation::Presentation] with a proof issued from the `Credential` subject.
89  #[serde(rename = "nonTransferable", skip_serializing_if = "Option::is_none")]
90  pub non_transferable: Option<bool>,
91  /// Miscellaneous properties.
92  #[serde(flatten)]
93  pub properties: T,
94  /// Optional cryptographic proof, unrelated to JWT.
95  #[serde(skip_serializing_if = "Option::is_none")]
96  pub proof: Option<Proof>,
97}
98
99impl<T> Credential<T> {
100  /// Returns the base context for `Credential`s.
101  pub fn base_context() -> &'static Context {
102    &BASE_CONTEXT
103  }
104
105  /// Returns the base type for `Credential`s.
106  pub fn base_type() -> &'static str {
107    "VerifiableCredential"
108  }
109
110  /// Creates a `Credential` from a `CredentialBuilder`.
111  pub fn from_builder(mut builder: CredentialBuilder<T>) -> Result<Self> {
112    if builder.context.first() != Some(Self::base_context()) {
113      builder.context.insert(0, Self::base_context().clone());
114    }
115
116    if builder.types.first().map(String::as_str) != Some(Self::base_type()) {
117      builder.types.insert(0, Self::base_type().to_owned());
118    }
119
120    let this = Self {
121      context: OneOrMany::Many(builder.context),
122      id: builder.id,
123      types: builder.types.into(),
124      credential_subject: builder.subject.into(),
125      issuer: builder.issuer.ok_or(Error::MissingIssuer)?,
126      valid_from: builder.issuance_date.unwrap_or_default(),
127      valid_until: builder.expiration_date,
128      credential_status: builder.status,
129      credential_schema: builder.schema.into(),
130      refresh_service: builder.refresh_service.into(),
131      terms_of_use: builder.terms_of_use.into(),
132      evidence: builder.evidence.into(),
133      non_transferable: builder.non_transferable,
134      properties: builder.properties,
135      proof: builder.proof,
136    };
137
138    this.check_structure()?;
139
140    Ok(this)
141  }
142
143  /// Validates the semantic structure of the `Credential`.
144  pub(crate) fn check_structure(&self) -> Result<()> {
145    // Ensure the base context is present and in the correct location
146    match self.context.get(0) {
147      Some(context) if context == Self::base_context() => {}
148      Some(_) | None => return Err(Error::MissingBaseContext),
149    }
150
151    // The set of types MUST contain the base type
152    if !self.types.iter().any(|type_| type_ == Self::base_type()) {
153      return Err(Error::MissingBaseType);
154    }
155
156    // Credentials MUST have at least one subject
157    if self.credential_subject.is_empty() {
158      return Err(Error::MissingSubject);
159    }
160
161    // Each subject is defined as one or more properties - no empty objects
162    for subject in self.credential_subject.iter() {
163      if subject.id.is_none() && subject.properties.is_empty() {
164        return Err(Error::InvalidSubject);
165      }
166    }
167
168    Ok(())
169  }
170}
171
172impl<T> Display for Credential<T>
173where
174  T: Serialize,
175{
176  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> core::fmt::Result {
177    self.fmt_json(f)
178  }
179}
180
181impl<T> CredentialSealed for Credential<T> {}
182
183impl<T> CredentialT for Credential<T>
184where
185  T: Clone + Serialize + DeserializeOwned,
186{
187  type Properties = T;
188
189  fn base_context(&self) -> &'static Context {
190    Self::base_context()
191  }
192
193  fn type_(&self) -> &OneOrMany<String> {
194    &self.types
195  }
196
197  fn context(&self) -> &OneOrMany<Context> {
198    &self.context
199  }
200
201  fn subject(&self) -> &OneOrMany<Subject> {
202    &self.credential_subject
203  }
204
205  fn issuer(&self) -> &Issuer {
206    &self.issuer
207  }
208
209  fn valid_from(&self) -> Timestamp {
210    self.valid_from
211  }
212
213  fn valid_until(&self) -> Option<Timestamp> {
214    self.valid_until
215  }
216
217  fn properties(&self) -> &Self::Properties {
218    &self.properties
219  }
220
221  fn status(&self) -> Option<&Status> {
222    self.credential_status.as_ref()
223  }
224
225  fn non_transferable(&self) -> bool {
226    self.non_transferable.unwrap_or_default()
227  }
228
229  fn serialize_jwt(&self, custom_claims: Option<Object>) -> Result<String> {
230    self.serialize_jwt(custom_claims)
231  }
232}
233
234impl<T> Credential<T>
235where
236  T: Serialize,
237{
238  /// Serializes the [`Credential`] as a JWT claims set
239  /// in accordance with [VC Data Model v2.0](https://www.w3.org/TR/vc-data-model-2.0/).
240  ///
241  /// The resulting string can be used as the payload of a JWS when issuing the credential.  
242  pub fn serialize_jwt(&self, _custom_claims: Option<Object>) -> Result<String> {
243    self
244      .to_json()
245      .map_err(|err| Error::JwtClaimsSetSerializationError(err.into()))
246  }
247}
248
249#[cfg(test)]
250mod tests {
251  use identity_verification::jws::Decoder;
252
253  use super::*;
254
255  #[test]
256  fn valid_from_json_str() {
257    let json_credential = r#"
258{
259  "@context": [
260    "https://www.w3.org/ns/credentials/v2",
261    "https://www.w3.org/ns/credentials/examples/v2"
262  ],
263  "id": "http://university.example/credentials/3732",
264  "type": [
265    "VerifiableCredential",
266    "ExampleDegreeCredential"
267  ],
268  "issuer": "https://university.example/issuers/565049",
269  "validFrom": "2010-01-01T00:00:00Z",
270  "credentialSubject": {
271    "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
272    "degree": {
273      "type": "ExampleBachelorDegree",
274      "name": "Bachelor of Science and Arts"
275    }
276  }
277}
278    "#;
279    serde_json::from_str::<Credential>(json_credential).expect("valid VC using Data Model 2.0");
280  }
281
282  #[test]
283  fn invalid_from_json_str() {
284    let json_credential = include_str!("../../tests/fixtures/credential-1.json");
285    let _error = serde_json::from_str::<Credential>(json_credential).unwrap_err();
286  }
287
288  #[test]
289  fn parsed_from_jwt_payload() {
290    let jwt = "eyJraWQiOiJFeEhrQk1XOWZtYmt2VjI2Nm1ScHVQMnNVWV9OX0VXSU4xbGFwVXpPOHJvIiwiYWxnIjoiRVMyNTYifQ.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjIiXSwiaWQiOiJodHRwOi8vdW5pdmVyc2l0eS5leGFtcGxlL2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRXhhbXBsZURlZ3JlZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly91bml2ZXJzaXR5LmV4YW1wbGUvaXNzdWVycy81NjUwNDkiLCJ2YWxpZEZyb20iOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmV4YW1wbGU6ZWJmZWIxZjcxMmViYzZmMWMyNzZlMTJlYzIxIiwiZGVncmVlIjp7InR5cGUiOiJFeGFtcGxlQmFjaGVsb3JEZWdyZWUiLCJuYW1lIjoiQmFjaGVsb3Igb2YgU2NpZW5jZSBhbmQgQXJ0cyJ9fX0.YEsG9at9Hnt_j-UykCrnl494fcYMTjzpgvlK0KzzjvfmZmSg-sNVJqMZWizYhWv_eRUvAoZohvSJWeagwj_Ajw";
291    let decoded_jwt = Decoder::new()
292      .decode_compact_serialization(jwt.as_bytes(), None)
293      .expect("valid JWT");
294
295    let _credential: Credential<Object> = serde_json::from_slice(decoded_jwt.claims()).expect("valid JWT payload");
296  }
297}