identity_credential/revocation/revocation_bitmap_2022/
bitmap.rsuse std::borrow::Cow;
use std::io::Write;
use flate2::write::ZlibDecoder;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use identity_core::common::Object;
use identity_core::common::Url;
use identity_core::convert::Base;
use identity_core::convert::BaseEncoding;
use identity_did::DIDUrl;
use roaring::RoaringBitmap;
use crate::revocation::error::RevocationError;
use identity_document::service::Service;
use identity_document::service::ServiceEndpoint;
const DATA_URL_PATTERN: &str = "data:application/octet-stream;base64,";
#[derive(Clone, Debug, Default, PartialEq)]
pub struct RevocationBitmap(RoaringBitmap);
impl RevocationBitmap {
pub const TYPE: &'static str = "RevocationBitmap2022";
pub fn new() -> Self {
Self(RoaringBitmap::new())
}
pub fn is_revoked(&self, index: u32) -> bool {
self.0.contains(index)
}
pub fn revoke(&mut self, index: u32) -> bool {
self.0.insert(index)
}
pub fn unrevoke(&mut self, index: u32) -> bool {
self.0.remove(index)
}
pub fn len(&self) -> u64 {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn to_service(&self, service_id: DIDUrl) -> Result<Service, RevocationError> {
let endpoint: ServiceEndpoint = self.to_endpoint()?;
Service::builder(Object::new())
.id(service_id)
.type_(RevocationBitmap::TYPE)
.service_endpoint(endpoint)
.build()
.map_err(|_| RevocationError::InvalidService("service builder error"))
}
pub(crate) fn to_endpoint(&self) -> Result<ServiceEndpoint, RevocationError> {
let endpoint_data: String = self.serialize_compressed_base64()?;
let data_url = format!("{DATA_URL_PATTERN}{endpoint_data}");
Url::parse(data_url)
.map(ServiceEndpoint::One)
.map_err(|e| RevocationError::UrlConstructionError(e.into()))
}
pub(crate) fn try_from_endpoint(service_endpoint: &ServiceEndpoint) -> Result<Self, RevocationError> {
if let ServiceEndpoint::One(url) = service_endpoint {
let Some(encoded_bitmap) = url.as_str().strip_prefix(DATA_URL_PATTERN) else {
return Err(RevocationError::InvalidService(
"invalid url - expected an `application/octet-stream;base64` data url",
));
};
RevocationBitmap::deserialize_compressed_base64(encoded_bitmap)
} else {
Err(RevocationError::InvalidService(
"invalid endpoint - expected a single data url",
))
}
}
pub(crate) fn deserialize_compressed_base64<T>(data: &T) -> Result<Self, RevocationError>
where
T: AsRef<str> + ?Sized,
{
let mut data = Cow::Borrowed(data.as_ref());
if !data.starts_with("eJy") {
let decoded = BaseEncoding::decode(&data, Base::Base64)
.map_err(|e| RevocationError::Base64DecodingError(data.into_owned(), e))?;
data = Cow::Owned(
String::from_utf8(decoded)
.map_err(|_| RevocationError::InvalidService("invalid data url - expected valid utf-8"))?,
);
}
let decoded_data: Vec<u8> = BaseEncoding::decode(&data, Base::Base64Url)
.map_err(|e| RevocationError::Base64DecodingError(data.as_ref().to_owned(), e))?;
let decompressed_data: Vec<u8> = Self::decompress_zlib(decoded_data)?;
Self::deserialize_slice(&decompressed_data)
}
pub(crate) fn serialize_compressed_base64(&self) -> Result<String, RevocationError> {
let serialized_data: Vec<u8> = self.serialize_vec()?;
Self::compress_zlib(serialized_data).map(|data| BaseEncoding::encode(&data, Base::Base64Url))
}
fn deserialize_slice(data: &[u8]) -> Result<Self, RevocationError> {
RoaringBitmap::deserialize_from(data)
.map_err(RevocationError::BitmapDecodingError)
.map(Self)
}
fn serialize_vec(&self) -> Result<Vec<u8>, RevocationError> {
let mut output: Vec<u8> = Vec::with_capacity(self.0.serialized_size());
self
.0
.serialize_into(&mut output)
.map_err(RevocationError::BitmapEncodingError)?;
Ok(output)
}
fn compress_zlib<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, RevocationError> {
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder
.write_all(input.as_ref())
.map_err(RevocationError::BitmapEncodingError)?;
encoder.finish().map_err(RevocationError::BitmapEncodingError)
}
fn decompress_zlib<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, RevocationError> {
let mut writer = Vec::new();
let mut decoder = ZlibDecoder::new(writer);
decoder
.write_all(input.as_ref())
.map_err(RevocationError::BitmapDecodingError)?;
writer = decoder.finish().map_err(RevocationError::BitmapDecodingError)?;
Ok(writer)
}
}
impl TryFrom<&Service> for RevocationBitmap {
type Error = RevocationError;
fn try_from(service: &Service) -> Result<Self, RevocationError> {
if !service.type_().contains(Self::TYPE) {
return Err(RevocationError::InvalidService(
"invalid type - expected `RevocationBitmap2022`",
));
}
Self::try_from_endpoint(service.service_endpoint())
}
}
#[cfg(test)]
mod tests {
use identity_core::common::Url;
use super::RevocationBitmap;
#[test]
fn test_serialize_base64_round_trip() {
let mut embedded_revocation_list = RevocationBitmap::new();
let base64_compressed_revocation_list: String = embedded_revocation_list.serialize_compressed_base64().unwrap();
assert_eq!(&base64_compressed_revocation_list, "eJyzMmAAAwADKABr");
assert_eq!(
RevocationBitmap::deserialize_compressed_base64(&base64_compressed_revocation_list).unwrap(),
embedded_revocation_list
);
for credential in [0, 5, 6, 8] {
embedded_revocation_list.revoke(credential);
}
let base64_compressed_revocation_list: String = embedded_revocation_list.serialize_compressed_base64().unwrap();
assert_eq!(
&base64_compressed_revocation_list,
"eJyzMmBgYGQAAWYGATDNysDGwMEAAAscAJI"
);
assert_eq!(
RevocationBitmap::deserialize_compressed_base64(&base64_compressed_revocation_list).unwrap(),
embedded_revocation_list
);
}
#[test]
fn test_revocation_bitmap_test_vector_1() {
const URL: &str = "data:application/octet-stream;base64,eJyzMmAAAwADKABr";
let bitmap: RevocationBitmap = RevocationBitmap::try_from_endpoint(
&identity_document::service::ServiceEndpoint::One(Url::parse(URL).unwrap()),
)
.unwrap();
assert!(bitmap.is_empty());
}
#[test]
fn test_revocation_bitmap_test_vector_2() {
const URL: &str = "data:application/octet-stream;base64,eJyzMmBgYGQAAWYGATDNysDGwMEAAAscAJI";
const EXPECTED: &[u32] = &[0, 5, 6, 8];
let bitmap: RevocationBitmap = RevocationBitmap::try_from_endpoint(
&identity_document::service::ServiceEndpoint::One(Url::parse(URL).unwrap()),
)
.unwrap();
for revoked in EXPECTED {
assert!(bitmap.is_revoked(*revoked));
}
assert_eq!(bitmap.len(), 4);
}
#[test]
fn test_revocation_bitmap_test_vector_3() {
const URL: &str = "data:application/octet-stream;base64,eJyzMmBgYGQAAWYGASCpxbCEMUNAYAkAEpcCeg";
const EXPECTED: &[u32] = &[42, 420, 4200, 42000];
let bitmap: RevocationBitmap = RevocationBitmap::try_from_endpoint(
&identity_document::service::ServiceEndpoint::One(Url::parse(URL).unwrap()),
)
.unwrap();
for &index in EXPECTED {
assert!(bitmap.is_revoked(index));
}
}
#[test]
fn test_revocation_bitmap_pre_1291_fix() {
const URL: &str = "data:application/octet-stream;base64,ZUp5ek1tQmdZR0lBQVVZZ1pHQ1FBR0laSUdabDZHUGN3UW9BRXVvQjlB";
const EXPECTED: &[u32] = &[5, 398, 67000];
let bitmap: RevocationBitmap = RevocationBitmap::try_from_endpoint(
&identity_document::service::ServiceEndpoint::One(Url::parse(URL).unwrap()),
)
.unwrap();
for revoked in EXPECTED {
assert!(bitmap.is_revoked(*revoked));
}
assert_eq!(bitmap.len(), 3);
}
}