identity_credential/revocation/revocation_bitmap_2022/
bitmap.rs

1// Copyright 2020-2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::borrow::Cow;
5use std::io::Write;
6
7use flate2::write::ZlibDecoder;
8use flate2::write::ZlibEncoder;
9use flate2::Compression;
10use identity_core::common::Object;
11use identity_core::common::Url;
12use identity_core::convert::Base;
13use identity_core::convert::BaseEncoding;
14use identity_did::DIDUrl;
15use roaring::RoaringBitmap;
16
17use crate::revocation::error::RevocationError;
18use identity_document::service::Service;
19use identity_document::service::ServiceEndpoint;
20
21const DATA_URL_PATTERN: &str = "data:application/octet-stream;base64,";
22
23/// A compressed bitmap for managing credential revocation.
24#[derive(Clone, Debug, Default, PartialEq)]
25pub struct RevocationBitmap(RoaringBitmap);
26
27impl RevocationBitmap {
28  /// The name of the service type.
29  pub const TYPE: &'static str = "RevocationBitmap2022";
30
31  /// Constructs a new empty [`RevocationBitmap`].
32  pub fn new() -> Self {
33    Self(RoaringBitmap::new())
34  }
35
36  /// Returns `true` if the credential at the given `index` is revoked.
37  pub fn is_revoked(&self, index: u32) -> bool {
38    self.0.contains(index)
39  }
40
41  /// Mark the given `index` as revoked.
42  ///
43  /// Returns true if the `index` was absent from the set.
44  pub fn revoke(&mut self, index: u32) -> bool {
45    self.0.insert(index)
46  }
47
48  /// Mark the `index` as not revoked.
49  ///
50  /// Returns true if the `index` was present in the set.
51  pub fn unrevoke(&mut self, index: u32) -> bool {
52    self.0.remove(index)
53  }
54
55  /// Returns the number of revoked credentials.
56  pub fn len(&self) -> u64 {
57    self.0.len()
58  }
59
60  /// Returns `true` if no credentials are revoked, `false` otherwise.
61  pub fn is_empty(&self) -> bool {
62    self.0.is_empty()
63  }
64
65  /// Return a [`Service`] with:
66  /// - the service's id set to `service_id`,
67  /// - of type `RevocationBitmap2022`,
68  /// - and with the bitmap embedded in a data url in the service's endpoint.
69  pub fn to_service(&self, service_id: DIDUrl) -> Result<Service, RevocationError> {
70    let endpoint: ServiceEndpoint = self.to_endpoint()?;
71    Service::builder(Object::new())
72      .id(service_id)
73      .type_(RevocationBitmap::TYPE)
74      .service_endpoint(endpoint)
75      .build()
76      .map_err(|_| RevocationError::InvalidService("service builder error"))
77  }
78
79  /// Return the bitmap as a data url embedded in a service endpoint.
80  pub(crate) fn to_endpoint(&self) -> Result<ServiceEndpoint, RevocationError> {
81    let endpoint_data: String = self.serialize_compressed_base64()?;
82
83    let data_url = format!("{DATA_URL_PATTERN}{endpoint_data}");
84    Url::parse(data_url)
85      .map(ServiceEndpoint::One)
86      .map_err(|e| RevocationError::UrlConstructionError(e.into()))
87  }
88
89  /// Construct a `RevocationBitmap` from a data url embedded in `service_endpoint`.
90  pub(crate) fn try_from_endpoint(service_endpoint: &ServiceEndpoint) -> Result<Self, RevocationError> {
91    if let ServiceEndpoint::One(url) = service_endpoint {
92      let Some(encoded_bitmap) = url.as_str().strip_prefix(DATA_URL_PATTERN) else {
93        return Err(RevocationError::InvalidService(
94          "invalid url - expected an `application/octet-stream;base64` data url",
95        ));
96      };
97
98      RevocationBitmap::deserialize_compressed_base64(encoded_bitmap)
99    } else {
100      Err(RevocationError::InvalidService(
101        "invalid endpoint - expected a single data url",
102      ))
103    }
104  }
105
106  /// Deserializes a compressed [`RevocationBitmap`] base64-encoded `data`.
107  pub(crate) fn deserialize_compressed_base64<T>(data: &T) -> Result<Self, RevocationError>
108  where
109    T: AsRef<str> + ?Sized,
110  {
111    // Fixes issue #1291.
112    // Before this fix, revocation bitmaps had been encoded twice, like so:
113    // Base64Url(Base64(compressed_bitmap)).
114    // This fix checks if the encoded string it receives as input has undergone such process
115    // and undo the inner Base64 encoding before processing the input further.
116    let mut data = Cow::Borrowed(data.as_ref());
117    if !data.starts_with("eJy") {
118      // Base64 encoded zlib default compression header
119      let decoded = BaseEncoding::decode(&data, Base::Base64)
120        .map_err(|e| RevocationError::Base64DecodingError(data.into_owned(), e))?;
121      data = Cow::Owned(
122        String::from_utf8(decoded)
123          .map_err(|_| RevocationError::InvalidService("invalid data url - expected valid utf-8"))?,
124      );
125    }
126    let decoded_data: Vec<u8> = BaseEncoding::decode(&data, Base::Base64Url)
127      .map_err(|e| RevocationError::Base64DecodingError(data.as_ref().to_owned(), e))?;
128    let decompressed_data: Vec<u8> = Self::decompress_zlib(decoded_data)?;
129    Self::deserialize_slice(&decompressed_data)
130  }
131
132  /// Serializes and compressess [`RevocationBitmap`] as a base64-encoded `String`.
133  pub(crate) fn serialize_compressed_base64(&self) -> Result<String, RevocationError> {
134    let serialized_data: Vec<u8> = self.serialize_vec()?;
135    Self::compress_zlib(serialized_data).map(|data| BaseEncoding::encode(&data, Base::Base64Url))
136  }
137
138  /// Deserializes [`RevocationBitmap`] from a slice of bytes.
139  fn deserialize_slice(data: &[u8]) -> Result<Self, RevocationError> {
140    RoaringBitmap::deserialize_from(data)
141      .map_err(RevocationError::BitmapDecodingError)
142      .map(Self)
143  }
144
145  /// Serializes a [`RevocationBitmap`] as a vector of bytes.
146  fn serialize_vec(&self) -> Result<Vec<u8>, RevocationError> {
147    let mut output: Vec<u8> = Vec::with_capacity(self.0.serialized_size());
148    self
149      .0
150      .serialize_into(&mut output)
151      .map_err(RevocationError::BitmapEncodingError)?;
152    Ok(output)
153  }
154
155  fn compress_zlib<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, RevocationError> {
156    let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
157    encoder
158      .write_all(input.as_ref())
159      .map_err(RevocationError::BitmapEncodingError)?;
160    encoder.finish().map_err(RevocationError::BitmapEncodingError)
161  }
162
163  fn decompress_zlib<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, RevocationError> {
164    let mut writer = Vec::new();
165    let mut decoder = ZlibDecoder::new(writer);
166    decoder
167      .write_all(input.as_ref())
168      .map_err(RevocationError::BitmapDecodingError)?;
169    writer = decoder.finish().map_err(RevocationError::BitmapDecodingError)?;
170    Ok(writer)
171  }
172}
173
174impl TryFrom<&Service> for RevocationBitmap {
175  type Error = RevocationError;
176
177  /// Try to construct a `RevocationBitmap` from a service
178  /// if it is a valid Revocation Bitmap Service.
179  fn try_from(service: &Service) -> Result<Self, RevocationError> {
180    if !service.type_().contains(Self::TYPE) {
181      return Err(RevocationError::InvalidService(
182        "invalid type - expected `RevocationBitmap2022`",
183      ));
184    }
185
186    Self::try_from_endpoint(service.service_endpoint())
187  }
188}
189
190#[cfg(test)]
191mod tests {
192  use identity_core::common::Url;
193
194  use super::RevocationBitmap;
195
196  #[test]
197  fn test_serialize_base64_round_trip() {
198    let mut embedded_revocation_list = RevocationBitmap::new();
199    let base64_compressed_revocation_list: String = embedded_revocation_list.serialize_compressed_base64().unwrap();
200
201    assert_eq!(&base64_compressed_revocation_list, "eJyzMmAAAwADKABr");
202    assert_eq!(
203      RevocationBitmap::deserialize_compressed_base64(&base64_compressed_revocation_list).unwrap(),
204      embedded_revocation_list
205    );
206
207    for credential in [0, 5, 6, 8] {
208      embedded_revocation_list.revoke(credential);
209    }
210    let base64_compressed_revocation_list: String = embedded_revocation_list.serialize_compressed_base64().unwrap();
211
212    assert_eq!(
213      &base64_compressed_revocation_list,
214      "eJyzMmBgYGQAAWYGATDNysDGwMEAAAscAJI"
215    );
216    assert_eq!(
217      RevocationBitmap::deserialize_compressed_base64(&base64_compressed_revocation_list).unwrap(),
218      embedded_revocation_list
219    );
220  }
221
222  #[test]
223  fn test_revocation_bitmap_test_vector_1() {
224    const URL: &str = "data:application/octet-stream;base64,eJyzMmAAAwADKABr";
225
226    let bitmap: RevocationBitmap = RevocationBitmap::try_from_endpoint(
227      &identity_document::service::ServiceEndpoint::One(Url::parse(URL).unwrap()),
228    )
229    .unwrap();
230
231    assert!(bitmap.is_empty());
232  }
233
234  #[test]
235  fn test_revocation_bitmap_test_vector_2() {
236    const URL: &str = "data:application/octet-stream;base64,eJyzMmBgYGQAAWYGATDNysDGwMEAAAscAJI";
237    const EXPECTED: &[u32] = &[0, 5, 6, 8];
238
239    let bitmap: RevocationBitmap = RevocationBitmap::try_from_endpoint(
240      &identity_document::service::ServiceEndpoint::One(Url::parse(URL).unwrap()),
241    )
242    .unwrap();
243
244    for revoked in EXPECTED {
245      assert!(bitmap.is_revoked(*revoked));
246    }
247
248    assert_eq!(bitmap.len(), 4);
249  }
250
251  #[test]
252  fn test_revocation_bitmap_test_vector_3() {
253    const URL: &str = "data:application/octet-stream;base64,eJyzMmBgYGQAAWYGASCpxbCEMUNAYAkAEpcCeg";
254    const EXPECTED: &[u32] = &[42, 420, 4200, 42000];
255
256    let bitmap: RevocationBitmap = RevocationBitmap::try_from_endpoint(
257      &identity_document::service::ServiceEndpoint::One(Url::parse(URL).unwrap()),
258    )
259    .unwrap();
260
261    for &index in EXPECTED {
262      assert!(bitmap.is_revoked(index));
263    }
264  }
265
266  #[test]
267  fn test_revocation_bitmap_pre_1291_fix() {
268    const URL: &str = "data:application/octet-stream;base64,ZUp5ek1tQmdZR0lBQVVZZ1pHQ1FBR0laSUdabDZHUGN3UW9BRXVvQjlB";
269    const EXPECTED: &[u32] = &[5, 398, 67000];
270
271    let bitmap: RevocationBitmap = RevocationBitmap::try_from_endpoint(
272      &identity_document::service::ServiceEndpoint::One(Url::parse(URL).unwrap()),
273    )
274    .unwrap();
275
276    for revoked in EXPECTED {
277      assert!(bitmap.is_revoked(*revoked));
278    }
279
280    assert_eq!(bitmap.len(), 3);
281  }
282}