identity_credential/revocation/status_list_2021/
credential.rs1use std::fmt::Display;
5use std::ops::Deref;
6use std::str::FromStr;
7
8use identity_core::common::Context;
9use identity_core::common::OneOrMany;
10use identity_core::common::Timestamp;
11use identity_core::common::Url;
12use serde::Deserialize;
13use serde::Serialize;
14use serde_json::Value;
15use thiserror::Error;
16
17pub const CREDENTIAL_TYPE: &str = "StatusList2021Credential";
19const CREDENTIAL_SUBJECT_TYPE: &str = "StatusList2021";
20
21#[derive(Clone, Debug, Error, strum::IntoStaticStr, PartialEq, Eq)]
24pub enum StatusList2021CredentialError {
25 #[error("A StatusList2021Credential may only have one credentialSubject")]
27 MultipleCredentialSubject,
28 #[error("Invalid property \"{0}\"")]
30 InvalidProperty(&'static str),
31 #[error("Missing property \"{0}\"")]
33 MissingProperty(&'static str),
34 #[error(transparent)]
36 StatusListError(#[from] StatusListError),
37 #[error("Cannot set the status of a credential without a \"credentialSubject.id\".")]
39 Unreferenceable,
40 #[error("A previously revoked credential cannot be unrevoked.")]
42 UnreversibleRevocation,
43}
44
45use crate::credential::Credential;
46use crate::credential::CredentialBuilder;
47use crate::credential::Issuer;
48use crate::credential::Proof;
49use crate::credential::Subject;
50
51use super::status_list::StatusListError;
52use super::StatusList2021;
53use super::StatusList2021Entry;
54
55#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
57#[serde(try_from = "Credential", into = "Credential")]
58pub struct StatusList2021Credential {
59 inner: Credential,
60 subject: StatusList2021CredentialSubject,
61}
62
63impl Display for StatusList2021Credential {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 write!(f, "{}", &self.inner)
66 }
67}
68
69impl From<StatusList2021Credential> for Credential {
70 fn from(value: StatusList2021Credential) -> Self {
71 value.into_inner()
72 }
73}
74
75impl Deref for StatusList2021Credential {
76 type Target = Credential;
77 fn deref(&self) -> &Self::Target {
78 &self.inner
79 }
80}
81
82impl TryFrom<Credential> for StatusList2021Credential {
83 type Error = StatusList2021CredentialError;
84 fn try_from(mut credential: Credential) -> Result<Self, Self::Error> {
85 let has_right_credential_type = credential.types.contains(&CREDENTIAL_TYPE.to_owned());
86 let subject = StatusList2021CredentialSubject::try_from_credential(&mut credential)?;
87
88 if has_right_credential_type {
89 Ok(Self {
90 inner: credential,
91 subject,
92 })
93 } else {
94 Err(StatusList2021CredentialError::InvalidProperty("type"))
95 }
96 }
97}
98
99impl StatusList2021Credential {
100 pub fn into_inner(self) -> Credential {
102 let Self { mut inner, subject } = self;
103 inner.credential_subject = OneOrMany::One(subject.into());
104 inner
105 }
106
107 pub fn id(&self) -> Option<&Url> {
109 self.subject.id.as_ref()
110 }
111
112 pub fn purpose(&self) -> StatusPurpose {
114 self.subject.status_purpose
115 }
116
117 fn status_list(&self) -> Result<StatusList2021, StatusListError> {
118 StatusList2021::try_from_encoded_str(&self.subject.encoded_list)
119 }
120
121 pub fn set_credential_status(
129 &mut self,
130 credential: &mut Credential,
131 index: usize,
132 revoked_or_suspended: bool,
133 ) -> Result<StatusList2021Entry, StatusList2021CredentialError> {
134 let id = self
135 .id()
136 .cloned()
137 .ok_or(StatusList2021CredentialError::Unreferenceable)?;
138 let entry = StatusList2021Entry::new(id, self.purpose(), index, None);
139
140 self.set_entry(index, revoked_or_suspended)?;
141 credential.credential_status = Some(entry.clone().into());
142
143 Ok(entry)
144 }
145
146 pub fn update<F>(&mut self, update_fn: F) -> Result<(), StatusList2021CredentialError>
148 where
149 F: FnOnce(&mut MutStatusList) -> Result<(), StatusList2021CredentialError>,
150 {
151 let mut encapsuled_status_list = MutStatusList {
152 status_list: self.status_list()?,
153 purpose: self.purpose(),
154 };
155 update_fn(&mut encapsuled_status_list)?;
156
157 self.subject.encoded_list = encapsuled_status_list.status_list.into_encoded_str();
158 Ok(())
159 }
160
161 pub(crate) fn set_entry(&mut self, index: usize, value: bool) -> Result<(), StatusList2021CredentialError> {
163 let mut status_list = self.status_list()?;
164 let entry_status = status_list.get(index)?;
165 if self.purpose() == StatusPurpose::Revocation && !value && entry_status {
166 return Err(StatusList2021CredentialError::UnreversibleRevocation);
167 }
168 status_list.set(index, value)?;
169 self.subject.encoded_list = status_list.into_encoded_str();
170
171 Ok(())
172 }
173
174 pub fn entry(&self, index: usize) -> Result<CredentialStatus, StatusList2021CredentialError> {
176 let status_list = self.status_list()?;
177 Ok(match (self.purpose(), status_list.get(index)?) {
178 (StatusPurpose::Revocation, true) => CredentialStatus::Revoked,
179 (StatusPurpose::Suspension, true) => CredentialStatus::Suspended,
180 _ => CredentialStatus::Valid,
181 })
182 }
183}
184
185pub struct MutStatusList {
188 status_list: StatusList2021,
189 purpose: StatusPurpose,
190}
191
192impl MutStatusList {
193 pub fn set_entry(&mut self, index: usize, value: bool) -> Result<(), StatusList2021CredentialError> {
195 let entry_status = self.status_list.get(index)?;
196 if self.purpose == StatusPurpose::Revocation && !value && entry_status {
197 return Err(StatusList2021CredentialError::UnreversibleRevocation);
198 }
199 self.status_list.set(index, value)?;
200 Ok(())
201 }
202}
203
204#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
206pub enum CredentialStatus {
207 Revoked,
209 Suspended,
211 Valid,
213}
214
215#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
217#[serde(rename_all = "lowercase")]
218pub enum StatusPurpose {
219 #[default]
221 Revocation,
222 Suspension,
224}
225
226impl Display for StatusPurpose {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 let s = match self {
229 Self::Revocation => "revocation",
230 Self::Suspension => "suspension",
231 };
232 write!(f, "{s}")
233 }
234}
235
236impl FromStr for StatusPurpose {
237 type Err = ();
238 fn from_str(s: &str) -> Result<Self, Self::Err> {
239 match s {
240 "revocation" => Ok(Self::Revocation),
241 "suspension" => Ok(Self::Suspension),
242 _ => Err(()),
243 }
244 }
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Default)]
248struct StatusList2021CredentialSubject {
249 status_purpose: StatusPurpose,
250 encoded_list: String,
251 id: Option<Url>,
252}
253
254impl From<StatusList2021CredentialSubject> for Subject {
255 fn from(value: StatusList2021CredentialSubject) -> Self {
256 let properties = [
257 (
258 "statusPurpose".to_owned(),
259 Value::String(value.status_purpose.to_string()),
260 ),
261 ("type".to_owned(), Value::String(CREDENTIAL_SUBJECT_TYPE.to_owned())),
262 ("encodedList".to_owned(), Value::String(value.encoded_list)),
263 ]
264 .into_iter()
265 .collect();
266
267 if let Some(id) = value.id {
268 Subject::with_id_and_properties(id, properties)
269 } else {
270 Subject::with_properties(properties)
271 }
272 }
273}
274
275impl StatusList2021CredentialSubject {
276 fn try_from_credential(credential: &mut Credential) -> Result<Self, StatusList2021CredentialError> {
278 let OneOrMany::One(mut subject) = std::mem::take(&mut credential.credential_subject) else {
279 return Err(StatusList2021CredentialError::MultipleCredentialSubject);
280 };
281 if let Some(subject_type) = subject.properties.get("type") {
282 if subject_type.as_str() != Some(CREDENTIAL_SUBJECT_TYPE) {
283 return Err(StatusList2021CredentialError::InvalidProperty("credentialSubject.type"));
284 }
285 } else {
286 return Err(StatusList2021CredentialError::MissingProperty("credentialSubject.type"));
287 }
288 let status_purpose = subject
289 .properties
290 .get("statusPurpose")
291 .ok_or(StatusList2021CredentialError::MissingProperty(
292 "credentialSubject.statusPurpose",
293 ))
294 .and_then(|value| {
295 value
296 .as_str()
297 .and_then(|purpose| StatusPurpose::from_str(purpose).ok())
298 .ok_or(StatusList2021CredentialError::InvalidProperty(
299 "credentialSubject.statusPurpose",
300 ))
301 })?;
302 let encoded_list = subject
303 .properties
304 .get_mut("encodedList")
305 .ok_or(StatusList2021CredentialError::MissingProperty(
306 "credentialSubject.encodedList",
307 ))
308 .and_then(|value| {
309 if let Value::String(ref mut s) = value {
310 Ok(s)
311 } else {
312 Err(StatusList2021CredentialError::InvalidProperty(
313 "credentialSubject.encodedList",
314 ))
315 }
316 })
317 .map(std::mem::take)?;
318
319 Ok(StatusList2021CredentialSubject {
320 id: subject.id,
321 encoded_list,
322 status_purpose,
323 })
324 }
325}
326
327#[derive(Debug, Default)]
329pub struct StatusList2021CredentialBuilder {
330 inner_builder: CredentialBuilder,
331 credential_subject: StatusList2021CredentialSubject,
332}
333
334impl StatusList2021CredentialBuilder {
335 pub fn new(status_list: StatusList2021) -> Self {
337 let credential_subject = StatusList2021CredentialSubject {
338 encoded_list: status_list.into_encoded_str(),
339 ..Default::default()
340 };
341 Self {
342 credential_subject,
343 ..Default::default()
344 }
345 }
346
347 pub const fn purpose(mut self, purpose: StatusPurpose) -> Self {
349 self.credential_subject.status_purpose = purpose;
350 self
351 }
352
353 pub fn subject_id(mut self, id: Url) -> Self {
355 self.credential_subject.id = Some(id);
356 self
357 }
358
359 pub const fn expiration_date(mut self, time: Timestamp) -> Self {
361 self.inner_builder.expiration_date = Some(time);
362 self
363 }
364
365 pub fn issuer(mut self, issuer: Issuer) -> Self {
367 self.inner_builder.issuer = Some(issuer);
368 self
369 }
370
371 pub fn context(mut self, ctx: Context) -> Self {
373 self.inner_builder.context.push(ctx);
374 self
375 }
376
377 pub fn add_type(mut self, type_: String) -> Self {
379 self.inner_builder.types.push(type_);
380 self
381 }
382
383 pub fn proof(mut self, proof: Proof) -> Self {
385 self.inner_builder.proof = Some(proof);
386 self
387 }
388
389 pub fn build(mut self) -> Result<StatusList2021Credential, crate::Error> {
391 let id = self.credential_subject.id.clone().map(|mut url| {
392 url.set_fragment(None);
393 url
394 });
395 self.inner_builder.id = id;
396 self
397 .inner_builder
398 .type_(CREDENTIAL_TYPE)
399 .issuance_date(Timestamp::now_utc())
400 .subject(Subject {
401 id: self.credential_subject.id.clone(),
402 ..Default::default()
403 })
404 .build()
405 .map(|mut credential| {
406 credential.credential_subject = OneOrMany::default();
407 StatusList2021Credential {
408 subject: self.credential_subject,
409 inner: credential,
410 }
411 })
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 const STATUS_LIST_2021_CREDENTIAL_SAMPLE: &str = r#"
420{
421 "@context": [
422 "https://www.w3.org/2018/credentials/v1",
423 "https://w3id.org/vc/status-list/2021/v1"
424 ],
425 "id": "https://example.com/credentials/status/3",
426 "type": ["VerifiableCredential", "StatusList2021Credential"],
427 "issuer": "did:example:12345",
428 "issuanceDate": "2021-04-05T14:27:40Z",
429 "credentialSubject": {
430 "id": "https://example.com/status/3#list",
431 "type": "StatusList2021",
432 "statusPurpose": "revocation",
433 "encodedList": "H4sIAAAAAAAAA-3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA"
434 }
435}
436 "#;
437
438 #[test]
439 fn status_purpose_serialization_works() {
440 assert_eq!(
441 serde_json::to_string(&StatusPurpose::Revocation).ok(),
442 Some(format!("\"{}\"", StatusPurpose::Revocation))
443 );
444 }
445 #[test]
446 fn status_purpose_deserialization_works() {
447 assert_eq!(
448 serde_json::from_str::<StatusPurpose>("\"suspension\"").ok(),
449 Some(StatusPurpose::Suspension),
450 )
451 }
452 #[test]
453 fn status_list_2021_credential_deserialization_works() {
454 let credential = serde_json::from_str::<StatusList2021Credential>(STATUS_LIST_2021_CREDENTIAL_SAMPLE)
455 .expect("Failed to deserialize");
456 assert_eq!(credential.purpose(), StatusPurpose::Revocation);
457 }
458 #[test]
459 fn revoked_credential_cannot_be_unrevoked() {
460 let url = Url::parse("http://example.com").unwrap();
461 let mut status_list_credential = StatusList2021CredentialBuilder::new(StatusList2021::default())
462 .issuer(Issuer::Url(url.clone()))
463 .purpose(StatusPurpose::Revocation)
464 .subject_id(url)
465 .build()
466 .unwrap();
467
468 assert!(status_list_credential.set_entry(420, false).is_ok());
469 status_list_credential.set_entry(420, true).unwrap();
470 assert_eq!(
471 status_list_credential.set_entry(420, false),
472 Err(StatusList2021CredentialError::UnreversibleRevocation)
473 );
474 }
475 #[test]
476 fn suspended_credential_can_be_unsuspended() {
477 let url = Url::parse("http://example.com").unwrap();
478 let mut status_list_credential = StatusList2021CredentialBuilder::new(StatusList2021::default())
479 .issuer(Issuer::Url(url.clone()))
480 .purpose(StatusPurpose::Suspension)
481 .subject_id(url)
482 .build()
483 .unwrap();
484
485 assert!(status_list_credential.set_entry(420, false).is_ok());
486 status_list_credential.set_entry(420, true).unwrap();
487 assert!(status_list_credential.set_entry(420, false).is_ok());
488 }
489}