use crate::{Credential, CredentialStatus, Issuer};
use async_trait::async_trait;
use bitvec::prelude::Lsb0;
use bitvec::slice::BitSlice;
use bitvec::vec::BitVec;
use core::convert::TryFrom;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use ssi_core::one_or_many::OneOrMany;
use ssi_core::uri::URI;
use ssi_dids::did_resolve::DIDResolver;
use ssi_json_ld::{ContextLoader, REVOCATION_LIST_2020_V1_CONTEXT, STATUS_LIST_2021_V1_CONTEXT};
use ssi_ldp::VerificationResult;
use thiserror::Error;
#[allow(clippy::upper_case_acronyms)]
type URL = String;
pub const MIN_BITSTRING_LENGTH: usize = 131072;
pub const MAX_RESPONSE_LENGTH: usize = 2097152;
const EMPTY_RLIST: &str = "H4sIAAAAAAAA_-3AMQEAAADCoPVPbQwfKAAAAAAAAAAAAAAAAAAAAOBthtJUqwBAAAA";
pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct RevocationList2020Status {
pub id: URI,
pub revocation_list_index: RevocationListIndex,
pub revocation_list_credential: URL,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct StatusList2021Entry {
pub id: URI,
pub status_purpose: String,
pub status_list_index: RevocationListIndex,
pub status_list_credential: URL,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(try_from = "String")]
#[serde(into = "String")]
pub struct RevocationListIndex(usize);
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct RevocationList2020Credential {
pub id: URI,
pub issuer: Issuer,
pub credential_subject: RevocationList2020Subject,
#[serde(flatten)]
pub more_properties: Value,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type")]
pub enum RevocationList2020Subject {
RevocationList2020(RevocationList2020),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type")]
pub enum StatusList2021Subject {
StatusList2021(StatusList2021),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct StatusList2021Credential {
pub id: URI,
pub issuer: Issuer,
pub credential_subject: StatusList2021Subject,
#[serde(flatten)]
pub more_properties: Value,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct RevocationList2020 {
pub encoded_list: EncodedList,
#[serde(flatten)]
pub more_properties: Value,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct StatusList2021 {
pub encoded_list: EncodedList,
#[serde(flatten)]
pub more_properties: Value,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct EncodedList(pub String);
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct List(pub Vec<u8>);
impl TryFrom<String> for RevocationListIndex {
type Error = std::num::ParseIntError;
fn try_from(string: String) -> Result<Self, Self::Error> {
Ok(Self(string.parse()?))
}
}
impl From<RevocationListIndex> for String {
fn from(idx: RevocationListIndex) -> String {
idx.0.to_string()
}
}
#[derive(Error, Debug)]
pub enum SetStatusError {
#[error("Encode list: {0}")]
Encode(#[from] EncodeListError),
#[error("Decode list: {0}")]
Decode(#[from] DecodeListError),
#[error("Out of bounds: bitstring index {0} but length is {1}")]
OutOfBounds(usize, usize),
#[error("Revocation list bitstring is too large for BitVec: {0}")]
ListTooLarge(usize),
#[error("Revocation list bitstring is too small: {0}. Minimum: {1}")]
ListTooSmall(usize, usize),
}
impl RevocationList2020 {
pub fn set_status(&mut self, index: usize, revoked: bool) -> Result<(), SetStatusError> {
let mut list = List::try_from(&self.encoded_list)?;
let bitstring_len = list.0.len() * 8;
let mut bitstring = BitVec::<Lsb0, u8>::try_from_vec(list.0)
.map_err(|_| SetStatusError::ListTooLarge(bitstring_len))?;
if bitstring_len < MIN_BITSTRING_LENGTH {
return Err(SetStatusError::ListTooSmall(
bitstring_len,
MIN_BITSTRING_LENGTH,
));
}
if let Some(mut bitref) = bitstring.get_mut(index) {
*bitref = revoked;
} else {
return Err(SetStatusError::OutOfBounds(index, bitstring_len));
}
list.0 = bitstring.into_vec();
self.encoded_list = EncodedList::try_from(&list)?;
Ok(())
}
}
#[derive(Error, Debug)]
pub enum NewStatusListError {
#[error("Unable to encode list")]
EncodedList(#[source] NewEncodedListError),
}
impl StatusList2021 {
pub fn new(len: usize) -> Result<Self, NewStatusListError> {
Ok(StatusList2021 {
encoded_list: EncodedList::new(len).map_err(NewStatusListError::EncodedList)?,
more_properties: serde_json::Value::Null,
})
}
pub fn set_status(&mut self, index: usize, revoked: bool) -> Result<(), SetStatusError> {
let mut list = List::try_from(&self.encoded_list)?;
let bitstring_len = list.0.len() * 8;
let mut bitstring = BitVec::<Lsb0, u8>::try_from_vec(list.0)
.map_err(|_| SetStatusError::ListTooLarge(bitstring_len))?;
if bitstring_len < MIN_BITSTRING_LENGTH {
return Err(SetStatusError::ListTooSmall(
bitstring_len,
MIN_BITSTRING_LENGTH,
));
}
if let Some(mut bitref) = bitstring.get_mut(index) {
*bitref = revoked;
} else {
return Err(SetStatusError::OutOfBounds(index, bitstring_len));
}
list.0 = bitstring.into_vec();
self.encoded_list = EncodedList::try_from(&list)?;
Ok(())
}
}
#[derive(Error, Debug)]
pub enum ListIterDecodeError {
#[error("Unable to reference indexes: {0}")]
BitSpan(#[from] bitvec::ptr::BitSpanError<u8>),
#[error("Revocation list bitstring is too small: {0}. Minimum: {1}")]
ListTooSmall(usize, usize),
}
impl List {
pub fn iter_revoked_indexes(
&self,
) -> Result<bitvec::slice::IterOnes<Lsb0, u8>, ListIterDecodeError> {
let bitstring = BitSlice::<Lsb0, u8>::from_slice(&self.0[..])?;
if bitstring.len() < MIN_BITSTRING_LENGTH {
return Err(ListIterDecodeError::ListTooSmall(
bitstring.len(),
MIN_BITSTRING_LENGTH,
));
}
Ok(bitstring.iter_ones())
}
}
#[derive(Error, Debug)]
pub enum DecodeListError {
#[error("Base64url: {0}")]
Build(#[from] base64::DecodeError),
#[error("Decompression: {0}")]
Decompress(#[from] std::io::Error),
}
#[derive(Error, Debug)]
pub enum EncodeListError {
#[error("Compression: {0}")]
Compress(#[from] std::io::Error),
}
impl Default for EncodedList {
fn default() -> Self {
Self(EMPTY_RLIST.to_string())
}
}
#[derive(Error, Debug)]
pub enum NewEncodedListError {
#[error("Length is not a multiple of 8: {0}")]
LengthMultiple8(usize),
#[error("Unable to encode list")]
Encode(#[source] EncodeListError),
}
impl EncodedList {
pub fn new(bit_len: usize) -> Result<Self, NewEncodedListError> {
if bit_len % 8 != 0 {
return Err(NewEncodedListError::LengthMultiple8(bit_len));
}
let byte_len = bit_len / 8;
let vec: Vec<u8> = vec![0; byte_len];
let list = List(vec);
EncodedList::try_from(&list).map_err(NewEncodedListError::Encode)
}
}
impl TryFrom<&EncodedList> for List {
type Error = DecodeListError;
fn try_from(encoded_list: &EncodedList) -> Result<Self, Self::Error> {
let string = &encoded_list.0;
let bytes = base64::decode_config(string, base64::URL_SAFE)?;
let mut data = Vec::new();
use flate2::bufread::GzDecoder;
use std::io::Read;
GzDecoder::new(bytes.as_slice()).read_to_end(&mut data)?;
Ok(Self(data))
}
}
impl TryFrom<&List> for EncodedList {
type Error = EncodeListError;
fn try_from(list: &List) -> Result<Self, Self::Error> {
use flate2::{write::GzEncoder, Compression};
use std::io::Write;
let mut e = GzEncoder::new(Vec::new(), Compression::default());
e.write_all(&list.0)?;
let bytes = e.finish()?;
let string = base64::encode_config(bytes, base64::URL_SAFE_NO_PAD);
Ok(EncodedList(string))
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl CredentialStatus for RevocationList2020Status {
async fn check(
&self,
credential: &Credential,
resolver: &dyn DIDResolver,
context_loader: &mut ContextLoader,
) -> VerificationResult {
let mut result = VerificationResult::new();
let issuer_id = match &credential.issuer {
Some(issuer) => issuer.get_id().clone(),
None => {
return result.with_error("Credential is missing issuer".to_string());
}
};
if !credential
.context
.contains_uri(REVOCATION_LIST_2020_V1_CONTEXT)
{
return result.with_error(format!(
"Missing expected context URI {} for credential using RevocationList2020",
REVOCATION_LIST_2020_V1_CONTEXT
));
}
if self.id == URI::String(self.revocation_list_credential.clone()) {
return result.with_error(format!(
"Expected revocationListCredential to be different from status id: {}",
self.id
));
}
match self.revocation_list_credential.split_once(':') {
Some(("https", _)) => (),
Some((_scheme, _)) => return result.with_error(format!("Invalid schema: {}", self.id)),
_ => return result.with_error(format!("Invalid rsrc: {}", self.id)),
}
let revocation_list_credential =
match load_credential(&self.revocation_list_credential).await {
Ok(credential) => credential,
Err(e) => {
return result
.with_error(format!("Unable to fetch revocation list credential: {}", e));
}
};
let list_issuer_id = match &revocation_list_credential.issuer {
Some(issuer) => issuer.get_id().clone(),
None => {
return result
.with_error("Revocation list credential is missing issuer".to_string());
}
};
if issuer_id != list_issuer_id {
return result.with_error(format!(
"Revocation list issuer mismatch. Credential: {}, Revocation list: {}",
issuer_id, list_issuer_id
));
}
if let Err(e) = revocation_list_credential.validate() {
return result.with_error(format!("Invalid list credential: {}", e));
}
let vc_result = revocation_list_credential
.verify(None, resolver, context_loader)
.await;
for warning in vc_result.warnings {
result
.warnings
.push(format!("Revocation list: {}", warning));
}
for error in vc_result.errors {
result.errors.push(format!("Revocation list: {}", error));
}
if !result.errors.is_empty() {
return result;
}
let revocation_list_credential =
match RevocationList2020Credential::try_from(revocation_list_credential) {
Ok(credential) => credential,
Err(e) => {
return result
.with_error(format!("Unable to parse revocation list credential: {}", e));
}
};
if revocation_list_credential.id != URI::String(self.revocation_list_credential.to_string())
{
return result.with_error(format!(
"Revocation list credential id mismatch. revocationListCredential: {}, id: {}",
self.revocation_list_credential, revocation_list_credential.id
));
}
let RevocationList2020Subject::RevocationList2020(revocation_list) =
revocation_list_credential.credential_subject;
let list = match List::try_from(&revocation_list.encoded_list) {
Ok(list) => list,
Err(e) => return result.with_error(format!("Unable to decode revocation list: {}", e)),
};
let credential_index = self.revocation_list_index.0;
use bitvec::prelude::*;
let bitstring = match BitVec::<Lsb0, u8>::try_from_vec(list.0) {
Ok(bitstring) => bitstring,
Err(list) => {
return result.with_error(format!(
"Revocation list is too large for bitvec: {}",
list.len()
))
}
};
let revoked = match bitstring.get(credential_index) {
Some(bitref) => *bitref,
None => {
return result
.with_error("Credential index in revocation list is invalid.".to_string());
}
};
if revoked {
return result.with_error("Credential is revoked.".to_string());
}
result
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl CredentialStatus for StatusList2021Entry {
async fn check(
&self,
credential: &Credential,
resolver: &dyn DIDResolver,
context_loader: &mut ContextLoader,
) -> VerificationResult {
let mut result = VerificationResult::new();
let issuer_id = match &credential.issuer {
Some(issuer) => issuer.get_id().clone(),
None => {
return result.with_error("Credential is missing issuer".to_string());
}
};
if !credential.context.contains_uri(STATUS_LIST_2021_V1_CONTEXT) {
return result.with_error(format!(
"Missing expected context URI {} for credential using StatusList2021",
STATUS_LIST_2021_V1_CONTEXT
));
}
if self.id == URI::String(self.status_list_credential.clone()) {
return result.with_error(format!(
"Expected statusListCredential to be different from status id: {}",
self.id
));
}
match self.status_list_credential.split_once(':') {
Some(("https", _)) => (),
Some((_scheme, _)) => return result.with_error(format!("Invalid schema: {}", self.id)),
_ => return result.with_error(format!("Invalid rsrc: {}", self.id)),
}
let status_list_credential = match load_credential(&self.status_list_credential).await {
Ok(credential) => credential,
Err(e) => {
return result.with_error(format!("Unable to fetch status list credential: {}", e));
}
};
let list_issuer_id = match &status_list_credential.issuer {
Some(issuer) => issuer.get_id().clone(),
None => {
return result.with_error("Status list credential is missing issuer".to_string());
}
};
if issuer_id != list_issuer_id {
return result.with_error(format!(
"Status list issuer mismatch. Credential: {}, Status list: {}",
issuer_id, list_issuer_id
));
}
if let Err(e) = status_list_credential.validate() {
return result.with_error(format!("Invalid list credential: {}", e));
}
let vc_result = status_list_credential
.verify(None, resolver, context_loader)
.await;
for warning in vc_result.warnings {
result.warnings.push(format!("Status list: {}", warning));
}
if let Some(error) = vc_result.errors.into_iter().next() {
result.errors.push(format!("Status list: {}", error));
return result;
}
let status_list_credential =
match StatusList2021Credential::try_from(status_list_credential) {
Ok(credential) => credential,
Err(e) => {
return result
.with_error(format!("Unable to parse status list credential: {}", e));
}
};
if status_list_credential.id != URI::String(self.status_list_credential.to_string()) {
return result.with_error(format!(
"Status list credential id mismatch. statusListCredential: {}, id: {}",
self.status_list_credential, status_list_credential.id
));
}
let StatusList2021Subject::StatusList2021(status_list) =
status_list_credential.credential_subject;
let list = match List::try_from(&status_list.encoded_list) {
Ok(list) => list,
Err(e) => return result.with_error(format!("Unable to decode status list: {}", e)),
};
let credential_index = self.status_list_index.0;
use bitvec::prelude::*;
let bitstring = match BitVec::<Lsb0, u8>::try_from_vec(list.0) {
Ok(bitstring) => bitstring,
Err(list) => {
return result.with_error(format!(
"Revocation list is too large for bitvec: {}",
list.len()
))
}
};
let revoked = match bitstring.get(credential_index) {
Some(bitref) => *bitref,
None => {
return result
.with_error("Credential index in revocation list is invalid.".to_string());
}
};
if revoked {
return result.with_error("Credential is revoked.".to_string());
}
result
}
}
#[derive(Error, Debug)]
pub enum LoadResourceError {
#[error("Error building HTTP client: {0}")]
Build(reqwest::Error),
#[error("Error sending HTTP request: {0}")]
Request(reqwest::Error),
#[error("Parse error: {0}")]
Response(String),
#[error("Not found")]
NotFound,
#[error("HTTP error: {0}")]
HTTP(String),
#[error("Resource is too large: {size}, expected maximum: {max}")]
TooLarge {
size: usize,
max: usize,
},
#[error("Unable to convert content-length header value")]
ContentLengthConversion(#[source] std::num::TryFromIntError),
}
async fn load_resource(url: &str) -> Result<Vec<u8>, LoadResourceError> {
#[cfg(test)]
match url {
crate::tests::EXAMPLE_REVOCATION_2020_LIST_URL => {
return Ok(crate::tests::EXAMPLE_REVOCATION_2020_LIST.to_vec());
}
crate::tests::EXAMPLE_STATUS_LIST_2021_URL => {
return Ok(crate::tests::EXAMPLE_STATUS_LIST_2021.to_vec());
}
_ => {}
}
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
"User-Agent",
reqwest::header::HeaderValue::from_static(USER_AGENT),
);
let client = reqwest::Client::builder()
.default_headers(headers)
.build()
.map_err(LoadResourceError::Build)?;
let accept = "application/json".to_string();
let resp = client
.get(url)
.header("Accept", accept)
.send()
.await
.map_err(LoadResourceError::Request)?;
if let Err(err) = resp.error_for_status_ref() {
if err.status() == Some(reqwest::StatusCode::NOT_FOUND) {
return Err(LoadResourceError::NotFound);
}
return Err(LoadResourceError::HTTP(err.to_string()));
}
#[allow(unused_variables)]
let content_length_opt = if let Some(content_length) = resp.content_length() {
let len =
usize::try_from(content_length).map_err(LoadResourceError::ContentLengthConversion)?;
if len > MAX_RESPONSE_LENGTH {
return Err(LoadResourceError::TooLarge {
size: len,
max: MAX_RESPONSE_LENGTH,
});
}
Some(len)
} else {
None
};
#[cfg(target_arch = "wasm32")]
{
let bytes = resp
.bytes()
.await
.map_err(|e| LoadResourceError::Response(e.to_string()))?
.to_vec();
if bytes.len() > MAX_RESPONSE_LENGTH {
return Err(LoadResourceError::TooLarge {
size: bytes.len(),
max: MAX_RESPONSE_LENGTH,
});
}
Ok(bytes)
}
#[cfg(not(target_arch = "wasm32"))]
{
let mut bytes = if let Some(len) = content_length_opt {
Vec::with_capacity(len)
} else {
Vec::new()
};
let mut resp = resp;
while let Some(chunk) = resp
.chunk()
.await
.map_err(|e| LoadResourceError::Response(e.to_string()))?
{
let len = bytes.len() + chunk.len();
if len > MAX_RESPONSE_LENGTH {
return Err(LoadResourceError::TooLarge {
size: len,
max: MAX_RESPONSE_LENGTH,
});
}
bytes.append(&mut chunk.to_vec());
}
Ok(bytes)
}
}
#[derive(Error, Debug)]
pub enum LoadCredentialError {
#[error("Unable to load resource: {0}")]
Load(#[from] LoadResourceError),
#[error("Error reading HTTP response: {0}")]
Parse(#[from] serde_json::Error),
}
pub async fn load_credential(url: &str) -> Result<Credential, LoadCredentialError> {
let data = load_resource(url).await?;
let credential: Credential = serde_json::from_slice(&data)?;
Ok(credential)
}
#[derive(Error, Debug)]
pub enum CredentialConversionError {
#[error("Conversion to JSON: {0}")]
ToValue(serde_json::Error),
#[error("Conversion from JSON: {0}")]
FromValue(serde_json::Error),
#[error("Missing expected URI in @context: {0}")]
MissingContext(&'static str),
#[error("Missing expected type: {0}. Found: {0:?}")]
MissingType(&'static str, OneOrMany<String>),
#[error("Missing issuer")]
MissingIssuer,
}
impl TryFrom<Credential> for RevocationList2020Credential {
type Error = CredentialConversionError;
fn try_from(credential: Credential) -> Result<Self, Self::Error> {
if !credential
.context
.contains_uri(REVOCATION_LIST_2020_V1_CONTEXT)
{
return Err(CredentialConversionError::MissingContext(
REVOCATION_LIST_2020_V1_CONTEXT,
));
}
if !credential
.type_
.contains(&"RevocationList2020Credential".to_string())
{
return Err(CredentialConversionError::MissingType(
"RevocationList2020Credential",
credential.type_,
));
}
let credential =
serde_json::to_value(credential).map_err(CredentialConversionError::ToValue)?;
let credential =
serde_json::from_value(credential).map_err(CredentialConversionError::FromValue)?;
Ok(credential)
}
}
impl TryFrom<RevocationList2020Credential> for Credential {
type Error = CredentialConversionError;
fn try_from(credential: RevocationList2020Credential) -> Result<Self, Self::Error> {
let mut credential =
serde_json::to_value(credential).map_err(CredentialConversionError::ToValue)?;
use crate::DEFAULT_CONTEXT;
use serde_json::json;
credential["@context"] = json!([DEFAULT_CONTEXT, REVOCATION_LIST_2020_V1_CONTEXT]);
credential["type"] = json!(["VerifiableCredential", "RevocationList2020Credential"]);
let credential =
serde_json::from_value(credential).map_err(CredentialConversionError::FromValue)?;
Ok(credential)
}
}
impl TryFrom<Credential> for StatusList2021Credential {
type Error = CredentialConversionError;
fn try_from(credential: Credential) -> Result<Self, Self::Error> {
if !credential.context.contains_uri(STATUS_LIST_2021_V1_CONTEXT) {
return Err(CredentialConversionError::MissingContext(
STATUS_LIST_2021_V1_CONTEXT,
));
}
if !credential
.type_
.contains(&"StatusList2021Credential".to_string())
{
return Err(CredentialConversionError::MissingType(
"StatusList2021Credential",
credential.type_,
));
}
let credential =
serde_json::to_value(credential).map_err(CredentialConversionError::ToValue)?;
let credential =
serde_json::from_value(credential).map_err(CredentialConversionError::FromValue)?;
Ok(credential)
}
}
impl TryFrom<StatusList2021Credential> for Credential {
type Error = CredentialConversionError;
fn try_from(credential: StatusList2021Credential) -> Result<Self, Self::Error> {
let mut credential =
serde_json::to_value(credential).map_err(CredentialConversionError::ToValue)?;
use crate::DEFAULT_CONTEXT;
use serde_json::json;
credential["@context"] = json!([DEFAULT_CONTEXT, STATUS_LIST_2021_V1_CONTEXT]);
credential["type"] = json!(["VerifiableCredential", "StatusList2021Credential"]);
let credential =
serde_json::from_value(credential).map_err(CredentialConversionError::FromValue)?;
Ok(credential)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_list() {
let list = List(vec![0; MIN_BITSTRING_LENGTH / 8]);
let revoked_indexes = list.iter_revoked_indexes().unwrap().collect::<Vec<usize>>();
let empty: Vec<usize> = Vec::new();
assert_eq!(revoked_indexes, empty);
let el = EncodedList::try_from(&list).unwrap();
assert_eq!(EncodedList::default(), el);
let decoded_list = List::try_from(&el).unwrap();
assert_eq!(decoded_list, list);
}
#[test]
fn set_status() {
let mut rl = RevocationList2020::default();
rl.set_status(1, true).unwrap();
rl.set_status(5, true).unwrap();
let decoded_list = List::try_from(&rl.encoded_list).unwrap();
let revoked_indexes = decoded_list
.iter_revoked_indexes()
.unwrap()
.collect::<Vec<usize>>();
assert_eq!(revoked_indexes, vec![1, 5]);
}
}