diff --git a/Cargo.lock b/Cargo.lock index f7b369444..4eb53debe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14027,6 +14027,27 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "signing_admin" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "aws-config", + "aws-sdk-kms", + "base64 0.13.1", + "clap 4.5.21", + "movement-signer", + "movement-signer-aws-kms", + "movement-signer-hashicorp-vault", + "reqwest 0.11.27", + "serde_json", + "simple_asn1 0.6.2", + "tokio", + "uuid", + "vaultrs", +] + [[package]] name = "simd-adler32" version = "0.3.7" diff --git a/Cargo.toml b/Cargo.toml index fdae4084a..47fcd8a9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ members = [ "util/signing/integrations/aptos", "util/signing/providers/aws-kms", "util/signing/providers/hashicorp-vault", + "util/signing/signing-admin", "demo/hsm" ] diff --git a/demo/hsm/src/cli/mod.rs b/demo/hsm/src/cli/mod.rs index e3008ed44..7430596d9 100644 --- a/demo/hsm/src/cli/mod.rs +++ b/demo/hsm/src/cli/mod.rs @@ -4,14 +4,14 @@ use clap::Parser; #[derive(Parser)] #[clap(rename_all = "kebab-case")] pub enum HsmDemo { - #[clap(subcommand)] - Server(server::Server), + #[clap(subcommand)] + Server(server::Server), } impl HsmDemo { - pub async fn run(&self) -> Result<(), anyhow::Error> { - match self { - HsmDemo::Server(server) => server.run().await, - } - } + pub async fn run(&self) -> Result<(), anyhow::Error> { + match self { + HsmDemo::Server(server) => server.run().await, + } + } } diff --git a/util/signing/providers/hashicorp-vault/src/hsm/mod.rs b/util/signing/providers/hashicorp-vault/src/hsm/mod.rs index fb4dd61ca..989902ec2 100644 --- a/util/signing/providers/hashicorp-vault/src/hsm/mod.rs +++ b/util/signing/providers/hashicorp-vault/src/hsm/mod.rs @@ -94,7 +94,17 @@ where return Err(SignerError::Internal("Invalid signature format".to_string())); } - let signature_str = res.signature.split_at(9).1; + // Extract the key version from the signature + let version_end_index = res.signature[6..] + .find(':') + .ok_or_else(|| SignerError::Internal("Invalid signature format".to_string()))? + + 6; + + // Determine split index dynamically + let split_index = version_end_index + 1; + + // Split and decode the signature + let signature_str = &res.signature[split_index..]; let signature = base64::decode(signature_str) .context("Failed to decode base64 signature from Vault") @@ -108,15 +118,17 @@ where } let parsed_signature = C::Signature::try_from_bytes(&signature).map_err(|e| { - SignerError::Internal(format!("Failed to parse signature into expected format: {:?}", e)) + SignerError::Internal(format!( + "Failed to parse signature into expected format: {:?}", + e + )) })?; Ok(parsed_signature) } - async fn public_key(&self) -> Result { - println!("Attempting to read key: {}", self.key_name); + println!("Attempting to read Vault key: {}", self.key_name); // Read the key from Vault let res = transit_key::read( @@ -146,13 +158,11 @@ where ReadKeyData::Asymmetric(keys) => { // Use the number of items in the map as the version let latest_version = keys.len().to_string(); - println!("Using key version: {}", latest_version); let key = keys.get(&latest_version).context("Key version not found").map_err(|e| { println!("Key version '{}' not found: {:?}", latest_version, e); SignerError::KeyNotFound })?; - println!("Using public key for version {}: {}", latest_version, key.public_key); base64::decode(&key.public_key).map_err(|e| { println!("Failed to decode public key: {:?}", e); diff --git a/util/signing/signing-admin/Cargo.toml b/util/signing/signing-admin/Cargo.toml new file mode 100644 index 000000000..2fc23ff5d --- /dev/null +++ b/util/signing/signing-admin/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "signing_admin" +version = "0.1.0" +edition = "2021" +authors = ["Your Name "] +description = "CLI for managing signing keys" +license = "MIT" +repository = "https://github.com/your/repo" + +[dependencies] +anyhow = "1.0" +async-trait = { workspace = true } +aws-config = { workspace = true } +aws-sdk-kms = { workspace = true } +base64 = { workspace = true } +clap = { version = "4.0", features = ["derive"] } +movement-signer = { workspace = true } +movement-signer-aws-kms = { workspace = true } +movement-signer-hashicorp-vault = { workspace = true } +reqwest = { version = "0.11", features = ["json"] } +serde_json = { workspace = true } +simple_asn1 = "0.6" +tokio = { version = "1", features = ["full"] } +uuid = { workspace = true } +vaultrs = { workspace = true } + +[[bin]] +name = "signing-admin" +path = "src/main.rs" diff --git a/util/signing/signing-admin/src/application/mod.rs b/util/signing/signing-admin/src/application/mod.rs new file mode 100644 index 000000000..eb50b543b --- /dev/null +++ b/util/signing/signing-admin/src/application/mod.rs @@ -0,0 +1,42 @@ +use anyhow::Result; + +#[async_trait::async_trait] +pub trait Application { + async fn notify_public_key(&self, public_key: Vec) -> Result<()>; +} + +pub struct HttpApplication { + app_url: String, +} + +impl HttpApplication { + pub fn new(app_url: String) -> Self { + Self { app_url } + } +} + +#[async_trait::async_trait] +impl Application for HttpApplication { + async fn notify_public_key(&self, public_key: Vec) -> Result<()> { + let endpoint = format!("{}/public_key/set", self.app_url); + println!("Notifying application at {} with public key {:?}", endpoint, public_key); + + let client = reqwest::Client::new(); + let response = client + .post(&endpoint) + .json(&serde_json::json!({ "public_key": public_key })) + .send() + .await?; + + if !response.status().is_success() { + anyhow::bail!( + "Failed to notify application. Status: {}, Body: {:?}", + response.status(), + response.text().await? + ); + } + + println!("Successfully notified the application."); + Ok(()) + } +} diff --git a/util/signing/signing-admin/src/backend/aws.rs b/util/signing/signing-admin/src/backend/aws.rs new file mode 100644 index 000000000..18dbb82ca --- /dev/null +++ b/util/signing/signing-admin/src/backend/aws.rs @@ -0,0 +1,66 @@ +use anyhow::{Context, Result}; +use aws_config; +use aws_sdk_kms::{Client as KmsClient}; +use aws_sdk_kms::types::Tag; +use super::SigningBackend; + +pub struct AwsBackend; + +impl AwsBackend { + pub fn new() -> Self { + Self {} + } + + async fn create_client() -> Result { + let aws_config = aws_config::load_from_env().await; + Ok(KmsClient::new(&aws_config)) + } + + async fn create_key(client: &KmsClient) -> Result { + let response = client + .create_key() + .description("Key for signing and verification") + .key_usage(aws_sdk_kms::types::KeyUsageType::SignVerify) + .customer_master_key_spec(aws_sdk_kms::types::CustomerMasterKeySpec::EccSecgP256K1) + .tags( + Tag::builder() + .tag_key("unique_id") + .tag_value("tag") + .build() + .context("Failed to build AWS KMS tag")?, + ) + .send() + .await + .context("Failed to create key with AWS KMS")?; + + response + .key_metadata() + .and_then(|meta| Some(meta.key_id().to_string())) + .context("Failed to retrieve key ID from AWS response") + } +} + +#[async_trait::async_trait] +impl SigningBackend for AwsBackend { + async fn rotate_key(&self, key_id: &str) -> Result<()> { + let client = Self::create_client().await?; + + // Ensure the key_id starts with "alias/" + let full_alias = if key_id.starts_with("alias/") { + key_id.to_string() + } else { + format!("alias/{}", key_id) + }; + + let new_key_id = Self::create_key(&client).await?; + client + .update_alias() + .alias_name(&full_alias) + .target_key_id(&new_key_id) + .send() + .await + .context("Failed to update AWS KMS alias")?; + + Ok(()) + } +} diff --git a/util/signing/signing-admin/src/backend/mod.rs b/util/signing/signing-admin/src/backend/mod.rs new file mode 100644 index 000000000..8a2ca7829 --- /dev/null +++ b/util/signing/signing-admin/src/backend/mod.rs @@ -0,0 +1,30 @@ +pub mod aws; +pub mod vault; + +use anyhow::Result; +use async_trait::async_trait; +use aws::AwsBackend; +use vault::VaultBackend; + +/// The trait that all signing backends must implement. +#[async_trait] +pub trait SigningBackend { + async fn rotate_key(&self, key_id: &str) -> Result<()>; +} + +/// Enum to represent the different backends. +pub enum Backend { + Aws(AwsBackend), + Vault(VaultBackend), +} + +/// Implement the SigningBackend trait for the Backend enum. +#[async_trait] +impl SigningBackend for Backend { + async fn rotate_key(&self, key_id: &str) -> Result<()> { + match self { + Backend::Aws(aws) => aws.rotate_key(key_id).await, + Backend::Vault(vault) => vault.rotate_key(key_id).await, + } + } +} diff --git a/util/signing/signing-admin/src/backend/vault.rs b/util/signing/signing-admin/src/backend/vault.rs new file mode 100644 index 000000000..db958e8cb --- /dev/null +++ b/util/signing/signing-admin/src/backend/vault.rs @@ -0,0 +1,32 @@ +use anyhow::{Context, Result}; +use vaultrs::client::{VaultClient, VaultClientSettingsBuilder}; +use vaultrs::transit::key::rotate; +use super::SigningBackend; + +pub struct VaultBackend; + +impl VaultBackend { + pub fn new() -> Self { + Self {} + } + + async fn create_client(vault_url: &str, token: &str) -> Result { + let settings = VaultClientSettingsBuilder::default() + .address(vault_url) + .token(token) + .namespace(Some("admin".to_string())) + .build() + .context("Failed to build Vault client settings")?; + VaultClient::new(settings).context("Failed to create Vault client") + } +} + +#[async_trait::async_trait] +impl SigningBackend for VaultBackend { + async fn rotate_key(&self, key_id: &str) -> Result<()> { + let vault_url = std::env::var("VAULT_URL").context("Missing VAULT_URL environment variable")?; + let token = std::env::var("VAULT_TOKEN").context("Missing VAULT_TOKEN environment variable")?; + let client = Self::create_client(&vault_url, &token).await?; + rotate(&client, "transit", key_id).await.context("Failed to rotate key in Vault") + } +} diff --git a/util/signing/signing-admin/src/cli/mod.rs b/util/signing/signing-admin/src/cli/mod.rs new file mode 100644 index 000000000..48ea8f79c --- /dev/null +++ b/util/signing/signing-admin/src/cli/mod.rs @@ -0,0 +1,24 @@ +use clap::{Parser, Subcommand}; + +pub mod rotate_key; + +#[derive(Parser, Debug)] +#[clap(name = "signing-admin", about = "CLI for managing signing keys")] +pub struct CLI { + #[clap(subcommand)] + pub command: Commands, +} + +#[derive(Subcommand, Debug)] +pub enum Commands { + RotateKey { + #[clap(long, help = "Canonical string of the key (alias for the backend key)")] + canonical_string: String, + + #[clap(long, help = "Application URL to notify about the key rotation")] + application_url: String, + + #[clap(long, help = "Backend to use (e.g., 'vault', 'aws')")] + backend: String, + }, +} diff --git a/util/signing/signing-admin/src/cli/rotate_key.rs b/util/signing/signing-admin/src/cli/rotate_key.rs new file mode 100644 index 000000000..65952a629 --- /dev/null +++ b/util/signing/signing-admin/src/cli/rotate_key.rs @@ -0,0 +1,105 @@ +use anyhow::{Context, Result}; +use movement_signer::{ + cryptography::{secp256k1::Secp256k1, ed25519::Ed25519}, + Signing, +}; +use signing_admin::{ + application::{Application, HttpApplication}, + backend::{aws::AwsBackend, vault::VaultBackend, Backend}, + key_manager::KeyManager, +}; +use movement_signer_aws_kms::hsm::AwsKms; +use movement_signer_hashicorp_vault::hsm::HashiCorpVault; +use vaultrs::client::{VaultClient, VaultClientSettingsBuilder}; + +/// Enum to encapsulate different signers +enum SignerBackend { + Vault(HashiCorpVault), + Aws(AwsKms), +} + +impl SignerBackend { + /// Retrieve the public key from the signer + async fn public_key(&self) -> Result> { + match self { + SignerBackend::Vault(signer) => { + let public_key = signer.public_key().await?; + Ok(public_key.as_bytes().to_vec()) + } + SignerBackend::Aws(signer) => { + let public_key = signer.public_key().await?; + Ok(public_key.as_bytes().to_vec()) + } + } + } +} + +pub async fn rotate_key( + canonical_string: String, + application_url: String, + backend_name: String, +) -> Result<()> { + let application = HttpApplication::new(application_url); + + let backend = match backend_name.as_str() { + "vault" => Backend::Vault(VaultBackend::new()), + "aws" => Backend::Aws(AwsBackend::new()), + _ => return Err(anyhow::anyhow!("Unsupported backend: {}", backend_name)), + }; + + let signer = match backend_name.as_str() { + "vault" => { + let vault_url = std::env::var("VAULT_URL") + .context("Missing VAULT_URL environment variable")?; + let vault_token = std::env::var("VAULT_TOKEN") + .context("Missing VAULT_TOKEN environment variable")?; + + let client = VaultClient::new( + VaultClientSettingsBuilder::default() + .address(vault_url) + .token(vault_token) + .namespace(Some("admin".to_string())) + .build() + .context("Failed to build Vault client settings")?, + ) + .context("Failed to create Vault client")?; + + SignerBackend::Vault(HashiCorpVault::::new( + client, + canonical_string.clone(), + "transit".to_string(), + )) + } + "aws" => { + let aws_config = aws_config::load_from_env().await; + let client = aws_sdk_kms::Client::new(&aws_config); + + SignerBackend::Aws(AwsKms::::new( + client, + canonical_string.clone(), + )) + } + _ => return Err(anyhow::anyhow!("Unsupported signer backend: {}", backend_name)), + }; + + let key_manager = KeyManager::new(application, backend); + + key_manager + .rotate_key(&canonical_string) + .await + .context("Failed to rotate the key")?; + + let public_key = signer + .public_key() + .await + .context("Failed to fetch the public key from signer")?; + + key_manager + .application + .notify_public_key(public_key) + .await + .context("Failed to notify the application with the public key")?; + + println!("Key rotation and notification completed successfully."); + Ok(()) +} diff --git a/util/signing/signing-admin/src/key_manager.rs b/util/signing/signing-admin/src/key_manager.rs new file mode 100644 index 000000000..ad259f12e --- /dev/null +++ b/util/signing/signing-admin/src/key_manager.rs @@ -0,0 +1,24 @@ +use anyhow::Result; +use movement_signer::Signing; +use super::application::Application; +use super::backend::SigningBackend; + +pub struct KeyManager { + pub application: A, + pub backend: B, +} + +impl KeyManager +where + A: Application, + B: SigningBackend, +{ + pub fn new(application: A, backend: B) -> Self { + Self { application, backend } + } + + pub async fn rotate_key(&self, key_id: &str) -> Result<()> { + self.backend.rotate_key(key_id).await + } + +} diff --git a/util/signing/signing-admin/src/lib.rs b/util/signing/signing-admin/src/lib.rs new file mode 100644 index 000000000..d4dc23dd5 --- /dev/null +++ b/util/signing/signing-admin/src/lib.rs @@ -0,0 +1,4 @@ +pub mod application; +pub mod backend; +pub mod key_manager; + diff --git a/util/signing/signing-admin/src/main.rs b/util/signing/signing-admin/src/main.rs new file mode 100644 index 000000000..c995beb60 --- /dev/null +++ b/util/signing/signing-admin/src/main.rs @@ -0,0 +1,21 @@ +use anyhow::Result; +use clap::Parser; + +mod cli; + +#[tokio::main] +async fn main() -> Result<()> { + let cli = cli::CLI::parse(); + + match cli.command { + cli::Commands::RotateKey { + canonical_string, + application_url, + backend, + } => { + cli::rotate_key::rotate_key(canonical_string, application_url, backend).await?; + } + } + + Ok(()) +}