Skip to content

Commit

Permalink
Recommend unique credentialStatus.id in RevocationBitmap2022 (#1039)
Browse files Browse the repository at this point in the history
* Update spec

* Implement `index` query in revocation status

* Test revocation status query requirements

* Improve tests

* Add index query recommendations in spec

* Clarify test context

* Fix clippy lints

* Be agnostic about implementations

* Rename index property constant

* Deduplicate str -> u32 conversion
  • Loading branch information
PhilippGackstatter committed Sep 23, 2022
1 parent 1572434 commit 268c3a4
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 28 deletions.
6 changes: 3 additions & 3 deletions documentation/docs/specs/revocation_bitmap_2022.md
Expand Up @@ -15,7 +15,7 @@ keywords:

## Abstract

This specification describes an on-Tangle mechanism for publishing the revocation status of [Verifiable Credentials](../concepts/verifiable_credentials/overview) embedded in an issuer's DID document.
This specification describes a mechanism for publishing the revocation status of [Verifiable Credentials](../concepts/verifiable_credentials/overview) embedded in an issuer's DID document.

## Introduction

Expand All @@ -33,7 +33,7 @@ For an issuer to enable verifiers to check the status of a verifiable credential

| Property | Description |
| :--- | :--- |
| `id` | The constraints on the `id` property are listed in the [Verifiable Credentials Data Model specification](https://www.w3.org/TR/vc-data-model/). The `id` MUST be a [DID URL](https://www.w3.org/TR/did-core/#did-url-syntax) that resolves to a [Revocation Bitmap Service](#revocation-bitmap-service) in the DID Document of the issuer. |
| `id` | The constraints on the `id` property are listed in the [Verifiable Credentials Data Model specification](https://www.w3.org/TR/vc-data-model/). The `id` MUST be a [DID URL](https://www.w3.org/TR/did-core/#did-url-syntax) that is the URL to a [Revocation Bitmap Service](#revocation-bitmap-service) in the DID Document of the issuer. It SHOULD include an `index` query set to the same value as `revocationBitmapIndex`, to uniquely identify the `credentialStatus`. If the `index` query is present, implementations SHOULD reject statuses where the `index` query's value does not match `revocationBitmapIndex`. |
| `type` | The `type` property MUST be `"RevocationBitmap2022"`. |
| `revocationBitmapIndex` | The `revocationBitmapIndex` property MUST be an unsigned, 32-bit integer expressed as a string. This is the index of the credential in the issuer's revocation bitmap. Each index SHOULD be unique among all credentials linking to the same [Revocation Bitmap Service](#revocation-bitmap-service). |

Expand All @@ -55,7 +55,7 @@ An example of a verifiable credential with a `credentialStatus` of type `Revocat
"issuer": "did:iota:EvaQhPXXsJsGgxSXGhZGMCvTt63KuAFtaGThx6a5nSpw",
"issuanceDate": "2022-06-13T08:04:36Z",
"credentialStatus": {
"id": "did:iota:EvaQhPXXsJsGgxSXGhZGMCvTt63KuAFtaGThx6a5nSpw#revocation",
"id": "did:iota:EvaQhPXXsJsGgxSXGhZGMCvTt63KuAFtaGThx6a5nSpw?index=5#revocation",
"type": "RevocationBitmap2022",
"revocationBitmapIndex": "5"
},
Expand Down
148 changes: 123 additions & 25 deletions identity_credential/src/credential/revocation_bitmap_status.rs
Expand Up @@ -19,14 +19,34 @@ use crate::error::Result;
pub struct RevocationBitmapStatus(Status);

impl RevocationBitmapStatus {
const INDEX_PROPERTY_NAME: &'static str = "revocationBitmapIndex";
const INDEX_PROPERTY: &'static str = "revocationBitmapIndex";
/// Type name of the revocation bitmap.
pub const TYPE: &'static str = "RevocationBitmap2022";

/// Creates a new `RevocationBitmapStatus`.
pub fn new<D: DID>(id: DIDUrl<D>, index: u32) -> Self {
///
/// The query of the `id` url is overwritten where "index" is set to `index`.
///
/// # Example
///
/// ```
/// # use identity_credential::credential::RevocationBitmapStatus;
/// # use identity_did::did::DIDUrl;
/// # use identity_did::did::CoreDID;
/// let did_url: DIDUrl<CoreDID> = DIDUrl::parse("did:method:0xffff#revocation-1").unwrap();
/// let status: RevocationBitmapStatus = RevocationBitmapStatus::new(did_url, 5);
/// assert_eq!(
/// status.id::<CoreDID>().unwrap().to_string(),
/// "did:method:0xffff?index=5#revocation-1"
/// );
/// assert_eq!(status.index().unwrap(), 5);
/// ```
pub fn new<D: DID>(mut id: DIDUrl<D>, index: u32) -> Self {
id.set_query(Some(&format!("index={index}")))
.expect("the string should be non-empty and a valid URL query");

let mut object = Object::new();
object.insert(Self::INDEX_PROPERTY_NAME.to_owned(), Value::String(index.to_string()));
object.insert(Self::INDEX_PROPERTY.to_owned(), Value::String(index.to_string()));
RevocationBitmapStatus(Status::new_with_properties(
Url::from(id),
Self::TYPE.to_owned(),
Expand All @@ -43,18 +63,12 @@ impl RevocationBitmapStatus {

/// Returns the index of the credential in the issuer's revocation bitmap if it can be decoded.
pub fn index(&self) -> Result<u32> {
if let Some(Value::String(index)) = self.0.properties.get(Self::INDEX_PROPERTY_NAME) {
u32::from_str(index).map_err(|err| {
Error::InvalidStatus(format!(
"expected {} to be an unsigned 32-bit integer: {}",
Self::INDEX_PROPERTY_NAME,
err
))
})
if let Some(Value::String(index)) = self.0.properties.get(Self::INDEX_PROPERTY) {
try_index_to_u32(index, Self::INDEX_PROPERTY)
} else {
Err(Error::InvalidStatus(format!(
"expected {} to be an unsigned 32-bit integer expressed as a string",
Self::INDEX_PROPERTY_NAME
Self::INDEX_PROPERTY
)))
}
}
Expand All @@ -65,19 +79,47 @@ impl TryFrom<Status> for RevocationBitmapStatus {

fn try_from(status: Status) -> Result<Self> {
if status.type_ != Self::TYPE {
Err(Error::InvalidStatus(format!(
return Err(Error::InvalidStatus(format!(
"expected type '{}', got '{}'",
Self::TYPE,
status.type_
)))
} else if !status.properties.contains_key(Self::INDEX_PROPERTY_NAME) {
Err(Error::InvalidStatus(format!(
"missing required property '{}'",
Self::INDEX_PROPERTY_NAME
)))
)));
}

let revocation_bitmap_index: &Value =
if let Some(revocation_bitmap_index) = status.properties.get(Self::INDEX_PROPERTY) {
revocation_bitmap_index
} else {
return Err(Error::InvalidStatus(format!(
"missing required property '{}'",
Self::INDEX_PROPERTY
)));
};

let revocation_bitmap_index: u32 = if let Value::String(index) = revocation_bitmap_index {
try_index_to_u32(index, Self::INDEX_PROPERTY)?
} else {
Ok(Self(status))
return Err(Error::InvalidStatus(format!(
"property '{}' is not a string",
Self::INDEX_PROPERTY
)));
};

// If the index query is present it must match the revocationBitmapIndex.
// It is allowed not to be present to maintain backwards-compatibility
// with an earlier version of the RevocationBitmap spec.
for pair in status.id.query_pairs() {
if pair.0 == "index" {
let index: u32 = try_index_to_u32(pair.1.as_ref(), "value of index query")?;
if index != revocation_bitmap_index {
return Err(Error::InvalidStatus(format!(
"value of index query `{index}` does not match revocationBitmapIndex `{revocation_bitmap_index}`"
)));
}
}
}

Ok(Self(status))
}
}

Expand All @@ -87,30 +129,39 @@ impl From<RevocationBitmapStatus> for Status {
}
}

/// Attempts to convert the given index string to a u32.
fn try_index_to_u32(index: &str, name: &str) -> Result<u32> {
u32::from_str(index).map_err(|err| {
Error::InvalidStatus(format!(
"{name} cannot be converted to an unsigned, 32-bit integer: {err}",
))
})
}

#[cfg(test)]
mod tests {
use identity_core::common::Object;
use identity_core::common::Url;
use identity_core::common::Value;
use identity_core::convert::FromJson;
use identity_did::did::CoreDID;
use identity_did::did::DIDUrl;

use crate::Error;

use super::RevocationBitmapStatus;
use super::Status;

const TAG: &str = "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV";
const SERVICE: &str = "revocation";

#[test]
fn test_embedded_status_invariants() {
let url: Url = Url::parse(format!("did:iota:{}#{}", TAG, SERVICE)).unwrap();
let url: Url = Url::parse("did:method:0xabcd?index=0#revocation").unwrap();
let did_url: DIDUrl<CoreDID> = DIDUrl::parse(url.clone().into_string()).unwrap();
let revocation_list_index: u32 = 0;
let embedded_revocation_status: RevocationBitmapStatus =
RevocationBitmapStatus::new(did_url, revocation_list_index);

let object: Object = Object::from([(
RevocationBitmapStatus::INDEX_PROPERTY_NAME.to_owned(),
RevocationBitmapStatus::INDEX_PROPERTY.to_owned(),
Value::String(revocation_list_index.to_string()),
)]);
let status: Status =
Expand All @@ -124,4 +175,51 @@ mod tests {
let status_wrong_type: Status = Status::new_with_properties(url, "DifferentType".to_owned(), object);
assert!(RevocationBitmapStatus::try_from(status_wrong_type).is_err());
}

#[test]
fn test_revocation_bitmap_status_index_query() {
// index is set.
let did_url: DIDUrl<CoreDID> = DIDUrl::parse("did:method:0xffff#rev-0").unwrap();
let revocation_status: RevocationBitmapStatus = RevocationBitmapStatus::new(did_url, 250);
assert_eq!(revocation_status.id::<CoreDID>().unwrap().query().unwrap(), "index=250");

// index is overwritten.
let did_url: DIDUrl<CoreDID> = DIDUrl::parse("did:method:0xffff?index=300#rev-0").unwrap();
let revocation_status: RevocationBitmapStatus = RevocationBitmapStatus::new(did_url, 250);
assert_eq!(revocation_status.id::<CoreDID>().unwrap().query().unwrap(), "index=250");
}

#[test]
fn test_revocation_bitmap_status_index_requirements() {
// INVALID: index mismatch in id and property.
let status: Status = Status::from_json_value(serde_json::json!({
"id": "did:method:0xffff?index=10#rev-0",
"type": RevocationBitmapStatus::TYPE,
RevocationBitmapStatus::INDEX_PROPERTY: "5",
}))
.unwrap();

assert!(matches!(
RevocationBitmapStatus::try_from(status).unwrap_err(),
Error::InvalidStatus(_)
));

// VALID: index matches in id and property.
let status: Status = Status::from_json_value(serde_json::json!({
"id": "did:method:0xffff?index=5#rev-0",
"type": RevocationBitmapStatus::TYPE,
RevocationBitmapStatus::INDEX_PROPERTY: "5",
}))
.unwrap();
assert!(RevocationBitmapStatus::try_from(status).is_ok());

// VALID: missing index in id is allowed to be backwards-compatible.
let status: Status = Status::from_json_value(serde_json::json!({
"id": "did:method:0xffff#rev-0",
"type": RevocationBitmapStatus::TYPE,
RevocationBitmapStatus::INDEX_PROPERTY: "5",
}))
.unwrap();
assert!(RevocationBitmapStatus::try_from(status).is_ok());
}
}

0 comments on commit 268c3a4

Please sign in to comment.