use std::process::ExitCode;
use anyhow::{Context, Result};
use chrono::Duration;
use clap::Parser;
use mockforge_registry_server::database::Database;
#[cfg(feature = "platform-signing-aws-kms")]
use mockforge_registry_server::platform_signing::{aws_kms_controller_from_env, OperatorIdentity};
use uuid::Uuid;
#[derive(Parser, Debug)]
#[command(
name = "rotate-platform-key",
about = "One-shot platform signing-root rotation (Issue #568)",
long_about = "Drives the same audit-aware begin_handover the HTTP \
endpoint uses, against the registry's database. Skips \
the registry process — operators run this directly \
against the DB for air-gapped deployments or for the \
very first rotation in a new cluster."
)]
struct Args {
#[arg(long)]
to_key_id: String,
#[arg(long, default_value_t = 30)]
transition_window_days: i64,
#[arg(long)]
operator_org_id: Uuid,
#[arg(long)]
operator_user_id: Uuid,
}
#[tokio::main]
async fn main() -> ExitCode {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
"rotate_platform_key=info,mockforge_registry_server=info".into()
}),
)
.init();
match run().await {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
tracing::error!(error = ?err, "rotation failed");
eprintln!("Error: {err:#}");
ExitCode::FAILURE
}
}
}
#[cfg(feature = "platform-signing-aws-kms")]
async fn run() -> Result<()> {
let args = Args::parse();
if args.transition_window_days <= 0 {
anyhow::bail!("--transition-window-days must be positive");
}
let config = mockforge_registry_server::config::Config::load()
.context("loading registry config (DATABASE_URL etc.)")?;
let db = Database::connect(&config.database_url)
.await
.context("connecting to database")?;
let controller = aws_kms_controller_from_env(db.pool().clone())
.await
.context("initializing AWS KMS platform-signing controller")?
.ok_or_else(|| {
anyhow::anyhow!(
"MOCKFORGE_PLATFORM_SIGNING_KMS_KEY_ID is not set — \
this binary can only rotate from an already-configured \
active key. Run the one-time setup in the runbook first.",
)
})?;
let operator = OperatorIdentity {
org_id: args.operator_org_id,
user_id: args.operator_user_id,
ip_address: None,
user_agent: Some(format!("rotate-platform-key/{}", env!("CARGO_PKG_VERSION"))),
};
let event = controller
.begin_handover(&operator, &args.to_key_id, Duration::days(args.transition_window_days))
.await
.map_err(|e| anyhow::anyhow!("begin_handover failed: {e}"))?;
println!("{}", serde_json::to_string_pretty(&event)?);
tracing::info!(
from_key_id = %event.payload.from_key_id,
to_key_id = %event.payload.to_key_id,
transition_until = %event.payload.transition_until,
"rotation handover signed and recorded; plugin-hosts will pick it up on their next poll"
);
Ok(())
}
#[cfg(not(feature = "platform-signing-aws-kms"))]
async fn run() -> Result<()> {
let _ = Args::parse();
anyhow::bail!(
"this build of rotate-platform-key was compiled without the \
`platform-signing-aws-kms` feature — rebuild with \
`--features saas` or `--features platform-signing-aws-kms`"
)
}