identity_credential/sd_jwt_vc/
claims.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::ops::Deref;
5use std::ops::DerefMut;
6
7use identity_core::common::StringOrUrl;
8use identity_core::common::Timestamp;
9use identity_core::common::Url;
10use sd_jwt::Disclosure;
11use sd_jwt::SdJwtClaims;
12use serde::Deserialize;
13use serde::Serialize;
14use serde_json::Value;
15
16use super::Error;
17use super::Result;
18use super::Status;
19
20/// JOSE payload claims for SD-JWT VC.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22#[non_exhaustive]
23pub struct SdJwtVcClaims {
24  /// Issuer. Explicitly indicated the issuer of the verifiable credential when not conveyed by other means.
25  #[serde(skip_serializing_if = "Option::is_none")]
26  pub iss: Option<Url>,
27  /// Not before.
28  /// See [RFC7519 section 4.1.5](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.5) for more information.
29  #[serde(skip_serializing_if = "Option::is_none")]
30  pub nbf: Option<Timestamp>,
31  /// Expiration.
32  /// See [RFC7519 section 4.1.4](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.4) for more information.
33  #[serde(skip_serializing_if = "Option::is_none")]
34  pub exp: Option<Timestamp>,
35  /// Verifiable credential type.
36  /// See [SD-JWT VC specification](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-13.html#name-verifiable-credential-type-)
37  /// for more information.
38  pub vct: String,
39  /// Token's status.
40  /// See [OAuth status list specification](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-status-list-02)
41  /// for more information.
42  #[serde(skip_serializing_if = "Option::is_none")]
43  pub status: Option<Status>,
44  /// Issued at.
45  /// See [RFC7519 section 4.1.6](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.6) for more information.
46  #[serde(skip_serializing_if = "Option::is_none")]
47  pub iat: Option<Timestamp>,
48  /// Subject.
49  /// See [RFC7519 section 4.1.2](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.2) for more information.
50  #[serde(skip_serializing_if = "Option::is_none")]
51  pub sub: Option<StringOrUrl>,
52  #[serde(flatten)]
53  pub(crate) sd_jwt_claims: SdJwtClaims,
54}
55
56impl Deref for SdJwtVcClaims {
57  type Target = SdJwtClaims;
58  fn deref(&self) -> &Self::Target {
59    &self.sd_jwt_claims
60  }
61}
62
63impl DerefMut for SdJwtVcClaims {
64  fn deref_mut(&mut self) -> &mut Self::Target {
65    &mut self.sd_jwt_claims
66  }
67}
68
69impl SdJwtVcClaims {
70  pub(crate) fn try_from_sd_jwt_claims(mut claims: SdJwtClaims, disclosures: &[Disclosure]) -> Result<Self> {
71    let check_disclosed = |claim_name: &'static str| {
72      disclosures
73        .iter()
74        .any(|disclosure| disclosure.claim_name.as_deref() == Some(claim_name))
75        .then_some(Error::DisclosedClaim(claim_name))
76    };
77    let iss = claims
78      .remove("iss")
79      .map(|value| {
80        value
81          .as_str()
82          .and_then(|s| Url::parse(s).ok())
83          .ok_or_else(|| Error::InvalidClaimValue {
84            name: "iss",
85            expected: "URL",
86            found: value,
87          })
88      })
89      .transpose()?;
90    let nbf = {
91      if let Some(value) = claims.remove("nbf") {
92        value
93          .as_number()
94          .and_then(|t| t.as_i64())
95          .and_then(|t| Timestamp::from_unix(t).ok())
96          .ok_or_else(|| Error::InvalidClaimValue {
97            name: "nbf",
98            expected: "unix timestamp",
99            found: value,
100          })
101          .map(Some)?
102      } else {
103        if let Some(err) = check_disclosed("nbf") {
104          return Err(err);
105        }
106        None
107      }
108    };
109    let exp = {
110      if let Some(value) = claims.remove("exp") {
111        value
112          .as_number()
113          .and_then(|t| t.as_i64())
114          .and_then(|t| Timestamp::from_unix(t).ok())
115          .ok_or_else(|| Error::InvalidClaimValue {
116            name: "exp",
117            expected: "unix timestamp",
118            found: value,
119          })
120          .map(Some)?
121      } else {
122        if let Some(err) = check_disclosed("exp") {
123          return Err(err);
124        }
125        None
126      }
127    };
128    let vct = claims
129      .remove("vct")
130      .ok_or(Error::MissingClaim("vct"))
131      .map_err(|e| check_disclosed("vct").unwrap_or(e))
132      .and_then(|value| {
133        value
134          .as_str()
135          .map(ToOwned::to_owned)
136          .ok_or_else(|| Error::InvalidClaimValue {
137            name: "vct",
138            expected: "String",
139            found: value,
140          })
141      })?;
142    let status = {
143      if let Some(value) = claims.remove("status") {
144        serde_json::from_value::<Status>(value.clone())
145          .map_err(|_| Error::InvalidClaimValue {
146            name: "status",
147            expected: "credential's status object",
148            found: value,
149          })
150          .map(Some)?
151      } else {
152        if let Some(err) = check_disclosed("status") {
153          return Err(err);
154        }
155        None
156      }
157    };
158    let sub = claims
159      .remove("sub")
160      .map(|value| {
161        value
162          .as_str()
163          .and_then(|s| StringOrUrl::parse(s).ok())
164          .ok_or_else(|| Error::InvalidClaimValue {
165            name: "sub",
166            expected: "String or URL",
167            found: value,
168          })
169      })
170      .transpose()?;
171    let iat = claims
172      .remove("iat")
173      .map(|value| {
174        value
175          .as_number()
176          .and_then(|t| t.as_i64())
177          .and_then(|t| Timestamp::from_unix(t).ok())
178          .ok_or_else(|| Error::InvalidClaimValue {
179            name: "iat",
180            expected: "unix timestamp",
181            found: value,
182          })
183      })
184      .transpose()?;
185
186    Ok(Self {
187      iss,
188      nbf,
189      exp,
190      vct,
191      status,
192      iat,
193      sub,
194      sd_jwt_claims: claims,
195    })
196  }
197}
198
199impl From<SdJwtVcClaims> for SdJwtClaims {
200  fn from(claims: SdJwtVcClaims) -> Self {
201    let SdJwtVcClaims {
202      iss,
203      nbf,
204      exp,
205      vct,
206      status,
207      iat,
208      sub,
209      mut sd_jwt_claims,
210    } = claims;
211
212    iss.and_then(|iss| sd_jwt_claims.insert("iss".to_string(), Value::String(iss.to_string())));
213    nbf.and_then(|t| sd_jwt_claims.insert("nbf".to_string(), Value::Number(t.to_unix().into())));
214    exp.and_then(|t| sd_jwt_claims.insert("exp".to_string(), Value::Number(t.to_unix().into())));
215    sd_jwt_claims.insert("vct".to_string(), Value::String(vct));
216    status.and_then(|status| sd_jwt_claims.insert("status".to_string(), serde_json::to_value(status).unwrap()));
217    iat.and_then(|t| sd_jwt_claims.insert("iat".to_string(), Value::Number(t.to_unix().into())));
218    sub.and_then(|sub| sd_jwt_claims.insert("sub".to_string(), Value::String(sub.into())));
219
220    sd_jwt_claims
221  }
222}