identity_credential/revocation/revocation_bitmap_2022/
bitmap.rs1use 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#[derive(Clone, Debug, Default, PartialEq)]
25pub struct RevocationBitmap(RoaringBitmap);
26
27impl RevocationBitmap {
28 pub const TYPE: &'static str = "RevocationBitmap2022";
30
31 pub fn new() -> Self {
33 Self(RoaringBitmap::new())
34 }
35
36 pub fn is_revoked(&self, index: u32) -> bool {
38 self.0.contains(index)
39 }
40
41 pub fn revoke(&mut self, index: u32) -> bool {
45 self.0.insert(index)
46 }
47
48 pub fn unrevoke(&mut self, index: u32) -> bool {
52 self.0.remove(index)
53 }
54
55 pub fn len(&self) -> u64 {
57 self.0.len()
58 }
59
60 pub fn is_empty(&self) -> bool {
62 self.0.is_empty()
63 }
64
65 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 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 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 pub(crate) fn deserialize_compressed_base64<T>(data: &T) -> Result<Self, RevocationError>
108 where
109 T: AsRef<str> + ?Sized,
110 {
111 let mut data = Cow::Borrowed(data.as_ref());
117 if !data.starts_with("eJy") {
118 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 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 fn deserialize_slice(data: &[u8]) -> Result<Self, RevocationError> {
140 RoaringBitmap::deserialize_from(data)
141 .map_err(RevocationError::BitmapDecodingError)
142 .map(Self)
143 }
144
145 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 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}