Skip to main content

identity_kit/sd_jwt_vc/
status.rs

1//! The Token Status List: the `status` claim of a referenced token, the status list and the
2//! status list token.
3
4use core::fmt;
5
6use chrono::{DateTime, Utc};
7use indexmap::IndexMap;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use serde_json::Value;
10
11use crate::jose::{JwsError, JwsHeader, Signer, base64url};
12use crate::vc::{Jwt, JwtClaims};
13
14/// The `status` claim of a referenced token: at least one status mechanism, `status_list`
15/// being the one this crate implements.
16///
17/// # Examples
18///
19/// ```
20/// use identity_kit::sd_jwt_vc::{Status, StatusListRef};
21/// use serde_json::json;
22///
23/// // Token Status List §6.2.
24/// let status: Status = serde_json::from_value(json!({
25///   "status_list": { "idx": 0, "uri": "https://example.com/statuslists/1" }
26/// }))?;
27/// let reference = status.status_list.as_ref().unwrap();
28/// assert_eq!(reference.idx, 0);
29/// assert_eq!(reference.uri, "https://example.com/statuslists/1");
30/// assert_eq!(status, Status::status_list(StatusListRef::new("https://example.com/statuslists/1", 0)));
31/// # Ok::<(), serde_json::Error>(())
32/// ```
33#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
34#[non_exhaustive]
35pub struct Status {
36    /// The `status_list` mechanism.
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub status_list: Option<StatusListRef>,
39    /// Every other mechanism, by name.
40    #[serde(flatten)]
41    pub additional: IndexMap<String, Value>,
42}
43
44impl Status {
45    /// A `status` claim with no mechanism yet.
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// A `status` claim referencing a status list.
51    pub fn status_list(reference: StatusListRef) -> Self {
52        Self {
53            status_list: Some(reference),
54            additional: IndexMap::new(),
55        }
56    }
57
58    /// This claim with the mechanism `name` set to `value`.
59    #[must_use]
60    pub fn with_mechanism(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
61        self.additional.insert(name.into(), value.into());
62        self
63    }
64
65    /// Whether no mechanism is given.
66    pub fn is_empty(&self) -> bool {
67        self.status_list.is_none() && self.additional.is_empty()
68    }
69
70    /// The mechanisms, the status list first.
71    pub fn mechanisms(&self) -> impl Iterator<Item = StatusMechanism<'_>> {
72        self.status_list
73            .iter()
74            .map(StatusMechanism::StatusList)
75            .chain(
76                self.additional
77                    .iter()
78                    .map(|(name, value)| StatusMechanism::Other { name, value }),
79            )
80    }
81}
82
83/// One mechanism of a [`Status`] claim.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85#[non_exhaustive]
86pub enum StatusMechanism<'a> {
87    /// A Token Status List reference.
88    StatusList(&'a StatusListRef),
89    /// A mechanism this crate does not implement.
90    Other {
91        /// Its name, the member of `status` it is given under.
92        name: &'a str,
93        /// Its value.
94        value: &'a Value,
95    },
96}
97
98/// A reference to a status list token (`status.status_list`): the token's URI and the index
99/// of the referenced token in its list.
100#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
101#[non_exhaustive]
102pub struct StatusListRef {
103    /// `idx`: the index in the status list.
104    pub idx: u64,
105    /// `uri`: where the status list token is.
106    pub uri: String,
107}
108
109impl StatusListRef {
110    /// A reference to index `idx` of the status list token at `uri`.
111    pub fn new(uri: impl Into<String>, idx: u64) -> Self {
112        Self {
113            idx,
114            uri: uri.into(),
115        }
116    }
117}
118
119/// A status type value (Token Status List §7): a number in `0..=255`, with the registered
120/// meanings as constants.
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
122pub struct StatusType(pub u8);
123
124impl StatusType {
125    /// `0x00`: the referenced token is valid.
126    pub const VALID: Self = Self(0);
127    /// `0x01`: the referenced token is revoked.
128    pub const INVALID: Self = Self(1);
129    /// `0x02`: the referenced token is suspended.
130    pub const SUSPENDED: Self = Self(2);
131
132    /// The value.
133    pub const fn value(self) -> u8 {
134        self.0
135    }
136
137    /// Whether this is [`VALID`](Self::VALID).
138    pub const fn is_valid(self) -> bool {
139        self.0 == 0
140    }
141
142    /// The registered name of the value, if it has one.
143    pub const fn name(self) -> Option<&'static str> {
144        match self.0 {
145            0 => Some("VALID"),
146            1 => Some("INVALID"),
147            2 => Some("SUSPENDED"),
148            _ => None,
149        }
150    }
151}
152
153impl fmt::Display for StatusType {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        match self.name() {
156            Some(name) => write!(f, "{name} ({:#04x})", self.0),
157            None => write!(f, "{:#04x}", self.0),
158        }
159    }
160}
161
162impl From<u8> for StatusType {
163    fn from(value: u8) -> Self {
164        Self(value)
165    }
166}
167
168/// Why a status list or status list token could not be used.
169#[derive(Debug, thiserror::Error)]
170#[non_exhaustive]
171pub enum StatusListError {
172    /// `bits` is not 1, 2, 4 or 8.
173    #[error("`bits` is {0}, not 1, 2, 4 or 8")]
174    InvalidBits(u8),
175    /// A status value does not fit in `bits`.
176    #[error("status value {value} does not fit in {bits} bits")]
177    InvalidStatusValue {
178        /// The value.
179        value: u8,
180        /// The bits per status.
181        bits: u8,
182    },
183    /// The index is beyond the end of the list.
184    #[error("index {idx} is out of range of a status list of {len} entries")]
185    IndexOutOfRange {
186        /// The index.
187        idx: u64,
188        /// The number of entries in the list.
189        len: usize,
190    },
191    /// `lst` is not base64url, or not a ZLIB-compressed byte array.
192    #[error("invalid status list: {0}")]
193    Compression(String),
194    /// The decompressed list is larger than the limit.
195    #[error("the status list decompresses to more than {limit} bytes")]
196    TooLarge {
197        /// The limit.
198        limit: usize,
199    },
200    /// The status list token is not a compact JWS this crate decodes.
201    #[error("invalid JWS: {0}")]
202    Jws(#[from] JwsError),
203    /// The status list token's `typ` is not `statuslist+jwt`.
204    #[error("header `typ` is {0:?}, not `statuslist+jwt`")]
205    UnexpectedTyp(Option<String>),
206    /// The status list token's payload is not a status list token claims set.
207    #[error("the payload is not a status list token: {0}")]
208    Payload(String),
209    /// The token's `sub` is not the `uri` of the reference.
210    #[error("status list token `sub` {sub:?} is not the referenced {uri:?}")]
211    SubjectMismatch {
212        /// The token's `sub`.
213        sub: String,
214        /// The reference's `uri`.
215        uri: String,
216    },
217    /// The status list token has expired.
218    #[error("status list token expired at {} (now is {})", expired_at.to_rfc3339(), now.to_rfc3339())]
219    Expired {
220        /// Its `exp`.
221        expired_at: DateTime<Utc>,
222        /// The validation time.
223        now: DateTime<Utc>,
224    },
225}
226
227/// A status list (Token Status List §4): one status of `bits` bits per referenced token,
228/// packed into a byte array from the least significant bit up, ZLIB-compressed and
229/// base64url-encoded as `lst` in JSON.
230///
231/// # Examples
232///
233/// ```
234/// use identity_kit::sd_jwt_vc::{StatusList, StatusType};
235/// use serde_json::json;
236///
237/// // Token Status List §4.1: 16 statuses of 1 bit.
238/// let list: StatusList = serde_json::from_value(json!({ "bits": 1, "lst": "eNrbuRgAAhcBXQ" }))?;
239/// assert_eq!(list.as_bytes(), &[0xb9, 0xa3]);
240/// assert_eq!(list.get(0), Some(StatusType::INVALID));
241/// assert_eq!(list.get(1), Some(StatusType::VALID));
242/// assert_eq!(list.get(15), Some(StatusType::INVALID));
243/// assert_eq!(list.get(16), None);
244///
245/// let mut rebuilt = StatusList::new(1, 16)?;
246/// for index in [0, 3, 4, 5, 7, 8, 9, 13, 15] {
247///     rebuilt.set(index, StatusType::INVALID)?;
248/// }
249/// assert_eq!(rebuilt.as_bytes(), list.as_bytes());
250/// # Ok::<(), Box<dyn std::error::Error>>(())
251/// ```
252#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct StatusList {
254    bits: u8,
255    bytes: Vec<u8>,
256    aggregation_uri: Option<String>,
257}
258
259impl StatusList {
260    /// The largest decompressed list [`Deserialize`] accepts: 16 MiB, room for 16 million
261    /// 8-bit statuses.
262    pub const MAX_BYTES: usize = 1 << 24;
263
264    /// An all-`VALID` list of `size` statuses of `bits` bits each.
265    ///
266    /// # Errors
267    ///
268    /// [`StatusListError::InvalidBits`] unless `bits` is 1, 2, 4 or 8.
269    pub fn new(bits: u8, size: usize) -> Result<Self, StatusListError> {
270        check_bits(bits)?;
271        let bytes = vec![0; (size * usize::from(bits)).div_ceil(8)];
272        Ok(Self {
273            bits,
274            bytes,
275            aggregation_uri: None,
276        })
277    }
278
279    /// A list over the uncompressed byte array `bytes`.
280    ///
281    /// # Errors
282    ///
283    /// [`StatusListError::InvalidBits`] unless `bits` is 1, 2, 4 or 8.
284    pub fn from_bytes(bits: u8, bytes: Vec<u8>) -> Result<Self, StatusListError> {
285        check_bits(bits)?;
286        Ok(Self {
287            bits,
288            bytes,
289            aggregation_uri: None,
290        })
291    }
292
293    /// This list with `aggregation_uri`.
294    #[must_use]
295    pub fn with_aggregation_uri(mut self, uri: impl Into<String>) -> Self {
296        self.aggregation_uri = Some(uri.into());
297        self
298    }
299
300    /// The bits per status: 1, 2, 4 or 8.
301    pub const fn bits(&self) -> u8 {
302        self.bits
303    }
304
305    /// The uncompressed byte array.
306    pub fn as_bytes(&self) -> &[u8] {
307        &self.bytes
308    }
309
310    /// The `aggregation_uri`, if any.
311    pub fn aggregation_uri(&self) -> Option<&str> {
312        self.aggregation_uri.as_deref()
313    }
314
315    /// The number of statuses the list holds.
316    pub fn len(&self) -> usize {
317        self.bytes.len() * 8 / usize::from(self.bits)
318    }
319
320    /// Whether the list holds no status.
321    pub fn is_empty(&self) -> bool {
322        self.bytes.is_empty()
323    }
324
325    /// The status at `index`, or `None` beyond the end of the list.
326    pub fn get(&self, index: u64) -> Option<StatusType> {
327        let (byte, shift) = self.position(index)?;
328        let mask = self.mask();
329        Some(StatusType((self.bytes.get(byte)? >> shift) & mask))
330    }
331
332    /// Sets the status at `index`.
333    ///
334    /// # Errors
335    ///
336    /// [`StatusListError::IndexOutOfRange`] or [`StatusListError::InvalidStatusValue`] when
337    /// the value does not fit in `bits`.
338    pub fn set(&mut self, index: u64, status: StatusType) -> Result<(), StatusListError> {
339        let mask = self.mask();
340        if status.0 & !mask != 0 {
341            return Err(StatusListError::InvalidStatusValue {
342                value: status.0,
343                bits: self.bits,
344            });
345        }
346        let len = self.len();
347        let out_of_range = || StatusListError::IndexOutOfRange { idx: index, len };
348        let (byte, shift) = self.position(index).ok_or_else(out_of_range)?;
349        let slot = self.bytes.get_mut(byte).ok_or_else(out_of_range)?;
350        *slot = (*slot & !(mask << shift)) | (status.0 << shift);
351        Ok(())
352    }
353
354    /// The byte and the bit offset of `index`.
355    fn position(&self, index: u64) -> Option<(usize, u8)> {
356        let bits = u64::from(self.bits);
357        let bit = index.checked_mul(bits)?;
358        let byte = usize::try_from(bit / 8).ok()?;
359        let shift = u8::try_from(bit % 8).ok()?;
360        (byte < self.bytes.len()).then_some((byte, shift))
361    }
362
363    /// The mask of one status.
364    const fn mask(&self) -> u8 {
365        if self.bits == 8 {
366            0xff
367        } else {
368            (1 << self.bits) - 1
369        }
370    }
371
372    /// The ZLIB-compressed byte array, base64url-encoded: the `lst` member.
373    pub fn to_lst(&self) -> String {
374        let compressed = miniz_oxide::deflate::compress_to_vec_zlib(&self.bytes, 10);
375        base64url::encode(&compressed)
376    }
377
378    /// Decodes `lst`, refusing a list that decompresses to more than `max_bytes`.
379    ///
380    /// # Errors
381    ///
382    /// [`StatusListError::InvalidBits`], [`StatusListError::Compression`] or
383    /// [`StatusListError::TooLarge`].
384    pub fn from_lst(bits: u8, lst: &str, max_bytes: usize) -> Result<Self, StatusListError> {
385        check_bits(bits)?;
386        let compressed = base64url::decode(lst)
387            .map_err(|e| StatusListError::Compression(format!("`lst` is not base64url: {e}")))?;
388        let bytes = miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(&compressed, max_bytes)
389            .map_err(|e| match e.status {
390                miniz_oxide::inflate::TINFLStatus::HasMoreOutput => {
391                    StatusListError::TooLarge { limit: max_bytes }
392                }
393                status => {
394                    StatusListError::Compression(format!("`lst` is not ZLIB data: {status:?}"))
395                }
396            })?;
397        Ok(Self {
398            bits,
399            bytes,
400            aggregation_uri: None,
401        })
402    }
403}
404
405/// Checks `bits` is 1, 2, 4 or 8.
406fn check_bits(bits: u8) -> Result<(), StatusListError> {
407    match bits {
408        1 | 2 | 4 | 8 => Ok(()),
409        _ => Err(StatusListError::InvalidBits(bits)),
410    }
411}
412
413/// The JSON form of a [`StatusList`].
414#[derive(Serialize, Deserialize)]
415struct StatusListJson {
416    bits: u8,
417    lst: String,
418    #[serde(default, skip_serializing_if = "Option::is_none")]
419    aggregation_uri: Option<String>,
420}
421
422impl Serialize for StatusList {
423    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
424        StatusListJson {
425            bits: self.bits,
426            lst: self.to_lst(),
427            aggregation_uri: self.aggregation_uri.clone(),
428        }
429        .serialize(serializer)
430    }
431}
432
433impl<'de> Deserialize<'de> for StatusList {
434    /// Decodes `lst` with [`StatusList::MAX_BYTES`] as the limit.
435    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
436        let json = StatusListJson::deserialize(deserializer)?;
437        let mut list = Self::from_lst(json.bits, &json.lst, Self::MAX_BYTES)
438            .map_err(serde::de::Error::custom)?;
439        list.aggregation_uri = json.aggregation_uri;
440        Ok(list)
441    }
442}
443
444/// The claims of a status list token in JWT format (Token Status List §5.1).
445///
446/// # Examples
447///
448/// ```
449/// use chrono::{TimeZone as _, Utc};
450/// use identity_kit::sd_jwt_vc::{StatusListRef, StatusListToken, StatusType};
451/// use identity_kit::vc::Jwt;
452///
453/// // Token Status List §8.2.
454/// let jwt: Jwt = "eyJhbGciOiJFUzI1NiIsImtpZCI6IjEyIiwidHlwIjoic3RhdHVzbGlzdCtqd3QifQ.e\
455///     yJleHAiOjIyOTE3MjAxNzAsImlhdCI6MTY4NjkyMDE3MCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsInN0YXR1\
456///     c19saXN0Ijp7ImJpdHMiOjEsImxzdCI6ImVOcmJ1UmdBQWhjQlhRIn0sInN1YiI6Imh0dHBzOi8vZXhhbXBsZS5jb20\
457///     vc3RhdHVzbGlzdHMvMSIsInR0bCI6NDMyMDB9.2lKUUNG503R9htu4aHAYi7vjmr3sgApbfoDvPrl65N3URUO1EYqq\
458///     Ql45Jfzd-Av4QzlKa3oVALpLwOEUOq-U_g".parse()?;
459/// let (header, token) = StatusListToken::decode(&jwt)?;
460/// assert_eq!(header.kid.as_deref(), Some("12"));
461/// assert_eq!(token.sub, "https://example.com/statuslists/1");
462/// assert_eq!(token.ttl, Some(43200));
463///
464/// let now = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
465/// let reference = StatusListRef::new("https://example.com/statuslists/1", 0);
466/// assert_eq!(token.status_of(&reference, now)?, StatusType::INVALID);
467/// assert_eq!(token.status_of(&StatusListRef::new(&reference.uri, 1), now)?, StatusType::VALID);
468/// assert!(token.status_of(&StatusListRef::new(&reference.uri, 16), now).is_err());
469/// # Ok::<(), Box<dyn std::error::Error>>(())
470/// ```
471#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
472#[non_exhaustive]
473pub struct StatusListToken {
474    /// `sub`: the URI of the token, the `uri` referenced tokens use.
475    pub sub: String,
476    /// `iat`: when the token was issued. A `NumericDate` with a fraction is floored.
477    #[serde(deserialize_with = "crate::vc::required_numeric_date")]
478    pub iat: i64,
479    /// `exp`: when the token expires. A `NumericDate` with a fraction is floored.
480    #[serde(
481        default,
482        skip_serializing_if = "Option::is_none",
483        deserialize_with = "crate::vc::numeric_date"
484    )]
485    pub exp: Option<i64>,
486    /// `ttl`: how many seconds the token may be cached.
487    #[serde(default, skip_serializing_if = "Option::is_none")]
488    pub ttl: Option<u64>,
489    /// `status_list`: the list.
490    pub status_list: StatusList,
491    /// Every other claim, in insertion order.
492    #[serde(flatten)]
493    pub additional: IndexMap<String, Value>,
494}
495
496impl StatusListToken {
497    /// The `typ` of a status list token, `statuslist+jwt`.
498    pub const TYP: &'static str = "statuslist+jwt";
499
500    /// The media type of a status list token, `application/statuslist+jwt`.
501    pub const MEDIA_TYPE: &'static str = "application/statuslist+jwt";
502
503    /// A token for the list at `sub`, issued at `iat`.
504    pub fn new(sub: impl Into<String>, iat: DateTime<Utc>, status_list: StatusList) -> Self {
505        Self {
506            sub: sub.into(),
507            iat: JwtClaims::numeric_date(iat),
508            exp: None,
509            ttl: None,
510            status_list,
511            additional: IndexMap::new(),
512        }
513    }
514
515    /// This token with `exp`.
516    #[must_use]
517    pub fn with_expires_at(mut self, time: DateTime<Utc>) -> Self {
518        self.exp = Some(JwtClaims::numeric_date(time));
519        self
520    }
521
522    /// This token with `ttl`.
523    #[must_use]
524    pub fn with_ttl(mut self, seconds: u64) -> Self {
525        self.ttl = Some(seconds);
526        self
527    }
528
529    /// This token with the claim `name` set to `value`.
530    #[must_use]
531    pub fn with_claim(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
532        self.additional.insert(name.into(), value.into());
533        self
534    }
535
536    /// Decodes `jwt` without verifying it: the header, whose `typ` must be
537    /// `statuslist+jwt`, and the claims.
538    ///
539    /// # Errors
540    ///
541    /// [`StatusListError::Jws`], [`StatusListError::UnexpectedTyp`] or
542    /// [`StatusListError::Payload`].
543    pub fn decode(jwt: &Jwt) -> Result<(JwsHeader, Self), StatusListError> {
544        let jws = jwt.decode()?;
545        if jws.header().typ.as_deref() != Some(Self::TYP) {
546            return Err(StatusListError::UnexpectedTyp(jws.header().typ.clone()));
547        }
548        let token = serde_json::from_slice(jws.payload())
549            .map_err(|e| StatusListError::Payload(e.to_string()))?;
550        Ok((jws.header().clone(), token))
551    }
552
553    /// Signs this token with `signer`, `typ: statuslist+jwt` and the signer's `kid`.
554    ///
555    /// # Errors
556    ///
557    /// [`StatusListError::Jws`] when signing fails.
558    pub async fn sign(&self, signer: &dyn Signer) -> Result<Jwt, StatusListError> {
559        let mut header = JwsHeader::new(signer.alg()).with_typ(Self::TYP);
560        header.kid = signer.kid().map(str::to_owned);
561        let payload = serde_json::to_vec(self).expect("a token serializes");
562        let jwt = crate::jose::encode_compact(&header, &payload, signer).await?;
563        Ok(Jwt::new(jwt).expect("a freshly encoded JWS decodes"))
564    }
565
566    /// The status of the token `reference` points at (Token Status List §8.3, steps 4 to 6):
567    /// `sub` must be the referenced URI, the token must not have expired at `now`, the index
568    /// must be in range.
569    ///
570    /// # Errors
571    ///
572    /// [`StatusListError::SubjectMismatch`], [`StatusListError::Expired`] or
573    /// [`StatusListError::IndexOutOfRange`].
574    pub fn status_of(
575        &self,
576        reference: &StatusListRef,
577        now: DateTime<Utc>,
578    ) -> Result<StatusType, StatusListError> {
579        if self.sub != reference.uri {
580            return Err(StatusListError::SubjectMismatch {
581                sub: self.sub.clone(),
582                uri: reference.uri.clone(),
583            });
584        }
585        if let Some(exp) = self.exp {
586            let expired_at = JwtClaims::timestamp(exp).unwrap_or(DateTime::<Utc>::MIN_UTC);
587            if expired_at <= now {
588                return Err(StatusListError::Expired { expired_at, now });
589            }
590        }
591        self.status_list
592            .get(reference.idx)
593            .ok_or(StatusListError::IndexOutOfRange {
594                idx: reference.idx,
595                len: self.status_list.len(),
596            })
597    }
598}