Skip to content

Commit

Permalink
feat: implement 2PC with WAL file
Browse files Browse the repository at this point in the history
  • Loading branch information
andygolay committed Jan 22, 2025
1 parent 9c415ad commit 362504a
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 106 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions util/signing/signing-admin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
34 changes: 28 additions & 6 deletions util/signing/signing-admin/src/backend/aws.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -40,27 +40,49 @@ impl AwsBackend {
}
}

#[async_trait::async_trait]
#[async_trait]
impl SigningBackend for AwsBackend {
async fn create_key(&self, _key_id: &str) -> Result<String> {
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()
} else {
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)
.target_key_id(&new_key_id)
.send()
.await
.context("Failed to update AWS KMS alias")?;

Ok(())
}
}

8 changes: 8 additions & 0 deletions util/signing/signing-admin/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>; // 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),
Expand All @@ -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<String> {
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,
Expand Down
26 changes: 24 additions & 2 deletions util/signing/signing-admin/src/backend/vault.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<String> {
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(())
}
}


1 change: 1 addition & 0 deletions util/signing/signing-admin/src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -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")]
Expand Down
150 changes: 54 additions & 96 deletions util/signing/signing-admin/src/cli/rotate_key.rs
Original file line number Diff line number Diff line change
@@ -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<Ed25519>),
Aws(AwsKms<Secp256k1>),
}

impl SignerBackend {
/// Retrieve the public key from the signer
async fn public_key(&self) -> Result<Vec<u8>> {
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::<Ed25519>::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::<Secp256k1>::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(())
}
66 changes: 66 additions & 0 deletions util/signing/signing-admin/src/cli/wal.rs
Original file line number Diff line number Diff line change
@@ -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<String>, // Store the public key in base64 or hex
pub key_id: Option<String>, // Store the AWS or Vault Key ID
}

pub fn read_wal() -> Result<Vec<WalEntry>> {
if !Path::new(WAL_FILE).exists() {
return Ok(vec![]);
}
let file = fs::File::open(WAL_FILE)?;
let reader = BufReader::new(file);
let entries: Vec<WalEntry> = 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<F>(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)
}
Loading

0 comments on commit 362504a

Please sign in to comment.