- Add badges, competitive comparison, and 30-sec demo to README - Add Production Status section showing OQS backend is production-ready - Mark PQC KEM/signing operations complete in roadmap - Fix GitHub URL - Create CHANGELOG.md documenting all recent changes Positions SecretumVault as first Rust vault with production PQC.
429 lines
14 KiB
Rust
429 lines
14 KiB
Rust
#[cfg(feature = "cli")]
|
|
use std::path::PathBuf;
|
|
#[cfg(feature = "cli")]
|
|
use std::sync::Arc;
|
|
|
|
#[cfg(feature = "cli")]
|
|
use clap::Parser;
|
|
#[cfg(feature = "cli")]
|
|
use secretumvault::cli::{Cli, Command, OperatorCommand, SecretCommand};
|
|
#[cfg(feature = "cli")]
|
|
use secretumvault::config::VaultConfig;
|
|
#[cfg(feature = "cli")]
|
|
use secretumvault::core::VaultCore;
|
|
|
|
#[tokio::main]
|
|
#[cfg(feature = "cli")]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let cli = Cli::parse();
|
|
|
|
// Set up logging
|
|
tracing_subscriber::fmt()
|
|
.with_max_level(
|
|
cli.log_level
|
|
.parse::<tracing::Level>()
|
|
.unwrap_or(tracing::Level::INFO),
|
|
)
|
|
.init();
|
|
|
|
// Determine config path
|
|
let config_path = cli.config.unwrap_or_else(|| PathBuf::from("svault.toml"));
|
|
|
|
match cli.command {
|
|
Command::Server { address, port } => server_command(&config_path, &address, port).await?,
|
|
Command::Operator(cmd) => operator_command(&config_path, cmd).await?,
|
|
Command::Secret(cmd) => secret_command(cmd).await?,
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "cli")]
|
|
async fn server_command(
|
|
config_path: &PathBuf,
|
|
cli_address: &str,
|
|
cli_port: u16,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
#[cfg(feature = "server")]
|
|
{
|
|
use secretumvault::api::server::build_router;
|
|
|
|
tracing::info!("Loading configuration from {:?}", config_path);
|
|
let config = VaultConfig::from_file(config_path)?;
|
|
let vault = Arc::new(VaultCore::from_config(&config).await?);
|
|
tracing::info!("Vault initialized successfully");
|
|
|
|
let bind_address = resolve_bind_address(&config.server.address, cli_address, cli_port);
|
|
let router = build_router(vault);
|
|
let tls_config = build_tls_config(&config.server);
|
|
|
|
start_server(&bind_address, router, tls_config).await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(feature = "server"))]
|
|
{
|
|
tracing::error!("Server feature not enabled. Compile with --features server");
|
|
Err("Server feature not enabled".into())
|
|
}
|
|
}
|
|
|
|
#[cfg(all(feature = "cli", feature = "server"))]
|
|
fn resolve_bind_address(config_address: &str, cli_address: &str, cli_port: u16) -> String {
|
|
if cli_address != "127.0.0.1" || cli_port != 8200 {
|
|
format!("{}:{}", cli_address, cli_port)
|
|
} else {
|
|
config_address.to_string()
|
|
}
|
|
}
|
|
|
|
#[cfg(all(feature = "cli", feature = "server"))]
|
|
fn build_tls_config(
|
|
server_config: &secretumvault::config::ServerSection,
|
|
) -> Option<secretumvault::api::tls::TlsConfig> {
|
|
match (&server_config.tls_cert, &server_config.tls_key) {
|
|
(Some(cert), Some(key)) => Some(secretumvault::api::tls::TlsConfig::new(
|
|
cert.clone(),
|
|
key.clone(),
|
|
server_config.tls_client_ca.clone(),
|
|
)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
#[cfg(all(feature = "cli", feature = "server"))]
|
|
async fn shutdown_signal() {
|
|
use tokio::signal;
|
|
|
|
let ctrl_c = async {
|
|
signal::ctrl_c()
|
|
.await
|
|
.expect("Failed to install Ctrl+C handler");
|
|
};
|
|
|
|
#[cfg(unix)]
|
|
let terminate = async {
|
|
signal::unix::signal(signal::unix::SignalKind::terminate())
|
|
.expect("Failed to install signal handler")
|
|
.recv()
|
|
.await;
|
|
};
|
|
|
|
#[cfg(not(unix))]
|
|
let terminate = std::future::pending::<()>();
|
|
|
|
tokio::select! {
|
|
_ = ctrl_c => {},
|
|
_ = terminate => {},
|
|
}
|
|
|
|
tracing::info!("Shutdown signal received, stopping server gracefully...");
|
|
}
|
|
|
|
#[cfg(all(feature = "cli", feature = "server"))]
|
|
async fn start_server(
|
|
bind_address: &str,
|
|
app: axum::Router,
|
|
tls_config: Option<secretumvault::api::tls::TlsConfig>,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
use std::net::SocketAddr;
|
|
|
|
use tokio::net::TcpListener;
|
|
|
|
let addr: SocketAddr = bind_address
|
|
.parse()
|
|
.map_err(|_| format!("Invalid bind address: {}", bind_address))?;
|
|
|
|
let listener = TcpListener::bind(addr).await?;
|
|
|
|
match tls_config {
|
|
Some(tls) => {
|
|
tls.validate()?;
|
|
|
|
let rustls_config = if tls.client_ca_path.is_some() {
|
|
secretumvault::api::tls::load_server_config_with_mtls(&tls)?
|
|
} else {
|
|
secretumvault::api::tls::load_server_config(&tls)?
|
|
};
|
|
|
|
tracing::info!("Starting HTTPS server on https://{}", addr);
|
|
if tls.client_ca_path.is_some() {
|
|
tracing::info!("mTLS enabled - client certificate verification required");
|
|
}
|
|
|
|
let tls_acceptor = tokio_rustls::TlsAcceptor::from(std::sync::Arc::new(rustls_config));
|
|
|
|
serve_with_tls(listener, app, tls_acceptor, shutdown_signal()).await?;
|
|
Ok(())
|
|
}
|
|
None => {
|
|
tracing::warn!("Starting HTTP server on http://{}", addr);
|
|
tracing::warn!("TLS not configured. For production, configure tls_cert and tls_key");
|
|
|
|
serve_plain(listener, app, shutdown_signal()).await?;
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(all(feature = "cli", feature = "server"))]
|
|
async fn serve_plain(
|
|
listener: tokio::net::TcpListener,
|
|
app: axum::Router,
|
|
shutdown: impl std::future::Future<Output = ()> + Send + 'static,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
axum::serve(listener, app)
|
|
.with_graceful_shutdown(shutdown)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(all(feature = "cli", feature = "server"))]
|
|
async fn serve_with_tls(
|
|
listener: tokio::net::TcpListener,
|
|
app: axum::Router,
|
|
tls_acceptor: tokio_rustls::TlsAcceptor,
|
|
shutdown: impl std::future::Future<Output = ()> + Send + 'static,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
use hyper_util::rt::{TokioExecutor, TokioIo};
|
|
use hyper_util::server::conn::auto::Builder;
|
|
use hyper_util::service::TowerToHyperService;
|
|
|
|
let app = app.into_service();
|
|
|
|
tokio::pin!(shutdown);
|
|
|
|
loop {
|
|
tokio::select! {
|
|
result = listener.accept() => {
|
|
let (tcp_stream, _remote_addr) = result?;
|
|
let tls_acceptor = tls_acceptor.clone();
|
|
let app = app.clone();
|
|
|
|
tokio::spawn(async move {
|
|
// TLS handshake
|
|
let tls_stream = match tls_acceptor.accept(tcp_stream).await {
|
|
Ok(stream) => stream,
|
|
Err(e) => {
|
|
tracing::warn!("TLS handshake failed: {}", e);
|
|
return;
|
|
}
|
|
};
|
|
|
|
let io = TokioIo::new(tls_stream);
|
|
let hyper_service = TowerToHyperService::new(app);
|
|
|
|
if let Err(err) = Builder::new(TokioExecutor::new())
|
|
.serve_connection(io, hyper_service)
|
|
.await
|
|
{
|
|
tracing::warn!("Error serving connection: {}", err);
|
|
}
|
|
});
|
|
}
|
|
_ = &mut shutdown => {
|
|
tracing::info!("Shutdown signal received, stopping server...");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "cli")]
|
|
async fn operator_command(
|
|
config_path: &PathBuf,
|
|
cmd: OperatorCommand,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
tracing::info!("Loading configuration from {:?}", config_path);
|
|
|
|
let config = VaultConfig::from_file(config_path)?;
|
|
let vault = Arc::new(VaultCore::from_config(&config).await?);
|
|
|
|
tracing::info!("Vault loaded successfully");
|
|
|
|
match cmd {
|
|
OperatorCommand::Init { shares, threshold } => {
|
|
tracing::info!(
|
|
"Initializing vault with {} shares, {} threshold",
|
|
shares,
|
|
threshold
|
|
);
|
|
|
|
match secretumvault::cli::commands::init_vault(&vault, shares, threshold).await {
|
|
Ok(share_list) => {
|
|
secretumvault::cli::commands::print_init_result(&share_list, threshold as u64);
|
|
tracing::info!("Vault initialized successfully");
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to initialize vault: {}", e);
|
|
return Err(format!("Init failed: {}", e).into());
|
|
}
|
|
}
|
|
}
|
|
|
|
OperatorCommand::Unseal { shares } => {
|
|
tracing::info!("Unsealing vault with {} shares", shares.len());
|
|
|
|
match secretumvault::cli::commands::unseal_vault(&vault, &shares).await {
|
|
Ok(success) => {
|
|
if success {
|
|
println!("✓ Vault unsealed successfully!");
|
|
tracing::info!("Vault unsealed");
|
|
} else {
|
|
println!("✗ Vault is still sealed (more shares needed?)");
|
|
tracing::warn!("Vault still sealed");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to unseal vault: {}", e);
|
|
return Err(format!("Unseal failed: {}", e).into());
|
|
}
|
|
}
|
|
}
|
|
|
|
OperatorCommand::Seal => {
|
|
tracing::info!("Sealing vault");
|
|
|
|
match secretumvault::cli::commands::seal_vault(&vault).await {
|
|
Ok(()) => {
|
|
println!("✓ Vault sealed successfully!");
|
|
tracing::info!("Vault sealed");
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to seal vault: {}", e);
|
|
return Err(format!("Seal failed: {}", e).into());
|
|
}
|
|
}
|
|
}
|
|
|
|
OperatorCommand::Status => {
|
|
tracing::info!("Checking vault status");
|
|
|
|
match secretumvault::cli::commands::vault_status(&vault).await {
|
|
Ok((sealed, initialized)) => {
|
|
secretumvault::cli::commands::print_status(sealed, initialized);
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to get vault status: {}", e);
|
|
return Err(format!("Status check failed: {}", e).into());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "cli")]
|
|
async fn secret_command(cmd: SecretCommand) -> Result<(), Box<dyn std::error::Error>> {
|
|
use secretumvault::cli::client::VaultClient;
|
|
|
|
match cmd {
|
|
SecretCommand::Read {
|
|
path,
|
|
address,
|
|
port,
|
|
token,
|
|
} => {
|
|
tracing::info!("Reading secret from {}:{}: {}", address, port, path);
|
|
|
|
let client = VaultClient::new(&address, port, token);
|
|
match client.read_secret(&path).await {
|
|
Ok(data) => {
|
|
println!("{}", serde_json::to_string_pretty(&data)?);
|
|
tracing::info!("Secret read successfully");
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to read secret: {}", e);
|
|
return Err(format!("Read failed: {}", e).into());
|
|
}
|
|
}
|
|
}
|
|
|
|
SecretCommand::Write {
|
|
path,
|
|
data,
|
|
address,
|
|
port,
|
|
token,
|
|
} => {
|
|
tracing::info!("Writing secret to {}:{}: {}", address, port, path);
|
|
|
|
let client = VaultClient::new(&address, port, token);
|
|
let payload: serde_json::Value = serde_json::from_str(&data)?;
|
|
|
|
match client.write_secret(&path, &payload).await {
|
|
Ok(response) => {
|
|
println!("✓ Secret written successfully!");
|
|
if let Some(data) = response.get("data") {
|
|
println!("{}", serde_json::to_string_pretty(data)?);
|
|
}
|
|
tracing::info!("Secret written successfully");
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to write secret: {}", e);
|
|
return Err(format!("Write failed: {}", e).into());
|
|
}
|
|
}
|
|
}
|
|
|
|
SecretCommand::Delete {
|
|
path,
|
|
address,
|
|
port,
|
|
token,
|
|
} => {
|
|
tracing::info!("Deleting secret from {}:{}: {}", address, port, path);
|
|
|
|
let client = VaultClient::new(&address, port, token);
|
|
match client.delete_secret(&path).await {
|
|
Ok(()) => {
|
|
println!("✓ Secret deleted successfully!");
|
|
tracing::info!("Secret deleted successfully");
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to delete secret: {}", e);
|
|
return Err(format!("Delete failed: {}", e).into());
|
|
}
|
|
}
|
|
}
|
|
|
|
SecretCommand::List {
|
|
path,
|
|
address,
|
|
port,
|
|
token,
|
|
} => {
|
|
tracing::info!("Listing secrets at {}:{}: {}", address, port, path);
|
|
|
|
let client = VaultClient::new(&address, port, token);
|
|
match client.list_secrets(&path).await {
|
|
Ok(keys) => {
|
|
println!("\nSecrets at {}:", path);
|
|
println!("━━━━━━━━━━━━━━━━━━━━━━");
|
|
for key in keys {
|
|
println!(" {}", key);
|
|
}
|
|
println!();
|
|
tracing::info!("Secrets listed successfully");
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to list secrets: {}", e);
|
|
return Err(format!("List failed: {}", e).into());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(feature = "cli"))]
|
|
fn main() {
|
|
eprintln!("CLI feature not enabled. Compile with --features cli");
|
|
std::process::exit(1);
|
|
}
|