Skip to main content

identity_kit/status_list/
mod.rs

1//! The IETF Token Status List ([draft-ietf-oauth-status-list-21]): the `status` claim of a
2//! referenced token, the status list and the status list token.
3//!
4//! Section numbers below are those of draft 21. The module implements its JSON and JWT
5//! formats. It does not implement the CBOR and CWT formats (§4.3, §5.2, §6.3), historical
6//! resolution (§8.4), following an `aggregation_uri` (§9, though [`StatusList`] carries
7//! one), caching by `ttl` (§13.7, though [`StatusListToken`] carries it) or the X.509
8//! extended key usage (§10).
9//!
10//! The draft defines the `status` claim for any JWT (§6.1), so the module serves every
11//! credential format of the crate: SD-JWT VCs and VCDM 2.0 credentials secured with SD-JWT
12//! ([`sd_jwt_vc`](crate::sd_jwt_vc), feature `sd-jwt`) as much as JWT credentials of either
13//! profile ([`vc`](crate::vc)). The validators of both hand the claim to a
14//! [`StatusCheck`](crate::vc::validation::StatusCheck) as
15//! [`CredentialStatus::StatusClaim`](crate::vc::validation::CredentialStatus::StatusClaim).
16//!
17//! - [`Status`] is the claim, [`StatusListRef`] its `status_list` mechanism and
18//!   [`StatusMechanism`] one mechanism of it.
19//! - [`StatusList`] is the list (§4): one [`StatusType`] of 1, 2, 4 or 8 bits per referenced
20//!   token, ZLIB-compressed in its `lst` member.
21//! - [`StatusListToken`] is the signed list (§5.1), issued with
22//!   [`sign`](StatusListToken::sign) and read with [`status_of`](StatusListToken::status_of).
23//! - `StatusListCheck` (feature `did-web` as well) is the [`StatusCheck`] that fetches the
24//!   token a reference names over an `HttpClient` and verifies it (§8).
25//!
26//! [`StatusCheck`]: crate::vc::validation::StatusCheck
27//! [draft-ietf-oauth-status-list-21]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-status-list-21
28
29use core::fmt;
30use core::str::FromStr;
31
32use chrono::{DateTime, Utc};
33use indexmap::IndexMap;
34use serde::{Deserialize, Deserializer, Serialize, Serializer};
35use serde_json::Value;
36
37use crate::cause::cause;
38use crate::jose::{JwsError, JwsHeader, Signer, base64url};
39use crate::vc::{Jwt, JwtClaims};
40
41#[cfg(feature = "did-web")]
42mod check;
43
44#[cfg(feature = "did-web")]
45#[cfg_attr(docsrs, doc(cfg(feature = "did-web")))]
46pub use check::StatusListCheck;
47
48/// The `status` claim of a referenced token: at least one status mechanism, `status_list`
49/// being the one this crate implements.
50///
51/// # Examples
52///
53/// ```
54/// use identity_kit::status_list::{Status, StatusListRef};
55/// use serde_json::json;
56///
57/// // Token Status List §6.2.
58/// let status: Status = serde_json::from_value(json!({
59///   "status_list": { "idx": 0, "uri": "https://example.com/statuslists/1" }
60/// }))?;
61/// let reference = status.status_list.as_ref().unwrap();
62/// assert_eq!(reference.idx, 0);
63/// assert_eq!(reference.uri, "https://example.com/statuslists/1");
64/// assert_eq!(status, Status::status_list(StatusListRef::new("https://example.com/statuslists/1", 0)));
65/// # Ok::<(), serde_json::Error>(())
66/// ```
67#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
68#[non_exhaustive]
69pub struct Status {
70    /// The `status_list` mechanism.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub status_list: Option<StatusListRef>,
73    /// Every other mechanism, by name.
74    #[serde(flatten)]
75    pub additional: IndexMap<String, Value>,
76}
77
78impl Status {
79    /// A `status` claim with no mechanism yet.
80    pub fn new() -> Self {
81        Self::default()
82    }
83
84    /// A `status` claim referencing a status list.
85    pub fn status_list(reference: StatusListRef) -> Self {
86        Self {
87            status_list: Some(reference),
88            additional: IndexMap::new(),
89        }
90    }
91
92    /// This claim with the mechanism `name` set to `value`.
93    #[must_use]
94    pub fn with_mechanism(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
95        self.additional.insert(name.into(), value.into());
96        self
97    }
98
99    /// Whether no mechanism is given.
100    pub fn is_empty(&self) -> bool {
101        self.status_list.is_none() && self.additional.is_empty()
102    }
103
104    /// The mechanisms, the status list first.
105    pub fn mechanisms(&self) -> impl Iterator<Item = StatusMechanism<'_>> {
106        self.status_list
107            .iter()
108            .map(StatusMechanism::StatusList)
109            .chain(
110                self.additional
111                    .iter()
112                    .map(|(name, value)| StatusMechanism::Other { name, value }),
113            )
114    }
115}
116
117/// One mechanism of a [`Status`] claim.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119#[non_exhaustive]
120pub enum StatusMechanism<'a> {
121    /// A Token Status List reference.
122    StatusList(&'a StatusListRef),
123    /// A mechanism this crate does not implement.
124    Other {
125        /// Its name, the member of `status` it is given under.
126        name: &'a str,
127        /// Its value.
128        value: &'a Value,
129    },
130}
131
132/// A reference to a status list token (`status.status_list`): the token's URI and the index
133/// of the referenced token in its list.
134#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
135#[non_exhaustive]
136pub struct StatusListRef {
137    /// `idx`: the index in the status list.
138    pub idx: u64,
139    /// `uri`: where the status list token is.
140    pub uri: String,
141}
142
143impl StatusListRef {
144    /// A reference to index `idx` of the status list token at `uri`.
145    pub fn new(uri: impl Into<String>, idx: u64) -> Self {
146        Self {
147            idx,
148            uri: uri.into(),
149        }
150    }
151}
152
153/// A status type value (Token Status List §7): a number in `0..=255`, with the registered
154/// meanings as constants.
155///
156/// It serializes as its registered name when it has one of its own (`"VALID"`, `"INVALID"`,
157/// `"SUSPENDED"`) and as a number otherwise. `APPLICATION_SPECIFIC` names `0x03` and
158/// `0x0C`-`0x0F` alike, so those serialize as numbers too. It deserializes from a registered
159/// name, a number in `0..=255`, or that number as a decimal string; [`parse`](Self::parse) and
160/// [`FromStr`] accept the same strings. Names are case-sensitive. Since the form depends on the
161/// value, deserializing needs a self-describing format such as JSON or CBOR.
162///
163/// # Examples
164///
165/// ```
166/// use identity_kit::status_list::StatusType;
167/// use serde_json::json;
168///
169/// assert_eq!(serde_json::to_value(StatusType::SUSPENDED)?, json!("SUSPENDED"));
170/// assert_eq!(serde_json::to_value(StatusType(3))?, json!(3));
171/// assert_eq!(serde_json::from_value::<StatusType>(json!("INVALID"))?, StatusType::INVALID);
172/// assert_eq!(serde_json::from_value::<StatusType>(json!(12))?, StatusType(0x0C));
173/// assert_eq!("VALID".parse::<StatusType>()?, StatusType::VALID);
174/// assert!("APPLICATION_SPECIFIC".parse::<StatusType>().is_err());
175/// # Ok::<(), Box<dyn std::error::Error>>(())
176/// ```
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
178pub struct StatusType(pub u8);
179
180impl StatusType {
181    /// `0x00`: the referenced token is valid.
182    pub const VALID: Self = Self(0);
183    /// `0x01`: the referenced token is revoked.
184    pub const INVALID: Self = Self(1);
185    /// `0x02`: the referenced token is suspended.
186    pub const SUSPENDED: Self = Self(2);
187
188    /// The value.
189    pub const fn value(self) -> u8 {
190        self.0
191    }
192
193    /// Whether this is [`VALID`](Self::VALID).
194    pub const fn is_valid(self) -> bool {
195        self.0 == 0
196    }
197
198    /// The registered name of the value, if it has one.
199    pub const fn name(self) -> Option<&'static str> {
200        match self.0 {
201            0 => Some("VALID"),
202            1 => Some("INVALID"),
203            2 => Some("SUSPENDED"),
204            _ => None,
205        }
206    }
207
208    /// The value named `s`: `VALID`, `INVALID`, `SUSPENDED`, or a decimal number in
209    /// `0..=255`.
210    ///
211    /// # Errors
212    ///
213    /// [`StatusListError::UnknownStatusType`] for anything else, `APPLICATION_SPECIFIC`
214    /// included: it names several values.
215    pub fn parse(s: &str) -> Result<Self, StatusListError> {
216        match s {
217            "VALID" => Ok(Self::VALID),
218            "INVALID" => Ok(Self::INVALID),
219            "SUSPENDED" => Ok(Self::SUSPENDED),
220            digits if !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit()) => digits
221                .parse()
222                .map(Self)
223                .map_err(|_| StatusListError::UnknownStatusType(s.to_owned())),
224            _ => Err(StatusListError::UnknownStatusType(s.to_owned())),
225        }
226    }
227}
228
229impl fmt::Display for StatusType {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        match self.name() {
232            Some(name) => write!(f, "{name} ({:#04x})", self.0),
233            None => write!(f, "{:#04x}", self.0),
234        }
235    }
236}
237
238impl From<u8> for StatusType {
239    fn from(value: u8) -> Self {
240        Self(value)
241    }
242}
243
244impl FromStr for StatusType {
245    type Err = StatusListError;
246
247    fn from_str(s: &str) -> Result<Self, Self::Err> {
248        Self::parse(s)
249    }
250}
251
252impl Serialize for StatusType {
253    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
254        match self.name() {
255            Some(name) => serializer.serialize_str(name),
256            None => serializer.serialize_u8(self.0),
257        }
258    }
259}
260
261impl<'de> Deserialize<'de> for StatusType {
262    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
263        struct StatusTypeVisitor;
264
265        impl serde::de::Visitor<'_> for StatusTypeVisitor {
266            type Value = StatusType;
267
268            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269                f.write_str("VALID, INVALID, SUSPENDED or a number in 0..=255")
270            }
271
272            fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<StatusType, E> {
273                StatusType::parse(s).map_err(E::custom)
274            }
275
276            fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<StatusType, E> {
277                u8::try_from(value)
278                    .map(StatusType)
279                    .map_err(|_| E::invalid_value(serde::de::Unexpected::Unsigned(value), &self))
280            }
281
282            fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<StatusType, E> {
283                u8::try_from(value)
284                    .map(StatusType)
285                    .map_err(|_| E::invalid_value(serde::de::Unexpected::Signed(value), &self))
286            }
287        }
288
289        deserializer.deserialize_any(StatusTypeVisitor)
290    }
291}
292
293/// Why a status list or status list token could not be used.
294#[derive(Debug, thiserror::Error)]
295#[non_exhaustive]
296pub enum StatusListError {
297    /// `bits` is not 1, 2, 4 or 8.
298    #[error("`bits` is {0}, not 1, 2, 4 or 8")]
299    InvalidBits(u8),
300    /// A status value does not fit in `bits`.
301    #[error("status value {value} does not fit in {bits} bits")]
302    InvalidStatusValue {
303        /// The value.
304        value: u8,
305        /// The bits per status.
306        bits: u8,
307    },
308    /// The index is beyond the end of the list.
309    #[error("index {idx} is out of range of a status list of {len} entries")]
310    IndexOutOfRange {
311        /// The index.
312        idx: u64,
313        /// The number of entries in the list.
314        len: usize,
315    },
316    /// `lst` is not base64url, or not a ZLIB-compressed byte array. The source is the base64url
317    /// error, when that is the cause.
318    #[error("invalid status list: {reason}{}", cause(.source.as_ref()))]
319    Compression {
320        /// What is wrong with `lst`.
321        reason: String,
322        /// The underlying error.
323        source: Option<Box<dyn std::error::Error + Send + Sync>>,
324    },
325    /// The decompressed list is larger than the limit.
326    #[error("the status list decompresses to more than {limit} bytes")]
327    TooLarge {
328        /// The limit.
329        limit: usize,
330    },
331    /// The status list token is not a compact JWS this crate decodes.
332    #[error("invalid JWS: {0}")]
333    Jws(#[from] JwsError),
334    /// A string that is neither a registered status type name of its own nor a number in
335    /// `0..=255`.
336    #[error("{0:?} is not VALID, INVALID, SUSPENDED or a number in 0..=255")]
337    UnknownStatusType(String),
338    /// The status list token's `typ` is not `statuslist+jwt`.
339    #[error("header `typ` is {0:?}, not `statuslist+jwt`")]
340    UnexpectedTyp(Option<String>),
341    /// The status list token's payload is not a status list token claims set; the source is
342    /// the JSON error.
343    #[error("the payload is not a status list token: {0}")]
344    Payload(#[source] serde_json::Error),
345    /// The token's `sub` is not the `uri` of the reference.
346    #[error("status list token `sub` {sub:?} is not the referenced {uri:?}")]
347    SubjectMismatch {
348        /// The token's `sub`.
349        sub: String,
350        /// The reference's `uri`.
351        uri: String,
352    },
353    /// The status list token has expired.
354    #[error("status list token expired at {} (now is {})", expired_at.to_rfc3339(), now.to_rfc3339())]
355    Expired {
356        /// Its `exp`.
357        expired_at: DateTime<Utc>,
358        /// The validation time.
359        now: DateTime<Utc>,
360    },
361}
362
363/// A status list (Token Status List §4): one status of `bits` bits per referenced token,
364/// packed into a byte array from the least significant bit up, ZLIB-compressed and
365/// base64url-encoded as `lst` in JSON.
366///
367/// # Examples
368///
369/// ```
370/// use identity_kit::status_list::{StatusList, StatusType};
371/// use serde_json::json;
372///
373/// // Token Status List §4.1: 16 statuses of 1 bit.
374/// let list: StatusList = serde_json::from_value(json!({ "bits": 1, "lst": "eNrbuRgAAhcBXQ" }))?;
375/// assert_eq!(list.as_bytes(), &[0xb9, 0xa3]);
376/// assert_eq!(list.get(0), Some(StatusType::INVALID));
377/// assert_eq!(list.get(1), Some(StatusType::VALID));
378/// assert_eq!(list.get(15), Some(StatusType::INVALID));
379/// assert_eq!(list.get(16), None);
380///
381/// let mut rebuilt = StatusList::new(1, 16)?;
382/// for index in [0, 3, 4, 5, 7, 8, 9, 13, 15] {
383///     rebuilt.set(index, StatusType::INVALID)?;
384/// }
385/// assert_eq!(rebuilt.as_bytes(), list.as_bytes());
386/// # Ok::<(), Box<dyn std::error::Error>>(())
387/// ```
388#[derive(Debug, Clone, PartialEq, Eq)]
389pub struct StatusList {
390    bits: u8,
391    bytes: Vec<u8>,
392    aggregation_uri: Option<String>,
393}
394
395impl StatusList {
396    /// The largest decompressed list [`Deserialize`] accepts: 16 MiB, room for 16 million
397    /// 8-bit statuses.
398    pub const MAX_BYTES: usize = 1 << 24;
399
400    /// An all-`VALID` list of `size` statuses of `bits` bits each.
401    ///
402    /// # Errors
403    ///
404    /// [`StatusListError::InvalidBits`] unless `bits` is 1, 2, 4 or 8.
405    pub fn new(bits: u8, size: usize) -> Result<Self, StatusListError> {
406        check_bits(bits)?;
407        let bytes = vec![0; (size * usize::from(bits)).div_ceil(8)];
408        Ok(Self {
409            bits,
410            bytes,
411            aggregation_uri: None,
412        })
413    }
414
415    /// A list over the uncompressed byte array `bytes`.
416    ///
417    /// # Errors
418    ///
419    /// [`StatusListError::InvalidBits`] unless `bits` is 1, 2, 4 or 8.
420    pub fn from_bytes(bits: u8, bytes: Vec<u8>) -> Result<Self, StatusListError> {
421        check_bits(bits)?;
422        Ok(Self {
423            bits,
424            bytes,
425            aggregation_uri: None,
426        })
427    }
428
429    /// This list with `aggregation_uri`.
430    #[must_use]
431    pub fn with_aggregation_uri(mut self, uri: impl Into<String>) -> Self {
432        self.aggregation_uri = Some(uri.into());
433        self
434    }
435
436    /// The bits per status: 1, 2, 4 or 8.
437    pub const fn bits(&self) -> u8 {
438        self.bits
439    }
440
441    /// The uncompressed byte array.
442    pub fn as_bytes(&self) -> &[u8] {
443        &self.bytes
444    }
445
446    /// The `aggregation_uri`, if any.
447    pub fn aggregation_uri(&self) -> Option<&str> {
448        self.aggregation_uri.as_deref()
449    }
450
451    /// The number of statuses the list holds.
452    pub fn len(&self) -> usize {
453        self.bytes.len() * 8 / usize::from(self.bits)
454    }
455
456    /// Whether the list holds no status.
457    pub fn is_empty(&self) -> bool {
458        self.bytes.is_empty()
459    }
460
461    /// The status at `index`, or `None` beyond the end of the list.
462    pub fn get(&self, index: u64) -> Option<StatusType> {
463        let (byte, shift) = self.position(index)?;
464        let mask = self.mask();
465        Some(StatusType((self.bytes.get(byte)? >> shift) & mask))
466    }
467
468    /// Sets the status at `index`.
469    ///
470    /// # Errors
471    ///
472    /// [`StatusListError::IndexOutOfRange`] or [`StatusListError::InvalidStatusValue`] when
473    /// the value does not fit in `bits`.
474    pub fn set(&mut self, index: u64, status: StatusType) -> Result<(), StatusListError> {
475        let mask = self.mask();
476        if status.0 & !mask != 0 {
477            return Err(StatusListError::InvalidStatusValue {
478                value: status.0,
479                bits: self.bits,
480            });
481        }
482        let len = self.len();
483        let out_of_range = || StatusListError::IndexOutOfRange { idx: index, len };
484        let (byte, shift) = self.position(index).ok_or_else(out_of_range)?;
485        let slot = self.bytes.get_mut(byte).ok_or_else(out_of_range)?;
486        *slot = (*slot & !(mask << shift)) | (status.0 << shift);
487        Ok(())
488    }
489
490    /// The byte and the bit offset of `index`.
491    fn position(&self, index: u64) -> Option<(usize, u8)> {
492        let bits = u64::from(self.bits);
493        let bit = index.checked_mul(bits)?;
494        let byte = usize::try_from(bit / 8).ok()?;
495        let shift = u8::try_from(bit % 8).ok()?;
496        (byte < self.bytes.len()).then_some((byte, shift))
497    }
498
499    /// The mask of one status.
500    const fn mask(&self) -> u8 {
501        if self.bits == 8 {
502            0xff
503        } else {
504            (1 << self.bits) - 1
505        }
506    }
507
508    /// The ZLIB-compressed byte array, base64url-encoded: the `lst` member.
509    pub fn to_lst(&self) -> String {
510        let compressed = miniz_oxide::deflate::compress_to_vec_zlib(&self.bytes, 10);
511        base64url::encode(&compressed)
512    }
513
514    /// Decodes `lst`, refusing a list that decompresses to more than `max_bytes`.
515    ///
516    /// # Errors
517    ///
518    /// [`StatusListError::InvalidBits`], [`StatusListError::Compression`] or
519    /// [`StatusListError::TooLarge`].
520    pub fn from_lst(bits: u8, lst: &str, max_bytes: usize) -> Result<Self, StatusListError> {
521        check_bits(bits)?;
522        let compressed = base64url::decode(lst).map_err(|e| StatusListError::Compression {
523            reason: "`lst` is not base64url".to_owned(),
524            source: Some(Box::new(e)),
525        })?;
526        let bytes = miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(&compressed, max_bytes)
527            .map_err(|e| match e.status {
528                miniz_oxide::inflate::TINFLStatus::HasMoreOutput => {
529                    StatusListError::TooLarge { limit: max_bytes }
530                }
531                status => StatusListError::Compression {
532                    reason: format!("`lst` is not ZLIB data: {status:?}"),
533                    source: None,
534                },
535            })?;
536        Ok(Self {
537            bits,
538            bytes,
539            aggregation_uri: None,
540        })
541    }
542}
543
544/// Checks `bits` is 1, 2, 4 or 8.
545fn check_bits(bits: u8) -> Result<(), StatusListError> {
546    match bits {
547        1 | 2 | 4 | 8 => Ok(()),
548        _ => Err(StatusListError::InvalidBits(bits)),
549    }
550}
551
552/// The JSON form of a [`StatusList`].
553#[derive(Serialize, Deserialize)]
554struct StatusListJson {
555    bits: u8,
556    lst: String,
557    #[serde(default, skip_serializing_if = "Option::is_none")]
558    aggregation_uri: Option<String>,
559}
560
561impl Serialize for StatusList {
562    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
563        StatusListJson {
564            bits: self.bits,
565            lst: self.to_lst(),
566            aggregation_uri: self.aggregation_uri.clone(),
567        }
568        .serialize(serializer)
569    }
570}
571
572impl<'de> Deserialize<'de> for StatusList {
573    /// Decodes `lst` with [`StatusList::MAX_BYTES`] as the limit.
574    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
575        let json = StatusListJson::deserialize(deserializer)?;
576        let mut list = Self::from_lst(json.bits, &json.lst, Self::MAX_BYTES)
577            .map_err(serde::de::Error::custom)?;
578        list.aggregation_uri = json.aggregation_uri;
579        Ok(list)
580    }
581}
582
583/// The claims of a status list token in JWT format (Token Status List §5.1).
584///
585/// # Examples
586///
587/// ```
588/// use chrono::{TimeZone as _, Utc};
589/// use identity_kit::status_list::{StatusListRef, StatusListToken, StatusType};
590/// use identity_kit::vc::Jwt;
591///
592/// // Token Status List §8.2.
593/// let jwt: Jwt = "eyJhbGciOiJFUzI1NiIsImtpZCI6IjEyIiwidHlwIjoic3RhdHVzbGlzdCtqd3QifQ.e\
594///     yJleHAiOjIyOTE3MjAxNzAsImlhdCI6MTY4NjkyMDE3MCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsInN0YXR1\
595///     c19saXN0Ijp7ImJpdHMiOjEsImxzdCI6ImVOcmJ1UmdBQWhjQlhRIn0sInN1YiI6Imh0dHBzOi8vZXhhbXBsZS5jb20\
596///     vc3RhdHVzbGlzdHMvMSIsInR0bCI6NDMyMDB9.2lKUUNG503R9htu4aHAYi7vjmr3sgApbfoDvPrl65N3URUO1EYqq\
597///     Ql45Jfzd-Av4QzlKa3oVALpLwOEUOq-U_g".parse()?;
598/// let (header, token) = StatusListToken::decode(&jwt)?;
599/// assert_eq!(header.kid.as_deref(), Some("12"));
600/// assert_eq!(token.sub, "https://example.com/statuslists/1");
601/// assert_eq!(token.ttl, Some(43200));
602///
603/// let now = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
604/// let reference = StatusListRef::new("https://example.com/statuslists/1", 0);
605/// assert_eq!(token.status_of(&reference, now)?, StatusType::INVALID);
606/// assert_eq!(token.status_of(&StatusListRef::new(&reference.uri, 1), now)?, StatusType::VALID);
607/// assert!(token.status_of(&StatusListRef::new(&reference.uri, 16), now).is_err());
608/// # Ok::<(), Box<dyn std::error::Error>>(())
609/// ```
610#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
611#[non_exhaustive]
612pub struct StatusListToken {
613    /// `sub`: the URI of the token, the `uri` referenced tokens use.
614    pub sub: String,
615    /// `iat`: when the token was issued. A `NumericDate` with a fraction is floored.
616    #[serde(deserialize_with = "crate::vc::required_numeric_date")]
617    pub iat: i64,
618    /// `exp`: when the token expires. A `NumericDate` with a fraction is floored.
619    #[serde(
620        default,
621        skip_serializing_if = "Option::is_none",
622        deserialize_with = "crate::vc::numeric_date"
623    )]
624    pub exp: Option<i64>,
625    /// `ttl`: how many seconds the token may be cached.
626    #[serde(default, skip_serializing_if = "Option::is_none")]
627    pub ttl: Option<u64>,
628    /// `status_list`: the list.
629    pub status_list: StatusList,
630    /// Every other claim, in insertion order.
631    #[serde(flatten)]
632    pub additional: IndexMap<String, Value>,
633}
634
635impl StatusListToken {
636    /// The `typ` of a status list token, `statuslist+jwt`.
637    pub const TYP: &'static str = "statuslist+jwt";
638
639    /// The media type of a status list token, `application/statuslist+jwt`.
640    pub const MEDIA_TYPE: &'static str = "application/statuslist+jwt";
641
642    /// A token for the list at `sub`, issued at `iat`.
643    pub fn new(sub: impl Into<String>, iat: DateTime<Utc>, status_list: StatusList) -> Self {
644        Self {
645            sub: sub.into(),
646            iat: JwtClaims::numeric_date(iat),
647            exp: None,
648            ttl: None,
649            status_list,
650            additional: IndexMap::new(),
651        }
652    }
653
654    /// This token with `exp`.
655    #[must_use]
656    pub fn with_expires_at(mut self, time: DateTime<Utc>) -> Self {
657        self.exp = Some(JwtClaims::numeric_date(time));
658        self
659    }
660
661    /// This token with `ttl`.
662    #[must_use]
663    pub fn with_ttl(mut self, seconds: u64) -> Self {
664        self.ttl = Some(seconds);
665        self
666    }
667
668    /// This token with the claim `name` set to `value`.
669    #[must_use]
670    pub fn with_claim(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
671        self.additional.insert(name.into(), value.into());
672        self
673    }
674
675    /// Decodes `jwt` without verifying it: the header, whose `typ` must be
676    /// `statuslist+jwt`, and the claims.
677    ///
678    /// # Errors
679    ///
680    /// [`StatusListError::Jws`], [`StatusListError::UnexpectedTyp`] or
681    /// [`StatusListError::Payload`].
682    pub fn decode(jwt: &Jwt) -> Result<(JwsHeader, Self), StatusListError> {
683        let jws = jwt.decode()?;
684        if jws.header().typ.as_deref() != Some(Self::TYP) {
685            return Err(StatusListError::UnexpectedTyp(jws.header().typ.clone()));
686        }
687        let token = serde_json::from_slice(jws.payload()).map_err(StatusListError::Payload)?;
688        Ok((jws.header().clone(), token))
689    }
690
691    /// Signs this token with `signer`, `typ: statuslist+jwt` and the signer's `kid`.
692    ///
693    /// # Errors
694    ///
695    /// [`StatusListError::Jws`] when signing fails.
696    ///
697    /// # Panics
698    ///
699    /// Never: the token always serializes to JSON, and the JWS is one this function has just
700    /// encoded.
701    pub async fn sign(&self, signer: &dyn Signer) -> Result<Jwt, StatusListError> {
702        let mut header = JwsHeader::new(signer.alg()).with_typ(Self::TYP);
703        header.kid = signer.kid().map(str::to_owned);
704        let payload = serde_json::to_vec(self).expect("a token serializes");
705        let jwt = crate::jose::encode_compact(&header, &payload, signer).await?;
706        Ok(Jwt::new(jwt).expect("a freshly encoded JWS decodes"))
707    }
708
709    /// The status of the token `reference` points at (Token Status List §8.3, steps 4 to 6):
710    /// `sub` must be the referenced URI, the token must not have expired at `now`, the index
711    /// must be in range.
712    ///
713    /// # Errors
714    ///
715    /// [`StatusListError::SubjectMismatch`], [`StatusListError::Expired`] or
716    /// [`StatusListError::IndexOutOfRange`].
717    pub fn status_of(
718        &self,
719        reference: &StatusListRef,
720        now: DateTime<Utc>,
721    ) -> Result<StatusType, StatusListError> {
722        if self.sub != reference.uri {
723            return Err(StatusListError::SubjectMismatch {
724                sub: self.sub.clone(),
725                uri: reference.uri.clone(),
726            });
727        }
728        if let Some(exp) = self.exp {
729            let expired_at = JwtClaims::timestamp(exp).unwrap_or(DateTime::<Utc>::MIN_UTC);
730            if expired_at <= now {
731                return Err(StatusListError::Expired { expired_at, now });
732            }
733        }
734        self.status_list
735            .get(reference.idx)
736            .ok_or(StatusListError::IndexOutOfRange {
737                idx: reference.idx,
738                len: self.status_list.len(),
739            })
740    }
741}