ssi_vc/v1/revocation/
v2020.rs

1//! Revocation List 2020.
2//!
3//! See: <https://w3c-ccg.github.io/vc-status-rl-2020/>
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::REVOCATION_LIST_2020_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, StatusCheckError},
19        RequiredContext, SpecializedJsonCredential,
20    },
21};
22
23use super::{
24    CredentialStatus, EncodedList, List, RevocationListIndex, SetStatusError, StatusCheck,
25    MIN_BITSTRING_LENGTH,
26};
27pub struct RevocationList2020Context;
28
29impl RequiredContext for RevocationList2020Context {
30    const CONTEXT_IRI: &'static iref::Iri = iri!("https://w3id.org/vc-revocation-list-2020/v1");
31}
32
33/// Credential Status object for use in a Verifiable Credential.
34///
35/// See: <https://w3c-ccg.github.io/vc-status-rl-2020/#revocationlist2020status>
36#[derive(Debug, Serialize, Deserialize, Clone)]
37#[serde(rename_all = "camelCase")]
38pub struct RevocationList2020Status {
39    /// URL for status information of the verifiable credential - but not the URL of the revocation
40    /// list.
41    pub id: UriBuf,
42
43    /// Index of this credential's status in the revocation list credential
44    pub revocation_list_index: RevocationListIndex,
45
46    /// URL to a [RevocationList2020Credential]
47    pub revocation_list_credential: UriBuf,
48}
49pub struct RevocationList2020CredentialType;
50
51impl RequiredType for RevocationList2020CredentialType {
52    const REQUIRED_TYPE: &'static str = "RevocationList2020Credential";
53}
54
55/// Verifiable Credential of type RevocationList2020Credential.
56///
57/// <https://w3c-ccg.github.io/vc-status-rl-2020/#revocationlist2020credential>
58pub type RevocationList2020Credential = SpecializedJsonCredential<
59    RevocationList2020Subject,
60    RevocationList2020Context,
61    RevocationList2020CredentialType,
62>;
63
64/// [Credential subject](https://www.w3.org/TR/vc-data-model/#credential-subject) of a [RevocationList2020Credential]
65#[derive(Debug, Serialize, Deserialize, Clone)]
66#[serde(tag = "type")]
67pub enum RevocationList2020Subject {
68    RevocationList2020(RevocationList2020),
69}
70
71/// Credential subject of type RevocationList2020, expected to be used in a Verifiable Credential of type [RevocationList2020Credential]
72/// <https://w3c-ccg.github.io/vc-status-rl-2020/#revocationlist2020credential>
73#[derive(Debug, Serialize, Deserialize, Clone, Default)]
74#[serde(rename_all = "camelCase")]
75pub struct RevocationList2020 {
76    pub encoded_list: EncodedList,
77
78    #[serde(flatten)]
79    pub more_properties: BTreeMap<String, json_syntax::Value>,
80}
81
82impl RevocationList2020 {
83    /// Set the revocation status for a given index in the list.
84    pub fn set_status(&mut self, index: usize, revoked: bool) -> Result<(), SetStatusError> {
85        let mut list = super::List::try_from(&self.encoded_list)?;
86        let bitstring_len = list.0.len() * 8;
87        let mut bitstring = BitVec::<Lsb0, u8>::try_from_vec(list.0)
88            .map_err(|_| SetStatusError::ListTooLarge(bitstring_len))?;
89        if bitstring_len < MIN_BITSTRING_LENGTH {
90            return Err(SetStatusError::ListTooSmall(
91                bitstring_len,
92                MIN_BITSTRING_LENGTH,
93            ));
94        }
95        if let Some(mut bitref) = bitstring.get_mut(index) {
96            *bitref = revoked;
97        } else {
98            return Err(SetStatusError::OutOfBounds(index, bitstring_len));
99        }
100        list.0 = bitstring.into_vec();
101        self.encoded_list = EncodedList::try_from(&list)?;
102        Ok(())
103    }
104}
105
106impl CredentialStatus for RevocationList2020Status {
107    /// Validate a credential's revocation status according to
108    /// [Revocation List 2020][1].
109    ///
110    /// [1]: https://w3c-ccg.github.io/vc-status-rl-2020/#validate-algorithm
111    async fn check(
112        &self,
113        credential: &AnyDataIntegrity<SpecializedJsonCredential>,
114        resolver: &impl VerificationMethodResolver<Method = AnyMethod>,
115    ) -> Result<StatusCheck, StatusCheckError> {
116        use bitvec::prelude::*;
117
118        // Check context.
119        if !credential
120            .context
121            .contains_iri(REVOCATION_LIST_2020_V1_CONTEXT)
122        {
123            // TODO: support JSON-LD credentials defining the terms elsewhere.
124            return Ok(StatusCheck::Invalid(Reason::MissingRequiredLdContext(
125                REVOCATION_LIST_2020_V1_CONTEXT.to_owned(),
126            )));
127        }
128
129        if self.id == self.revocation_list_credential {
130            return Ok(StatusCheck::Invalid(Reason::StatusIdMatchesCredentialId(
131                self.id.clone(),
132            )));
133        }
134
135        // Check the revocation list URL before attempting to load it.
136        // Revocation List 2020 does not specify an expected URL scheme (URI scheme), but
137        // examples and test vectors use https.
138        if self.revocation_list_credential.scheme().as_str() != "https" {
139            return Ok(StatusCheck::Invalid(Reason::UnsupportedUriScheme(
140                self.revocation_list_credential.scheme().to_owned(),
141            )));
142        }
143
144        let credential_data = load_resource(&self.revocation_list_credential).await?;
145        let revocation_list_credential = serde_json::from_slice::<
146            AnyDataIntegrity<RevocationList2020Credential>,
147        >(&credential_data)?;
148
149        if credential.issuer.id() != revocation_list_credential.issuer.id() {
150            return Ok(StatusCheck::Invalid(Reason::IssuerMismatch(
151                credential.issuer.id().to_owned(),
152                revocation_list_credential.issuer.id().to_owned(),
153            )));
154        }
155
156        let params = VerificationParameters::from_resolver(resolver);
157        let vc_result = revocation_list_credential.verify(&params).await?;
158
159        if let Err(e) = vc_result {
160            return Ok(StatusCheck::Invalid(Reason::CredentialVerification(e)));
161        }
162
163        if revocation_list_credential.id.as_deref()
164            != Some(self.revocation_list_credential.as_uri())
165        {
166            return Ok(StatusCheck::Invalid(Reason::IdMismatch(
167                credential.issuer.id().to_owned(),
168                revocation_list_credential.issuer.id().to_owned(),
169            )));
170        }
171
172        let revocation_list = match revocation_list_credential.credential_subjects.as_ref() {
173            [RevocationList2020Subject::RevocationList2020(l)] => l,
174            [] => return Ok(StatusCheck::Invalid(Reason::MissingCredentialSubject)),
175            _ => return Ok(StatusCheck::Invalid(Reason::TooManyCredentialSubjects)),
176        };
177
178        let list = match List::try_from(&revocation_list.encoded_list) {
179            Ok(list) => list,
180            Err(e) => return Ok(StatusCheck::Invalid(Reason::DecodeListError(e))),
181        };
182
183        let credential_index = self.revocation_list_index.0;
184        let bitstring = match BitVec::<Lsb0, u8>::try_from_vec(list.0) {
185            Ok(bitstring) => bitstring,
186            Err(list) => return Err(StatusCheckError::RevocationListTooLarge(list.len())),
187        };
188
189        let revoked = match bitstring.get(credential_index) {
190            Some(bitref) => *bitref,
191            None => return Ok(StatusCheck::Invalid(Reason::InvalidRevocationListIndex)),
192        };
193
194        if revoked {
195            return Ok(StatusCheck::Invalid(Reason::Revoked));
196        }
197
198        Ok(StatusCheck::Valid)
199    }
200}