ssi_vc/v1/revocation/
v2021.rs

1//! Status List 2021.
2//!
3//! See: <https://www.w3.org/community/reports/credentials/CG-FINAL-vc-status-list-2021-20230102/>
4use bitvec::prelude::Lsb0;
5use bitvec::vec::BitVec;
6use iref::UriBuf;
7use serde::{Deserialize, Serialize};
8use ssi_claims_core::VerificationParameters;
9use ssi_data_integrity::AnyDataIntegrity;
10use ssi_json_ld::STATUS_LIST_2021_V1_CONTEXT;
11use ssi_verification_methods::{AnyMethod, VerificationMethodResolver};
12use static_iref::iri;
13use std::collections::BTreeMap;
14
15use crate::{
16    syntax::RequiredType,
17    v1::{
18        revocation::{load_resource, Reason},
19        RequiredContext, SpecializedJsonCredential,
20    },
21};
22
23use super::{
24    CredentialStatus, EncodedList, List, NewEncodedListError, RevocationListIndex, SetStatusError,
25    StatusCheck, StatusCheckError, MIN_BITSTRING_LENGTH,
26};
27
28pub struct StatusList2021Context;
29
30impl RequiredContext for StatusList2021Context {
31    const CONTEXT_IRI: &'static iref::Iri = iri!("https://w3id.org/vc/status-list/2021/v1");
32}
33
34/// Revocation List 2021 Status object, for use in a Verifiable Credential's credentialStatus
35/// property.
36/// <https://w3c-ccg.github.io/vc-status-list-2021/#statuslist2021entry>
37#[derive(Debug, Serialize, Deserialize, Clone)]
38#[serde(rename_all = "camelCase")]
39pub struct StatusList2021Entry {
40    /// URL for status information of the verifiable credential - but not the URL of the status
41    /// list.
42    pub id: UriBuf,
43
44    /// Status purpose
45    ///
46    /// Defined in <https://w3c-ccg.github.io/vc-status-list-2021/#statuslist2021entry>
47    /// and <https://w3c-ccg.github.io/vc-status-list-2021/#statuslist2021credential>
48    ///
49    /// It is allowed to be an arbitrary string, although specific values "revocation" and
50    /// "suspension" are defined.
51    pub status_purpose: String,
52
53    /// Index of this credential's status in the status list credential
54    pub status_list_index: RevocationListIndex,
55
56    /// URL to a [StatusList2021Credential]
57    pub status_list_credential: UriBuf,
58}
59
60/// [Credential subject](https://www.w3.org/TR/vc-data-model/#credential-subject) of a [StatusList2021Credential]
61#[derive(Debug, Serialize, Deserialize, Clone)]
62#[serde(tag = "type")]
63pub enum StatusList2021Subject {
64    StatusList2021(StatusList2021),
65}
66
67pub struct StatusList2021CredentialType;
68
69impl RequiredType for StatusList2021CredentialType {
70    const REQUIRED_TYPE: &'static str = "StatusList2021Credential";
71}
72
73/// Verifiable Credential of type RevocationList2020Credential.
74///
75/// <https://w3c-ccg.github.io/vc-status-rl-2020/#revocationlist2020credential>
76pub type StatusList2021Credential = SpecializedJsonCredential<
77    StatusList2021Subject,
78    StatusList2021Context,
79    StatusList2021CredentialType,
80>;
81
82/// Credential subject of type StatusList2021, expected to be used in a Verifiable Credential of type [StatusList2021Credential](https://w3c-ccg.github.io/vc-status-list-2021/#statuslist2021credential)
83#[derive(Debug, Serialize, Deserialize, Clone, Default)]
84#[serde(rename_all = "camelCase")]
85pub struct StatusList2021 {
86    pub encoded_list: EncodedList,
87
88    #[serde(flatten)]
89    pub more_properties: BTreeMap<String, json_syntax::Value>,
90}
91
92/// Error resulting from attempting to construct a [new StatusList2021](StatusList2021::new)
93#[derive(Debug, thiserror::Error)]
94pub enum NewStatusListError {
95    #[error("Unable to encode list")]
96    EncodedList(#[source] NewEncodedListError),
97}
98
99impl StatusList2021 {
100    /// Construct a new empty [StatusList2021]
101    pub fn new(len: usize) -> Result<Self, NewStatusListError> {
102        Ok(StatusList2021 {
103            encoded_list: EncodedList::new(len).map_err(NewStatusListError::EncodedList)?,
104            more_properties: BTreeMap::new(),
105        })
106    }
107
108    /// Set the revocation status for a given index in the list.
109    // TODO: dedupe with RevocationList2020::set_status
110    pub fn set_status(&mut self, index: usize, revoked: bool) -> Result<(), SetStatusError> {
111        let mut list = List::try_from(&self.encoded_list)?;
112        let bitstring_len = list.0.len() * 8;
113        let mut bitstring = BitVec::<Lsb0, u8>::try_from_vec(list.0)
114            .map_err(|_| SetStatusError::ListTooLarge(bitstring_len))?;
115
116        if bitstring_len < MIN_BITSTRING_LENGTH {
117            return Err(SetStatusError::ListTooSmall(
118                bitstring_len,
119                MIN_BITSTRING_LENGTH,
120            ));
121        }
122
123        if let Some(mut bitref) = bitstring.get_mut(index) {
124            *bitref = revoked;
125        } else {
126            return Err(SetStatusError::OutOfBounds(index, bitstring_len));
127        }
128
129        list.0 = bitstring.into_vec();
130        self.encoded_list = EncodedList::try_from(&list)?;
131        Ok(())
132    }
133}
134
135impl CredentialStatus for StatusList2021Entry {
136    /// Validate a credential's revocation status according to
137    /// [Status List 2021][1].
138    ///
139    /// [1]: https://w3c-ccg.github.io/vc-status-list-2021/#validate-algorithm
140    async fn check(
141        &self,
142        credential: &AnyDataIntegrity<SpecializedJsonCredential>,
143        resolver: &impl VerificationMethodResolver<Method = AnyMethod>,
144    ) -> Result<StatusCheck, StatusCheckError> {
145        use bitvec::prelude::*;
146
147        // Check context.
148        if !credential.context.contains_iri(STATUS_LIST_2021_V1_CONTEXT) {
149            // TODO: support JSON-LD credentials defining the terms elsewhere.
150            return Ok(StatusCheck::Invalid(Reason::MissingRequiredLdContext(
151                STATUS_LIST_2021_V1_CONTEXT.to_owned(),
152            )));
153        }
154
155        if self.id == self.status_list_credential {
156            return Ok(StatusCheck::Invalid(Reason::StatusIdMatchesCredentialId(
157                self.id.clone(),
158            )));
159        }
160
161        // Check the status list URL before attempting to load it.
162        // Status List 2021 does not specify an expected URL scheme (URI scheme), but
163        // examples and test vectors use https.
164        if self.status_list_credential.scheme().as_str() != "https" {
165            return Ok(StatusCheck::Invalid(Reason::UnsupportedUriScheme(
166                self.status_list_credential.scheme().to_owned(),
167            )));
168        }
169
170        let credential_data = load_resource(&self.status_list_credential).await?;
171        let status_list_credential =
172            serde_json::from_slice::<AnyDataIntegrity<StatusList2021Credential>>(&credential_data)?;
173
174        if credential.issuer.id() != status_list_credential.issuer.id() {
175            return Ok(StatusCheck::Invalid(Reason::IssuerMismatch(
176                credential.issuer.id().to_owned(),
177                status_list_credential.issuer.id().to_owned(),
178            )));
179        }
180
181        let params = VerificationParameters::from_resolver(resolver);
182        let vc_result = status_list_credential.verify(params).await?;
183
184        if let Err(e) = vc_result {
185            return Ok(StatusCheck::Invalid(Reason::CredentialVerification(e)));
186        }
187
188        if status_list_credential.id.as_deref() != Some(self.status_list_credential.as_uri()) {
189            return Ok(StatusCheck::Invalid(Reason::IdMismatch(
190                credential.issuer.id().to_owned(),
191                status_list_credential.issuer.id().to_owned(),
192            )));
193        }
194
195        let revocation_list = match status_list_credential.credential_subjects.as_ref() {
196            [StatusList2021Subject::StatusList2021(l)] => l,
197            [] => return Ok(StatusCheck::Invalid(Reason::MissingCredentialSubject)),
198            _ => return Ok(StatusCheck::Invalid(Reason::TooManyCredentialSubjects)),
199        };
200
201        let list = match List::try_from(&revocation_list.encoded_list) {
202            Ok(list) => list,
203            Err(e) => return Ok(StatusCheck::Invalid(Reason::DecodeListError(e))),
204        };
205
206        let credential_index = self.status_list_index.0;
207        let bitstring = match BitVec::<Lsb0, u8>::try_from_vec(list.0) {
208            Ok(bitstring) => bitstring,
209            Err(list) => return Err(StatusCheckError::RevocationListTooLarge(list.len())),
210        };
211
212        let revoked = match bitstring.get(credential_index) {
213            Some(bitref) => *bitref,
214            None => return Ok(StatusCheck::Invalid(Reason::InvalidRevocationListIndex)),
215        };
216
217        if revoked {
218            return Ok(StatusCheck::Invalid(Reason::Revoked));
219        }
220
221        Ok(StatusCheck::Valid)
222    }
223}