Skip to main content

dpp_vc/
status_list.rs

1//! W3C Bitstring Status List v1.0 — pure decoding + bit indexing.
2//!
3//! Revocation/suspension of a Verifiable Credential is expressed by a bit in a
4//! shared, published "status list" credential. **Fetching** that credential
5//! over the network is an infrastructure concern and lives outside core; this
6//! module provides only the pure parts so `dpp-crypto` stays infra-free:
7//!
8//! 1. decode a status list's `encodedList` (multibase base64url of a GZIP-
9//!    compressed bitstring) into the raw bitstring, and
10//! 2. test the status bit for a holder's index.
11//!
12//! Source: <https://www.w3.org/TR/vc-bitstring-status-list/>
13
14use std::io::Read;
15
16use base64::Engine;
17use flate2::read::GzDecoder;
18
19/// A decoded Bitstring Status List — the GZIP-decompressed bitstring.
20#[derive(Debug, Clone)]
21pub struct StatusList {
22    bits: Vec<u8>,
23}
24
25impl StatusList {
26    /// Decode a W3C `encodedList`: an optional multibase `u` prefix followed by
27    /// base64url (no padding) of a GZIP-compressed bitstring.
28    pub fn from_encoded_list(encoded: &str) -> anyhow::Result<Self> {
29        let b64 = encoded.strip_prefix('u').unwrap_or(encoded);
30        let compressed = base64::engine::general_purpose::URL_SAFE_NO_PAD
31            .decode(b64)
32            .map_err(|e| anyhow::anyhow!("status list base64url: {e}"))?;
33        let mut bits = Vec::new();
34        GzDecoder::new(&compressed[..])
35            .read_to_end(&mut bits)
36            .map_err(|e| anyhow::anyhow!("status list gunzip: {e}"))?;
37        Ok(Self { bits })
38    }
39
40    /// Build directly from a decompressed bitstring (tests / non-encoded sources).
41    pub fn from_bitstring(bits: Vec<u8>) -> Self {
42        Self { bits }
43    }
44
45    /// Number of status entries this list can address.
46    pub fn len_bits(&self) -> usize {
47        self.bits.len() * 8
48    }
49
50    /// Test the status bit at `index`. Bits are big-endian within each byte: the
51    /// zeroth bit is the most-significant bit of the first byte. Returns `None`
52    /// when `index` is outside the list (caller should treat as indeterminate).
53    pub fn get(&self, index: usize) -> Option<bool> {
54        let byte = self.bits.get(index / 8)?;
55        Some(byte & (0x80 >> (index % 8)) != 0)
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use flate2::{Compression, write::GzEncoder};
63    use std::io::Write;
64
65    fn encode_list(bits: &[u8]) -> String {
66        let mut enc = GzEncoder::new(Vec::new(), Compression::default());
67        enc.write_all(bits).unwrap();
68        let gz = enc.finish().unwrap();
69        format!(
70            "u{}",
71            base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(gz)
72        )
73    }
74
75    #[test]
76    fn bit_indexing_is_big_endian_within_byte() {
77        let list = StatusList::from_bitstring(vec![0b1000_0001]);
78        assert_eq!(list.get(0), Some(true));
79        assert_eq!(list.get(1), Some(false));
80        assert_eq!(list.get(7), Some(true));
81        assert_eq!(list.get(8), None, "out-of-range index is None");
82    }
83
84    #[test]
85    fn encoded_list_round_trips() {
86        let bits = vec![0b0000_0100, 0b0000_0000];
87        let encoded = encode_list(&bits);
88        let list = StatusList::from_encoded_list(&encoded).expect("decode");
89        assert_eq!(list.get(5), Some(true));
90        assert_eq!(list.get(4), Some(false));
91        assert_eq!(list.get(6), Some(false));
92        assert_eq!(list.len_bits(), 16);
93    }
94
95    #[test]
96    fn decodes_without_multibase_prefix() {
97        let bits = vec![0b1000_0000];
98        let encoded = encode_list(&bits);
99        let without_u = encoded.strip_prefix('u').unwrap();
100        let list = StatusList::from_encoded_list(without_u).expect("decode");
101        assert_eq!(list.get(0), Some(true));
102    }
103
104    #[test]
105    fn garbage_is_an_error_not_a_panic() {
106        assert!(StatusList::from_encoded_list("u!!!not-base64!!!").is_err());
107        assert!(StatusList::from_encoded_list("udGhpcyBpcyBub3QgZ3ppcA").is_err());
108    }
109}