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