ssi_status/impl/bitstring_status_list_20240406/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_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 #[serde(rename = "@context")]
44 pub context: Context,
45
46 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub id: Option<UriBuf>,
49
50 #[serde(rename = "type")]
52 pub types: JsonCredentialTypes<BitstringStatusListCredentialType>,
53
54 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub valid_from: Option<xsd_types::DateTimeStamp>,
57
58 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub valid_until: Option<xsd_types::DateTimeStamp>,
61
62 pub credential_subject: BitstringStatusList,
64
65 #[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 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}