1use 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#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
34#[non_exhaustive]
35pub struct Status {
36 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub status_list: Option<StatusListRef>,
39 #[serde(flatten)]
41 pub additional: IndexMap<String, Value>,
42}
43
44impl Status {
45 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn status_list(reference: StatusListRef) -> Self {
52 Self {
53 status_list: Some(reference),
54 additional: IndexMap::new(),
55 }
56 }
57
58 #[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 pub fn is_empty(&self) -> bool {
67 self.status_list.is_none() && self.additional.is_empty()
68 }
69
70 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85#[non_exhaustive]
86pub enum StatusMechanism<'a> {
87 StatusList(&'a StatusListRef),
89 Other {
91 name: &'a str,
93 value: &'a Value,
95 },
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
101#[non_exhaustive]
102pub struct StatusListRef {
103 pub idx: u64,
105 pub uri: String,
107}
108
109impl StatusListRef {
110 pub fn new(uri: impl Into<String>, idx: u64) -> Self {
112 Self {
113 idx,
114 uri: uri.into(),
115 }
116 }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
122pub struct StatusType(pub u8);
123
124impl StatusType {
125 pub const VALID: Self = Self(0);
127 pub const INVALID: Self = Self(1);
129 pub const SUSPENDED: Self = Self(2);
131
132 pub const fn value(self) -> u8 {
134 self.0
135 }
136
137 pub const fn is_valid(self) -> bool {
139 self.0 == 0
140 }
141
142 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#[derive(Debug, thiserror::Error)]
170#[non_exhaustive]
171pub enum StatusListError {
172 #[error("`bits` is {0}, not 1, 2, 4 or 8")]
174 InvalidBits(u8),
175 #[error("status value {value} does not fit in {bits} bits")]
177 InvalidStatusValue {
178 value: u8,
180 bits: u8,
182 },
183 #[error("index {idx} is out of range of a status list of {len} entries")]
185 IndexOutOfRange {
186 idx: u64,
188 len: usize,
190 },
191 #[error("invalid status list: {0}")]
193 Compression(String),
194 #[error("the status list decompresses to more than {limit} bytes")]
196 TooLarge {
197 limit: usize,
199 },
200 #[error("invalid JWS: {0}")]
202 Jws(#[from] JwsError),
203 #[error("header `typ` is {0:?}, not `statuslist+jwt`")]
205 UnexpectedTyp(Option<String>),
206 #[error("the payload is not a status list token: {0}")]
208 Payload(String),
209 #[error("status list token `sub` {sub:?} is not the referenced {uri:?}")]
211 SubjectMismatch {
212 sub: String,
214 uri: String,
216 },
217 #[error("status list token expired at {} (now is {})", expired_at.to_rfc3339(), now.to_rfc3339())]
219 Expired {
220 expired_at: DateTime<Utc>,
222 now: DateTime<Utc>,
224 },
225}
226
227#[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 pub const MAX_BYTES: usize = 1 << 24;
263
264 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 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 #[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 pub const fn bits(&self) -> u8 {
302 self.bits
303 }
304
305 pub fn as_bytes(&self) -> &[u8] {
307 &self.bytes
308 }
309
310 pub fn aggregation_uri(&self) -> Option<&str> {
312 self.aggregation_uri.as_deref()
313 }
314
315 pub fn len(&self) -> usize {
317 self.bytes.len() * 8 / usize::from(self.bits)
318 }
319
320 pub fn is_empty(&self) -> bool {
322 self.bytes.is_empty()
323 }
324
325 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 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 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 const fn mask(&self) -> u8 {
365 if self.bits == 8 {
366 0xff
367 } else {
368 (1 << self.bits) - 1
369 }
370 }
371
372 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 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
405fn check_bits(bits: u8) -> Result<(), StatusListError> {
407 match bits {
408 1 | 2 | 4 | 8 => Ok(()),
409 _ => Err(StatusListError::InvalidBits(bits)),
410 }
411}
412
413#[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 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
472#[non_exhaustive]
473pub struct StatusListToken {
474 pub sub: String,
476 #[serde(deserialize_with = "crate::vc::required_numeric_date")]
478 pub iat: i64,
479 #[serde(
481 default,
482 skip_serializing_if = "Option::is_none",
483 deserialize_with = "crate::vc::numeric_date"
484 )]
485 pub exp: Option<i64>,
486 #[serde(default, skip_serializing_if = "Option::is_none")]
488 pub ttl: Option<u64>,
489 pub status_list: StatusList,
491 #[serde(flatten)]
493 pub additional: IndexMap<String, Value>,
494}
495
496impl StatusListToken {
497 pub const TYP: &'static str = "statuslist+jwt";
499
500 pub const MEDIA_TYPE: &'static str = "application/statuslist+jwt";
502
503 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 #[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 #[must_use]
524 pub fn with_ttl(mut self, seconds: u64) -> Self {
525 self.ttl = Some(seconds);
526 self
527 }
528
529 #[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 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 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 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}