Skip to content

Commit

Permalink
Polish identity_resolver and identity_storage (#1204)
Browse files Browse the repository at this point in the history
* Polish resolver

* Add resolver README

* Polish JWK document ext

* Fix typo

* Remove `Key{Id}StorageError::Unavailable`

* Make `Storage` doc more precise

* Revert "Remove `Key{Id}StorageError::Unavailable`"

This reverts commit 3ac94aa.

* Make `JwkGenOutput` non-exhaustive

* Enable lints from other crates for storage

* Add lints to resolver crate

* Add documentation lint and fix it

* Use `JwkGenOutput` constructor

* Move credential-related tests to credential crate

* Test more `sign_bytes` cases
  • Loading branch information
PhilippGackstatter authored Jul 17, 2023
1 parent 5eabeff commit 5421f26
Show file tree
Hide file tree
Showing 32 changed files with 445 additions and 287 deletions.
2 changes: 1 addition & 1 deletion bindings/wasm/src/did/wasm_core_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@ impl WasmCoreDocument {
}

/// Produces a JWT where the payload is produced from the given `credential`
/// in accordance with [VC-JWT version 1.1.](https://w3c.github.io/vc-jwt/#version-1.1).
/// in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1).
///
/// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be
/// produced by the corresponding private key backed by the `storage` in accordance with the passed `options`.
Expand Down
2 changes: 1 addition & 1 deletion bindings/wasm/src/iota/iota_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -735,7 +735,7 @@ impl WasmIotaDocument {
}

/// Produces a JWS where the payload is produced from the given `credential`
/// in accordance with [VC-JWT version 1.1.](https://w3c.github.io/vc-jwt/#version-1.1).
/// in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1).
///
/// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be
/// produced by the corresponding private key backed by the `storage` in accordance with the passed `options`.
Expand Down
5 changes: 1 addition & 4 deletions bindings/wasm/src/storage/jwk_gen_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ pub struct WasmJwkGenOutput(pub(crate) JwkGenOutput);
impl WasmJwkGenOutput {
#[wasm_bindgen(constructor)]
pub fn new(key_id: String, jwk: &WasmJwk) -> Self {
Self(JwkGenOutput {
key_id: KeyId::new(key_id),
jwk: jwk.clone().into(),
})
Self(JwkGenOutput::new(KeyId::new(key_id), jwk.clone().into()))
}

/// Returns the generated public JWK.
Expand Down
2 changes: 1 addition & 1 deletion identity_credential/src/credential/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ impl<T> Credential<T> {
}

/// Serializes the [`Credential`] as a JWT claims set
/// in accordance with [VC-JWT version 1.1.](https://w3c.github.io/vc-jwt/#version-1.1).
/// in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1).
///
/// The resulting string can be used as the payload of a JWS when issuing the credential.
pub fn serialize_jwt(&self) -> Result<String>
Expand Down
2 changes: 1 addition & 1 deletion identity_credential/src/presentation/presentation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ impl<CRED, T> Presentation<CRED, T> {
}

/// Serializes the [`Presentation`] as a JWT claims set
/// in accordance with [VC-JWT version 1.1.](https://w3c.github.io/vc-jwt/#version-1.1).
/// in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1).
///
/// The resulting string can be used as the payload of a JWS when issuing the credential.
pub fn serialize_jwt(&self, options: &JwtPresentationOptions) -> Result<String>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,9 @@ impl Default for CredentialValidator {

#[cfg(test)]
mod tests {
use crate::credential::Subject;
use identity_core::common::Duration;

// All tests here are essentially adaptations of the old CredentialValidator tests.
use super::*;
use identity_core::common::Object;
Expand Down Expand Up @@ -536,6 +538,132 @@ mod tests {
static ref SIMPLE_CREDENTIAL: Credential = Credential::<Object>::from_json(SIMPLE_CREDENTIAL_JSON).unwrap();
}

#[test]
fn issued_on_or_before() {
assert!(CredentialValidator::check_issued_on_or_before(
&SIMPLE_CREDENTIAL,
SIMPLE_CREDENTIAL
.issuance_date
.checked_sub(Duration::minutes(1))
.unwrap()
)
.is_err());

// and now with a later timestamp
assert!(CredentialValidator::check_issued_on_or_before(
&SIMPLE_CREDENTIAL,
SIMPLE_CREDENTIAL
.issuance_date
.checked_add(Duration::minutes(1))
.unwrap()
)
.is_ok());
}

#[test]
fn check_subject_holder_relationship() {
let mut credential: Credential = SIMPLE_CREDENTIAL.clone();

// first ensure that holder_url is the subject and set the nonTransferable property
let actual_holder_url = credential.credential_subject.first().unwrap().id.clone().unwrap();
assert_eq!(credential.credential_subject.len(), 1);
credential.non_transferable = Some(true);

// checking with holder = subject passes for all defined subject holder relationships:
assert!(CredentialValidator::check_subject_holder_relationship(
&credential,
&actual_holder_url,
SubjectHolderRelationship::AlwaysSubject
)
.is_ok());

assert!(CredentialValidator::check_subject_holder_relationship(
&credential,
&actual_holder_url,
SubjectHolderRelationship::SubjectOnNonTransferable
)
.is_ok());

assert!(CredentialValidator::check_subject_holder_relationship(
&credential,
&actual_holder_url,
SubjectHolderRelationship::Any
)
.is_ok());

// check with a holder different from the subject of the credential:
let issuer_url = Url::parse("did:core:0x1234567890").unwrap();
assert!(actual_holder_url != issuer_url);

assert!(CredentialValidator::check_subject_holder_relationship(
&credential,
&issuer_url,
SubjectHolderRelationship::AlwaysSubject
)
.is_err());

assert!(CredentialValidator::check_subject_holder_relationship(
&credential,
&issuer_url,
SubjectHolderRelationship::SubjectOnNonTransferable
)
.is_err());

assert!(CredentialValidator::check_subject_holder_relationship(
&credential,
&issuer_url,
SubjectHolderRelationship::Any
)
.is_ok());

let mut credential_transferable = credential.clone();

credential_transferable.non_transferable = Some(false);

assert!(CredentialValidator::check_subject_holder_relationship(
&credential_transferable,
&issuer_url,
SubjectHolderRelationship::SubjectOnNonTransferable
)
.is_ok());

credential_transferable.non_transferable = None;

assert!(CredentialValidator::check_subject_holder_relationship(
&credential_transferable,
&issuer_url,
SubjectHolderRelationship::SubjectOnNonTransferable
)
.is_ok());

// two subjects (even when they are both the holder) should fail for all defined values except "Any"
let mut credential_duplicated_holder = credential;
credential_duplicated_holder
.credential_subject
.push(Subject::with_id(actual_holder_url));

assert!(CredentialValidator::check_subject_holder_relationship(
&credential_duplicated_holder,
&issuer_url,
SubjectHolderRelationship::AlwaysSubject
)
.is_err());

assert!(CredentialValidator::check_subject_holder_relationship(
&credential_duplicated_holder,
&issuer_url,
SubjectHolderRelationship::SubjectOnNonTransferable
)
.is_err());

assert!(CredentialValidator::check_subject_holder_relationship(
&credential_duplicated_holder,
&issuer_url,
SubjectHolderRelationship::Any
)
.is_ok());
}

#[test]
fn simple_expires_on_or_after_with_expiration_date() {
let later_than_expiration_date = SIMPLE_CREDENTIAL
Expand Down
6 changes: 6 additions & 0 deletions identity_document/src/verifiable/jws_verification_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use identity_verification::MethodScope;

/// Holds additional options for verifying a JWS with
/// [`CoreDocument::verify_jws`](crate::document::CoreDocument::verify_jws()).
#[non_exhaustive]
#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct JwsVerificationOptions {
Expand All @@ -17,6 +18,11 @@ pub struct JwsVerificationOptions {
}

impl JwsVerificationOptions {
/// Creates a new [`JwsVerificationOptions`].
pub fn new() -> Self {
Self::default()
}

/// Set the expected value for the `nonce` parameter of the protected header.
pub fn nonce(mut self, value: impl Into<String>) -> Self {
self.nonce = Some(value.into());
Expand Down
2 changes: 1 addition & 1 deletion identity_resolver/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ edition.workspace = true
homepage.workspace = true
keywords = ["iota", "did", "identity", "resolver", "resolution"]
license.workspace = true
readme = "../README.md"
readme = "./README.md"
repository.workspace = true
rust-version.workspace = true
description = "DID Resolution utilities for the identity.rs library."
Expand Down
4 changes: 4 additions & 0 deletions identity_resolver/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
IOTA Identity - Resolver
===

This crate provides a pluggable Resolver implementation that allows for abstracting over the resolution of different DID methods.
8 changes: 7 additions & 1 deletion identity_resolver/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2020-2022 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

/// Alias for a `Result` with the error type [`Error`].
pub type Result<T, E = Error> = core::result::Result<T, E>;

/// Error returned from the [Resolver's](crate::Resolver) methods.
Expand Down Expand Up @@ -49,17 +50,22 @@ pub enum ErrorCause {
#[error("did resolution failed: could not parse the given did")]
#[non_exhaustive]
DIDParsingError {
/// The source of the parsing error.
source: Box<dyn std::error::Error + Send + Sync + 'static>,
},
/// A handler attached to the [`Resolver`](crate::resolution::Resolver) attempted to resolve the DID, but the
/// resolution did not succeed.
#[error("did resolution failed: the attached handler failed")]
#[non_exhaustive]
HandlerError {
/// The source of the handler error.
source: Box<dyn std::error::Error + Send + Sync + 'static>,
},
/// Caused by attempting to resolve a DID whose method does not have a corresponding handler attached to the
/// [`Resolver`](crate::resolution::Resolver).
#[error("did resolution failed: the DID method \"{method}\" is not supported by the resolver")]
UnsupportedMethodError { method: String },
UnsupportedMethodError {
/// The method that is unsupported.
method: String,
},
}
13 changes: 13 additions & 0 deletions identity_resolver/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
// Copyright 2020-2022 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

#![forbid(unsafe_code)]
#![doc = include_str!("./../README.md")]
#![warn(
rust_2018_idioms,
unreachable_pub,
missing_docs,
rustdoc::missing_crate_level_docs,
rustdoc::broken_intra_doc_links,
rustdoc::private_intra_doc_links,
rustdoc::private_doc_tests,
clippy::missing_safety_doc
)]

mod error;
mod resolution;

Expand Down
9 changes: 7 additions & 2 deletions identity_resolver/src/resolution/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use std::pin::Pin;
/// support for both multi-threaded and single threaded use cases.
pub trait Command<'a, T>: std::fmt::Debug + private::Sealed {
type Output: Future<Output = T> + 'a;

fn apply(&self, input: &'a str) -> Self::Output;
}

Expand Down Expand Up @@ -71,10 +72,11 @@ impl<DOC: 'static> SendSyncCommand<DOC> {
DIDERR: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
{
let fun: SendSyncCallback<DOC> = Box::new(move |input: &str| {
let handler_clone = handler.clone();
let handler_clone: F = handler.clone();
let did_parse_attempt = D::try_from(input)
.map_err(|error| ErrorCause::DIDParsingError { source: error.into() })
.map_err(Error::new);

Box::pin(async move {
let did: D = did_parse_attempt?;
handler_clone(did)
Expand All @@ -84,6 +86,7 @@ impl<DOC: 'static> SendSyncCommand<DOC> {
.map_err(Error::new)
})
});

Self { fun }
}
}
Expand Down Expand Up @@ -119,10 +122,11 @@ impl<DOC: 'static> SingleThreadedCommand<DOC> {
DIDERR: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
{
let fun: SingleThreadedCallback<DOC> = Box::new(move |input: &str| {
let handler_clone = handler.clone();
let handler_clone: F = handler.clone();
let did_parse_attempt = D::try_from(input)
.map_err(|error| ErrorCause::DIDParsingError { source: error.into() })
.map_err(Error::new);

Box::pin(async move {
let did: D = did_parse_attempt?;
handler_clone(did)
Expand All @@ -132,6 +136,7 @@ impl<DOC: 'static> SingleThreadedCommand<DOC> {
.map_err(Error::new)
})
});

Self { fun }
}
}
2 changes: 2 additions & 0 deletions identity_resolver/src/resolution/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// Copyright 2020-2022 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

mod commands;
mod resolver;
#[cfg(test)]
mod tests;

use self::commands::SingleThreadedCommand;
use identity_document::document::CoreDocument;

pub use resolver::Resolver;
/// Alias for a [`Resolver`] that is not [`Send`] + [`Sync`].
pub type SingleThreadedResolver<DOC = CoreDocument> = Resolver<DOC, SingleThreadedCommand<DOC>>;
10 changes: 8 additions & 2 deletions identity_resolver/src/resolution/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ where
/// Constructs a new [`Resolver`].
///
/// # Example
///
/// Construct a `Resolver` that resolves DID documents of type
/// [`CoreDocument`](::identity_document::document::CoreDocument).
/// ```
Expand All @@ -59,10 +60,12 @@ where
/// Fetches the DID Document of the given DID.
///
/// # Errors
///
/// Errors if the resolver has not been configured to handle the method corresponding to the given DID or the
/// resolution process itself fails.
///
/// ## Example
///
/// ```
/// # use identity_resolver::Resolver;
/// # use identity_did::CoreDID;
Expand All @@ -87,8 +90,8 @@ where
/// }
/// ```
pub async fn resolve<D: DID>(&self, did: &D) -> Result<DOC> {
let method = did.method();
let delegate = self
let method: &str = did.method();
let delegate: &M = self
.command_map
.get(method)
.ok_or_else(|| ErrorCause::UnsupportedMethodError {
Expand All @@ -109,6 +112,7 @@ where
/// * If `dids` contains duplicates, these will be resolved only once.
pub async fn resolve_multiple<D: DID>(&self, dids: &[D]) -> Result<HashMap<D, DOC>> {
let futures = FuturesUnordered::new();

// Create set to remove duplicates to avoid unnecessary resolution.
let dids_set: HashSet<D> = dids.iter().cloned().collect();
for did in dids_set {
Expand All @@ -117,7 +121,9 @@ where
doc.map(|doc| (did, doc))
});
}

let documents: HashMap<D, DOC> = futures.try_collect().await?;

Ok(documents)
}
}
Expand Down
1 change: 0 additions & 1 deletion identity_storage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ tokio = { version = "1.23.0", default-features = false, features = ["macros", "s
[dev-dependencies]
identity_credential = { version = "=0.7.0-alpha.6", path = "../identity_credential", features = ["revocation-bitmap"] }
once_cell = { version = "1.17.1", default-features = false }
proptest = { version = "1.0.0", default-features = false, features = ["std"] }
tokio = { version = "1.23.0", default-features = false, features = ["macros", "sync", "rt"] }

[features]
Expand Down
Loading

0 comments on commit 5421f26

Please sign in to comment.