From 642227eeaf7d823ce43a8cba5c1f4b4868649094 Mon Sep 17 00:00:00 2001 From: Liam Monninger Date: Tue, 17 Dec 2024 16:50:43 +0100 Subject: [PATCH] fix: google and amazon. --- Cargo.lock | 183 ++++++++++++++++++-- Cargo.toml | 4 + demo/hsm/Cargo.toml | 6 + demo/hsm/src/action_stream/join.rs | 41 +++++ demo/hsm/src/action_stream/mod.rs | 2 + demo/hsm/src/action_stream/notify_verify.rs | 28 +++ demo/hsm/src/action_stream/random.rs | 12 +- demo/hsm/src/hsm/aws_kms.rs | 99 +++++++++++ demo/hsm/src/hsm/google_kms.rs | 138 +++++++++++++++ demo/hsm/src/hsm/hashi_corp_vault.rs | 106 +++++++++--- demo/hsm/src/hsm/mod.rs | 2 + demo/hsm/src/lib.rs | 45 +++-- demo/hsm/src/main.rs | 18 +- payload.json | 5 + 14 files changed, 631 insertions(+), 58 deletions(-) create mode 100644 demo/hsm/src/action_stream/join.rs create mode 100644 demo/hsm/src/action_stream/notify_verify.rs create mode 100644 demo/hsm/src/hsm/aws_kms.rs create mode 100644 demo/hsm/src/hsm/google_kms.rs create mode 100644 payload.json diff --git a/Cargo.lock b/Cargo.lock index d47dca924..b3b1c12e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3192,7 +3192,7 @@ dependencies = [ "aws-sdk-sts", "aws-smithy-async", "aws-smithy-http", - "aws-smithy-json", + "aws-smithy-json 0.60.7", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -3223,9 +3223,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.4.3" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a10d5c055aa540164d9561a0e2e74ad30f0dcf7393c3a92f6733ddf9c5762468" +checksum = "b5ac934720fbb46206292d2c75b57e67acfc56fe7dfd34fb9a02334af08409ea" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -3247,6 +3247,28 @@ dependencies = [ "uuid", ] +[[package]] +name = "aws-sdk-kms" +version = "1.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c30f6fd5646b99d9b45ec3a0c22e67112c175b2383100c960d7ee39d96c8d96" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json 0.61.1", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes 1.8.0", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + [[package]] name = "aws-sdk-s3" version = "1.61.0" @@ -3260,7 +3282,7 @@ dependencies = [ "aws-smithy-checksums", "aws-smithy-eventstream", "aws-smithy-http", - "aws-smithy-json", + "aws-smithy-json 0.60.7", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -3291,7 +3313,7 @@ dependencies = [ "aws-runtime", "aws-smithy-async", "aws-smithy-http", - "aws-smithy-json", + "aws-smithy-json 0.60.7", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -3313,7 +3335,7 @@ dependencies = [ "aws-runtime", "aws-smithy-async", "aws-smithy-http", - "aws-smithy-json", + "aws-smithy-json 0.60.7", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -3335,7 +3357,7 @@ dependencies = [ "aws-runtime", "aws-smithy-async", "aws-smithy-http", - "aws-smithy-json", + "aws-smithy-json 0.60.7", "aws-smithy-query", "aws-smithy-runtime", "aws-smithy-runtime-api", @@ -3350,9 +3372,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.2.5" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5619742a0d8f253be760bfbb8e8e8368c69e3587e4637af5754e488a611499b1" +checksum = "7d3820e0c08d0737872ff3c7c1f21ebbb6693d832312d6152bf18ef50a5471c2" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -3450,6 +3472,15 @@ dependencies = [ "aws-smithy-types", ] +[[package]] +name = "aws-smithy-json" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4e69cc50921eb913c6b662f8d909131bb3e6ad6cb6090d3a39b66fc5c52095" +dependencies = [ + "aws-smithy-types", +] + [[package]] name = "aws-smithy-query" version = "0.60.7" @@ -3462,9 +3493,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.7.3" +version = "1.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be28bd063fa91fd871d131fc8b68d7cd4c5fa0869bea68daca50dcb1cbd76be2" +checksum = "9f20685047ca9d6f17b994a07f629c813f08b5bce65523e47124879e60103d45" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -6952,7 +6983,7 @@ checksum = "931bedb2264cb00f914b0a6a5c304e34865c34306632d3932e0951a073e4a67d" dependencies = [ "async-trait", "base64 0.21.7", - "google-cloud-metadata", + "google-cloud-metadata 0.3.2", "google-cloud-token", "home", "jsonwebtoken 8.3.0", @@ -6966,6 +6997,28 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "google-cloud-auth" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57a13fbacc5e9c41ded3ad8d0373175a6b7a6ad430d99e89d314ac121b7ab06" +dependencies = [ + "async-trait", + "base64 0.21.7", + "google-cloud-metadata 0.5.0", + "google-cloud-token", + "home", + "jsonwebtoken 9.3.0", + "reqwest 0.12.9", + "serde", + "serde_json", + "thiserror 1.0.69", + "time", + "tokio", + "tracing", + "urlencoding", +] + [[package]] name = "google-cloud-gax" version = "0.15.0" @@ -6982,6 +7035,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "google-cloud-gax" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de13e62d7e0ffc3eb40a0113ddf753cf6ec741be739164442b08893db4f9bfca" +dependencies = [ + "google-cloud-token", + "http 1.1.0", + "thiserror 1.0.69", + "tokio", + "tokio-retry2", + "tonic 0.12.3", + "tower 0.4.13", + "tracing", +] + [[package]] name = "google-cloud-googleapis" version = "0.10.0" @@ -6993,6 +7062,34 @@ dependencies = [ "tonic 0.9.2", ] +[[package]] +name = "google-cloud-googleapis" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0edfdfb507593d47605b3bb2fb36628b391e3d397e520b85852dea2412c8e2d1" +dependencies = [ + "prost 0.13.3", + "prost-types 0.13.3", + "tonic 0.12.3", +] + +[[package]] +name = "google-cloud-kms" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8842723521d34b9bf43305c84fd469c4d1858c42a630e1cb8c0d9c53781551" +dependencies = [ + "google-cloud-auth 0.17.2", + "google-cloud-gax 0.19.2", + "google-cloud-googleapis 0.16.0", + "google-cloud-token", + "prost-types 0.13.3", + "serde", + "serde_json", + "thiserror 1.0.69", + "tracing", +] + [[package]] name = "google-cloud-metadata" version = "0.3.2" @@ -7004,6 +7101,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "google-cloud-metadata" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f945a208886a13d07636f38fb978da371d0abc3e34bad338124b9f8c135a8f" +dependencies = [ + "reqwest 0.12.9", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "google-cloud-pubsub" version = "0.18.0" @@ -7012,9 +7120,9 @@ checksum = "095b104502b6e1abbad9b9768af944b9202e032dbc7f0947d3c30d4191761071" dependencies = [ "async-channel", "async-stream", - "google-cloud-auth", - "google-cloud-gax", - "google-cloud-googleapis", + "google-cloud-auth 0.12.0", + "google-cloud-gax 0.15.0", + "google-cloud-googleapis 0.10.0", "google-cloud-token", "prost-types 0.11.9", "thiserror 1.0.69", @@ -7033,8 +7141,8 @@ dependencies = [ "base64 0.21.7", "bytes 1.8.0", "futures-util", - "google-cloud-auth", - "google-cloud-metadata", + "google-cloud-auth 0.12.0", + "google-cloud-metadata 0.3.2", "google-cloud-token", "hex", "once_cell", @@ -7412,8 +7520,14 @@ version = "0.0.2" dependencies = [ "anyhow", "async-trait", + "aws-config", + "aws-sdk-kms", "base64 0.13.1", + "ed25519 2.2.3", + "google-cloud-kms", + "k256", "rand 0.7.3", + "ring-compat", "tokio", "vaultrs", ] @@ -11867,7 +11981,7 @@ dependencies = [ "field_count", "futures", "futures-util", - "google-cloud-googleapis", + "google-cloud-googleapis 0.10.0", "google-cloud-pubsub", "google-cloud-storage", "hex", @@ -12759,6 +12873,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ring-compat" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccce7bae150b815f0811db41b8312fcb74bffa4cab9cee5429ee00f356dd5bd4" +dependencies = [ + "aead", + "digest 0.10.7", + "ecdsa 0.16.9", + "ed25519 2.2.3", + "generic-array", + "p256 0.13.2", + "p384", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "ring 0.17.8", + "signature 2.2.0", +] + [[package]] name = "ripemd" version = "0.1.3" @@ -14709,6 +14842,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-retry2" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903934dba1c4c2f2e9cb460ef10b5695e0b0ecad3bf9ee7c8675e540c5e8b2d1" +dependencies = [ + "pin-project 1.1.7", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -14937,6 +15080,7 @@ dependencies = [ "axum 0.7.9", "base64 0.22.1", "bytes 1.8.0", + "flate2", "h2 0.4.6", "http 1.1.0", "http-body 1.0.1", @@ -14947,13 +15091,16 @@ dependencies = [ "percent-encoding", "pin-project 1.1.7", "prost 0.13.3", + "rustls-pemfile 2.2.0", "socket2 0.5.7", "tokio", + "tokio-rustls 0.26.0", "tokio-stream", "tower 0.4.13", "tower-layer", "tower-service", "tracing", + "webpki-roots 0.26.6", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9109baa69..fb608a2cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,6 +116,8 @@ aptos-account-whitelist = { path = "protocol-units/access-control/aptos/account- ## vault vaultrs = { version = "0.7.3" } +aws-sdk-kms = "1.51.0" +google-cloud-kms = "0.6.0" base64 = { version = "0.13.0" } # Serialization and Deserialization @@ -306,6 +308,8 @@ tracing-test = "0.2.5" trie-db = "0.28.0" url = "2.2.2" x25519-dalek = "1.0.1" +ed25519 = "2.2.3" +ring-compat = "0.8.0" zstd-sys = "2.0.9" zstd = "0.13" inotify = "0.10.2" diff --git a/demo/hsm/Cargo.toml b/demo/hsm/Cargo.toml index 304b5e854..047e4bfaf 100644 --- a/demo/hsm/Cargo.toml +++ b/demo/hsm/Cargo.toml @@ -14,8 +14,14 @@ tokio = { workspace = true } async-trait = { workspace = true } anyhow = { workspace = true } vaultrs = { workspace = true } +aws-sdk-kms = { workspace = true } +aws-config = { workspace = true } rand = { workspace = true } base64 = { workspace = true } +ed25519 = { workspace = true } +ring-compat = { workspace = true } +k256 = { workspace = true, features = ["ecdsa", "pkcs8"] } +google-cloud-kms = { workspace = true } [lints] workspace = true diff --git a/demo/hsm/src/action_stream/join.rs b/demo/hsm/src/action_stream/join.rs new file mode 100644 index 000000000..2926ffd3e --- /dev/null +++ b/demo/hsm/src/action_stream/join.rs @@ -0,0 +1,41 @@ +use crate::{ActionStream, Message}; + +/// Joins several streams together. +/// Notifies all streams of messages emitted from elsewhere in the system. +/// Round-robins through streams for next. +pub struct Join { + streams: Vec>, + current: usize, +} + +impl Join { + /// Creates a new `Join` stream. + pub fn new(streams: Vec>) -> Self { + Self { streams, current: 0 } + } +} + +#[async_trait::async_trait] +impl ActionStream for Join { + /// Notifies the stream of a message emitted from elsewhere in the system. + async fn notify(&mut self, message: Message) -> Result<(), anyhow::Error> { + for stream in &mut self.streams { + stream.notify(message.clone()).await?; + } + Ok(()) + } + + /// Gets the message to act upon. + async fn next(&mut self) -> Result, anyhow::Error> { + let mut next = None; + for _ in 0..self.streams.len() { + let stream = &mut self.streams[self.current]; + next = stream.next().await?; + self.current = (self.current + 1) % self.streams.len(); + if next.is_some() { + break; + } + } + Ok(next) + } +} diff --git a/demo/hsm/src/action_stream/mod.rs b/demo/hsm/src/action_stream/mod.rs index 7bcbfe0e2..2a1ec4208 100644 --- a/demo/hsm/src/action_stream/mod.rs +++ b/demo/hsm/src/action_stream/mod.rs @@ -1 +1,3 @@ +pub mod join; +pub mod notify_verify; pub mod random; diff --git a/demo/hsm/src/action_stream/notify_verify.rs b/demo/hsm/src/action_stream/notify_verify.rs new file mode 100644 index 000000000..7670fe266 --- /dev/null +++ b/demo/hsm/src/action_stream/notify_verify.rs @@ -0,0 +1,28 @@ +use crate::{ActionStream, Message}; +use std::collections::VecDeque; + +/// Adds all verify messages of which the stream is notified back to the stream +pub struct NotifyVerify { + buffer: VecDeque, +} + +impl NotifyVerify { + /// Creates a new `NotifyVerify` stream. + pub fn new() -> Self { + Self { buffer: VecDeque::new() } + } +} + +#[async_trait::async_trait] +impl ActionStream for NotifyVerify { + /// Notifies the stream of a message emitted from elsewhere in the system. + async fn notify(&mut self, message: Message) -> Result<(), anyhow::Error> { + self.buffer.push_back(message); + Ok(()) + } + + /// Gets the message to act upon. + async fn next(&mut self) -> Result, anyhow::Error> { + Ok(self.buffer.pop_front()) + } +} diff --git a/demo/hsm/src/action_stream/random.rs b/demo/hsm/src/action_stream/random.rs index a9cdce65e..81c89227c 100644 --- a/demo/hsm/src/action_stream/random.rs +++ b/demo/hsm/src/action_stream/random.rs @@ -6,13 +6,19 @@ pub struct Random; #[async_trait::async_trait] impl ActionStream for Random { - async fn next(&mut self) -> Option { + /// Notifies the stream of a message emitted from elsewhere in the system. + async fn notify(&mut self, _message: Message) -> Result<(), anyhow::Error> { + Ok(()) + } + + /// Gets the message to act upon. + async fn next(&mut self) -> Result, anyhow::Error> { // Generate a random vec of bytes let mut rng = rand::thread_rng(); - let len = rng.gen_range(1, 10); + let len = rng.gen_range(32, 256); let mut bytes = vec![0u8; len]; rng.fill_bytes(&mut bytes); - Some(Message::Sign(Bytes(bytes))) + Ok(Some(Message::Sign(Bytes(bytes)))) } } diff --git a/demo/hsm/src/hsm/aws_kms.rs b/demo/hsm/src/hsm/aws_kms.rs new file mode 100644 index 000000000..eb952432e --- /dev/null +++ b/demo/hsm/src/hsm/aws_kms.rs @@ -0,0 +1,99 @@ +use crate::{Bytes, Hsm, PublicKey, Signature}; +use anyhow::Context; +use aws_sdk_kms::primitives::Blob; +use aws_sdk_kms::types::{KeySpec, KeyUsageType, SigningAlgorithmSpec}; +use aws_sdk_kms::Client; +use k256::ecdsa::{self, VerifyingKey}; +use k256::pkcs8::DecodePublicKey; +use ring_compat::signature::Verifier; + +/// A AWS KMS HSM. +pub struct AwsKms { + client: Client, + key_id: String, + public_key: PublicKey, +} + +impl AwsKms { + /// Creates a new AWS KMS HSM + pub fn new(client: Client, key_id: String, public_key: PublicKey) -> Self { + Self { client, key_id, public_key } + } + + /// Tries to create a new AWS KMS HSM from the environment + pub async fn try_from_env() -> Result { + let key_id = std::env::var("AWS_KMS_KEY_ID").context("AWS_KMS_KEY_ID not set")?; + let public_key = std::env::var("AWS_KMS_PUBLIC_KEY").unwrap_or_default(); + + let config = aws_config::load_from_env().await; + let client = aws_sdk_kms::Client::new(&config); + + Ok(Self::new(client, key_id, PublicKey(Bytes(public_key.as_bytes().to_vec())))) + } + + /// Creates in AWS KMS matching the provided key id. + pub async fn create_key(self) -> Result { + let res = self + .client + .create_key() + .key_spec(KeySpec::EccSecgP256K1) + .key_usage(KeyUsageType::SignVerify) + .send() + .await?; + + let key_id = res.key_metadata().context("No key metadata available")?.key_id().to_string(); + + Ok(Self::new(self.client, key_id, self.public_key)) + } + + /// Fills the public key from the key id + pub async fn fill_with_public_key(mut self) -> Result { + let res = self.client.get_public_key().key_id(&self.key_id).send().await?; + let public_key = PublicKey(Bytes( + res.public_key().context("No public key available")?.as_ref().to_vec(), + )); + self.public_key = public_key; + Ok(self) + } +} + +#[async_trait::async_trait] +impl Hsm for AwsKms { + async fn sign(&self, message: Bytes) -> Result<(Bytes, PublicKey, Signature), anyhow::Error> { + let blob = Blob::new(message.clone().0); + let request = self + .client + .sign() + .key_id(&self.key_id) + .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256) + .message(blob); + + let res = request.send().await?; + println!("res: {:?}", res); + let signature = + Signature(Bytes(res.signature().context("No signature available")?.as_ref().to_vec())); + + Ok((message, self.public_key.clone(), signature)) + } + + async fn verify( + &self, + message: Bytes, + public_key: PublicKey, + signature: Signature, + ) -> Result { + let verifying_key = VerifyingKey::from_public_key_der(&public_key.0 .0) + .context("Failed to create verifying key")?; + + let signature = + ecdsa::Signature::from_der(&signature.0 .0).context("Failed to create signature")?; + + match verifying_key.verify(message.0.as_slice(), &signature) { + Ok(_) => Ok(true), + Err(e) => { + println!("Error verifying signature: {:?}", e); + Ok(false) + } + } + } +} diff --git a/demo/hsm/src/hsm/google_kms.rs b/demo/hsm/src/hsm/google_kms.rs new file mode 100644 index 000000000..e3049fd9d --- /dev/null +++ b/demo/hsm/src/hsm/google_kms.rs @@ -0,0 +1,138 @@ +use crate::{Bytes, Hsm, PublicKey, Signature}; +use anyhow::Context; +use google_cloud_kms::client::{Client, ClientConfig}; +use google_cloud_kms::grpc::kms::v1::{ + AsymmetricSignRequest, CreateCryptoKeyRequest, CreateKeyRingRequest, CryptoKey, Digest, + GetPublicKeyRequest, +}; +use k256::ecdsa::{self, VerifyingKey}; +use k256::pkcs8::DecodePublicKey; +use ring_compat::signature::Verifier; + +pub struct GoogleKms { + client: Client, + project: String, + location: String, + key_ring: String, + key_name: String, + public_key: PublicKey, +} + +impl GoogleKms { + pub fn new( + client: Client, + project: String, + location: String, + key_ring: String, + key_name: String, + public_key: PublicKey, + ) -> Self { + Self { client, project, location, key_ring, key_name, public_key } + } + + /// Tries to create a new Google KMS HSM from the environment + pub async fn try_from_env() -> Result { + let project = std::env::var("GOOGLE_KMS_PROJECT").context("GOOGLE_KMS_PROJECT not set")?; + let location = + std::env::var("GOOGLE_KMS_LOCATION").context("GOOGLE_KMS_LOCATION not set")?; + let key_ring = + std::env::var("GOOGLE_KMS_KEY_RING").context("GOOGLE_KMS_KEY_RING not set")?; + let key_name = + std::env::var("GOOGLE_KMS_KEY_NAME").context("GOOGLE_KMS_KEY_NAME not set")?; + let public_key = std::env::var("GOOGLE_KMS_PUBLIC_KEY").unwrap_or_default(); + + let config = ClientConfig::default().with_auth().await?; + let client = Client::new(config).await?; + + Ok(Self::new( + client, + project, + location, + key_ring, + key_name, + PublicKey(Bytes(public_key.as_bytes().to_vec())), + )) + } + + /// Tries to create a new key matching the provided key name. + pub async fn create_key_ring(self) -> Result { + let request = CreateKeyRingRequest { + parent: format!("projects/{}/locations/{}", self.project, self.location), + key_ring_id: self.key_ring.clone(), + key_ring: Default::default(), + }; + + self.client.create_key_ring(request, None).await?; + Ok(self) + } + + /// Tries to create a new key matching the provided key name. + pub async fn create_key(self) -> Result { + let request = CreateCryptoKeyRequest { + parent: self.key_ring.clone(), + crypto_key_id: self.key_name.clone(), + crypto_key: Some(CryptoKey { + purpose: 3, // Corresponds to ASYMETRIC_SIGN + version_template: Some(Default::default()), + ..Default::default() + }), + skip_initial_version_creation: false, + }; + + self.client.create_crypto_key(request, None).await?; + + Ok(self) + } + + /// Fills the public key from the key name + pub async fn fill_with_public_key(mut self) -> Result { + let request = GetPublicKeyRequest { name: self.key_name.clone() }; + + let res = self.client.get_public_key(request, None).await?; + + self.public_key = PublicKey(Bytes(res.pem.as_bytes().to_vec())); + + Ok(self) + } +} + +#[async_trait::async_trait] +impl Hsm for GoogleKms { + async fn sign(&self, message: Bytes) -> Result<(Bytes, PublicKey, Signature), anyhow::Error> { + let digest = Digest { + digest: Some(google_cloud_kms::grpc::kms::v1::digest::Digest::Sha256( + message.clone().0, + )), + ..Default::default() + }; + + let request = AsymmetricSignRequest { + name: self.key_name.clone(), + digest: Some(digest), + ..Default::default() + }; + + let response = + self.client.asymmetric_sign(request, None).await.context("Failed to sign")?; + + let signature = Signature(Bytes(response.signature)); + + Ok((message, self.public_key.clone(), signature)) + } + + async fn verify( + &self, + message: Bytes, + public_key: PublicKey, + signature: Signature, + ) -> Result { + let verifying_key = VerifyingKey::from_public_key_der(&public_key.0 .0) + .context("Failed to create verifying key")?; + + // use the pkcs8 der to decode + let k256_signature = + ecdsa::Signature::from_der(&signature.0 .0).context("Failed to create signature")?; + + Ok(verifying_key.verify(message.0.as_slice(), &k256_signature).is_ok()) + } +} diff --git a/demo/hsm/src/hsm/hashi_corp_vault.rs b/demo/hsm/src/hsm/hashi_corp_vault.rs index 6c25ce6e0..9b5457132 100644 --- a/demo/hsm/src/hsm/hashi_corp_vault.rs +++ b/demo/hsm/src/hsm/hashi_corp_vault.rs @@ -1,6 +1,11 @@ -use crate::{Bytes, Hsm, Signature}; -use vaultrs::api::transit::requests::CreateKeyRequest; +use crate::{Bytes, Hsm, PublicKey, Signature}; +use anyhow::Context; +use ring_compat::signature::{ + ed25519::{self, VerifyingKey}, + Verifier, +}; use vaultrs::api::transit::KeyType; +use vaultrs::api::transit::{requests::CreateKeyRequest, responses::ReadKeyData}; use vaultrs::client::{VaultClient, VaultClientSettingsBuilder}; use vaultrs::transit::data; use vaultrs::transit::key; @@ -10,65 +15,122 @@ pub struct HashiCorpVault { client: VaultClient, key_name: String, mount_name: String, + public_key: PublicKey, } impl HashiCorpVault { /// Creates a new HashiCorp Vault HSM - pub fn new(client: VaultClient, key_name: String, mount_name: String) -> Self { - Self { client, key_name, mount_name } + pub fn new( + client: VaultClient, + key_name: String, + mount_name: String, + public_key: PublicKey, + ) -> Self { + Self { client, key_name, mount_name, public_key } } /// Tries to create a new HashiCorp Vault HSM from the environment pub fn try_from_env() -> Result { - let address = std::env::var("VAULT_ADDR").unwrap_or_else(|_| "https:// - + let address = std::env::var("VAULT_ADDRESS").context("VAULT_ADDRESS not set")?; + let token = std::env::var("VAULT_TOKEN").context("VAULT_TOKEN not set")?; + let namespace = std::env::var("VAULT_NAMESPACE").unwrap_or_else(|_| "admin".to_string()); let client = VaultClient::new( VaultClientSettingsBuilder::default() - .address("https://127.0.0.1:8200") - .token("TOKEN") + .address(address.as_str()) + .token(token.as_str()) + .namespace(Some(namespace)) .build()?, )?; - let key_name = std::env::var("VAULT_KEY_NAME")?; - let mount_name = std::env::var("VAULT_MOUNT_NAME")?; - Ok(Self::new(client, key_name, mount_name)) + let key_name = std::env::var("VAULT_KEY_NAME").context("VAULT_KEY_NAME not set")?; + let mount_name = std::env::var("VAULT_MOUNT_NAME").context("VAULT_MOUNT_NAME not set")?; + let public_key = std::env::var("VAULT_PUBLIC_KEY").unwrap_or_default(); + + Ok(Self::new( + client, + key_name, + mount_name, + PublicKey(Bytes(public_key.as_bytes().to_vec())), + )) } /// Creates a new key in the transit backend - pub async fn new_key(self) -> Result<(), anyhow::Error> { + pub async fn create_key(self) -> Result { key::create( &self.client, self.mount_name.as_str(), self.key_name.as_str(), - Some(CreateKeyRequest::builder().key_type(KeyType::Ed25519)), + Some(CreateKeyRequest::builder().key_type(KeyType::Ed25519).derived(false)), ) - .await?; + .await + .context("Failed to create key")?; - Ok(()) + Ok(self) + } + + /// Fills with a public key fetched from vault. + pub async fn fill_with_public_key(self) -> Result { + let res = key::read(&self.client, self.mount_name.as_str(), self.key_name.as_str()) + .await + .context("Failed to read key")?; + println!("Read key: {:?}", res); + + let public_key = match res.keys { + ReadKeyData::Symmetric(_) => { + return Err(anyhow::anyhow!("Symmetric keys are not supported")); + } + ReadKeyData::Asymmetric(keys) => { + let key = keys.values().next().context("No key found")?; + base64::decode(key.public_key.as_str()).context("Failed to decode public key")? + } + }; + + println!("Public key: {:?}", public_key); + Ok(Self::new(self.client, self.key_name, self.mount_name, PublicKey(Bytes(public_key)))) } } #[async_trait::async_trait] impl Hsm for HashiCorpVault { - async fn sign(&self, message: Bytes) -> Result { + async fn sign(&self, message: Bytes) -> Result<(Bytes, PublicKey, Signature), anyhow::Error> { let res = data::sign( &self.client, self.mount_name.as_str(), self.key_name.as_str(), // convert bytes vec to base64 string - base64::encode(message.0).as_str(), + base64::encode(message.clone().0).as_str(), None, ) - .await?; + .await + .context("Failed to sign message")?; + + // the signature should be encoded valut:v1: check for match and split off the signature + // 1. check for match + if !res.signature.starts_with("vault:v1:") { + return Err(anyhow::anyhow!("Invalid signature format")); + } + // 2. split off the signature + let signature_str = res.signature.split_at(9).1; // decode base64 string to vec - let signature = base64::decode(res.signature)?; + let signature = base64::decode(signature_str).context("Failed to decode signature")?; // Sign the message using HashiCorp Vault - Ok(Signature(Bytes(signature))) + Ok((message, self.public_key.clone(), Signature(Bytes(signature)))) } - async fn verify(&self, _message: Bytes, _signature: Signature) -> Result { - Ok(true) + async fn verify( + &self, + message: Bytes, + public_key: PublicKey, + signature: Signature, + ) -> Result { + let verifying_key = VerifyingKey::from_slice(public_key.0 .0.as_slice()) + .context("Failed to create verifying key")?; + + let signature = ed25519::Signature::from_slice(signature.0 .0.as_slice()) + .context("Failed to create signature")?; + + Ok(verifying_key.verify(message.0.as_slice(), &signature).is_ok()) } } diff --git a/demo/hsm/src/hsm/mod.rs b/demo/hsm/src/hsm/mod.rs index 3be11e46e..08cd9d479 100644 --- a/demo/hsm/src/hsm/mod.rs +++ b/demo/hsm/src/hsm/mod.rs @@ -1 +1,3 @@ +pub mod aws_kms; +pub mod google_kms; pub mod hashi_corp_vault; diff --git a/demo/hsm/src/lib.rs b/demo/hsm/src/lib.rs index ad6ceabef..f5c863fb5 100644 --- a/demo/hsm/src/lib.rs +++ b/demo/hsm/src/lib.rs @@ -2,30 +2,44 @@ pub mod action_stream; pub mod hsm; /// A collection of bytes. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Bytes(pub Vec); /// A signature. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Signature(pub Bytes); +/// A public key. +#[derive(Debug, Clone)] +pub struct PublicKey(pub Bytes); + +#[derive(Debug, Clone)] /// A message to be signed or verified. pub enum Message { Sign(Bytes), - Verify(Bytes, Bytes), + Verify(Bytes, PublicKey, Signature), } /// A stream of messages to be signed or verified. #[async_trait::async_trait] pub trait ActionStream { - async fn next(&mut self) -> Option; + /// Notifies the stream of a message emitted from elsewhere in the system. + async fn notify(&mut self, message: Message) -> Result<(), anyhow::Error>; + + /// Gets the message to act upon. + async fn next(&mut self) -> Result, anyhow::Error>; } /// An HSM capable of signing and verifying messages. #[async_trait::async_trait] pub trait Hsm { - async fn sign(&self, message: Bytes) -> Result; - async fn verify(&self, message: Bytes, signature: Signature) -> Result; + async fn sign(&self, message: Bytes) -> Result<(Bytes, PublicKey, Signature), anyhow::Error>; + async fn verify( + &self, + message: Bytes, + public_key: PublicKey, + signature: Signature, + ) -> Result; } /// An application which reads a stream of messages to either sign or verify. @@ -42,18 +56,23 @@ impl Application { } /// Runs the application. - pub async fn run(&mut self) { - while let Some(message) = self.stream.next().await { + pub async fn run(&mut self) -> Result<(), anyhow::Error> { + while let Some(message) = self.stream.next().await? { + println!("RECEIVED: {:?}", message); match message { Message::Sign(message) => { - let signature = self.hsm.sign(message).await; - println!("Signed message: {:?}", signature); + println!("SIGNING: {:?}", message); + let (message, public_key, signature) = self.hsm.sign(message).await?; + println!("SIGNED:\n{:?}\n{:?}\n{:?}", message, public_key, signature); + self.stream.notify(Message::Verify(message, public_key, signature)).await?; } - Message::Verify(message, signature) => { - let verified = self.hsm.verify(message, Signature(signature)).await; - println!("Verified message: {:?}", verified); + Message::Verify(message, public_key, signature) => { + println!("VERIFYING:\n{:?}\n{:?}\n{:?}", message, public_key, signature); + let verified = self.hsm.verify(message, public_key, signature).await?; + println!("VERIFIED: {:?}", verified); } } } + Ok(()) } } diff --git a/demo/hsm/src/main.rs b/demo/hsm/src/main.rs index f612f9690..11a173cbc 100644 --- a/demo/hsm/src/main.rs +++ b/demo/hsm/src/main.rs @@ -2,9 +2,23 @@ use hsm_demo::{action_stream, hsm, Application}; #[tokio::main] pub async fn main() -> Result<(), anyhow::Error> { - let stream = action_stream::random::Random; + let random_stream = action_stream::random::Random; + let notify_verify_stream = action_stream::notify_verify::NotifyVerify::new(); + let join_stream = action_stream::join::Join::new(vec![ + Box::new(random_stream), + Box::new(notify_verify_stream), + ]); - let hsm = hsm::hashi_corp_vault::HashiCorpVault::new() + let hsm = hsm::aws_kms::AwsKms::try_from_env() + .await? + .create_key() + .await? + .fill_with_public_key() + .await?; + + let mut app = Application::new(Box::new(hsm), Box::new(join_stream)); + + app.run().await?; Ok(()) } diff --git a/payload.json b/payload.json new file mode 100644 index 000000000..0a9135ac8 --- /dev/null +++ b/payload.json @@ -0,0 +1,5 @@ +{ + "type": "ecdsa-p256", + "derived": false +} +