identity_credential/credential/
revocation_bitmap_status.rs

1// Copyright 2020-2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::str::FromStr;
5
6use identity_core::common::Object;
7use identity_core::common::Url;
8use identity_core::common::Value;
9use identity_did::DIDUrl;
10
11use crate::credential::Status;
12use crate::error::Error;
13use crate::error::Result;
14
15/// Information used to determine the current status of a [`Credential`][crate::credential::Credential]
16/// using the `RevocationBitmap2022` specification.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct RevocationBitmapStatus(Status);
19
20impl RevocationBitmapStatus {
21  const INDEX_PROPERTY: &'static str = "revocationBitmapIndex";
22  /// Type name of the revocation bitmap.
23  pub const TYPE: &'static str = "RevocationBitmap2022";
24
25  /// Creates a new `RevocationBitmapStatus`.
26  ///
27  /// The query of the `id` url is overwritten where "index" is set to `index`.
28  ///
29  /// # Example
30  ///
31  /// ```
32  /// # use identity_credential::credential::RevocationBitmapStatus;
33  /// # use identity_did::DIDUrl;
34  /// # use identity_did::CoreDID;
35  /// let did_url: DIDUrl = DIDUrl::parse("did:method:0xffff#revocation-1").unwrap();
36  /// let status: RevocationBitmapStatus = RevocationBitmapStatus::new(did_url, 5);
37  /// assert_eq!(
38  ///   status.id().unwrap().to_string(),
39  ///   "did:method:0xffff?index=5#revocation-1"
40  /// );
41  /// assert_eq!(status.index().unwrap(), 5);
42  /// ```
43  pub fn new(mut id: DIDUrl, index: u32) -> Self {
44    id.set_query(Some(&format!("index={index}")))
45      .expect("the string should be non-empty and a valid URL query");
46
47    let mut object = Object::new();
48    object.insert(Self::INDEX_PROPERTY.to_owned(), Value::String(index.to_string()));
49    RevocationBitmapStatus(Status::new_with_properties(
50      Url::from(id),
51      Self::TYPE.to_owned(),
52      object,
53    ))
54  }
55
56  /// Returns the [`DIDUrl`] of the `RevocationBitmapStatus`, which should resolve
57  /// to a `RevocationBitmap2022` service in a DID Document.
58  pub fn id(&self) -> Result<DIDUrl> {
59    DIDUrl::parse(self.0.id.as_str())
60      .map_err(|err| Error::InvalidStatus(format!("invalid DID Url '{}': {:?}", self.0.id, err)))
61  }
62
63  /// Returns the index of the credential in the issuer's revocation bitmap if it can be decoded.
64  pub fn index(&self) -> Result<u32> {
65    if let Some(Value::String(index)) = self.0.properties.get(Self::INDEX_PROPERTY) {
66      try_index_to_u32(index, Self::INDEX_PROPERTY)
67    } else {
68      Err(Error::InvalidStatus(format!(
69        "expected {} to be an unsigned 32-bit integer expressed as a string",
70        Self::INDEX_PROPERTY
71      )))
72    }
73  }
74}
75
76impl TryFrom<Status> for RevocationBitmapStatus {
77  type Error = Error;
78
79  fn try_from(status: Status) -> Result<Self> {
80    if status.type_ != Self::TYPE {
81      return Err(Error::InvalidStatus(format!(
82        "expected type '{}', got '{}'",
83        Self::TYPE,
84        status.type_
85      )));
86    }
87
88    let revocation_bitmap_index: &Value =
89      if let Some(revocation_bitmap_index) = status.properties.get(Self::INDEX_PROPERTY) {
90        revocation_bitmap_index
91      } else {
92        return Err(Error::InvalidStatus(format!(
93          "missing required property '{}'",
94          Self::INDEX_PROPERTY
95        )));
96      };
97
98    let revocation_bitmap_index: u32 = if let Value::String(index) = revocation_bitmap_index {
99      try_index_to_u32(index, Self::INDEX_PROPERTY)?
100    } else {
101      return Err(Error::InvalidStatus(format!(
102        "property '{}' is not a string",
103        Self::INDEX_PROPERTY
104      )));
105    };
106
107    // If the index query is present it must match the revocationBitmapIndex.
108    // It is allowed not to be present to maintain backwards-compatibility
109    // with an earlier version of the RevocationBitmap spec.
110    for pair in status.id.query_pairs() {
111      if pair.0 == "index" {
112        let index: u32 = try_index_to_u32(pair.1.as_ref(), "value of index query")?;
113        if index != revocation_bitmap_index {
114          return Err(Error::InvalidStatus(format!(
115            "value of index query `{index}` does not match revocationBitmapIndex `{revocation_bitmap_index}`"
116          )));
117        }
118      }
119    }
120
121    Ok(Self(status))
122  }
123}
124
125impl From<RevocationBitmapStatus> for Status {
126  fn from(status: RevocationBitmapStatus) -> Self {
127    status.0
128  }
129}
130
131/// Attempts to convert the given index string to a u32.
132pub fn try_index_to_u32(index: &str, name: &str) -> Result<u32> {
133  u32::from_str(index).map_err(|err| {
134    Error::InvalidStatus(format!(
135      "{name} cannot be converted to an unsigned, 32-bit integer: {err}",
136    ))
137  })
138}
139
140#[cfg(test)]
141mod tests {
142  use identity_core::common::Object;
143  use identity_core::common::Url;
144  use identity_core::common::Value;
145  use identity_core::convert::FromJson;
146  use identity_did::DIDUrl;
147
148  use crate::Error;
149
150  use super::RevocationBitmapStatus;
151  use super::Status;
152
153  #[test]
154  fn test_embedded_status_invariants() {
155    let url: Url = Url::parse("did:method:0xabcd?index=0#revocation").unwrap();
156    let did_url: DIDUrl = DIDUrl::parse(url.clone().into_string()).unwrap();
157    let revocation_list_index: u32 = 0;
158    let embedded_revocation_status: RevocationBitmapStatus =
159      RevocationBitmapStatus::new(did_url, revocation_list_index);
160
161    let object: Object = Object::from([(
162      RevocationBitmapStatus::INDEX_PROPERTY.to_owned(),
163      Value::String(revocation_list_index.to_string()),
164    )]);
165    let status: Status =
166      Status::new_with_properties(url.clone(), RevocationBitmapStatus::TYPE.to_owned(), object.clone());
167    assert_eq!(embedded_revocation_status, status.try_into().unwrap());
168
169    let status_missing_property: Status =
170      Status::new_with_properties(url.clone(), RevocationBitmapStatus::TYPE.to_owned(), Object::new());
171    assert!(RevocationBitmapStatus::try_from(status_missing_property).is_err());
172
173    let status_wrong_type: Status = Status::new_with_properties(url, "DifferentType".to_owned(), object);
174    assert!(RevocationBitmapStatus::try_from(status_wrong_type).is_err());
175  }
176
177  #[test]
178  fn test_revocation_bitmap_status_index_query() {
179    // index is set.
180    let did_url: DIDUrl = DIDUrl::parse("did:method:0xffff#rev-0").unwrap();
181    let revocation_status: RevocationBitmapStatus = RevocationBitmapStatus::new(did_url, 250);
182    assert_eq!(revocation_status.id().unwrap().query().unwrap(), "index=250");
183
184    // index is overwritten.
185    let did_url: DIDUrl = DIDUrl::parse("did:method:0xffff?index=300#rev-0").unwrap();
186    let revocation_status: RevocationBitmapStatus = RevocationBitmapStatus::new(did_url, 250);
187    assert_eq!(revocation_status.id().unwrap().query().unwrap(), "index=250");
188  }
189
190  #[test]
191  fn test_revocation_bitmap_status_index_requirements() {
192    // INVALID: index mismatch in id and property.
193    let status: Status = Status::from_json_value(serde_json::json!({
194      "id": "did:method:0xffff?index=10#rev-0",
195      "type": RevocationBitmapStatus::TYPE,
196      RevocationBitmapStatus::INDEX_PROPERTY: "5",
197    }))
198    .unwrap();
199
200    assert!(matches!(
201      RevocationBitmapStatus::try_from(status).unwrap_err(),
202      Error::InvalidStatus(_)
203    ));
204
205    // VALID: index matches in id and property.
206    let status: Status = Status::from_json_value(serde_json::json!({
207      "id": "did:method:0xffff?index=5#rev-0",
208      "type": RevocationBitmapStatus::TYPE,
209      RevocationBitmapStatus::INDEX_PROPERTY: "5",
210    }))
211    .unwrap();
212    assert!(RevocationBitmapStatus::try_from(status).is_ok());
213
214    // VALID: missing index in id is allowed to be backwards-compatible.
215    let status: Status = Status::from_json_value(serde_json::json!({
216      "id": "did:method:0xffff#rev-0",
217      "type": RevocationBitmapStatus::TYPE,
218      RevocationBitmapStatus::INDEX_PROPERTY: "5",
219    }))
220    .unwrap();
221    assert!(RevocationBitmapStatus::try_from(status).is_ok());
222  }
223}