diff --git a/bindings/wasm/src/did/wasm_core_document.rs b/bindings/wasm/src/did/wasm_core_document.rs index 43cefa603d..51f1c135f6 100644 --- a/bindings/wasm/src/did/wasm_core_document.rs +++ b/bindings/wasm/src/did/wasm_core_document.rs @@ -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`. diff --git a/bindings/wasm/src/iota/iota_document.rs b/bindings/wasm/src/iota/iota_document.rs index 160dbbbd7f..027e55e318 100644 --- a/bindings/wasm/src/iota/iota_document.rs +++ b/bindings/wasm/src/iota/iota_document.rs @@ -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`. diff --git a/bindings/wasm/src/storage/jwk_gen_output.rs b/bindings/wasm/src/storage/jwk_gen_output.rs index cdca0d9f55..dfcc71ac74 100644 --- a/bindings/wasm/src/storage/jwk_gen_output.rs +++ b/bindings/wasm/src/storage/jwk_gen_output.rs @@ -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. diff --git a/identity_credential/src/credential/credential.rs b/identity_credential/src/credential/credential.rs index a6134065c7..b70808da62 100644 --- a/identity_credential/src/credential/credential.rs +++ b/identity_credential/src/credential/credential.rs @@ -153,7 +153,7 @@ impl Credential { } /// 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 diff --git a/identity_credential/src/presentation/presentation.rs b/identity_credential/src/presentation/presentation.rs index 7519d5a246..12c3af4478 100644 --- a/identity_credential/src/presentation/presentation.rs +++ b/identity_credential/src/presentation/presentation.rs @@ -111,7 +111,7 @@ impl Presentation { } /// 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 diff --git a/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs b/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs index 5e365295ec..746e271abc 100644 --- a/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs +++ b/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs @@ -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; @@ -536,6 +538,132 @@ mod tests { static ref SIMPLE_CREDENTIAL: Credential = Credential::::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 diff --git a/identity_document/src/verifiable/jws_verification_options.rs b/identity_document/src/verifiable/jws_verification_options.rs index 9520856512..99690bcc99 100644 --- a/identity_document/src/verifiable/jws_verification_options.rs +++ b/identity_document/src/verifiable/jws_verification_options.rs @@ -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 { @@ -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) -> Self { self.nonce = Some(value.into()); diff --git a/identity_resolver/Cargo.toml b/identity_resolver/Cargo.toml index 26ff4b98ba..fd28d24b10 100644 --- a/identity_resolver/Cargo.toml +++ b/identity_resolver/Cargo.toml @@ -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." diff --git a/identity_resolver/README.md b/identity_resolver/README.md new file mode 100644 index 0000000000..4ce84dce0e --- /dev/null +++ b/identity_resolver/README.md @@ -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. diff --git a/identity_resolver/src/error.rs b/identity_resolver/src/error.rs index 0cc67ae579..5a8c3c63f4 100644 --- a/identity_resolver/src/error.rs +++ b/identity_resolver/src/error.rs @@ -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 = core::result::Result; /// Error returned from the [Resolver's](crate::Resolver) methods. @@ -49,6 +50,7 @@ pub enum ErrorCause { #[error("did resolution failed: could not parse the given did")] #[non_exhaustive] DIDParsingError { + /// The source of the parsing error. source: Box, }, /// A handler attached to the [`Resolver`](crate::resolution::Resolver) attempted to resolve the DID, but the @@ -56,10 +58,14 @@ pub enum ErrorCause { #[error("did resolution failed: the attached handler failed")] #[non_exhaustive] HandlerError { + /// The source of the handler error. source: Box, }, /// 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, + }, } diff --git a/identity_resolver/src/lib.rs b/identity_resolver/src/lib.rs index f0438e6fad..b773741b09 100644 --- a/identity_resolver/src/lib.rs +++ b/identity_resolver/src/lib.rs @@ -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; diff --git a/identity_resolver/src/resolution/commands.rs b/identity_resolver/src/resolution/commands.rs index 96155f076c..6dd186a841 100644 --- a/identity_resolver/src/resolution/commands.rs +++ b/identity_resolver/src/resolution/commands.rs @@ -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 + 'a; + fn apply(&self, input: &'a str) -> Self::Output; } @@ -71,10 +72,11 @@ impl SendSyncCommand { DIDERR: Into>, { let fun: SendSyncCallback = 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) @@ -84,6 +86,7 @@ impl SendSyncCommand { .map_err(Error::new) }) }); + Self { fun } } } @@ -119,10 +122,11 @@ impl SingleThreadedCommand { DIDERR: Into>, { let fun: SingleThreadedCallback = 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) @@ -132,6 +136,7 @@ impl SingleThreadedCommand { .map_err(Error::new) }) }); + Self { fun } } } diff --git a/identity_resolver/src/resolution/mod.rs b/identity_resolver/src/resolution/mod.rs index 1be55ea5f1..06a923d446 100644 --- a/identity_resolver/src/resolution/mod.rs +++ b/identity_resolver/src/resolution/mod.rs @@ -1,5 +1,6 @@ // Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 + mod commands; mod resolver; #[cfg(test)] @@ -7,6 +8,7 @@ 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 = Resolver>; diff --git a/identity_resolver/src/resolution/resolver.rs b/identity_resolver/src/resolution/resolver.rs index 1b28ed0b13..10d5359db8 100644 --- a/identity_resolver/src/resolution/resolver.rs +++ b/identity_resolver/src/resolution/resolver.rs @@ -40,6 +40,7 @@ where /// Constructs a new [`Resolver`]. /// /// # Example + /// /// Construct a `Resolver` that resolves DID documents of type /// [`CoreDocument`](::identity_document::document::CoreDocument). /// ``` @@ -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; @@ -87,8 +90,8 @@ where /// } /// ``` pub async fn resolve(&self, did: &D) -> Result { - 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 { @@ -109,6 +112,7 @@ where /// * If `dids` contains duplicates, these will be resolved only once. pub async fn resolve_multiple(&self, dids: &[D]) -> Result> { let futures = FuturesUnordered::new(); + // Create set to remove duplicates to avoid unnecessary resolution. let dids_set: HashSet = dids.iter().cloned().collect(); for did in dids_set { @@ -117,7 +121,9 @@ where doc.map(|doc| (did, doc)) }); } + let documents: HashMap = futures.try_collect().await?; + Ok(documents) } } diff --git a/identity_storage/Cargo.toml b/identity_storage/Cargo.toml index d980fe9362..82cdcf0c33 100644 --- a/identity_storage/Cargo.toml +++ b/identity_storage/Cargo.toml @@ -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] diff --git a/identity_storage/src/key_id_storage/key_id_storage_error.rs b/identity_storage/src/key_id_storage/key_id_storage_error.rs index ec9d8b57d2..046b6cc5f8 100644 --- a/identity_storage/src/key_id_storage/key_id_storage_error.rs +++ b/identity_storage/src/key_id_storage/key_id_storage_error.rs @@ -45,6 +45,7 @@ pub enum KeyIdStorageErrorKind { } impl KeyIdStorageErrorKind { + /// Returns the string representation of the error. pub const fn as_str(&self) -> &str { match self { Self::KeyIdAlreadyExists => "Key id already exists in storage", diff --git a/identity_storage/src/key_id_storage/memstore.rs b/identity_storage/src/key_id_storage/memstore.rs index 91731ee24e..ba5a5111d1 100644 --- a/identity_storage/src/key_id_storage/memstore.rs +++ b/identity_storage/src/key_id_storage/memstore.rs @@ -84,7 +84,7 @@ mod tests { use identity_verification::VerificationMethod; #[tokio::test] - pub async fn memstore_operations() { + async fn memstore_operations() { let verification_method: VerificationMethod = crate::storage::tests::test_utils::create_verification_method(); // Test insertion. diff --git a/identity_storage/src/key_id_storage/method_digest.rs b/identity_storage/src/key_id_storage/method_digest.rs index a24caa6af5..797d9bc737 100644 --- a/identity_storage/src/key_id_storage/method_digest.rs +++ b/identity_storage/src/key_id_storage/method_digest.rs @@ -15,10 +15,14 @@ pub type MethodDigestConstructionError = identity_core::common::SingleStructErro /// Characterization of the underlying cause of a [`MethodDigestConstructionError`]. #[derive(Debug)] pub enum MethodDigestConstructionErrorKind { - // Todo: Do we need this variant? It should be impossible to construct a VerificationMethod without a fragment. + /// Caused by a missing id on a verification method. + /// + /// This error should be impossible but exists for safety reasons. MissingIdFragment, + /// Caused by a failure to decode a method's [key material](identity_verification::MethodData). DataDecodingFailure, } + impl Display for MethodDigestConstructionErrorKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("method digest construction failure: ")?; @@ -41,7 +45,7 @@ pub struct MethodDigest { impl MethodDigest { /// Creates a new [`MethodDigest`]. pub fn new(verification_method: &VerificationMethod) -> Result { - // Method digest version 0 formula: SeaHash() + // Method digest version 0 formula: SeaHash() use MethodDigestConstructionErrorKind::*; let mut hasher: SeaHasher = SeaHasher::new(); let fragment: &str = verification_method.id().fragment().ok_or(MissingIdFragment)?; @@ -59,6 +63,7 @@ impl MethodDigest { }; let key_hash: u64 = hasher.finish(); + Ok(Self { version: 0, value: key_hash, @@ -101,7 +106,7 @@ mod test { use super::MethodDigest; #[test] - pub fn hash() { + fn hash() { // These values should be tested in the bindings too. let a: Value = json!( { @@ -125,7 +130,7 @@ mod test { } #[test] - pub fn pack() { + fn pack() { let verification_method: VerificationMethod = crate::storage::tests::test_utils::create_verification_method(); let method_digest: MethodDigest = MethodDigest::new(&verification_method).unwrap(); let packed: Vec = method_digest.pack(); @@ -134,7 +139,7 @@ mod test { } #[test] - pub fn unpack() { + fn unpack() { let packed: Vec = vec![0, 255, 212, 82, 63, 57, 19, 134, 193]; let method_digest_unpacked: MethodDigest = MethodDigest::unpack(packed).unwrap(); let method_digest_expected: MethodDigest = MethodDigest { @@ -145,7 +150,7 @@ mod test { } #[test] - pub fn invalid_unpack() { + fn invalid_unpack() { let packed: Vec = vec![1, 255, 212, 82, 63, 57, 19, 134, 193]; let method_digest_unpacked = MethodDigest::unpack(packed).unwrap_err(); let _expected_error = KeyIdStorageError::new(KeyIdStorageErrorKind::SerializationError); diff --git a/identity_storage/src/key_id_storage/mod.rs b/identity_storage/src/key_id_storage/mod.rs index 50087a5ef8..8ce5592e33 100644 --- a/identity_storage/src/key_id_storage/mod.rs +++ b/identity_storage/src/key_id_storage/mod.rs @@ -1,6 +1,13 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! A Key ID Storage is used to store the identifiers of keys +//! that were generated in a Key Storage. +//! +//! This module provides the [`KeyIdStorage`] trait that +//! stores the mapping from a method, identified by a [`MethodDigest`], +//! to its [`KeyId`](crate::key_storage::KeyId). + #[allow(clippy::module_inception)] mod key_id_storage; mod key_id_storage_error; diff --git a/identity_storage/src/key_storage/key_gen.rs b/identity_storage/src/key_storage/jwk_gen_output.rs similarity index 74% rename from identity_storage/src/key_storage/key_gen.rs rename to identity_storage/src/key_storage/jwk_gen_output.rs index 13e1572859..9904153fe2 100644 --- a/identity_storage/src/key_storage/key_gen.rs +++ b/identity_storage/src/key_storage/jwk_gen_output.rs @@ -6,13 +6,17 @@ use identity_verification::jose::jwk::Jwk; use super::KeyId; /// The output of a JWK key generation. +#[non_exhaustive] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct JwkGenOutput { + /// The key identifier of the generated JWK. pub key_id: KeyId, + /// The generated JWK. pub jwk: Jwk, } impl JwkGenOutput { + /// Constructs a new JWK generation output. pub fn new(key_id: KeyId, jwk: Jwk) -> Self { Self { key_id, jwk } } diff --git a/identity_storage/src/key_storage/jwk_storage.rs b/identity_storage/src/key_storage/jwk_storage.rs index c701def6b9..75169108be 100644 --- a/identity_storage/src/key_storage/jwk_storage.rs +++ b/identity_storage/src/key_storage/jwk_storage.rs @@ -1,16 +1,14 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -pub use crate::key_storage::KeyStorageError; -pub use crate::key_storage::KeyStorageErrorKind; - use crate::key_storage::KeyId; +use crate::key_storage::KeyStorageError; use crate::key_storage::KeyType; use async_trait::async_trait; use identity_verification::jose::jwk::Jwk; use identity_verification::jose::jws::JwsAlgorithm; -use super::key_gen::JwkGenOutput; +use super::jwk_gen_output::JwkGenOutput; /// Result of key storage operations. pub type KeyStorageResult = Result; @@ -33,7 +31,7 @@ mod storage_sub_trait { pub trait JwkStorage: storage_sub_trait::StorageSendSyncMaybe { /// Generate a new key represented as a JSON Web Key. /// - /// It's recommend that the implementer exposes constants for the supported [`KeyType`]. + /// It is recommended that the implementer exposes constants for the supported [`KeyType`]. async fn generate(&self, key_type: KeyType, alg: JwsAlgorithm) -> KeyStorageResult; /// Insert an existing JSON Web Key into the storage. @@ -45,6 +43,7 @@ pub trait JwkStorage: storage_sub_trait::StorageSendSyncMaybe { /// the corresponding `public_key` (see [`Jwk::alg`](Jwk::alg()) etc.). /// /// # Note + /// /// High level methods from this library calling this method are designed to always pass a `public_key` that /// corresponds to `key_id` and additional checks for this in the `sign` implementation are normally not required. /// This is however based on the expectation that the key material associated with a given [`KeyId`] is immutable. diff --git a/identity_storage/src/key_storage/key_storage_error.rs b/identity_storage/src/key_storage/key_storage_error.rs index ec89ac3034..59ff10968e 100644 --- a/identity_storage/src/key_storage/key_storage_error.rs +++ b/identity_storage/src/key_storage/key_storage_error.rs @@ -53,6 +53,7 @@ pub enum KeyStorageErrorKind { } impl KeyStorageErrorKind { + /// Returns the string representation of the error. pub const fn as_str(&self) -> &str { match self { Self::UnsupportedKeyType => "key generation failed: the provided multikey schema is not supported", @@ -67,6 +68,7 @@ impl KeyStorageErrorKind { } } } + impl AsRef for KeyStorageErrorKind { fn as_ref(&self) -> &str { self.as_str() diff --git a/identity_storage/src/key_storage/memstore.rs b/identity_storage/src/key_storage/memstore.rs index 473e6c53d5..b0860da6dc 100644 --- a/identity_storage/src/key_storage/memstore.rs +++ b/identity_storage/src/key_storage/memstore.rs @@ -17,7 +17,7 @@ use shared::Shared; use tokio::sync::RwLockReadGuard; use tokio::sync::RwLockWriteGuard; -use super::key_gen::JwkGenOutput; +use super::jwk_gen_output::JwkGenOutput; use super::KeyId; use super::KeyStorageError; use super::KeyStorageErrorKind; @@ -239,20 +239,19 @@ pub(crate) mod ed25519 { } } -const ED25519_KEY_TYPE_STR: &str = "Ed25519"; -pub const ED25519_KEY_TYPE: KeyType = KeyType::from_static_str(ED25519_KEY_TYPE_STR); - #[derive(Debug, Copy, Clone)] enum MemStoreKeyType { Ed25519, } impl JwkMemStore { - pub const ED25519_KEY_TYPE: KeyType = ED25519_KEY_TYPE; + const ED25519_KEY_TYPE_STR: &str = "Ed25519"; + /// The Ed25519 key type. + pub const ED25519_KEY_TYPE: KeyType = KeyType::from_static_str(Self::ED25519_KEY_TYPE_STR); } impl MemStoreKeyType { - pub const fn name(&self) -> &'static str { + const fn name(&self) -> &'static str { match self { MemStoreKeyType::Ed25519 => "Ed25519", } @@ -270,7 +269,7 @@ impl TryFrom<&KeyType> for MemStoreKeyType { fn try_from(value: &KeyType) -> Result { match value.as_str() { - ED25519_KEY_TYPE_STR => Ok(MemStoreKeyType::Ed25519), + JwkMemStore::ED25519_KEY_TYPE_STR => Ok(MemStoreKeyType::Ed25519), _ => Err(KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType)), } } @@ -337,18 +336,18 @@ pub(crate) mod shared { use tokio::sync::RwLockWriteGuard; #[derive(Default)] - pub struct Shared(RwLock); + pub(crate) struct Shared(RwLock); impl Shared { - pub fn new(data: T) -> Self { + pub(crate) fn new(data: T) -> Self { Self(RwLock::new(data)) } - pub async fn read(&self) -> RwLockReadGuard<'_, T> { + pub(crate) async fn read(&self) -> RwLockReadGuard<'_, T> { self.0.read().await } - pub async fn write(&self) -> RwLockWriteGuard<'_, T> { + pub(crate) async fn write(&self) -> RwLockWriteGuard<'_, T> { self.0.write().await } } @@ -377,7 +376,10 @@ mod tests { let test_msg: &[u8] = b"test"; let store: JwkMemStore = JwkMemStore::new(); - let JwkGenOutput { key_id, jwk } = store.generate(ED25519_KEY_TYPE, JwsAlgorithm::EdDSA).await.unwrap(); + let JwkGenOutput { key_id, jwk } = store + .generate(JwkMemStore::ED25519_KEY_TYPE, JwsAlgorithm::EdDSA) + .await + .unwrap(); let signature = store.sign(&key_id, test_msg, &jwk.to_public().unwrap()).await.unwrap(); diff --git a/identity_storage/src/key_storage/mod.rs b/identity_storage/src/key_storage/mod.rs index 90af3b3915..4db9b1793b 100644 --- a/identity_storage/src/key_storage/mod.rs +++ b/identity_storage/src/key_storage/mod.rs @@ -1,16 +1,21 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! A Key Storage is used to securely store private keys. +//! +//! This module provides the [`JwkStorage`] trait that +//! abstracts over storages that store JSON Web Keys. + +mod jwk_gen_output; mod jwk_storage; -mod key_gen; mod key_id; mod key_storage_error; mod key_type; #[cfg(feature = "memstore")] mod memstore; +pub use jwk_gen_output::*; pub use jwk_storage::*; -pub use key_gen::*; pub use key_id::*; pub use key_storage_error::*; pub use key_type::*; diff --git a/identity_storage/src/lib.rs b/identity_storage/src/lib.rs index 53ad039c6b..da1b0b66f4 100644 --- a/identity_storage/src/lib.rs +++ b/identity_storage/src/lib.rs @@ -1,6 +1,19 @@ // Copyright 2020-2023 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 +)] + pub mod key_id_storage; pub mod key_storage; pub mod storage; diff --git a/identity_storage/src/storage/error.rs b/identity_storage/src/storage/error.rs index 3f0447dc91..7abac68286 100644 --- a/identity_storage/src/storage/error.rs +++ b/identity_storage/src/storage/error.rs @@ -7,40 +7,48 @@ use crate::key_storage::KeyStorageError; /// Errors that can occur when working with the [`JwkDocumentExt`](crate::storage::JwkDocumentExt) API. #[derive(Debug, thiserror::Error)] +#[non_exhaustive] pub enum JwkStorageDocumentError { + /// Caused by a failure in the key storage. #[error("storage operation failed: key storage error")] KeyStorageError(KeyStorageError), + /// Caused by a failure in the key id storage. #[error("storage operation failed: key id storage error")] KeyIdStorageError(KeyIdStorageError), + /// Caused by an attempt to add a method with a fragment that already exists. #[error("could not add method: the fragment already exists")] FragmentAlreadyExists, + /// Caused by a missing verification method. #[error("method not found")] MethodNotFound, + /// Caused by the usage of a non-JWK method where a JWK method is expected. #[error("invalid method data format: expected publicKeyJwk")] NotPublicKeyJwk, + /// Caused by an invalid JWS algorithm. #[error("invalid JWS algorithm")] InvalidJwsAlgorithm, - #[error("cannot create jws: unable to produce kid header")] - MissingKid, + /// Caused by a failure to construct a verification method. #[error("method generation failed: unable to create a valid verification method")] VerificationMethodConstructionError(#[source] identity_verification::Error), + /// Caused by an encoding error. #[error("could not produce jwt: encoding error")] EncodingError(#[source] Box), + /// Caused by a failure to construct a method digest. #[error("unable to produce method digest")] MethodDigestConstructionError(#[source] MethodDigestConstructionError), + /// Caused by a failure during (de)serialization of JWS claims. #[error("could not produce JWS payload from the given claims: serialization failed")] ClaimsSerializationError(#[source] identity_credential::Error), + /// Caused by a failure to undo a failed storage operation. #[error("storage operation failed after altering state. Unable to undo operation(s): {message}")] UndoOperationFailed { + /// Additional error context. message: String, + /// The source error. source: Box, + /// The error that occurred during the undo operation. undo_error: Option>, }, - #[error("{0}")] - Custom( - &'static str, - #[source] Option>, - ), } #[cfg(test)] diff --git a/identity_storage/src/storage/jwk_document_ext.rs b/identity_storage/src/storage/jwk_document_ext.rs index fd719596a4..aef1841d1c 100644 --- a/identity_storage/src/storage/jwk_document_ext.rs +++ b/identity_storage/src/storage/jwk_document_ext.rs @@ -11,9 +11,9 @@ use crate::key_storage::KeyStorageResult; use crate::key_storage::KeyType; use super::JwkStorageDocumentError as Error; +use super::JwsSignatureOptions; use super::Storage; -use super::JwsSignatureOptions; use async_trait::async_trait; use identity_credential::credential::Credential; use identity_credential::credential::Jws; @@ -32,8 +32,18 @@ use identity_verification::MethodScope; use identity_verification::VerificationMethod; use serde::de::DeserializeOwned; use serde::Serialize; + +/// Alias for a `Result` with the error type [`Error`]. pub type StorageResult = Result; +/// Extension trait for JWK-based operations on DID documents. +/// +/// This trait is deliberately sealed and cannot be implemented by external crates. +/// The trait only exists as an extension of existing DID documents implemented in +/// dependent crates. Because those crates cannot also depend on this crate, +/// the extension trait is necessary. External crates however should simply wrap the methods +/// on the trait if they wish to reexport them on their DID document type. +/// This also allows them to use their own error type on those methods. #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] #[cfg_attr(feature = "send-sync-storage", async_trait)] pub trait JwkDocumentExt: private::Sealed { @@ -42,8 +52,7 @@ pub trait JwkDocumentExt: private::Sealed { /// /// - If no fragment is given the `kid` of the generated JWK is used, if it is set, otherwise an error is returned. /// - The `key_type` must be compatible with the given `storage`. [`Storage`]s are expected to export key type - /// constants - /// for that use case. + /// constants for that use case. /// /// The fragment of the generated method is returned. async fn generate_method( @@ -58,15 +67,19 @@ pub trait JwkDocumentExt: private::Sealed { K: JwkStorage, I: KeyIdStorage; - /// Remove the method identified by the given fragment from the document and delete the corresponding key material in + /// Remove the method identified by the given `id` from the document and delete the corresponding key material in /// the given `storage`. + /// + /// ## Warning + /// + /// This will delete the key material permanently and irrecoverably. async fn purge_method(&mut self, storage: &Storage, id: &DIDUrl) -> StorageResult<()> where K: JwkStorage, I: KeyIdStorage; - /// Sign the `payload` according to `options` with the storage backed private key corresponding to the public key - /// material in the verification method identified by the given `fragment. + /// Sign the arbitrary `payload` according to `options` with the storage backed private key corresponding to the + /// public key material in the verification method identified by the given `fragment. /// /// Upon success a string representing a JWS encoded according to the Compact JWS Serialization format is returned. /// See [RFC7515 section 3.1](https://www.rfc-editor.org/rfc/rfc7515#section-3.1). @@ -82,7 +95,7 @@ pub trait JwkDocumentExt: private::Sealed { I: KeyIdStorage; /// 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`. @@ -117,6 +130,7 @@ pub trait JwkDocumentExt: private::Sealed { T: ToOwned + Serialize + DeserializeOwned + Sync, CRED: ToOwned + Serialize + DeserializeOwned + Clone + Sync; } + mod private { pub trait Sealed {} impl Sealed for identity_document::document::CoreDocument {} @@ -166,6 +180,7 @@ macro_rules! generate_method_for_document_type { // Extract data from method before inserting it into the DID document. let method_digest: MethodDigest = MethodDigest::new(&method).map_err(Error::MethodDigestConstructionError)?; let method_id: DIDUrl = method.id().clone(); + // The fragment is always set on a method, so this error will never occur. let fragment: String = method_id .fragment() @@ -234,6 +249,7 @@ macro_rules! purge_method_for_document_type { let key_id_deletion_fut = ::delete_key_id(&storage.key_id_storage(), &method_digest); let (key_deletion_result, key_id_deletion_result): (KeyStorageResult<()>, KeyIdStorageResult<()>) = futures::join!(key_deletion_fut, key_id_deletion_fut); + // Check for any errors that may have occurred. Unfortunately this is somewhat involved. match (key_deletion_result, key_id_deletion_result) { (Ok(_), Ok(_)) => Ok(()), @@ -327,14 +343,15 @@ impl JwkDocumentExt for CoreDocument { let MethodData::PublicKeyJwk(ref jwk) = method.data() else { return Err(Error::NotPublicKeyJwk); }; - // Extract JwsAlgorithm + + // Extract JwsAlgorithm. let alg: JwsAlgorithm = jwk .alg() .unwrap_or("") .parse() .map_err(|_| Error::InvalidJwsAlgorithm)?; - // create JWS header in accordance with options + // Create JWS header in accordance with options. let header: JwsHeader = { let mut header = JwsHeader::new(); @@ -389,7 +406,7 @@ impl JwkDocumentExt for CoreDocument { CompactJwsEncodingOptions::Detached }; - let jws_encoder: CompactJwsEncoder = CompactJwsEncoder::new_with_options(payload, &header, encoding_options) + let jws_encoder: CompactJwsEncoder<'_> = CompactJwsEncoder::new_with_options(payload, &header, encoding_options) .map_err(|err| Error::EncodingError(err.into()))?; let signature = ::sign(storage.key_storage(), &key_id, jws_encoder.signing_input(), jwk) .await @@ -465,7 +482,7 @@ impl JwkDocumentExt for CoreDocument { } } -/// Attempt to revert key generation if this succeeds the original `source_error` is returned, +/// Attempt to revert key generation. If this succeeds the original `source_error` is returned, /// otherwise [`JwkStorageDocumentError::UndoOperationFailed`] is returned with the `source_error` attached as /// `source`. async fn try_undo_key_generation(storage: &Storage, key_id: &KeyId, source_error: Error) -> Error @@ -493,6 +510,7 @@ mod iota_document { use super::*; use identity_credential::credential::Jwt; use identity_iota_core::IotaDocument; + generate_method_for_document_type!(IotaDocument, generate_method_iota_document); purge_method_for_document_type!(IotaDocument, purge_method_iota_document); diff --git a/identity_storage/src/storage/mod.rs b/identity_storage/src/storage/mod.rs index 9e12b2daea..efbdc28cbb 100644 --- a/identity_storage/src/storage/mod.rs +++ b/identity_storage/src/storage/mod.rs @@ -1,5 +1,8 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 + +//! This module provides a type wrapping a key and key id storage. + mod error; mod jwk_document_ext; mod signature_options; @@ -10,7 +13,7 @@ pub use error::*; pub use jwk_document_ext::*; pub use signature_options::*; -/// A type wrapping a [`JwkStorage`](crate::key_storage::JwkStorage) and +/// A type wrapping a key and key id storage, typically used with [`JwkStorage`](crate::key_storage::JwkStorage) and /// [`KeyIdStorage`](crate::key_id_storage::KeyIdStorage) that should always be used together when calling methods from /// [`JwkDocumentExt`](crate::storage::JwkDocumentExt). pub struct Storage { diff --git a/identity_storage/src/storage/signature_options.rs b/identity_storage/src/storage/signature_options.rs index 553d7e2f60..63a5da3ab8 100644 --- a/identity_storage/src/storage/signature_options.rs +++ b/identity_storage/src/storage/signature_options.rs @@ -3,6 +3,8 @@ use identity_core::common::Url; +/// Options for creating a JSON Web Signature. +#[non_exhaustive] #[derive(Debug, Default, serde::Serialize, serde::Deserialize, Eq, PartialEq, Clone)] #[serde(rename_all = "camelCase")] #[serde(default)] @@ -40,6 +42,7 @@ pub struct JwsSignatureOptions { /// [More Info](https://tools.ietf.org/html/rfc8555#section-6.5.2) #[serde(skip_serializing_if = "Option::is_none")] pub nonce: Option, + /// Whether the payload should be detached from the JWS. /// /// [More Info](https://www.rfc-editor.org/rfc/rfc7515#appendix-F). @@ -65,14 +68,14 @@ impl JwsSignatureOptions { } /// Replace the value of the `typ` field. - pub fn typ(mut self, value: String) -> Self { - self.typ = Some(value); + pub fn typ(mut self, value: impl Into) -> Self { + self.typ = Some(value.into()); self } /// Replace the value of the `cty` field. - pub fn cty(mut self, value: String) -> Self { - self.cty = Some(value); + pub fn cty(mut self, value: impl Into) -> Self { + self.cty = Some(value.into()); self } @@ -83,8 +86,8 @@ impl JwsSignatureOptions { } /// Replace the value of the `nonce` field. - pub fn nonce(mut self, value: String) -> Self { - self.nonce = Some(value); + pub fn nonce(mut self, value: impl Into) -> Self { + self.nonce = Some(value.into()); self } diff --git a/identity_storage/src/storage/tests/api.rs b/identity_storage/src/storage/tests/api.rs index 1158e8156f..01e5eac3db 100644 --- a/identity_storage/src/storage/tests/api.rs +++ b/identity_storage/src/storage/tests/api.rs @@ -2,15 +2,20 @@ // SPDX-License-Identifier: Apache-2.0 use identity_core::common::Object; +use identity_core::common::Url; use identity_core::convert::FromJson; use identity_credential::credential::Credential; +use identity_credential::credential::Jws; use identity_credential::validator::CredentialValidationOptions; use identity_did::DIDUrl; use identity_document::document::CoreDocument; use identity_document::verifiable::JwsVerificationOptions; use identity_verification::jose::jws::EdDSAJwsVerifier; use identity_verification::jose::jws::JwsAlgorithm; +use identity_verification::jwk::Jwk; +use identity_verification::jws::DecodedJws; +use identity_verification::jwu::encode_b64; use identity_verification::MethodRelationship; use identity_verification::MethodScope; @@ -42,6 +47,24 @@ fn setup() -> (CoreDocument, MemStorage) { (mock_document, storage) } +async fn setup_with_method() -> (CoreDocument, MemStorage, String) { + let mut mock_document = CoreDocument::from_json(MOCK_DOCUMENT_JSON).unwrap(); + let storage = Storage::new(JwkMemStore::new(), KeyIdMemstore::new()); + + let method_fragment: String = mock_document + .generate_method( + &storage, + JwkMemStore::ED25519_KEY_TYPE, + JwsAlgorithm::EdDSA, + None, + MethodScope::VerificationMethod, + ) + .await + .unwrap(); + + (mock_document, storage, method_fragment) +} + #[tokio::test] async fn generation() { let (mut document, storage) = setup(); @@ -83,69 +106,145 @@ async fn generation() { } #[tokio::test] -async fn signing_bytes() { - let (mut document, storage) = setup(); - // Generate a method with the kid as fragment - let method_fragment: String = document - .generate_method( - &storage, - JwkMemStore::ED25519_KEY_TYPE, - JwsAlgorithm::EdDSA, - None, - MethodScope::VerificationMethod, - ) +async fn sign_bytes() { + let (document, storage, fragment) = setup_with_method().await; + + let payload: &[u8] = b"test"; + let signature_options: JwsSignatureOptions = JwsSignatureOptions::new(); + let verification_options: JwsVerificationOptions = JwsVerificationOptions::new(); + + let jws: Jws = document + .sign_bytes(&storage, &fragment, payload, &signature_options) .await .unwrap(); - let payload = b"test"; - // TODO: Check with more Options - let options = JwsSignatureOptions::new(); - let jws = document - .sign_bytes(&storage, &method_fragment, payload, &options) + assert!(document + .verify_jws(jws.as_str(), None, &EdDSAJwsVerifier::default(), &verification_options) + .is_ok()); +} + +#[tokio::test] +async fn sign_bytes_with_nonce() { + let (document, storage, fragment) = setup_with_method().await; + + let payload: &[u8] = b"test"; + let nonce: &str = "nonce"; + let signature_options: JwsSignatureOptions = JwsSignatureOptions::new().nonce(nonce); + let verification_options: JwsVerificationOptions = JwsVerificationOptions::new().nonce(nonce); + + let jws: Jws = document + .sign_bytes(&storage, &fragment, payload, &signature_options) .await .unwrap(); + assert!(document + .verify_jws(jws.as_str(), None, &EdDSAJwsVerifier::default(), &verification_options) + .is_ok()); + assert!(document .verify_jws( jws.as_str(), None, &EdDSAJwsVerifier::default(), - &JwsVerificationOptions::default() + &JwsVerificationOptions::new() ) - .is_ok()); + .is_err()); + + assert!(document + .verify_jws( + jws.as_str(), + None, + &EdDSAJwsVerifier::default(), + &JwsVerificationOptions::new().nonce("other") + ) + .is_err()); } #[tokio::test] -async fn signing_bytes_detached_without_b64() { - let (mut document, storage) = setup(); +async fn sign_bytes_with_header_copy_options() { + let (document, storage, fragment) = setup_with_method().await; - // Generate a method with the kid as fragment - let method_fragment: String = document - .generate_method( - &storage, - JwkMemStore::ED25519_KEY_TYPE, - JwsAlgorithm::EdDSA, - None, - MethodScope::VerificationMethod, - ) + let payload: &[u8] = b"test"; + let url: Url = Url::parse("https://example.com/").unwrap(); + let typ: &str = "typ"; + let cty: &str = "content_type"; + + let signature_options: JwsSignatureOptions = JwsSignatureOptions::new() + .url(url.clone()) + .typ(typ) + .cty(cty) + .attach_jwk_to_header(true); + + let verification_options: JwsVerificationOptions = JwsVerificationOptions::new(); + + let jws: Jws = document + .sign_bytes(&storage, &fragment, payload, &signature_options) .await .unwrap(); - let payload = b"test"; - let options = JwsSignatureOptions::new().b64(false).detached_payload(true); - let jws = document - .sign_bytes(&storage, method_fragment.as_ref(), payload, &options) + let decoded_jws: DecodedJws<'_> = document + .verify_jws(jws.as_str(), None, &EdDSAJwsVerifier::default(), &verification_options) + .unwrap(); + + let jwk: &Jwk = document + .resolve_method(&fragment, None) + .unwrap() + .data() + .try_public_key_jwk() + .unwrap(); + + assert_eq!(decoded_jws.protected.typ().unwrap(), typ); + assert_eq!(decoded_jws.protected.cty().unwrap(), cty); + assert_eq!(decoded_jws.protected.url().unwrap(), &url); + assert_eq!(decoded_jws.protected.jwk().unwrap(), jwk); +} + +#[tokio::test] +async fn sign_bytes_detached() { + let (document, storage, fragment) = setup_with_method().await; + + // ===================== + // Without B64 Encoding + // ===================== + + let payload: &[u8] = b"test"; + let signature_options: JwsSignatureOptions = JwsSignatureOptions::new().b64(false).detached_payload(true); + let verification_options: JwsVerificationOptions = JwsVerificationOptions::new(); + + let jws: Jws = document + .sign_bytes(&storage, fragment.as_ref(), payload, &signature_options) .await .unwrap(); - document + assert!(document .verify_jws( jws.as_str(), Some(payload), &EdDSAJwsVerifier::default(), - &JwsVerificationOptions::default(), + &verification_options, ) + .is_ok()); + + // ================== + // With B64 Encoding + // ================== + + let signature_options: JwsSignatureOptions = JwsSignatureOptions::new().b64(true).detached_payload(true); + + let jws: Jws = document + .sign_bytes(&storage, fragment.as_ref(), payload, &signature_options) + .await .unwrap(); + let payload_b64: String = encode_b64(payload); + + assert!(document + .verify_jws( + jws.as_str(), + Some(payload_b64.as_ref()), + &EdDSAJwsVerifier::default(), + &verification_options, + ) + .is_ok()); } #[tokio::test] diff --git a/identity_storage/src/storage/tests/credential_validation.rs b/identity_storage/src/storage/tests/credential_validation.rs index b9480b3976..5cfe0d1648 100644 --- a/identity_storage/src/storage/tests/credential_validation.rs +++ b/identity_storage/src/storage/tests/credential_validation.rs @@ -5,26 +5,20 @@ use identity_core::common::Duration; use identity_core::common::Object; use identity_core::common::Timestamp; use identity_core::common::Url; -use identity_core::convert::FromJson; -use identity_credential::credential::Credential; use identity_credential::credential::Jwt; use identity_credential::credential::RevocationBitmapStatus; use identity_credential::credential::Status; -use identity_credential::credential::Subject; use identity_credential::revocation::RevocationBitmap; use identity_credential::revocation::RevocationDocumentExt; use identity_credential::validator::CredentialValidationOptions; use identity_credential::validator::CredentialValidator; use identity_credential::validator::FailFast; use identity_credential::validator::StatusCheck; -use identity_credential::validator::SubjectHolderRelationship; use identity_credential::validator::ValidationError; use identity_did::DID; use identity_document::document::CoreDocument; use identity_document::service::Service; use identity_document::verifiable::JwsVerificationOptions; -use once_cell::sync::Lazy; -use proptest::proptest; use crate::storage::tests::test_utils; use crate::storage::tests::test_utils::CredentialSetup; @@ -32,62 +26,6 @@ use crate::storage::tests::test_utils::Setup; use crate::storage::JwkDocumentExt; use crate::storage::JwsSignatureOptions; -const SIMPLE_CREDENTIAL_JSON: &str = r#"{ - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1" - ], - "id": "http://example.edu/credentials/3732", - "type": ["VerifiableCredential", "UniversityDegreeCredential"], - "issuer": "https://example.edu/issuers/14", - "issuanceDate": "2010-01-01T19:23:24Z", - "expirationDate": "2020-01-01T19:23:24Z", - "credentialSubject": { - "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", - "degree": { - "type": "BachelorDegree", - "name": "Bachelor of Science in Mechanical Engineering" - } - } -}"#; - -// A simple credential shared by some of the tests in this module -static SIMPLE_CREDENTIAL: Lazy = - Lazy::new(|| Credential::::from_json(SIMPLE_CREDENTIAL_JSON).unwrap()); - -// TODO: Move to identity_credential? -#[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()); -} - -proptest! { - #[test] - fn property_based_issued_before(seconds in 0 ..1_000_000_000_u32) { - - let earlier_than_issuance_date = SIMPLE_CREDENTIAL.issuance_date.checked_sub(Duration::seconds(seconds)).unwrap(); - let later_than_issuance_date = SIMPLE_CREDENTIAL.issuance_date.checked_add(Duration::seconds(seconds)).unwrap(); - assert!(CredentialValidator::check_issued_on_or_before(&SIMPLE_CREDENTIAL, earlier_than_issuance_date).is_err()); - assert!(CredentialValidator::check_issued_on_or_before(&SIMPLE_CREDENTIAL, later_than_issuance_date).is_ok()); - } -} - async fn invalid_expiration_or_issuance_date_impl(setup: Setup) where T: JwkDocumentExt + AsRef, @@ -347,121 +285,6 @@ async fn verify_invalid_signature() { .await; } -async fn check_subject_holder_relationship_impl(setup: Setup) -where - T: JwkDocumentExt + AsRef, -{ - let Setup { issuer_doc, .. } = setup; - - 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(issuer_doc.as_ref().id().as_str()).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()); -} - -#[tokio::test] -async fn check_subject_holder_relationship() { - check_subject_holder_relationship_impl(test_utils::setup_coredocument(None, None).await).await; - check_subject_holder_relationship_impl(test_utils::setup_iotadocument(None, None).await).await; -} - fn check_status_impl(setup: Setup, insert_service: F) where T: JwkDocumentExt + AsRef + RevocationDocumentExt, diff --git a/identity_storage/src/storage/tests/test_utils.rs b/identity_storage/src/storage/tests/test_utils.rs index 322a112a1e..6b24c900ad 100644 --- a/identity_storage/src/storage/tests/test_utils.rs +++ b/identity_storage/src/storage/tests/test_utils.rs @@ -62,12 +62,12 @@ const ISSUER_IOTA_DOCUMENT_JSON: &str = r#" }"#; pub(super) struct Setup { - pub issuer_doc: T, - pub subject_doc: U, - pub issuer_storage: MemStorage, - pub issuer_method_fragment: String, - pub subject_storage: MemStorage, - pub subject_method_fragment: String, + pub(crate) issuer_doc: T, + pub(crate) subject_doc: U, + pub(crate) issuer_storage: MemStorage, + pub(crate) issuer_method_fragment: String, + pub(crate) subject_storage: MemStorage, + pub(crate) subject_method_fragment: String, } pub(super) async fn setup_iotadocument( @@ -131,9 +131,9 @@ where } pub(super) struct CredentialSetup { - pub credential: Credential, - pub issuance_date: Timestamp, - pub expiration_date: Timestamp, + pub(crate) credential: Credential, + pub(crate) issuance_date: Timestamp, + pub(crate) expiration_date: Timestamp, } pub(super) fn generate_credential, U: AsRef>(