diff --git a/Cargo.lock b/Cargo.lock index 4eb53debe..31f61f404 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14037,10 +14037,12 @@ dependencies = [ "aws-sdk-kms", "base64 0.13.1", "clap 4.5.21", + "hex", "movement-signer", "movement-signer-aws-kms", "movement-signer-hashicorp-vault", "reqwest 0.11.27", + "serde", "serde_json", "simple_asn1 0.6.2", "tokio", diff --git a/util/signing/signing-admin/Cargo.toml b/util/signing/signing-admin/Cargo.toml index 2fc23ff5d..16be831ee 100644 --- a/util/signing/signing-admin/Cargo.toml +++ b/util/signing/signing-admin/Cargo.toml @@ -14,10 +14,12 @@ aws-config = { workspace = true } aws-sdk-kms = { workspace = true } base64 = { workspace = true } clap = { version = "4.0", features = ["derive"] } +hex = { workspace = true } movement-signer = { workspace = true } movement-signer-aws-kms = { workspace = true } movement-signer-hashicorp-vault = { workspace = true } reqwest = { version = "0.11", features = ["json"] } +serde = { workspace = true } serde_json = { workspace = true } simple_asn1 = "0.6" tokio = { version = "1", features = ["full"] } diff --git a/util/signing/signing-admin/src/backend/aws.rs b/util/signing/signing-admin/src/backend/aws.rs index 18dbb82ca..738c44f35 100644 --- a/util/signing/signing-admin/src/backend/aws.rs +++ b/util/signing/signing-admin/src/backend/aws.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; +use async_trait::async_trait; use aws_config; -use aws_sdk_kms::{Client as KmsClient}; -use aws_sdk_kms::types::Tag; +use aws_sdk_kms::{Client as KmsClient, types::Tag}; use super::SigningBackend; pub struct AwsBackend; @@ -40,11 +40,32 @@ impl AwsBackend { } } -#[async_trait::async_trait] +#[async_trait] impl SigningBackend for AwsBackend { + async fn create_key(&self, _key_id: &str) -> Result { + let client = Self::create_client().await?; + + let response = client + .create_key() + .description("Key for signing and verification".to_string()) + .key_usage(aws_sdk_kms::types::KeyUsageType::SignVerify) + .key_spec(aws_sdk_kms::types::KeySpec::EccSecgP256K1) // Replaced deprecated method + .send() + .await + .context("Failed to create AWS KMS key")?; + + let new_key_id = response + .key_metadata() + .and_then(|meta| Some(meta.key_id())) + .map(String::from) + .ok_or_else(|| anyhow::anyhow!("Failed to extract new key ID"))?; + + Ok(new_key_id) + } + 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() @@ -52,7 +73,7 @@ impl SigningBackend for AwsBackend { format!("alias/{}", key_id) }; - let new_key_id = Self::create_key(&client).await?; + let new_key_id = self.create_key(key_id).await?; client .update_alias() .alias_name(&full_alias) @@ -60,7 +81,8 @@ impl SigningBackend for AwsBackend { .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 index 8a2ca7829..aaf726ce7 100644 --- a/util/signing/signing-admin/src/backend/mod.rs +++ b/util/signing/signing-admin/src/backend/mod.rs @@ -9,9 +9,11 @@ use vault::VaultBackend; /// The trait that all signing backends must implement. #[async_trait] pub trait SigningBackend { + async fn create_key(&self, key_id: &str) -> Result; // Now returns the new key ID async fn rotate_key(&self, key_id: &str) -> Result<()>; } + /// Enum to represent the different backends. pub enum Backend { Aws(AwsBackend), @@ -21,6 +23,12 @@ pub enum Backend { /// Implement the SigningBackend trait for the Backend enum. #[async_trait] impl SigningBackend for Backend { + async fn create_key(&self, key_id: &str) -> Result { + match self { + Backend::Aws(aws) => aws.create_key(key_id).await, + Backend::Vault(vault) => vault.create_key(key_id).await, + } + } async fn rotate_key(&self, key_id: &str) -> Result<()> { match self { Backend::Aws(aws) => aws.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 index db958e8cb..a1a8bb8ec 100644 --- a/util/signing/signing-admin/src/backend/vault.rs +++ b/util/signing/signing-admin/src/backend/vault.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use async_trait::async_trait; use vaultrs::client::{VaultClient, VaultClientSettingsBuilder}; use vaultrs::transit::key::rotate; use super::SigningBackend; @@ -21,12 +22,33 @@ impl VaultBackend { } } -#[async_trait::async_trait] +#[async_trait] impl SigningBackend for VaultBackend { + async fn create_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?; + + let mount_path = "transit"; + vaultrs::transit::key::create(&client, mount_path, key_id, Default::default()) + .await + .context("Failed to create key in Vault")?; + + Ok(key_id.to_string()) // Vault keys reuse the input key ID + } + 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") + + let mount_path = "transit"; + rotate(&client, mount_path, key_id) + .await + .context("Failed to rotate key in Vault")?; + + Ok(()) } } + + diff --git a/util/signing/signing-admin/src/cli/mod.rs b/util/signing/signing-admin/src/cli/mod.rs index 48ea8f79c..cb2d661cb 100644 --- a/util/signing/signing-admin/src/cli/mod.rs +++ b/util/signing/signing-admin/src/cli/mod.rs @@ -1,6 +1,7 @@ use clap::{Parser, Subcommand}; pub mod rotate_key; +pub mod wal; #[derive(Parser, Debug)] #[clap(name = "signing-admin", about = "CLI for managing signing keys")] diff --git a/util/signing/signing-admin/src/cli/rotate_key.rs b/util/signing/signing-admin/src/cli/rotate_key.rs index 65952a629..1f79d7c50 100644 --- a/util/signing/signing-admin/src/cli/rotate_key.rs +++ b/util/signing/signing-admin/src/cli/rotate_key.rs @@ -1,105 +1,63 @@ use anyhow::{Context, Result}; +use hex; use movement_signer::{ - cryptography::{secp256k1::Secp256k1, ed25519::Ed25519}, - Signing, + cryptography::{secp256k1::Secp256k1, ed25519::Ed25519}, + Signing, }; use signing_admin::{ - application::{Application, HttpApplication}, - backend::{aws::AwsBackend, vault::VaultBackend, Backend}, - key_manager::KeyManager, + 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()) - } - } - } -} +use crate::cli::wal::{append_to_wal, update_wal_entry, update_wal_status, WalEntry}; pub async fn rotate_key( - canonical_string: String, - application_url: String, - backend_name: String, + 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(()) + // Initialize the application + let application = HttpApplication::new(application_url); + + // Initialize the backend + 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)), + }; + + // Create the key manager + let key_manager = KeyManager::new(application, backend); + + // Phase 1: Create New Key + let new_key_id = key_manager.create_key(&canonical_string).await?; + append_to_wal(WalEntry { + operation: "rotate_key".to_string(), + canonical_string: canonical_string.clone(), + status: "key_created".to_string(), + public_key: None, + key_id: Some(new_key_id.clone()), + })?; + update_wal_status(&canonical_string, "key_created")?; + + // Fetch the public key from the new key + let public_key = new_key_id.as_bytes().to_vec(); + key_manager + .notify_application(public_key.clone()) + .await + .context("Failed to notify application with the public key")?; + update_wal_entry(&canonical_string, |entry| { + entry.public_key = Some(hex::encode(&public_key)); + })?; + + // Phase 2: Rotate Key + update_wal_status(&canonical_string, "commit")?; + key_manager + .rotate_key(&new_key_id) + .await + .context("Failed to rotate key to the new ID")?; + update_wal_status(&canonical_string, "completed")?; + + println!("Key rotation completed successfully."); + Ok(()) } diff --git a/util/signing/signing-admin/src/cli/wal.rs b/util/signing/signing-admin/src/cli/wal.rs new file mode 100644 index 000000000..3d071db72 --- /dev/null +++ b/util/signing/signing-admin/src/cli/wal.rs @@ -0,0 +1,66 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::fs::{self, OpenOptions}; +use std::io::{BufReader, BufWriter}; +use std::path::Path; + +const WAL_FILE: &str = "wal.json"; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct WalEntry { + pub operation: String, + pub canonical_string: String, + pub status: String, + pub public_key: Option, // Store the public key in base64 or hex + pub key_id: Option, // Store the AWS or Vault Key ID +} + +pub fn read_wal() -> Result> { + if !Path::new(WAL_FILE).exists() { + return Ok(vec![]); + } + let file = fs::File::open(WAL_FILE)?; + let reader = BufReader::new(file); + let entries: Vec = serde_json::from_reader(reader)?; + Ok(entries) +} + +pub fn write_wal(entries: &[WalEntry]) -> Result<()> { + let file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(WAL_FILE)?; + let writer = BufWriter::new(file); + serde_json::to_writer(writer, entries)?; + Ok(()) +} + +pub fn append_to_wal(entry: WalEntry) -> Result<()> { + let mut entries = read_wal()?; + entries.push(entry); + write_wal(&entries) +} + +pub fn update_wal_status(canonical_string: &str, new_status: &str) -> Result<()> { + let mut entries = read_wal()?; + for entry in &mut entries { + if entry.canonical_string == canonical_string { + entry.status = new_status.to_string(); + } + } + write_wal(&entries) +} + +pub fn update_wal_entry(canonical_string: &str, update_fn: F) -> Result<()> +where + F: Fn(&mut WalEntry), +{ + let mut entries = read_wal()?; + for entry in &mut entries { + if entry.canonical_string == canonical_string { + update_fn(entry); + } + } + write_wal(&entries) +} diff --git a/util/signing/signing-admin/src/key_manager.rs b/util/signing/signing-admin/src/key_manager.rs index ad259f12e..49fe0bfbe 100644 --- a/util/signing/signing-admin/src/key_manager.rs +++ b/util/signing/signing-admin/src/key_manager.rs @@ -17,8 +17,16 @@ where Self { application, backend } } - pub async fn rotate_key(&self, key_id: &str) -> Result<()> { - self.backend.rotate_key(key_id).await + pub async fn create_key(&self, key_id: &str) -> Result { + self.backend.create_key(key_id).await } + pub async fn rotate_key(&self, new_key_id: &str) -> Result<()> { + self.backend.rotate_key(new_key_id).await + } + + pub async fn notify_application(&self, public_key: Vec) -> Result<()> { + self.application.notify_public_key(public_key).await + } } +