identity_credential/revocation/validity_timeframe_2024/
revocation_timeframe_status.rs

1// Copyright 2020-2024 IOTA Stiftung, Fondazione Links
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::credential::Status;
5use crate::error::Error;
6use crate::error::Result;
7use identity_core::common::Duration;
8use identity_core::common::Object;
9use identity_core::common::Timestamp;
10use identity_core::common::Url;
11use identity_core::common::Value;
12use serde::de::Visitor;
13use serde::Deserialize;
14use serde::Serialize;
15
16fn deserialize_status_entry_type<'de, D>(deserializer: D) -> Result<String, D::Error>
17where
18  D: serde::Deserializer<'de>,
19{
20  struct ExactStrVisitor(&'static str);
21  impl Visitor<'_> for ExactStrVisitor {
22    type Value = &'static str;
23    fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24      write!(formatter, "the exact string \"{}\"", self.0)
25    }
26    fn visit_str<E: serde::de::Error>(self, str: &str) -> Result<Self::Value, E> {
27      if str == self.0 {
28        Ok(self.0)
29      } else {
30        Err(E::custom(format!("not \"{}\"", self.0)))
31      }
32    }
33  }
34
35  deserializer
36    .deserialize_str(ExactStrVisitor(RevocationTimeframeStatus::TYPE))
37    .map(ToOwned::to_owned)
38}
39
40/// Information used to determine the current status of a [`Credential`][crate::credential::Credential]
41#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)]
42#[serde(rename_all = "camelCase")]
43pub struct RevocationTimeframeStatus {
44  id: Url,
45  #[serde(rename = "type", deserialize_with = "deserialize_status_entry_type")]
46  type_: String,
47  start_validity_timeframe: Timestamp,
48  end_validity_timeframe: Timestamp,
49  #[serde(
50    deserialize_with = "serde_aux::prelude::deserialize_option_number_from_string",
51    skip_serializing_if = "Option::is_none"
52  )]
53  revocation_bitmap_index: Option<u32>,
54}
55
56impl RevocationTimeframeStatus {
57  /// startValidityTimeframe property name.
58  pub const START_TIMEFRAME_PROPERTY: &'static str = "startValidityTimeframe";
59  /// endValidityTimeframe property name.
60  pub const END_TIMEFRAME_PROPERTY: &'static str = "endValidityTimeframe";
61  /// Type name of the revocation mechanism.
62  pub const TYPE: &'static str = "RevocationTimeframe2024";
63  /// index property name for [`Status`] conversion
64  const INDEX_PROPERTY: &'static str = "revocationBitmapIndex";
65
66  /// Creates a new `RevocationTimeframeStatus`.
67  pub fn new(start_validity: Option<Timestamp>, duration: Duration, id: Url, index: u32) -> Result<Self> {
68    let start_validity_timeframe = start_validity.unwrap_or(Timestamp::now_utc());
69    let end_validity_timeframe = start_validity_timeframe
70      .checked_add(duration)
71      .ok_or(Error::InvalidStatus(
72        "With that granularity, endValidityTimeFrame will turn out not to be in the valid range for RFC 3339"
73          .to_owned(),
74      ))?;
75
76    Ok(Self {
77      id,
78      type_: Self::TYPE.to_owned(),
79      start_validity_timeframe,
80      end_validity_timeframe,
81      revocation_bitmap_index: Some(index),
82    })
83  }
84
85  /// Get startValidityTimeframe value.
86  pub fn start_validity_timeframe(&self) -> Timestamp {
87    self.start_validity_timeframe
88  }
89
90  /// Get endValidityTimeframe value.
91  pub fn end_validity_timeframe(&self) -> Timestamp {
92    self.end_validity_timeframe
93  }
94
95  /// Returns the [`Url`] of the `RevocationBitmapStatus`, which should resolve
96  /// to a `RevocationBitmap2022` service in a DID Document.
97  pub fn id(&self) -> &Url {
98    &self.id
99  }
100
101  /// Returns the index of the credential in the issuer's revocation bitmap if it can be decoded.
102  pub fn index(&self) -> Option<u32> {
103    self.revocation_bitmap_index
104  }
105}
106
107impl TryFrom<&Status> for RevocationTimeframeStatus {
108  type Error = Error;
109  fn try_from(status: &Status) -> Result<Self, Self::Error> {
110    // serialize into String to ensure macros work properly
111    // see [issue](https://github.com/iddm/serde-aux/issues/34#issuecomment-1508207530) in `serde-aux`
112    let json_status: String = serde_json::to_string(&status)
113      .map_err(|err| Self::Error::InvalidStatus(format!("failed to read `Status`; {}", &err.to_string())))?;
114    serde_json::from_str(&json_status).map_err(|err| {
115      Self::Error::InvalidStatus(format!(
116        "failed to convert `Status` to `RevocationTimeframeStatus`; {}",
117        &err.to_string(),
118      ))
119    })
120  }
121}
122
123impl From<RevocationTimeframeStatus> for Status {
124  fn from(revocation_timeframe_status: RevocationTimeframeStatus) -> Self {
125    let mut properties = Object::new();
126    properties.insert(
127      RevocationTimeframeStatus::START_TIMEFRAME_PROPERTY.to_owned(),
128      Value::String(revocation_timeframe_status.start_validity_timeframe().to_rfc3339()),
129    );
130    properties.insert(
131      RevocationTimeframeStatus::END_TIMEFRAME_PROPERTY.to_owned(),
132      Value::String(revocation_timeframe_status.end_validity_timeframe().to_rfc3339()),
133    );
134    if let Some(value) = revocation_timeframe_status.index() {
135      properties.insert(
136        RevocationTimeframeStatus::INDEX_PROPERTY.to_owned(),
137        Value::String(value.to_string()),
138      );
139    }
140
141    Status::new_with_properties(
142      revocation_timeframe_status.id,
143      RevocationTimeframeStatus::TYPE.to_owned(),
144      properties,
145    )
146  }
147}
148
149/// Verifier
150#[derive(Clone, Debug, PartialEq, Eq)]
151pub struct VerifierRevocationTimeframeStatus(pub(crate) RevocationTimeframeStatus);
152
153impl TryFrom<Status> for VerifierRevocationTimeframeStatus {
154  type Error = Error;
155
156  fn try_from(status: Status) -> Result<Self> {
157    Ok(Self((&status).try_into().map_err(|err: Error| {
158      Self::Error::InvalidStatus(format!(
159        "failed to convert `Status` to `VerifierRevocationTimeframeStatus`; {}",
160        &err.to_string()
161      ))
162    })?))
163  }
164}
165
166impl From<VerifierRevocationTimeframeStatus> for Status {
167  fn from(status: VerifierRevocationTimeframeStatus) -> Self {
168    status.0.into()
169  }
170}
171
172#[cfg(test)]
173mod tests {
174  use super::*;
175
176  const EXAMPLE_SERIALIZED: &str = r#"{
177    "id": "did:iota:snd:0xae6ccfdb155a69e0ef153fb5fcfd50c08a8fee36babe1f7d71dede8f4e202432#my-revocation-service",
178    "startValidityTimeframe": "2024-03-19T13:57:50Z",
179    "endValidityTimeframe": "2024-03-19T13:58:50Z",
180    "revocationBitmapIndex": "5",
181    "type": "RevocationTimeframe2024"
182  }"#;
183
184  fn get_example_status() -> anyhow::Result<RevocationTimeframeStatus> {
185    let duration = Duration::minutes(1);
186    let service_url = Url::parse(
187      "did:iota:snd:0xae6ccfdb155a69e0ef153fb5fcfd50c08a8fee36babe1f7d71dede8f4e202432#my-revocation-service",
188    )?;
189    let credential_index: u32 = 5;
190    let start_validity_timeframe = Timestamp::parse("2024-03-19T13:57:50Z")?;
191
192    Ok(RevocationTimeframeStatus::new(
193      Some(start_validity_timeframe),
194      duration,
195      service_url,
196      credential_index,
197    )?)
198  }
199
200  #[test]
201  fn revocation_timeframe_status_serialization_works() -> anyhow::Result<()> {
202    let status = get_example_status()?;
203
204    let serialized = serde_json::to_string(&status).expect("Failed to deserialize");
205    dbg!(&serialized);
206
207    Ok(())
208  }
209
210  #[test]
211  fn revocation_timeframe_status_deserialization_works() -> anyhow::Result<()> {
212    let status = get_example_status()?;
213    let deserialized =
214      serde_json::from_str::<RevocationTimeframeStatus>(EXAMPLE_SERIALIZED).expect("Failed to deserialize");
215
216    assert_eq!(status, deserialized);
217
218    Ok(())
219  }
220}