diff --git a/Cargo.toml b/Cargo.toml index c877899c4c..d4726e0d4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "identity_iota_core", "identity_resolver", "identity_verification", + "identity_stronghold", "identity_jose", "identity_eddsa_verifier", "examples", diff --git a/examples/0_basic/8_stronghold.rs b/examples/0_basic/8_stronghold.rs index f69fc97e59..a706e467e7 100644 --- a/examples/0_basic/8_stronghold.rs +++ b/examples/0_basic/8_stronghold.rs @@ -15,10 +15,10 @@ use identity_iota::storage::JwkDocumentExt; use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; use identity_iota::storage::Storage; -use identity_iota::storage::StrongholdStorage; use identity_iota::verification::jws::DecodedJws; use identity_iota::verification::jws::JwsAlgorithm; use identity_iota::verification::MethodScope; +use identity_stronghold::StrongholdStorage; use iota_sdk::client::secret::stronghold::StrongholdSecretManager; use iota_sdk::client::Client; use iota_sdk::client::Password; diff --git a/examples/Cargo.toml b/examples/Cargo.toml index d0a7aeeb6e..c07b2acf89 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -7,9 +7,9 @@ publish = false [dependencies] anyhow = "1.0.62" -identity_eddsa_verifier = { path = "../identity_eddsa_verifier" } -identity_iota = { path = "../identity_iota", features = ["memstore", "domain-linkage"] } -identity_storage = { path = "../identity_storage", features = ["stronghold"] } +identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false } +identity_iota = { path = "../identity_iota", default-features = false, features = ["memstore", "domain-linkage"] } +identity_stronghold = { path = "../identity_stronghold", default-features = false } iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] } primitive-types = "0.12.1" rand = "0.8.5" diff --git a/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs index 4b1dc56452..83e21ba3a6 100644 --- a/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs +++ b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs @@ -7,9 +7,7 @@ use identity_core::common::OneOrMany; use identity_core::common::Timestamp; use identity_core::common::Url; use identity_core::convert::FromJson; -use identity_did::CoreDID; use identity_did::DID; -use identity_document::document::CoreDocument; use identity_verification::jws::Decoder; use super::JwtValidationError; @@ -88,11 +86,14 @@ impl JwtCredentialValidatorUtils { /// /// Only supports `RevocationBitmap2022`. #[cfg(feature = "revocation-bitmap")] - pub fn check_status, T>( + pub fn check_status, T>( credential: &Credential, trusted_issuers: &[DOC], status_check: crate::validator::StatusCheck, ) -> ValidationUnitResult { + use identity_did::CoreDID; + use identity_document::document::CoreDocument; + if status_check == crate::validator::StatusCheck::SkipAll { return Ok(()); } @@ -128,7 +129,7 @@ impl JwtCredentialValidatorUtils { /// Check the given `status` against the matching [`RevocationBitmap`] service in the /// issuer's DID Document. #[cfg(feature = "revocation-bitmap")] - fn check_revocation_bitmap_status + ?Sized>( + fn check_revocation_bitmap_status + ?Sized>( issuer: &DOC, status: crate::credential::RevocationBitmapStatus, ) -> ValidationUnitResult { diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml index 28ade1d6e5..61bf70ba87 100644 --- a/identity_iota/Cargo.toml +++ b/identity_iota/Cargo.toml @@ -23,7 +23,7 @@ identity_verification = { version = "=0.7.0-alpha.7", path = "../identity_verifi [dev-dependencies] anyhow = "1.0.64" -iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] } +iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client"] } rand = "0.8.5" tokio = { version = "1.29.0", features = ["full"] } diff --git a/identity_storage/Cargo.toml b/identity_storage/Cargo.toml index de995e8606..2dff63d978 100644 --- a/identity_storage/Cargo.toml +++ b/identity_storage/Cargo.toml @@ -4,7 +4,7 @@ version = "0.7.0-alpha.7" authors.workspace = true edition.workspace = true homepage.workspace = true -keywords = ["iota", "storage", "identity", "kms", "stronghold"] +keywords = ["iota", "storage", "identity", "kms"] license.workspace = true readme = "./README.md" repository.workspace = true @@ -20,16 +20,13 @@ identity_did = { version = "=0.7.0-alpha.7", path = "../identity_did", default-f identity_document = { version = "=0.7.0-alpha.7", path = "../identity_document", default-features = false } identity_iota_core = { version = "=0.7.0-alpha.7", path = "../identity_iota_core", default-features = false, optional = true } identity_verification = { version = "=0.7.0-alpha.7", path = "../identity_verification", default_features = false } -iota-crypto = { version = "0.23", default-features = false, features = ["blake2b", "ed25519", "random"], optional = true } -iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"], optional = true } -iota_stronghold = { version = "2.0", optional = true, default-features = false } +iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"], optional = true } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"], optional = true } seahash = { version = "4.1.0", default_features = false } serde.workspace = true serde_json.workspace = true thiserror.workspace = true tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync"], optional = true } -zeroize = { version = "1.6.0", default_features = false, optional = true } [dev-dependencies] identity_credential = { version = "=0.7.0-alpha.7", path = "../identity_credential", features = ["revocation-bitmap"] } @@ -44,6 +41,4 @@ memstore = ["dep:tokio", "dep:rand", "dep:iota-crypto"] # Enables `Send` + `Sync` bounds for the storage traits. send-sync-storage = [] # Implements the JwkStorageDocumentExt trait for IotaDocument -# Exposes the stronghold implementations for the storage traits. -stronghold = ["dep:tokio", "dep:iota-sdk", "dep:iota_stronghold", "dep:zeroize", "dep:rand", "dep:iota-crypto"] iota-document = ["dep:identity_iota_core"] diff --git a/identity_storage/src/key_id_storage/mod.rs b/identity_storage/src/key_id_storage/mod.rs index cefa7adb00..02d84241f1 100644 --- a/identity_storage/src/key_id_storage/mod.rs +++ b/identity_storage/src/key_id_storage/mod.rs @@ -12,8 +12,6 @@ mod key_id_storage; mod key_id_storage_error; mod method_digest; -#[cfg(feature = "stronghold")] -mod stronghold; #[cfg(feature = "memstore")] mod memstore; diff --git a/identity_storage/src/key_id_storage/tests/mod.rs b/identity_storage/src/key_id_storage/tests/mod.rs index 78af71375a..a35db512e4 100644 --- a/identity_storage/src/key_id_storage/tests/mod.rs +++ b/identity_storage/src/key_id_storage/tests/mod.rs @@ -2,6 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 mod memstore; -#[cfg(feature = "stronghold")] -mod stronghold; mod utils; diff --git a/identity_storage/src/key_id_storage/tests/stronghold.rs b/identity_storage/src/key_id_storage/tests/stronghold.rs deleted file mode 100644 index d52eb1af5c..0000000000 --- a/identity_storage/src/key_id_storage/tests/stronghold.rs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use super::utils::test_storage_operations; -use crate::key_id_storage::method_digest::MethodDigest; -use crate::key_id_storage::KeyIdStorage; -use crate::key_id_storage::KeyIdStorageErrorKind; -use crate::key_storage::KeyId; -use crate::storage::tests::test_utils::create_verification_method; -use crate::test_utils::stronghold_test_utils::create_temp_file; -use crate::StrongholdStorage; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::Password; -use std::path::PathBuf; - -const PASS: &str = "secure_password"; - -#[tokio::test] -async fn test_stronghold() { - iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); - let file: PathBuf = create_temp_file(); - let secret_manager = StrongholdSecretManager::builder() - .password(Password::from(PASS.to_owned())) - .build(&file) - .unwrap(); - let stronghold_storage = StrongholdStorage::new(secret_manager); - test_storage_operations(stronghold_storage).await; -} - -#[tokio::test] -async fn write_to_disk() { - iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); - let file: PathBuf = create_temp_file(); - let secret_manager = StrongholdSecretManager::builder() - .password(Password::from(PASS.to_owned())) - .build(&file) - .unwrap(); - - let verification_method = create_verification_method(); - - let key_id_1 = KeyId::new("keyid"); - let method_digest: MethodDigest = MethodDigest::new(&verification_method).unwrap(); - let stronghold_storage = StrongholdStorage::new(secret_manager); - stronghold_storage - .insert_key_id(method_digest.clone(), key_id_1.clone()) - .await - .expect("inserting into memstore failed"); - - drop(stronghold_storage); - - let secret_manager = StrongholdSecretManager::builder() - .password(Password::from(PASS.to_owned())) - .build(&file) - .unwrap(); - let stronghold_storage = StrongholdStorage::new(secret_manager); - - let key_id: KeyId = stronghold_storage.get_key_id(&method_digest).await.unwrap(); - assert_eq!(key_id_1, key_id); - - stronghold_storage - .delete_key_id(&method_digest) - .await - .expect("deletion failed"); - - drop(stronghold_storage); - - let secret_manager = StrongholdSecretManager::builder() - .password(Password::from(PASS.to_owned())) - .build(&file) - .unwrap(); - let stronghold_storage = StrongholdStorage::new(secret_manager); - let error_kind: KeyIdStorageErrorKind = stronghold_storage - .get_key_id(&method_digest) - .await - .unwrap_err() - .kind() - .clone(); - - assert!(matches!(error_kind, KeyIdStorageErrorKind::KeyIdNotFound)); -} diff --git a/identity_storage/src/key_storage/mod.rs b/identity_storage/src/key_storage/mod.rs index 54e59b5e70..70006b0537 100644 --- a/identity_storage/src/key_storage/mod.rs +++ b/identity_storage/src/key_storage/mod.rs @@ -6,7 +6,7 @@ //! This module provides the [`JwkStorage`] trait that //! abstracts over storages that store JSON Web Keys. -#[cfg(any(feature = "stronghold", feature = "memstore"))] +#[cfg(feature = "memstore")] mod ed25519; mod jwk_gen_output; mod jwk_storage; @@ -15,8 +15,6 @@ mod key_storage_error; mod key_type; #[cfg(feature = "memstore")] mod memstore; -#[cfg(feature = "stronghold")] -mod stronghold; #[cfg(test)] pub(crate) mod tests; @@ -28,5 +26,3 @@ pub use key_storage_error::*; pub use key_type::*; #[cfg(feature = "memstore")] pub use memstore::*; -#[cfg(feature = "stronghold")] -pub use stronghold::*; diff --git a/identity_storage/src/key_storage/tests/mod.rs b/identity_storage/src/key_storage/tests/mod.rs index 2a3696b05e..c60818e1aa 100644 --- a/identity_storage/src/key_storage/tests/mod.rs +++ b/identity_storage/src/key_storage/tests/mod.rs @@ -3,8 +3,5 @@ mod memstore; -#[cfg(feature = "stronghold")] -mod stronghold; - #[cfg(test)] pub(crate) mod utils; diff --git a/identity_storage/src/key_storage/tests/stronghold.rs b/identity_storage/src/key_storage/tests/stronghold.rs deleted file mode 100644 index 73a1d54daa..0000000000 --- a/identity_storage/src/key_storage/tests/stronghold.rs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::path::PathBuf; - -use identity_verification::jwk::Jwk; -use identity_verification::jws::JwsAlgorithm; -use iota_sdk::client::Password; - -use crate::key_storage::tests::utils::generate_ed25519; -use crate::key_storage::JwkStorage; -use crate::key_storage::KeyType; -use crate::test_utils::stronghold_test_utils::create_stronghold_secret_manager; -use crate::test_utils::stronghold_test_utils::create_temp_file; -use crate::StrongholdStorage; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; - -use super::utils::test_generate_and_sign; -use super::utils::test_incompatible_key_alg; -use super::utils::test_incompatible_key_type; -use super::utils::test_insertion; -use super::utils::test_key_exists; - -#[tokio::test] -async fn insert() { - let stronghold_secret_manager = create_stronghold_secret_manager(); - let stronghold_storage = StrongholdStorage::new(stronghold_secret_manager); - test_insertion(stronghold_storage).await; -} - -#[tokio::test] -async fn incompatible_key_alg() { - let stronghold_secret_manager = create_stronghold_secret_manager(); - let stronghold_storage = StrongholdStorage::new(stronghold_secret_manager); - test_incompatible_key_alg(stronghold_storage).await; -} - -#[tokio::test] -async fn incompatible_key_types() { - let stronghold_secret_manager = create_stronghold_secret_manager(); - let stronghold_storage = StrongholdStorage::new(stronghold_secret_manager); - test_incompatible_key_type(stronghold_storage).await; -} - -#[tokio::test] -async fn generate_and_sign() { - let stronghold_secret_manager = create_stronghold_secret_manager(); - let stronghold_storage = StrongholdStorage::new(stronghold_secret_manager); - test_generate_and_sign(stronghold_storage).await; -} - -#[tokio::test] -async fn key_exists() { - let stronghold_secret_manager = create_stronghold_secret_manager(); - let stronghold_storage = StrongholdStorage::new(stronghold_secret_manager); - test_key_exists(stronghold_storage).await; -} - -// Tests the cases that require persisting to disk, generate, insert and delete. -#[tokio::test] -async fn write_to_disk() { - const PASS: &str = "secure_password"; - let file: PathBuf = create_temp_file(); - let secret_manager = StrongholdSecretManager::builder() - .password(Password::from(PASS.to_owned())) - .build(file.clone()) - .unwrap(); - let stronghold_storage = StrongholdStorage::new(secret_manager); - - let generate = stronghold_storage - .generate(KeyType::new("Ed25519"), JwsAlgorithm::EdDSA) - .await - .unwrap(); - let key_id = &generate.key_id; - - drop(stronghold_storage); - - let secret_manager = StrongholdSecretManager::builder() - .password(Password::from(PASS.to_owned())) - .build(&file) - .unwrap(); - let stronghold_storage = StrongholdStorage::new(secret_manager); - let exists = stronghold_storage.exists(key_id).await.unwrap(); - assert!(exists); - stronghold_storage.delete(key_id).await.unwrap(); - - drop(stronghold_storage); - - let secret_manager = StrongholdSecretManager::builder() - .password(Password::from(PASS.to_owned())) - .build(&file) - .unwrap(); - let stronghold_storage = StrongholdStorage::new(secret_manager); - let exists = stronghold_storage.exists(key_id).await.unwrap(); - assert!(!exists); - - let (private_key, public_key) = generate_ed25519(); - let mut jwk: Jwk = crate::key_storage::ed25519::encode_jwk(&private_key, &public_key); - jwk.set_alg(JwsAlgorithm::EdDSA.name()); - let key_id = stronghold_storage.insert(jwk).await.unwrap(); - - drop(stronghold_storage); - - let secret_manager = StrongholdSecretManager::builder() - .password(Password::from(PASS.to_owned())) - .build(&file) - .unwrap(); - let stronghold_storage = StrongholdStorage::new(secret_manager); - let exists = stronghold_storage.exists(&key_id).await.unwrap(); - assert!(exists); -} diff --git a/identity_storage/src/lib.rs b/identity_storage/src/lib.rs index 1e5efb256d..da1b0b66f4 100644 --- a/identity_storage/src/lib.rs +++ b/identity_storage/src/lib.rs @@ -17,13 +17,7 @@ pub mod key_id_storage; pub mod key_storage; pub mod storage; -#[cfg(feature = "stronghold")] -pub mod stronghold_storage; -#[cfg(test)] -mod test_utils; pub use key_id_storage::*; pub use key_storage::*; pub use storage::*; -#[cfg(feature = "stronghold")] -pub use stronghold_storage::*; diff --git a/identity_storage/src/stronghold_storage.rs b/identity_storage/src/stronghold_storage.rs deleted file mode 100644 index f9384b0706..0000000000 --- a/identity_storage/src/stronghold_storage.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -//! Wrapper around [`StrongholdSecretManager`](StrongholdSecretManager). - -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_stronghold::Stronghold; -use std::sync::Arc; -use tokio::sync::MutexGuard; - -/// Wrapper around a [`StrongholdSecretManager`] that implements the [`KeyIdStorage`](crate::KeyIdStorage) -/// and [`JwkStorage`](crate::JwkStorage) interfaces. -#[derive(Clone, Debug)] -pub struct StrongholdStorage(Arc); - -impl StrongholdStorage { - /// Creates a new [`StrongholdStorage`]. - pub fn new(stronghold_secret_manager: StrongholdSecretManager) -> Self { - Self(Arc::new(SecretManager::Stronghold(stronghold_secret_manager))) - } - - /// Shared reference to the inner [`SecretManager`]. - pub fn as_secret_manager(&self) -> &SecretManager { - self.0.as_ref() - } - - /// Acquire lock of the inner [`Stronghold`]. - pub(crate) async fn get_stronghold(&self) -> MutexGuard<'_, Stronghold> { - match *self.0 { - SecretManager::Stronghold(ref stronghold) => stronghold.inner().await, - _ => unreachable!("secret manager can be only constrcuted from stronghold"), - } - } -} diff --git a/identity_storage/src/test_utils/stronghold_test_utils.rs b/identity_storage/src/test_utils/stronghold_test_utils.rs deleted file mode 100644 index a4ba94fc83..0000000000 --- a/identity_storage/src/test_utils/stronghold_test_utils.rs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::path::PathBuf; - -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::Password; -use rand::distributions::DistString; - -pub(crate) fn create_stronghold_secret_manager() -> StrongholdSecretManager { - iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); - let mut file = std::env::temp_dir(); - file.push("test_strongholds"); - file.push(rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 32)); - file.set_extension("stronghold"); - - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(&file) - .unwrap() -} - -pub(crate) fn create_temp_file() -> PathBuf { - let mut file = std::env::temp_dir(); - file.push("test_strongholds"); - file.push(rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 32)); - file.set_extension("stronghold"); - file -} diff --git a/identity_stronghold/Cargo.toml b/identity_stronghold/Cargo.toml new file mode 100644 index 0000000000..16b6f6082a --- /dev/null +++ b/identity_stronghold/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "identity_stronghold" +version = "0.7.0-alpha.7" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +keywords = ["iota", "storage", "identity", "kms", "stronghold"] +license.workspace = true +readme = "./README.md" +repository.workspace = true +rust-version.workspace = true +description = "Secure JWK storage with Stronghold for IOTA Identity" + +[dependencies] +async-trait = { version = "0.1.64", default-features = false } +identity_storage = { version = "=0.7.0-alpha.7", path = "../identity_storage", default_features = false } +identity_verification = { version = "=0.7.0-alpha.7", path = "../identity_verification", default_features = false } +iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"] } +iota-sdk = { version = "1.0.2", default-features = false, features = ["client", "stronghold"] } +iota_stronghold = { version = "2.0", default-features = false } +rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] } +tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync"] } +zeroize = { version = "1.6.0", default_features = false } + +[dev-dependencies] +identity_did = { version = "=0.7.0-alpha.7", path = "../identity_did", default_features = false } +tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync", "rt"] } + +[features] +default = [] +# Enables `Send` + `Sync` bounds for the trait implementations on `StrongholdStorage`. +send-sync-storage = ["identity_storage/send-sync-storage"] diff --git a/identity_stronghold/README.md b/identity_stronghold/README.md new file mode 100644 index 0000000000..8114745d46 --- /dev/null +++ b/identity_stronghold/README.md @@ -0,0 +1,4 @@ +IOTA Identity - Stronghold +=== + +This crate provides an implementation of `JwkStorage` and `KeyIdStorage` traits on top of `Stronghold`. \ No newline at end of file diff --git a/identity_stronghold/src/ed25519.rs b/identity_stronghold/src/ed25519.rs new file mode 100644 index 0000000000..13c3135bb0 --- /dev/null +++ b/identity_stronghold/src/ed25519.rs @@ -0,0 +1,58 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crypto::signatures::ed25519::SecretKey; +use identity_verification::jose::jwk::EdCurve; +use identity_verification::jose::jwk::Jwk; +use identity_verification::jose::jwk::JwkParamsOkp; +use identity_verification::jose::jwu; + +use identity_storage::key_storage::KeyStorageError; +use identity_storage::key_storage::KeyStorageErrorKind; +use identity_storage::key_storage::KeyStorageResult; + +pub(crate) fn expand_secret_jwk(jwk: &Jwk) -> KeyStorageResult { + let params: &JwkParamsOkp = jwk.try_okp_params().unwrap(); + + if params + .try_ed_curve() + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType).with_source(err))? + != EdCurve::Ed25519 + { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("expected an {} key", EdCurve::Ed25519.name())), + ); + } + + let sk: [u8; SecretKey::LENGTH] = params + .d + .as_deref() + .map(jwu::decode_b64) + .ok_or_else(|| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("expected Jwk `d` param to be present") + })? + .map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("unable to decode `d` param") + .with_source(err) + })? + .try_into() + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message(format!("expected key of length {}", SecretKey::LENGTH)) + })?; + + Ok(SecretKey::from_bytes(&sk)) +} + +#[cfg(test)] +pub(crate) fn encode_jwk(private_key: &SecretKey, public_key: &crypto::signatures::ed25519::PublicKey) -> Jwk { + let x = jwu::encode_b64(public_key.as_ref()); + let d = jwu::encode_b64(private_key.to_bytes().as_ref()); + let mut params = JwkParamsOkp::new(); + params.x = x; + params.d = Some(d); + params.crv = EdCurve::Ed25519.name().to_owned(); + Jwk::from_params(params) +} diff --git a/identity_stronghold/src/lib.rs b/identity_stronghold/src/lib.rs new file mode 100644 index 0000000000..a5cfc8b475 --- /dev/null +++ b/identity_stronghold/src/lib.rs @@ -0,0 +1,11 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod ed25519; +mod stronghold_jwk_storage; +mod stronghold_key_id; +#[cfg(test)] +mod tests; + +pub use stronghold_jwk_storage::*; +pub use stronghold_key_id::*; diff --git a/identity_storage/src/key_storage/stronghold.rs b/identity_stronghold/src/stronghold_jwk_storage.rs similarity index 88% rename from identity_storage/src/key_storage/stronghold.rs rename to identity_stronghold/src/stronghold_jwk_storage.rs index 7e56d3a13b..44f2d5117e 100644 --- a/identity_storage/src/key_storage/stronghold.rs +++ b/identity_stronghold/src/stronghold_jwk_storage.rs @@ -1,22 +1,24 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use super::ed25519; -use super::JwkGenOutput; -use super::KeyId; -use super::KeyStorageError; -use super::KeyStorageErrorKind; -use super::KeyStorageResult; -use super::KeyType; -use crate::key_storage::JwkStorage; -use crate::StrongholdStorage; +//! Wrapper around [`StrongholdSecretManager`](StrongholdSecretManager). + use async_trait::async_trait; +use identity_storage::key_storage::JwkStorage; +use identity_storage::JwkGenOutput; +use identity_storage::KeyId; +use identity_storage::KeyStorageError; +use identity_storage::KeyStorageErrorKind; +use identity_storage::KeyStorageResult; +use identity_storage::KeyType; use identity_verification::jwk::EdCurve; use identity_verification::jwk::Jwk; use identity_verification::jwk::JwkParamsOkp; use identity_verification::jwk::JwkType; use identity_verification::jws::JwsAlgorithm; use identity_verification::jwu; +use iota_sdk::client::secret::stronghold::StrongholdSecretManager; +use iota_sdk::client::secret::SecretManager; use iota_stronghold::procedures::Ed25519Sign; use iota_stronghold::procedures::GenerateKey; use iota_stronghold::procedures::KeyType as ProceduresKeyType; @@ -29,8 +31,11 @@ use rand::distributions::DistString; use std::fmt::Display; use std::ops::Deref; use std::str::FromStr; +use std::sync::Arc; use tokio::sync::MutexGuard; +use crate::ed25519; + const ED25519_KEY_TYPE_STR: &str = "Ed25519"; static IDENTITY_VAULT_PATH: &str = "iota_identity_vault"; pub(crate) static IDENTITY_CLIENT_PATH: &[u8] = b"iota_identity_client"; @@ -38,6 +43,31 @@ pub(crate) static IDENTITY_CLIENT_PATH: &[u8] = b"iota_identity_client"; /// The Ed25519 key type. pub const ED25519_KEY_TYPE: &KeyType = &KeyType::from_static_str(ED25519_KEY_TYPE_STR); +/// Wrapper around a [`StrongholdSecretManager`] that implements the [`KeyIdStorage`](crate::KeyIdStorage) +/// and [`JwkStorage`](crate::JwkStorage) interfaces. +#[derive(Clone, Debug)] +pub struct StrongholdStorage(Arc); + +impl StrongholdStorage { + /// Creates a new [`StrongholdStorage`]. + pub fn new(stronghold_secret_manager: StrongholdSecretManager) -> Self { + Self(Arc::new(SecretManager::Stronghold(stronghold_secret_manager))) + } + + /// Shared reference to the inner [`SecretManager`]. + pub fn as_secret_manager(&self) -> &SecretManager { + self.0.as_ref() + } + + /// Acquire lock of the inner [`Stronghold`]. + pub(crate) async fn get_stronghold(&self) -> MutexGuard<'_, Stronghold> { + match *self.0 { + SecretManager::Stronghold(ref stronghold) => stronghold.inner().await, + _ => unreachable!("secret manager can be only constrcuted from stronghold"), + } + } +} + #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] #[cfg_attr(feature = "send-sync-storage", async_trait)] impl JwkStorage for StrongholdStorage { @@ -93,7 +123,7 @@ impl JwkStorage for StrongholdStorage { jwk.set_alg(alg.name()); jwk.set_kid(jwk.thumbprint_sha256_b64()); - Ok(JwkGenOutput { key_id, jwk }) + Ok(JwkGenOutput::new(key_id, jwk)) } async fn insert(&self, jwk: Jwk) -> KeyStorageResult { @@ -231,6 +261,7 @@ impl JwkStorage for StrongholdStorage { } } +/// Generate a random alphanumeric string of len 32. fn random_key_id() -> KeyId { KeyId::new(rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 32)) } @@ -240,7 +271,7 @@ fn check_key_alg_compatibility(key_type: StrongholdKeyType, alg: JwsAlgorithm) - match (key_type, alg) { (StrongholdKeyType::Ed25519, JwsAlgorithm::EdDSA) => Ok(()), (key_type, alg) => Err( - KeyStorageError::new(crate::key_storage::KeyStorageErrorKind::KeyAlgorithmMismatch) + KeyStorageError::new(identity_storage::KeyStorageErrorKind::KeyAlgorithmMismatch) .with_custom_message(format!("cannot use key type `{key_type}` with algorithm `{alg}`")), ), } diff --git a/identity_storage/src/key_id_storage/stronghold.rs b/identity_stronghold/src/stronghold_key_id.rs similarity index 92% rename from identity_storage/src/key_id_storage/stronghold.rs rename to identity_stronghold/src/stronghold_key_id.rs index e676e0efbc..dbfeb5865f 100644 --- a/identity_storage/src/key_id_storage/stronghold.rs +++ b/identity_stronghold/src/stronghold_key_id.rs @@ -3,15 +3,15 @@ use std::ops::Deref; -use super::KeyIdStorageError; -use super::KeyIdStorageErrorKind; -use crate::key_id_storage::KeyIdStorage; -use crate::key_id_storage::KeyIdStorageResult; -use crate::key_id_storage::MethodDigest; -use crate::key_storage::KeyId; -use crate::key_storage::IDENTITY_CLIENT_PATH; +use crate::stronghold_jwk_storage::IDENTITY_CLIENT_PATH; use crate::StrongholdStorage; use async_trait::async_trait; +use identity_storage::key_id_storage::KeyIdStorage; +use identity_storage::key_id_storage::KeyIdStorageResult; +use identity_storage::key_id_storage::MethodDigest; +use identity_storage::key_storage::KeyId; +use identity_storage::KeyIdStorageError; +use identity_storage::KeyIdStorageErrorKind; use iota_stronghold::Client; use iota_stronghold::ClientError; use iota_stronghold::Stronghold; diff --git a/identity_storage/src/test_utils/mod.rs b/identity_stronghold/src/tests/mod.rs similarity index 52% rename from identity_storage/src/test_utils/mod.rs rename to identity_stronghold/src/tests/mod.rs index a512b6943c..96db03f0aa 100644 --- a/identity_storage/src/test_utils/mod.rs +++ b/identity_stronghold/src/tests/mod.rs @@ -1,5 +1,6 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -#[cfg(feature = "stronghold")] -pub(crate) mod stronghold_test_utils; +mod test_jwk_storage; +mod test_key_id_storage; +pub(crate) mod utils; diff --git a/identity_stronghold/src/tests/test_jwk_storage.rs b/identity_stronghold/src/tests/test_jwk_storage.rs new file mode 100644 index 0000000000..73f6a41a1e --- /dev/null +++ b/identity_stronghold/src/tests/test_jwk_storage.rs @@ -0,0 +1,204 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::path::PathBuf; + +use identity_verification::jwk::Jwk; +use identity_verification::jws::JwsAlgorithm; +use iota_sdk::client::Password; + +use super::utils::create_stronghold_secret_manager; +use super::utils::create_temp_file; +use crate::tests::utils::generate_ed25519; +use crate::StrongholdStorage; +use identity_storage::key_storage::JwkStorage; +use identity_storage::key_storage::KeyType; +use iota_sdk::client::secret::stronghold::StrongholdSecretManager; + +#[tokio::test] +async fn insert() { + let stronghold_secret_manager = create_stronghold_secret_manager(); + let stronghold_storage = StrongholdStorage::new(stronghold_secret_manager); + jwk_storage_tests::test_insertion(stronghold_storage).await; +} + +#[tokio::test] +async fn incompatible_key_alg() { + let stronghold_secret_manager = create_stronghold_secret_manager(); + let stronghold_storage = StrongholdStorage::new(stronghold_secret_manager); + jwk_storage_tests::test_incompatible_key_alg(stronghold_storage).await; +} + +#[tokio::test] +async fn incompatible_key_types() { + let stronghold_secret_manager = create_stronghold_secret_manager(); + let stronghold_storage = StrongholdStorage::new(stronghold_secret_manager); + jwk_storage_tests::test_incompatible_key_type(stronghold_storage).await; +} + +#[tokio::test] +async fn generate_and_sign() { + let stronghold_secret_manager = create_stronghold_secret_manager(); + let stronghold_storage = StrongholdStorage::new(stronghold_secret_manager); + jwk_storage_tests::test_generate_and_sign(stronghold_storage).await; +} + +#[tokio::test] +async fn key_exists() { + let stronghold_secret_manager = create_stronghold_secret_manager(); + let stronghold_storage = StrongholdStorage::new(stronghold_secret_manager); + jwk_storage_tests::test_key_exists(stronghold_storage).await; +} + +// Tests the cases that require persisting to disk, generate, insert and delete. +#[tokio::test] +async fn write_to_disk() { + const PASS: &str = "secure_password"; + let file: PathBuf = create_temp_file(); + let secret_manager = StrongholdSecretManager::builder() + .password(Password::from(PASS.to_owned())) + .build(file.clone()) + .unwrap(); + let stronghold_storage = StrongholdStorage::new(secret_manager); + + let generate = stronghold_storage + .generate(KeyType::new("Ed25519"), JwsAlgorithm::EdDSA) + .await + .unwrap(); + let key_id = &generate.key_id; + + drop(stronghold_storage); + + let secret_manager = StrongholdSecretManager::builder() + .password(Password::from(PASS.to_owned())) + .build(&file) + .unwrap(); + let stronghold_storage = StrongholdStorage::new(secret_manager); + let exists = stronghold_storage.exists(key_id).await.unwrap(); + assert!(exists); + stronghold_storage.delete(key_id).await.unwrap(); + + drop(stronghold_storage); + + let secret_manager = StrongholdSecretManager::builder() + .password(Password::from(PASS.to_owned())) + .build(&file) + .unwrap(); + let stronghold_storage = StrongholdStorage::new(secret_manager); + let exists = stronghold_storage.exists(key_id).await.unwrap(); + assert!(!exists); + + let (private_key, public_key) = generate_ed25519(); + let mut jwk: Jwk = crate::ed25519::encode_jwk(&private_key, &public_key); + jwk.set_alg(JwsAlgorithm::EdDSA.name()); + let key_id = stronghold_storage.insert(jwk).await.unwrap(); + + drop(stronghold_storage); + + let secret_manager = StrongholdSecretManager::builder() + .password(Password::from(PASS.to_owned())) + .build(&file) + .unwrap(); + let stronghold_storage = StrongholdStorage::new(secret_manager); + let exists = stronghold_storage.exists(&key_id).await.unwrap(); + assert!(exists); +} + +mod jwk_storage_tests { + + use crypto::signatures::ed25519::PublicKey; + use crypto::signatures::ed25519::SecretKey; + use crypto::signatures::ed25519::Signature; + use identity_storage::key_storage::JwkStorage; + use identity_storage::key_storage::KeyId; + use identity_storage::key_storage::KeyStorageErrorKind; + use identity_storage::key_storage::KeyType; + use identity_verification::jose::jwk::EdCurve; + use identity_verification::jose::jwk::Jwk; + use identity_verification::jose::jwk::JwkParamsOkp; + use identity_verification::jose::jwu; + use identity_verification::jwk::EcCurve; + use identity_verification::jwk::JwkParamsEc; + use identity_verification::jws::JwsAlgorithm; + + use crate::ed25519; + + pub(crate) async fn test_insertion(store: impl JwkStorage) { + let (private_key, public_key) = generate_ed25519(); + let mut jwk: Jwk = ed25519::encode_jwk(&private_key, &public_key); + + // INVALID: Inserting a Jwk without an `alg` parameter should fail. + let err = store.insert(jwk.clone()).await.unwrap_err(); + assert!(matches!(err.kind(), KeyStorageErrorKind::UnsupportedSignatureAlgorithm)); + + // VALID: Inserting a Jwk with all private key components set should succeed. + jwk.set_alg(JwsAlgorithm::EdDSA.name()); + store.insert(jwk.clone()).await.unwrap(); + + // INVALID: Inserting a Jwk with all private key components unset should fail. + let err = store.insert(jwk.to_public().unwrap()).await.unwrap_err(); + assert!(matches!(err.kind(), KeyStorageErrorKind::Unspecified)) + } + + pub(crate) async fn test_incompatible_key_alg(store: impl JwkStorage) { + let (private_key, public_key) = generate_ed25519(); + let mut jwk: Jwk = ed25519::encode_jwk(&private_key, &public_key); + jwk.set_alg(JwsAlgorithm::ES256.name()); + + // INVALID: Inserting an Ed25519 key with the ES256 alg is not compatible. + let err = store.insert(jwk.clone()).await.unwrap_err(); + assert!(matches!(err.kind(), KeyStorageErrorKind::KeyAlgorithmMismatch)); + } + + pub(crate) async fn test_incompatible_key_type(store: impl JwkStorage) { + let mut ec_params = JwkParamsEc::new(); + ec_params.crv = EcCurve::P256.name().to_owned(); + ec_params.x = "".to_owned(); + ec_params.y = "".to_owned(); + ec_params.d = Some("".to_owned()); + let jwk_ec = Jwk::from_params(ec_params); + + let err = store.insert(jwk_ec).await.unwrap_err(); + assert!(matches!(err.kind(), KeyStorageErrorKind::UnsupportedKeyType)); + } + pub(crate) async fn test_generate_and_sign(store: impl JwkStorage) { + let test_msg: &[u8] = b"test"; + + let generate = store + .generate(KeyType::new("Ed25519"), JwsAlgorithm::EdDSA) + .await + .unwrap(); + + let signature = store.sign(&generate.key_id, test_msg, &generate.jwk).await.unwrap(); + + let signature: Signature = Signature::from_bytes(signature.try_into().unwrap()); + let public_key: PublicKey = expand_public_jwk(&generate.jwk); + assert!(public_key.verify(&signature, test_msg)); + + let key_id: KeyId = generate.key_id; + assert!(store.exists(&key_id).await.unwrap()); + store.delete(&key_id).await.unwrap(); + } + + pub(crate) async fn test_key_exists(store: impl JwkStorage) { + assert!(!store.exists(&KeyId::new("non-existent-id")).await.unwrap()); + } + + pub(crate) fn expand_public_jwk(jwk: &Jwk) -> PublicKey { + let params: &JwkParamsOkp = jwk.try_okp_params().unwrap(); + + if params.try_ed_curve().unwrap() != EdCurve::Ed25519 { + panic!("expected an ed25519 jwk"); + } + + let pk: [u8; PublicKey::LENGTH] = jwu::decode_b64(params.x.as_str()).unwrap().try_into().unwrap(); + + PublicKey::try_from(pk).unwrap() + } + + pub(crate) fn generate_ed25519() -> (SecretKey, PublicKey) { + let private_key = SecretKey::generate().unwrap(); + let public_key = private_key.public_key(); + (private_key, public_key) + } +} diff --git a/identity_stronghold/src/tests/test_key_id_storage.rs b/identity_stronghold/src/tests/test_key_id_storage.rs new file mode 100644 index 0000000000..7a6c70c386 --- /dev/null +++ b/identity_stronghold/src/tests/test_key_id_storage.rs @@ -0,0 +1,126 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use super::utils::create_temp_file; +use super::utils::create_verification_method; +use crate::StrongholdStorage; +use identity_storage::key_id_storage::KeyIdStorageErrorKind; +use identity_storage::key_id_storage::MethodDigest; +use identity_storage::key_storage::KeyId; +use identity_storage::KeyIdStorage; +use iota_sdk::client::secret::stronghold::StrongholdSecretManager; +use iota_sdk::client::Password; +use std::path::PathBuf; + +const PASS: &str = "secure_password"; + +#[tokio::test] +async fn test_stronghold() { + iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); + let file: PathBuf = create_temp_file(); + let secret_manager = StrongholdSecretManager::builder() + .password(Password::from(PASS.to_owned())) + .build(&file) + .unwrap(); + let stronghold_storage = StrongholdStorage::new(secret_manager); + key_id_tests::test_storage_operations(stronghold_storage).await; +} + +#[tokio::test] +async fn write_to_disk() { + iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); + let file: PathBuf = create_temp_file(); + let secret_manager = StrongholdSecretManager::builder() + .password(Password::from(PASS.to_owned())) + .build(&file) + .unwrap(); + + let verification_method = create_verification_method(); + + let key_id_1 = KeyId::new("keyid"); + let method_digest: MethodDigest = MethodDigest::new(&verification_method).unwrap(); + let stronghold_storage = StrongholdStorage::new(secret_manager); + stronghold_storage + .insert_key_id(method_digest.clone(), key_id_1.clone()) + .await + .expect("inserting into memstore failed"); + + drop(stronghold_storage); + + let secret_manager = StrongholdSecretManager::builder() + .password(Password::from(PASS.to_owned())) + .build(&file) + .unwrap(); + let stronghold_storage = StrongholdStorage::new(secret_manager); + + let key_id: KeyId = stronghold_storage.get_key_id(&method_digest).await.unwrap(); + assert_eq!(key_id_1, key_id); + + stronghold_storage + .delete_key_id(&method_digest) + .await + .expect("deletion failed"); + + drop(stronghold_storage); + + let secret_manager = StrongholdSecretManager::builder() + .password(Password::from(PASS.to_owned())) + .build(&file) + .unwrap(); + let stronghold_storage = StrongholdStorage::new(secret_manager); + let error_kind: KeyIdStorageErrorKind = stronghold_storage + .get_key_id(&method_digest) + .await + .unwrap_err() + .kind() + .clone(); + + assert!(matches!(error_kind, KeyIdStorageErrorKind::KeyIdNotFound)); +} + +mod key_id_tests { + // Copy of test in identity_storage. + + use identity_storage::key_id_storage::KeyIdStorage; + use identity_storage::key_id_storage::KeyIdStorageError; + use identity_storage::key_id_storage::KeyIdStorageErrorKind; + use identity_storage::key_id_storage::KeyIdStorageResult; + use identity_storage::key_id_storage::MethodDigest; + use identity_storage::key_storage::KeyId; + + use crate::tests::utils::create_verification_method; + + pub(crate) async fn test_storage_operations(storage: impl KeyIdStorage) { + // Create a Verification Method. + let verification_method = create_verification_method(); + + // Test insertion. + let key_id_1 = KeyId::new("keyid"); + let method_digest: MethodDigest = MethodDigest::new(&verification_method).unwrap(); + storage + .insert_key_id(method_digest.clone(), key_id_1.clone()) + .await + .expect("inserting into memstore failed"); + + // Double insertion. + let insertion_result = storage.insert_key_id(method_digest.clone(), key_id_1.clone()).await; + let _expected_error: KeyIdStorageError = KeyIdStorageError::new(KeyIdStorageErrorKind::KeyIdAlreadyExists); + assert!(matches!(insertion_result.unwrap_err(), _expected_error)); + + // Test retrieving. + let key_id: KeyId = storage.get_key_id(&method_digest).await.unwrap(); + assert_eq!(key_id_1, key_id); + + // Test deletion. + storage.delete_key_id(&method_digest).await.expect("deletion failed"); + + let retrieval_result: KeyIdStorageResult = storage.get_key_id(&method_digest).await; + let _expected_error: KeyIdStorageError = KeyIdStorageError::new(KeyIdStorageErrorKind::KeyIdNotFound); + assert!(matches!(retrieval_result.unwrap_err(), _expected_error)); + + // Double Deletion. + let repeat_deletion_result: KeyIdStorageResult<()> = storage.delete_key_id(&method_digest).await; + let _expected_error: KeyIdStorageError = KeyIdStorageError::new(KeyIdStorageErrorKind::KeyIdNotFound); + assert!(matches!(repeat_deletion_result.unwrap_err(), _expected_error)); + } +} diff --git a/identity_stronghold/src/tests/utils.rs b/identity_stronghold/src/tests/utils.rs new file mode 100644 index 0000000000..9fec954f0f --- /dev/null +++ b/identity_stronghold/src/tests/utils.rs @@ -0,0 +1,62 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crypto::signatures::ed25519::PublicKey; +use crypto::signatures::ed25519::SecretKey; +use identity_did::CoreDID; +use identity_verification::jwk::EdCurve; +use identity_verification::jwk::Jwk; +use identity_verification::jwk::JwkParamsOkp; +use identity_verification::jws::JwsAlgorithm; +use identity_verification::jwu; +use identity_verification::VerificationMethod; +use iota_sdk::client::secret::stronghold::StrongholdSecretManager; +use iota_sdk::client::Password; +use rand::distributions::DistString; +use std::path::PathBuf; + +pub(crate) fn create_verification_method() -> VerificationMethod { + let secret: SecretKey = SecretKey::generate().unwrap(); + let public: PublicKey = secret.public_key(); + let jwk: Jwk = encode_public_ed25519_jwk(&public); + let did: CoreDID = CoreDID::parse(format!("did:example:{}", jwk.thumbprint_sha256_b64())).unwrap(); + VerificationMethod::new_from_jwk(did, jwk, Some("#frag")).unwrap() +} + +pub(crate) fn encode_public_ed25519_jwk(public_key: &PublicKey) -> Jwk { + let x = jwu::encode_b64(public_key.as_ref()); + let mut params = JwkParamsOkp::new(); + params.x = x; + params.d = None; + params.crv = EdCurve::Ed25519.name().to_owned(); + let mut jwk = Jwk::from_params(params); + jwk.set_alg(JwsAlgorithm::EdDSA.name()); + jwk +} + +pub(crate) fn generate_ed25519() -> (SecretKey, PublicKey) { + let private_key = SecretKey::generate().unwrap(); + let public_key = private_key.public_key(); + (private_key, public_key) +} + +pub(crate) fn create_stronghold_secret_manager() -> StrongholdSecretManager { + iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); + let mut file = std::env::temp_dir(); + file.push("test_strongholds"); + file.push(rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 32)); + file.set_extension("stronghold"); + + StrongholdSecretManager::builder() + .password(Password::from("secure_password".to_owned())) + .build(&file) + .unwrap() +} + +pub(crate) fn create_temp_file() -> PathBuf { + let mut file = std::env::temp_dir(); + file.push("test_strongholds"); + file.push(rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 32)); + file.set_extension("stronghold"); + file +}