ssi_status/impl/bitstring_status_list_20240406/syntax/status_list/
credential.rs

1use std::{borrow::Cow, collections::HashMap, hash::Hash, io};
2
3use iref::UriBuf;
4use rdf_types::{Interpretation, Vocabulary, VocabularyMut};
5use serde::{Deserialize, Serialize};
6use ssi_claims_core::{
7    ClaimsValidity, DateTimeProvider, Eip712TypesLoaderProvider, InvalidClaims, ResolverProvider,
8    ValidateClaims,
9};
10use ssi_data_integrity::{
11    ssi_rdf::{LdEnvironment, LinkedDataResource, LinkedDataSubject},
12    AnySuite,
13};
14use ssi_json_ld::{
15    CompactJsonLd, Expandable, JsonLdError, JsonLdLoaderProvider, JsonLdNodeObject, JsonLdObject,
16    Loader,
17};
18use ssi_jwk::JWKResolver;
19use ssi_jws::{InvalidJws, JwsSlice, ValidateJwsHeader};
20use ssi_vc::{
21    syntax::RequiredType,
22    v2::syntax::{Context, JsonCredentialTypes},
23};
24use ssi_verification_methods::{AnyMethod, VerificationMethodResolver};
25
26use crate::{EncodedStatusMap, FromBytes, FromBytesOptions};
27
28use super::{BitstringStatusList, StatusList};
29
30pub const BITSTRING_STATUS_LIST_CREDENTIAL_TYPE: &str = "BitstringStatusListCredential";
31
32#[derive(Debug, Clone, Copy)]
33pub struct BitstringStatusListCredentialType;
34
35impl RequiredType for BitstringStatusListCredentialType {
36    const REQUIRED_TYPE: &'static str = BITSTRING_STATUS_LIST_CREDENTIAL_TYPE;
37}
38
39#[derive(Debug, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct BitstringStatusListCredential {
42    /// JSON-LD context.
43    #[serde(rename = "@context")]
44    pub context: Context,
45
46    /// Credential identifier.
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub id: Option<UriBuf>,
49
50    /// Credential type.
51    #[serde(rename = "type")]
52    pub types: JsonCredentialTypes<BitstringStatusListCredentialType>,
53
54    /// Valid from.
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub valid_from: Option<xsd_types::DateTimeStamp>,
57
58    /// Valid until.
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub valid_until: Option<xsd_types::DateTimeStamp>,
61
62    /// Status list.
63    pub credential_subject: BitstringStatusList,
64
65    /// Other properties.
66    #[serde(flatten)]
67    pub other_properties: HashMap<String, serde_json::Value>,
68}
69
70impl BitstringStatusListCredential {
71    pub fn new(id: Option<UriBuf>, credential_subject: BitstringStatusList) -> Self {
72        Self {
73            context: Context::default(),
74            id,
75            types: JsonCredentialTypes::default(),
76            valid_from: None,
77            valid_until: None,
78            credential_subject,
79            other_properties: HashMap::default(),
80        }
81    }
82
83    pub fn decode_status_list(&self) -> Result<StatusList, DecodeError> {
84        self.credential_subject.decode()
85    }
86}
87
88impl JsonLdObject for BitstringStatusListCredential {
89    fn json_ld_context(&self) -> Option<Cow<ssi_json_ld::syntax::Context>> {
90        Some(Cow::Borrowed(self.context.as_ref()))
91    }
92}
93
94impl JsonLdNodeObject for BitstringStatusListCredential {
95    fn json_ld_type(&self) -> ssi_json_ld::JsonLdTypes {
96        self.types.to_json_ld_types()
97    }
98}
99
100impl Expandable for BitstringStatusListCredential {
101    type Error = JsonLdError;
102
103    type Expanded<I: Interpretation, V: Vocabulary>
104        = ssi_json_ld::ExpandedDocument<V::Iri, V::BlankId>
105    where
106        I: Interpretation,
107        V: VocabularyMut,
108        V::Iri: LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
109        V::BlankId: LinkedDataResource<I, V> + LinkedDataSubject<I, V>;
110
111    #[allow(async_fn_in_trait)]
112    async fn expand_with<I, V>(
113        &self,
114        ld: &mut LdEnvironment<V, I>,
115        loader: &impl Loader,
116    ) -> Result<Self::Expanded<I, V>, Self::Error>
117    where
118        I: Interpretation,
119        V: VocabularyMut,
120        V::Iri: Clone + Eq + Hash + LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
121        V::BlankId: Clone + Eq + Hash + LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
122    {
123        CompactJsonLd(ssi_json_ld::syntax::to_value(self).unwrap())
124            .expand_with(ld, loader)
125            .await
126    }
127}
128
129impl<E, P> ValidateClaims<E, P> for BitstringStatusListCredential
130where
131    E: DateTimeProvider,
132{
133    fn validate_claims(&self, env: &E, _proof: &P) -> ClaimsValidity {
134        // TODO use `ssi`'s own VC DM v2.0 validation function once it's implemented.
135        let now = env.date_time();
136
137        if let Some(valid_from) = self.valid_from {
138            if now < valid_from {
139                return Err(InvalidClaims::Premature {
140                    now,
141                    valid_from: valid_from.into(),
142                });
143            }
144        }
145
146        if let Some(valid_until) = self.valid_until {
147            if now > valid_until {
148                return Err(InvalidClaims::Expired {
149                    now,
150                    valid_until: valid_until.into(),
151                });
152            }
153        }
154
155        Ok(())
156    }
157}
158
159impl<E> ValidateJwsHeader<E> for BitstringStatusListCredential {
160    fn validate_jws_header(&self, _env: &E, _header: &ssi_jws::Header) -> ClaimsValidity {
161        Ok(())
162    }
163}
164
165#[derive(Debug, thiserror::Error)]
166pub enum DecodeError {
167    #[error("invalid multibase: {0}")]
168    Multibase(#[from] multibase::Error),
169
170    #[error("GZIP error: {0}")]
171    Gzip(io::Error),
172}
173
174impl EncodedStatusMap for BitstringStatusListCredential {
175    type Decoded = StatusList;
176    type DecodeError = DecodeError;
177
178    fn decode(self) -> Result<Self::Decoded, Self::DecodeError> {
179        self.decode_status_list()
180    }
181}
182
183#[derive(Debug, thiserror::Error)]
184pub enum FromBytesError {
185    #[error("unexpected media type `{0}`")]
186    UnexpectedMediaType(String),
187
188    #[error(transparent)]
189    Jws(#[from] InvalidJws<Vec<u8>>),
190
191    #[error("invalid JWS: {0}")]
192    JWS(#[from] ssi_jws::DecodeError),
193
194    #[error(transparent)]
195    DataIntegrity(#[from] ssi_data_integrity::DecodeError),
196
197    #[error(transparent)]
198    Json(#[from] serde_json::Error),
199
200    #[error("proof preparation failed: {0}")]
201    Preparation(#[from] ssi_claims_core::ProofPreparationError),
202
203    #[error("proof validation failed: {0}")]
204    Verification(#[from] ssi_claims_core::ProofValidationError),
205
206    #[error("rejected claims: {0}")]
207    Rejected(#[from] ssi_claims_core::Invalid),
208}
209
210impl<V> FromBytes<V> for BitstringStatusListCredential
211where
212    V: ResolverProvider + DateTimeProvider + JsonLdLoaderProvider + Eip712TypesLoaderProvider,
213    V::Resolver: JWKResolver + VerificationMethodResolver<Method = AnyMethod>,
214{
215    type Error = FromBytesError;
216
217    async fn from_bytes_with(
218        bytes: &[u8],
219        media_type: &str,
220        params: &V,
221        options: FromBytesOptions,
222    ) -> Result<Self, Self::Error> {
223        match media_type {
224            "application/vc+ld+json+jwt" => {
225                let jws = JwsSlice::new(bytes)
226                    .map_err(InvalidJws::into_owned)?
227                    .decode()?
228                    .try_map::<Self, _>(|bytes| serde_json::from_slice(&bytes))?;
229                jws.verify(params).await??;
230                Ok(jws.signing_bytes.payload)
231            }
232            "application/vc+ld+json" => {
233                let vc = ssi_data_integrity::from_json_slice::<Self, AnySuite>(bytes)?;
234
235                if !options.allow_unsecured || !vc.proofs.is_empty() {
236                    vc.verify(params).await??;
237                }
238
239                Ok(vc.claims)
240            }
241            other => Err(FromBytesError::UnexpectedMediaType(other.to_owned())),
242        }
243    }
244}