ssi_status/impl/bitstring_status_list/syntax/status_list/
credential.rs1use 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 #[serde(rename = "@context")]
47 pub context: Context,
48
49 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub id: Option<UriBuf>,
52
53 #[serde(rename = "type")]
55 pub types: JsonCredentialTypes<BitstringStatusListCredentialType>,
56
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub valid_from: Option<xsd_types::DateTimeStamp>,
60
61 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub valid_until: Option<xsd_types::DateTimeStamp>,
64
65 pub credential_subject: BitstringStatusList,
67
68 #[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 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}