use std::sync::Arc;
use async_trait::async_trait;
use chrono::Duration;
use mockforge_platform_signing::{
PlatformSigner, RotationError, RotationEvent, RotationPhase, RotationStateMachine,
};
use mockforge_registry_core::models::{audit_log::record_audit_event, AuditEventType};
use serde_json::json;
use sqlx::PgPool;
use thiserror::Error;
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct OperatorIdentity {
pub org_id: Uuid,
pub user_id: Uuid,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
}
pub async fn audited_begin_handover<C, N>(
pool: &PgPool,
operator: &OperatorIdentity,
state_machine: &RotationStateMachine<C>,
next: &N,
transition_window: Duration,
) -> Result<RotationEvent, RotationError>
where
C: PlatformSigner,
N: PlatformSigner,
{
let event = state_machine.begin_handover(next, transition_window).await?;
let metadata = json!({
"from_key_id": event.payload.from_key_id,
"to_key_id": event.payload.to_key_id,
"from_algorithm": event.payload.from_algorithm,
"to_algorithm": event.payload.to_algorithm,
"issued_at": event.payload.issued_at,
"transition_until": event.payload.transition_until,
"from_public_key_b64": event.payload.from_public_key_b64,
"to_public_key_b64": event.payload.to_public_key_b64,
});
record_audit_event(
pool,
operator.org_id,
Some(operator.user_id),
AuditEventType::PlatformSigningRotationStarted,
format!(
"Platform signing-root rotation started: {} → {} (transition until {})",
event.payload.from_key_id, event.payload.to_key_id, event.payload.transition_until
),
Some(metadata),
operator.ip_address.as_deref(),
operator.user_agent.as_deref(),
)
.await;
Ok(event)
}
pub async fn audited_retire_old<C: PlatformSigner>(
pool: &PgPool,
operator: &OperatorIdentity,
state_machine: &RotationStateMachine<C>,
) -> Result<(), RotationError> {
let last_event = state_machine.last_event().await;
state_machine.retire_old().await?;
let metadata = last_event.map(|ev| {
json!({
"from_key_id": ev.payload.from_key_id,
"to_key_id": ev.payload.to_key_id,
"transition_closed_at": ev.payload.transition_until,
})
});
record_audit_event(
pool,
operator.org_id,
Some(operator.user_id),
AuditEventType::PlatformSigningKeyRetired,
"Platform signing-root: previous key retired after transition window".to_string(),
metadata,
operator.ip_address.as_deref(),
operator.user_agent.as_deref(),
)
.await;
Ok(())
}
pub async fn audited_emergency_revoke<C: PlatformSigner>(
pool: &PgPool,
operator: &OperatorIdentity,
state_machine: &RotationStateMachine<C>,
reason: &str,
) -> Result<(), RotationError> {
let current_key_id = state_machine_current_key_id(state_machine);
state_machine.emergency_revoke_current().await?;
let metadata = json!({
"reason": reason,
"key_id_revoked": current_key_id,
});
record_audit_event(
pool,
operator.org_id,
Some(operator.user_id),
AuditEventType::PlatformSigningKeyRevoked,
format!("Platform signing-root: emergency revocation — {reason}"),
Some(metadata),
operator.ip_address.as_deref(),
operator.user_agent.as_deref(),
)
.await;
Ok(())
}
fn state_machine_current_key_id<C: PlatformSigner>(
_sm: &RotationStateMachine<C>,
) -> Option<String> {
None
}
#[derive(Debug, Error)]
pub enum ControllerError {
#[error(transparent)]
Rotation(#[from] RotationError),
#[error("signer backend error: {0}")]
Backend(String),
#[error("platform-signing controller not configured")]
NotConfigured,
}
#[async_trait]
pub trait PlatformSigningController: Send + Sync {
async fn phase(&self) -> RotationPhase;
async fn last_event(&self) -> Option<RotationEvent>;
async fn trusted_key_ids(&self) -> Vec<String>;
async fn begin_handover(
&self,
operator: &OperatorIdentity,
to_key_id: &str,
transition_window: Duration,
) -> Result<RotationEvent, ControllerError>;
async fn retire_old(&self, operator: &OperatorIdentity) -> Result<(), ControllerError>;
async fn emergency_revoke(
&self,
operator: &OperatorIdentity,
reason: &str,
) -> Result<(), ControllerError>;
}
#[async_trait]
pub trait NextSignerFactory: Send + Sync {
async fn build(
&self,
key_id: &str,
) -> Result<Box<dyn PlatformSigner>, mockforge_platform_signing::SignerError>;
}
pub struct StateMachineController<S: PlatformSigner> {
pool: PgPool,
state_machine: RotationStateMachine<S>,
next_factory: Arc<dyn NextSignerFactory>,
}
impl<S: PlatformSigner + 'static> StateMachineController<S> {
pub fn new(
pool: PgPool,
state_machine: RotationStateMachine<S>,
next_factory: Arc<dyn NextSignerFactory>,
) -> Self {
Self {
pool,
state_machine,
next_factory,
}
}
}
#[async_trait]
impl<S: PlatformSigner + 'static> PlatformSigningController for StateMachineController<S> {
async fn phase(&self) -> RotationPhase {
self.state_machine.phase().await
}
async fn last_event(&self) -> Option<RotationEvent> {
self.state_machine.last_event().await
}
async fn trusted_key_ids(&self) -> Vec<String> {
let last = self.state_machine.last_event().await;
match (self.state_machine.phase().await, last) {
(RotationPhase::Transitioning, Some(ev)) => {
vec![ev.payload.from_key_id, ev.payload.to_key_id]
}
(RotationPhase::Active, Some(ev)) => vec![ev.payload.to_key_id],
(RotationPhase::Active, None) | (RotationPhase::Transitioning, None) => Vec::new(),
}
}
async fn begin_handover(
&self,
operator: &OperatorIdentity,
to_key_id: &str,
transition_window: Duration,
) -> Result<RotationEvent, ControllerError> {
let next = self
.next_factory
.build(to_key_id)
.await
.map_err(|e| ControllerError::Backend(e.to_string()))?;
let event = audited_begin_handover(
&self.pool,
operator,
&self.state_machine,
&next,
transition_window,
)
.await?;
Ok(event)
}
async fn retire_old(&self, operator: &OperatorIdentity) -> Result<(), ControllerError> {
audited_retire_old(&self.pool, operator, &self.state_machine).await?;
Ok(())
}
async fn emergency_revoke(
&self,
operator: &OperatorIdentity,
reason: &str,
) -> Result<(), ControllerError> {
audited_emergency_revoke(&self.pool, operator, &self.state_machine, reason).await?;
Ok(())
}
}
#[cfg(feature = "platform-signing-aws-kms")]
pub struct AwsKmsNextSignerFactory;
#[cfg(feature = "platform-signing-aws-kms")]
#[async_trait]
impl NextSignerFactory for AwsKmsNextSignerFactory {
async fn build(
&self,
key_id: &str,
) -> Result<Box<dyn PlatformSigner>, mockforge_platform_signing::SignerError> {
let signer =
mockforge_platform_signing::aws_kms::AwsKmsSigner::from_key_id(key_id.to_string())
.await?;
Ok(Box::new(signer))
}
}
#[cfg(feature = "platform-signing-aws-kms")]
pub async fn aws_kms_controller_from_env(
pool: PgPool,
) -> anyhow::Result<Option<Arc<dyn PlatformSigningController>>> {
use mockforge_platform_signing::aws_kms::{AwsKmsSigner, ENV_KEY_ID};
if std::env::var(ENV_KEY_ID).ok().filter(|v| !v.is_empty()).is_none() {
tracing::info!(
"{ENV_KEY_ID} not set — platform-signing rotation endpoints will return 503"
);
return Ok(None);
}
let signer = AwsKmsSigner::from_env().await?;
let state_machine = RotationStateMachine::new(signer);
let factory: Arc<dyn NextSignerFactory> = Arc::new(AwsKmsNextSignerFactory);
let controller: Arc<dyn PlatformSigningController> =
Arc::new(StateMachineController::new(pool, state_machine, factory));
tracing::info!("AWS-KMS platform-signing controller initialized");
Ok(Some(controller))
}
#[cfg(test)]
mod tests {
use super::*;
use mockforge_platform_signing::MockSigner;
use mockforge_platform_signing::SignerError;
use std::sync::Mutex;
pub(super) struct MockSignerFactory {
signers: Mutex<std::collections::HashMap<String, MockSigner>>,
}
impl MockSignerFactory {
pub fn new() -> Self {
Self {
signers: Mutex::new(std::collections::HashMap::new()),
}
}
pub fn insert(&self, key_id: &str, signer: MockSigner) {
self.signers.lock().unwrap().insert(key_id.to_string(), signer);
}
}
#[async_trait]
impl NextSignerFactory for MockSignerFactory {
async fn build(&self, key_id: &str) -> Result<Box<dyn PlatformSigner>, SignerError> {
let signer = self
.signers
.lock()
.unwrap()
.remove(key_id)
.ok_or_else(|| SignerError::InvalidKeyId(key_id.to_string()))?;
Ok(Box::new(signer))
}
}
#[tokio::test]
async fn trusted_key_ids_reflect_phase_transitions() {
let current = MockSigner::generate("key-old").unwrap();
let next = MockSigner::generate("key-new").unwrap();
let sm = RotationStateMachine::new(current);
assert_eq!(sm.phase().await, RotationPhase::Active);
assert!(sm.last_event().await.is_none());
let _event = sm.begin_handover(&next, Duration::milliseconds(50)).await.unwrap();
assert_eq!(sm.phase().await, RotationPhase::Transitioning);
let ev = sm.last_event().await.unwrap();
assert_eq!(ev.payload.from_key_id, "key-old");
assert_eq!(ev.payload.to_key_id, "key-new");
tokio::time::sleep(std::time::Duration::from_millis(80)).await;
sm.retire_old().await.unwrap();
assert_eq!(sm.phase().await, RotationPhase::Active);
}
}