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}