diff --git a/Cargo.toml b/Cargo.toml index c7ac5ad4..32958c24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,17 @@ [workspace] -resolver = "2" members = [ - "crates/*" -] \ No newline at end of file + "bindings/*", + "crates/*", +] +default-members = [ + "crates/*", +] +resolver = "2" + +[workspace.package] +homepage = "https://github.com/TBD54566975/web5-rs" +repository = "https://github.com/TBD54566975/web5-rs.git" +license-file = "LICENSE" + +[workspace.dependencies] +thiserror = "1.0.50" \ No newline at end of file diff --git a/bindings/README.md b/bindings/README.md new file mode 100644 index 00000000..0b3bf255 --- /dev/null +++ b/bindings/README.md @@ -0,0 +1,3 @@ +# Bindings + +This directory contains crates that generate bindings for various languages. \ No newline at end of file diff --git a/crates/crypto/Cargo.toml b/crates/crypto/Cargo.toml new file mode 100644 index 00000000..b0a8cfe7 --- /dev/null +++ b/crates/crypto/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "crypto" +version = "0.1.0" +edition = "2021" +homepage.workspace = true +repository.workspace = true +license-file.workspace = true + +[dependencies] +ssi-jwk = "0.1.2" +ssi-jws = "0.1.1" +thiserror = { workspace = true } \ No newline at end of file diff --git a/crates/crypto/README.md b/crates/crypto/README.md new file mode 100644 index 00000000..72e64fd0 --- /dev/null +++ b/crates/crypto/README.md @@ -0,0 +1,26 @@ +# Crypto + +`Crypto` is a library for cryptographic primitives in Rust, essential for Web5. + +This crate should _not_ include any binding specific code, and should be usable within any Rust application, conforming +to the Rust API guidelines. All binding related code should be placed in the `bindings` folder at the root of the workspace. + +## Build + +This crate is set to build with the workspace by default. + +To build this crate only, run: + +```bash +cargo build -p crypto +``` + +## Test + +This crate is set to test with the workspace by default. + +To test this crate only, run: + +```bash +cargo test -p crypto +``` \ No newline at end of file diff --git a/crates/crypto/src/key/key.rs b/crates/crypto/src/key/key.rs new file mode 100644 index 00000000..66ada448 --- /dev/null +++ b/crates/crypto/src/key/key.rs @@ -0,0 +1,22 @@ +use ssi_jwk::JWK; +use ssi_jws::Error as JWSError; + +/// Enum defining all supported cryptographic key types. +pub enum KeyType { + Secp256k1, + Secp256r1, + Ed25519, +} + +#[derive(thiserror::Error, Debug)] +pub enum KeyError { + #[error(transparent)] + JWSError(#[from] JWSError), + #[error("Algorithm not found on JWK")] + AlgorithmNotFound, +} + +/// Trait defining all common behavior for cryptographic keys. +pub trait Key { + fn jwk(&self) -> &JWK; +} diff --git a/crates/crypto/src/key/mod.rs b/crates/crypto/src/key/mod.rs new file mode 100644 index 00000000..50914220 --- /dev/null +++ b/crates/crypto/src/key/mod.rs @@ -0,0 +1,8 @@ +mod key; +pub use key::*; + +mod private_key; +pub use private_key::*; + +mod public_key; +pub use public_key::*; diff --git a/crates/crypto/src/key/private_key.rs b/crates/crypto/src/key/private_key.rs new file mode 100644 index 00000000..8bd6da7c --- /dev/null +++ b/crates/crypto/src/key/private_key.rs @@ -0,0 +1,59 @@ +use crate::key::{Key, KeyError, PublicKey}; +use ssi_jwk::JWK; +use ssi_jws::sign_bytes; + +#[derive(Clone, PartialEq, Debug)] +pub struct PrivateKey(pub(crate) JWK); + +impl PrivateKey { + /// Derive a [`PublicKey`] from the target [`PrivateKey`]. + pub fn to_public(&self) -> PublicKey { + PublicKey(self.0.to_public()) + } + + /// Sign a payload using the target [`PrivateKey`]. + pub fn sign(&self, payload: &[u8]) -> Result, KeyError> { + let algorithm = self.0.get_algorithm().ok_or(KeyError::AlgorithmNotFound)?; + let signed_bytes = sign_bytes(algorithm, &payload, &self.0)?; + + Ok(signed_bytes) + } +} + +impl Key for PrivateKey { + fn jwk(&self) -> &JWK { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ssi_jwk::JWK; + + fn new_private_key() -> PrivateKey { + PrivateKey(JWK::generate_secp256k1().unwrap()) + } + + #[test] + fn test_to_public() { + let private_key = new_private_key(); + let public_key = private_key.to_public(); + + assert_eq!( + private_key.jwk().thumbprint().unwrap(), + public_key.jwk().thumbprint().unwrap() + ); + + assert_ne!(private_key.jwk(), public_key.jwk()) + } + + #[test] + fn test_sign() { + let private_key = new_private_key(); + let payload: &[u8] = b"hello world"; + let signature = private_key.sign(payload).unwrap(); + + assert_ne!(payload, &signature) + } +} diff --git a/crates/crypto/src/key/public_key.rs b/crates/crypto/src/key/public_key.rs new file mode 100644 index 00000000..09b3475b --- /dev/null +++ b/crates/crypto/src/key/public_key.rs @@ -0,0 +1,57 @@ +use crate::key::{Key, KeyError}; +use ssi_jwk::JWK; +use ssi_jws::{verify_bytes_warnable, VerificationWarnings}; + +#[derive(PartialEq, Debug)] +pub struct PublicKey(pub(crate) JWK); + +impl PublicKey { + /// Verifies a payload with a given signature using the target [`PublicKey`]. + pub fn verify( + &self, + payload: &[u8], + signature: &[u8], + ) -> Result { + let algorithm = self.0.get_algorithm().ok_or(KeyError::AlgorithmNotFound)?; + + let verification_warnings = + verify_bytes_warnable(algorithm, &payload, &self.0, &signature)?; + + Ok(verification_warnings) + } +} + +impl Key for PublicKey { + fn jwk(&self) -> &JWK { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::key::PrivateKey; + + #[test] + fn test_verify() { + let private_key = PrivateKey(JWK::generate_secp256k1().unwrap()); + let payload: &[u8] = b"hello world"; + let signature = private_key.sign(payload).unwrap(); + + let public_key = private_key.to_public(); + let verification_warnings = public_key.verify(&payload, &signature).unwrap(); + assert_eq!(verification_warnings.len(), 0); + } + + #[test] + fn test_verify_failure() { + let private_key = PrivateKey(JWK::generate_secp256k1().unwrap()); + let payload: &[u8] = b"hello world"; + let signature = private_key.sign(payload).unwrap(); + + // public_key is unrelated to the private_key used to sign the payload, so it should fail + let public_key = PublicKey(JWK::generate_secp256k1().unwrap()); + let verification_warnings = public_key.verify(&payload, &signature); + assert!(verification_warnings.is_err()); + } +} diff --git a/crates/crypto/src/key_manager/key_manager.rs b/crates/crypto/src/key_manager/key_manager.rs new file mode 100644 index 00000000..b4ba690d --- /dev/null +++ b/crates/crypto/src/key_manager/key_manager.rs @@ -0,0 +1,37 @@ +use crate::key::{KeyError, KeyType, PublicKey}; +use crate::key_manager::key_store::KeyStoreError; +use ssi_jwk::Error as JWKError; + +#[derive(thiserror::Error, Debug)] +pub enum KeyManagerError { + #[error("Signing key not found in KeyManager")] + SigningKeyNotFound, + #[error(transparent)] + JWKError(#[from] JWKError), + #[error(transparent)] + KeyError(#[from] KeyError), + #[error(transparent)] + KeyStoreError(#[from] KeyStoreError), +} + +/// A key management trait for generating, storing, and utilizing keys private keys and their +/// associated public keys. +/// +/// Implementations of this trait might provide key management through various Key Management +/// Systems (KMS), such as AWS KMS, Google Cloud KMD, Hardware Security Modules (HSM), or simple +/// in-memory storage, each adhering to the same consistent API for usage within applications. +pub trait KeyManager: Send + Sync { + /// Generates and securely stores a private key based on the provided `key_type`, + /// returning a unique alias that can be utilized to reference the generated key for future + /// operations. + fn generate_private_key(&self, key_type: KeyType) -> Result; + + /// Returns the public key associated with the provided `key_alias`, if one exists. + fn get_public_key(&self, key_alias: &str) -> Result, KeyManagerError>; + + /// Signs the provided payload using the private key identified by the provided `key_alias`. + fn sign(&self, key_alias: &str, payload: &[u8]) -> Result, KeyManagerError>; + + /// Returns the key alias of a public key, as was originally returned by `generate_private_key`. + fn alias(&self, public_key: &PublicKey) -> Result; +} diff --git a/crates/crypto/src/key_manager/key_store/in_memory_key_store.rs b/crates/crypto/src/key_manager/key_store/in_memory_key_store.rs new file mode 100644 index 00000000..1808548f --- /dev/null +++ b/crates/crypto/src/key_manager/key_store/in_memory_key_store.rs @@ -0,0 +1,71 @@ +use crate::key::PrivateKey; +use crate::key_manager::key_store::{KeyStore, KeyStoreError}; +use std::collections::HashMap; +use std::sync::RwLock; + +/// An in-memory implementation of the [`KeyStore`] trait. +pub struct InMemoryKeyStore { + map: RwLock>, +} + +impl InMemoryKeyStore { + pub fn new() -> Self { + let map = RwLock::new(HashMap::new()); + Self { map } + } +} + +impl KeyStore for InMemoryKeyStore { + fn get(&self, key_alias: &str) -> Result, KeyStoreError> { + let map_lock = self.map.read().map_err(|e| { + KeyStoreError::InternalKeyStoreError(format!("Unable to acquire Mutex lock: {}", e)) + })?; + + if let Some(private_key) = map_lock.get(key_alias) { + Ok(Some(private_key.clone())) + } else { + Ok(None) + } + } + + fn insert(&self, key_alias: &str, private_key: PrivateKey) -> Result<(), KeyStoreError> { + let mut map_lock = self.map.write().map_err(|e| { + KeyStoreError::InternalKeyStoreError(format!("Unable to acquire Mutex lock: {}", e)) + })?; + + map_lock.insert(key_alias.to_string(), private_key); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::key::PrivateKey; + use ssi_jwk::JWK; + + fn new_private_key() -> PrivateKey { + PrivateKey(JWK::generate_secp256k1().unwrap()) + } + + #[test] + fn test_insert_get() { + let key_alias = "key-alias"; + let private_key = new_private_key(); + + let key_store = InMemoryKeyStore::new(); + key_store.insert(key_alias, private_key.clone()).unwrap(); + + let retrieved_private_key = key_store.get(key_alias).unwrap().unwrap(); + assert_eq!(private_key, retrieved_private_key); + } + + #[test] + fn test_get_missing() { + let key_alias = "key-alias"; + + let key_store = InMemoryKeyStore::new(); + let retrieved_private_key = key_store.get(key_alias).unwrap(); + assert!(retrieved_private_key.is_none()); + } +} diff --git a/crates/crypto/src/key_manager/key_store/key_store.rs b/crates/crypto/src/key_manager/key_store/key_store.rs new file mode 100644 index 00000000..e1bc23e7 --- /dev/null +++ b/crates/crypto/src/key_manager/key_store/key_store.rs @@ -0,0 +1,15 @@ +use crate::key::PrivateKey; + +#[derive(thiserror::Error, Debug)] +pub enum KeyStoreError { + #[error("{0}")] + InternalKeyStoreError(String), +} + +// Trait for storing and retrieving private keys. +// +// Implementations of this trait should be thread-safe and allow for concurrent access. +pub trait KeyStore: Send + Sync { + fn get(&self, key_alias: &str) -> Result, KeyStoreError>; + fn insert(&self, key_alias: &str, private_key: PrivateKey) -> Result<(), KeyStoreError>; +} diff --git a/crates/crypto/src/key_manager/key_store/mod.rs b/crates/crypto/src/key_manager/key_store/mod.rs new file mode 100644 index 00000000..b2a9af41 --- /dev/null +++ b/crates/crypto/src/key_manager/key_store/mod.rs @@ -0,0 +1,5 @@ +mod key_store; +pub use key_store::*; + +mod in_memory_key_store; +pub use in_memory_key_store::*; diff --git a/crates/crypto/src/key_manager/local_key_manager.rs b/crates/crypto/src/key_manager/local_key_manager.rs new file mode 100644 index 00000000..d814884f --- /dev/null +++ b/crates/crypto/src/key_manager/local_key_manager.rs @@ -0,0 +1,127 @@ +use crate::key::{KeyType, PrivateKey, PublicKey}; +use crate::key_manager::key_store::{InMemoryKeyStore, KeyStore}; +use crate::key_manager::{KeyManager, KeyManagerError}; +use ssi_jwk::JWK; +use std::sync::Arc; + +/// Implementation of the [`KeyManager`] trait with key generation local to the device/platform it +/// is being run. Key storage is provided by a [`KeyStore`] trait implementation, allowing the keys +/// to be stored wherever is most appropriate for the application. +pub struct LocalKeyManager { + key_store: Arc, +} + +impl LocalKeyManager { + /// Constructs a new `LocalKeyManager` that stores keys in the provided `KeyStore`. + pub fn new(key_store: Arc) -> Self { + Self { key_store } + } + + /// Constructs a new `LocalKeyManager` that stores keys in memory. + pub fn new_in_memory() -> Self { + Self { + key_store: Arc::new(InMemoryKeyStore::new()), + } + } +} + +impl KeyManager for LocalKeyManager { + fn generate_private_key(&self, key_type: KeyType) -> Result { + let jwk = match key_type { + KeyType::Secp256k1 => JWK::generate_secp256k1(), + KeyType::Secp256r1 => JWK::generate_p256(), + KeyType::Ed25519 => JWK::generate_ed25519(), + }?; + + let private_key = PrivateKey(jwk); + let public_key = private_key.to_public(); + let key_alias = self.alias(&public_key)?; + + self.key_store.insert(&key_alias, private_key)?; + + Ok(key_alias) + } + + fn get_public_key(&self, key_alias: &str) -> Result, KeyManagerError> { + if let Some(private_key) = self.key_store.get(key_alias)? { + Ok(Some(private_key.to_public())) + } else { + Ok(None) + } + } + + fn sign(&self, key_alias: &str, payload: &[u8]) -> Result, KeyManagerError> { + let private_key = self + .key_store + .get(key_alias)? + .ok_or(KeyManagerError::SigningKeyNotFound)?; + + let signed_payload = private_key.sign(&payload.to_vec())?; + + Ok(signed_payload) + } + + fn alias(&self, public_key: &PublicKey) -> Result { + Ok(public_key.0.thumbprint()?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_private_key() { + let key_manager = LocalKeyManager::new_in_memory(); + + key_manager + .generate_private_key(KeyType::Ed25519) + .expect("Failed to generate Ed25519 key"); + + key_manager + .generate_private_key(KeyType::Secp256k1) + .expect("Failed to generate secp256k1 key"); + + key_manager + .generate_private_key(KeyType::Secp256r1) + .expect("Failed to generate secp256r1 key"); + } + + #[test] + fn test_get_public_key() { + let key_manager = LocalKeyManager::new_in_memory(); + + let key_alias = key_manager.generate_private_key(KeyType::Ed25519).unwrap(); + + key_manager + .get_public_key(&key_alias) + .unwrap() + .expect("Public key not found"); + } + + #[test] + fn test_sign() { + let key_manager = LocalKeyManager::new_in_memory(); + let key_alias = key_manager.generate_private_key(KeyType::Ed25519).unwrap(); + + // Sign a payload + let payload: &[u8] = b"hello world"; + let signature = key_manager.sign(&key_alias, payload).unwrap(); + + // Get the public key that was used to sign the payload, and verify with it. + let public_key = key_manager.get_public_key(&key_alias).unwrap().unwrap(); + let verification_result = public_key.verify(payload, &signature).unwrap(); + assert_eq!(verification_result.len(), 0); + } + + #[test] + fn test_alias() { + let key_manager = LocalKeyManager::new_in_memory(); + let key_alias = key_manager.generate_private_key(KeyType::Ed25519).unwrap(); + + let public_key = key_manager.get_public_key(&key_alias).unwrap().unwrap(); + let alias = key_manager.alias(&public_key).unwrap(); + + assert_eq!(key_alias, alias); + } +} diff --git a/crates/crypto/src/key_manager/mod.rs b/crates/crypto/src/key_manager/mod.rs new file mode 100644 index 00000000..7b28f2f9 --- /dev/null +++ b/crates/crypto/src/key_manager/mod.rs @@ -0,0 +1,7 @@ +mod key_manager; +pub use key_manager::*; + +mod local_key_manager; +pub use local_key_manager::*; + +pub mod key_store; diff --git a/crates/crypto/src/lib.rs b/crates/crypto/src/lib.rs new file mode 100644 index 00000000..395500a9 --- /dev/null +++ b/crates/crypto/src/lib.rs @@ -0,0 +1,2 @@ +pub mod key; +pub mod key_manager;