Skip to main content

identity_kit/vc/
jwt.rs

1//! `Jwt`, a compact JWS carrying a JSON claims set, and the `chrono` conversions of
2//! [`JwtClaims`].
3
4use core::fmt;
5use core::str::FromStr;
6
7use chrono::{DateTime, Utc};
8use serde::de::Error as _;
9use serde::{Deserialize, Deserializer, Serialize};
10
11#[cfg(feature = "sd-jwt")]
12use crate::jose::numeric_date;
13use crate::jose::{CompactJws, JwsError, JwsHeader, JwtClaims, Signer, encode_compact};
14
15/// A JSON Web Token (RFC 7519) in compact serialization, kept as the string it is.
16///
17/// Construction checks that the string is a well-formed compact JWS with a supported `alg`;
18/// nothing is verified. The type serializes as a plain string, which is how a presentation
19/// carries its credentials.
20///
21/// # Examples
22///
23/// ```
24/// use identity_kit::vc::Jwt;
25///
26/// // RFC 8037 Appendix A.4.
27/// let jwt: Jwt = "eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc.\
28///     hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg"
29///     .parse()?;
30/// assert_eq!(jwt.decode()?.payload(), b"Example of Ed25519 signing");
31/// assert!("not.a.jwt".parse::<Jwt>().is_err());
32/// # Ok::<(), identity_kit::jose::JwsError>(())
33/// ```
34#[derive(Clone, PartialEq, Eq, Hash, Serialize)]
35#[serde(transparent)]
36pub struct Jwt(String);
37
38impl Jwt {
39    /// Wraps `token` after checking it is a compact JWS this crate can decode.
40    ///
41    /// # Errors
42    ///
43    /// The [`JwsError`] of [`CompactJws::decode`].
44    pub fn new(token: impl Into<String>) -> Result<Self, JwsError> {
45        let token = token.into();
46        CompactJws::decode(&token)?;
47        Ok(Self(token))
48    }
49
50    /// Signs `claims` under `header` with `signer`.
51    ///
52    /// The claims are serialized as compact JSON in their field order, `additional` last.
53    ///
54    /// # Errors
55    ///
56    /// [`JwsError::InvalidPayload`] when `additional` holds a registered claim, which would put
57    /// the member in the payload twice; otherwise as [`encode_compact`].
58    pub async fn sign(
59        header: &JwsHeader,
60        claims: &JwtClaims,
61        signer: &dyn Signer,
62    ) -> Result<Self, JwsError> {
63        if let Some(name) = registered_in_additional(claims) {
64            return Err(JwsError::InvalidPayload(format!(
65                "registered claim `{name}` is set under `additional`; set its field instead"
66            )));
67        }
68        let payload = serde_json::to_vec(claims).expect("a claims set serializes");
69        encode_compact(header, &payload, signer).await.map(Self)
70    }
71
72    /// The compact serialization.
73    pub fn as_str(&self) -> &str {
74        &self.0
75    }
76
77    /// The compact serialization.
78    pub fn into_string(self) -> String {
79        self.0
80    }
81
82    /// The decoded parts, unverified.
83    ///
84    /// # Errors
85    ///
86    /// Never, in practice: construction decoded the same string.
87    pub fn decode(&self) -> Result<CompactJws, JwsError> {
88        CompactJws::decode(&self.0)
89    }
90}
91
92impl fmt::Debug for Jwt {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        f.debug_tuple("Jwt").field(&self.0).finish()
95    }
96}
97
98impl fmt::Display for Jwt {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        f.write_str(&self.0)
101    }
102}
103
104impl AsRef<str> for Jwt {
105    fn as_ref(&self) -> &str {
106        &self.0
107    }
108}
109
110impl FromStr for Jwt {
111    type Err = JwsError;
112
113    fn from_str(s: &str) -> Result<Self, Self::Err> {
114        Self::new(s)
115    }
116}
117
118impl TryFrom<String> for Jwt {
119    type Error = JwsError;
120
121    fn try_from(token: String) -> Result<Self, Self::Error> {
122        Self::new(token)
123    }
124}
125
126impl<'de> Deserialize<'de> for Jwt {
127    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
128        Self::new(String::deserialize(deserializer)?).map_err(D::Error::custom)
129    }
130}
131
132/// The first registered claim name found under `additional` of `claims`, if any.
133fn registered_in_additional(claims: &JwtClaims) -> Option<&'static str> {
134    JwtClaims::REGISTERED
135        .into_iter()
136        .find(|name| claims.additional.contains_key(*name))
137}
138
139/// As [`numeric_date`], for a claim that must be present.
140#[cfg(feature = "sd-jwt")]
141pub(crate) fn required_numeric_date<'de, D: Deserializer<'de>>(
142    deserializer: D,
143) -> Result<i64, D::Error> {
144    numeric_date(deserializer)?.ok_or_else(|| serde::de::Error::custom("a NumericDate is required"))
145}
146
147/// The conversions between `NumericDate` values and `chrono` times, which come with the `vc`
148/// feature.
149#[cfg_attr(docsrs, doc(cfg(feature = "vc")))]
150impl JwtClaims {
151    /// `time` as a `NumericDate`: whole seconds since the Unix epoch, fractions dropped.
152    pub fn numeric_date(time: DateTime<Utc>) -> i64 {
153        time.timestamp()
154    }
155
156    /// The time a `NumericDate` names, if it is in the range `chrono` represents.
157    pub fn timestamp(numeric_date: i64) -> Option<DateTime<Utc>> {
158        DateTime::from_timestamp(numeric_date, 0)
159    }
160
161    /// This claims set with `exp` set to `time`.
162    #[must_use]
163    pub fn with_exp(mut self, time: DateTime<Utc>) -> Self {
164        self.exp = Some(Self::numeric_date(time));
165        self
166    }
167
168    /// This claims set with `nbf` set to `time`.
169    #[must_use]
170    pub fn with_nbf(mut self, time: DateTime<Utc>) -> Self {
171        self.nbf = Some(Self::numeric_date(time));
172        self
173    }
174
175    /// This claims set with `iat` set to `time`.
176    #[must_use]
177    pub fn with_iat(mut self, time: DateTime<Utc>) -> Self {
178        self.iat = Some(Self::numeric_date(time));
179        self
180    }
181}