use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::watch;
use tracing::info;
use vta_backup::restore::{PendingRestore, RestoreProvenance};
use vta_backup::{
BackupTarget, PreparedCommit, RestoreCommitter, RestoredSecrets, SeedStoreCommitter,
};
use vta_sdk::protocols::backup_management::types::BackupEnvironment;
use vti_common::error::AppError;
use vti_common::store::Store;
use crate::config::AppConfig;
use crate::server::AppState;
#[must_use]
pub fn environment_of(storage_key: Option<[u8; 32]>, in_enclave: bool) -> BackupEnvironment {
match (in_enclave, storage_key) {
(true, Some(_)) => BackupEnvironment::Tee,
(_, Some(_)) => BackupEnvironment::Hardened,
(_, None) => BackupEnvironment::Plain,
}
}
#[must_use]
pub fn persona_correlation_key(storage_key: Option<[u8; 32]>) -> [u8; 32] {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(b"vta-persona/correlation-index/v1");
h.update(storage_key.unwrap_or([0u8; 32]));
h.finalize().into()
}
pub async fn apply_pending_restore(
store: &Store,
storage_key: Option<[u8; 32]>,
environment: BackupEnvironment,
seed: &[u8],
config: &mut AppConfig,
) -> Result<Option<RestoreProvenance>, AppError> {
let target = BackupTarget {
store,
storage_key,
environment,
};
let ready = match vta_backup::restore::load_pending(&target, seed).await? {
PendingRestore::None | PendingRestore::Discarded { .. } => return Ok(None),
PendingRestore::Ready(ready) => ready,
};
let applied = ready.apply(&target, config).await?;
vta_persona::PersonaStore::new(
target.keyspace(crate::keyspaces::PERSONA)?,
persona_correlation_key(storage_key),
)
.rebuild_blinded_indexes()
.await?;
ready.finish(&target).await?;
info!(
restore_id = %applied.provenance.restore_id,
source_did = applied.provenance.source_did.as_deref().unwrap_or("unknown"),
source_environment = ?applied.provenance.source_environment,
target_environment = %applied.provenance.target_environment,
"this VTA's state now derives from a restore"
);
Ok(Some(applied.provenance))
}
pub async fn audit_restore_once(state: &AppState) -> Result<(), AppError> {
let access = state.backup_access();
let target = access.target();
let Some(p) = vta_backup::restore::read_provenance(&target).await? else {
return Ok(());
};
if p.audited {
return Ok(());
}
let detail = format!(
"restore={} source={} source_env={} target_env={} staged_at={}{}{}",
p.restore_id,
p.source_did.as_deref().unwrap_or("unknown"),
p.source_environment
.map_or_else(|| "unknown".to_string(), |e| e.to_string()),
p.target_environment,
p.staged_at.to_rfc3339(),
if p.internal_keys_lost.is_empty() {
String::new()
} else {
format!(" internal_keys_lost={}", p.internal_keys_lost.join(","))
},
if p.hosted_dids_detached.is_empty() {
String::new()
} else {
format!(" hosted_dids_detached={}", p.hosted_dids_detached.join(","))
},
);
crate::audit::record_with_detail(
&state.audit_sink,
"backup.restore.applied",
&p.staged_by,
p.source_did.as_deref(),
"success",
None,
None,
Some(&detail),
)
.await?;
vta_backup::restore::mark_provenance_audited(&target).await
}
static REBOOT_REQUESTED: AtomicBool = AtomicBool::new(false);
pub fn request_reboot(restart_tx: &watch::Sender<bool>) {
REBOOT_REQUESTED.store(true, Ordering::SeqCst);
crate::server::trigger_restart(restart_tx);
}
#[must_use]
pub fn reboot_requested() -> bool {
REBOOT_REQUESTED.load(Ordering::SeqCst)
}
pub fn reexec() -> ! {
let exe = std::env::current_exe();
let args: Vec<std::ffi::OsString> = std::env::args_os().skip(1).collect();
#[cfg(unix)]
if let Ok(exe) = &exe {
use std::os::unix::process::CommandExt;
info!("re-executing to apply the committed restore");
let err = std::process::Command::new(exe).args(&args).exec();
tracing::error!("re-exec failed: {err}");
}
tracing::error!(
"the VTA could not restart itself; start it again to apply the committed restore"
);
let _ = exe;
std::process::exit(75)
}
pub struct BackupAccess<'a> {
pub store: &'a Store,
pub storage_key: Option<[u8; 32]>,
pub in_enclave: bool,
pub seed_store: &'a dyn crate::keys::seed_store::SeedStore,
pub config: &'a tokio::sync::RwLock<AppConfig>,
}
impl<'a> BackupAccess<'a> {
pub fn target(&self) -> BackupTarget<'a> {
BackupTarget {
store: self.store,
storage_key: self.storage_key,
environment: environment_of(self.storage_key, self.in_enclave),
}
}
pub async fn committer(&self) -> ServiceCommitter<'a> {
#[cfg(feature = "tee")]
if self.in_enclave {
let config = self.config.read().await;
if let Some(kms) = config.tee.kms.clone() {
let running_jwt = config
.auth
.jwt_signing_key
.as_deref()
.and_then(|b64| {
use base64::Engine;
base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(b64)
.ok()
})
.and_then(|b| <[u8; 32]>::try_from(b.as_slice()).ok());
return ServiceCommitter::Enclave(Box::new(EnclaveCommitter {
kms,
store: self.store.clone(),
running_did: config.vta_did.clone(),
running_jwt,
}));
}
}
ServiceCommitter::SeedStore(SeedStoreCommitter {
seed_store: self.seed_store,
})
}
}
pub enum ServiceCommitter<'a> {
SeedStore(SeedStoreCommitter<'a>),
#[cfg(feature = "tee")]
Enclave(Box<EnclaveCommitter>),
}
#[async_trait::async_trait]
impl RestoreCommitter for ServiceCommitter<'_> {
async fn prepare(&self, secrets: &RestoredSecrets<'_>) -> Result<PreparedCommit, AppError> {
match self {
Self::SeedStore(c) => c.prepare(secrets).await,
#[cfg(feature = "tee")]
Self::Enclave(c) => c.prepare(secrets).await,
}
}
async fn commit(
&self,
secrets: &RestoredSecrets<'_>,
prepared: PreparedCommit,
) -> Result<(), AppError> {
match self {
Self::SeedStore(c) => c.commit(secrets, prepared).await,
#[cfg(feature = "tee")]
Self::Enclave(c) => c.commit(secrets, prepared).await,
}
}
async fn abort(&self) {
match self {
Self::SeedStore(c) => c.abort().await,
#[cfg(feature = "tee")]
Self::Enclave(c) => c.abort().await,
}
}
}
#[cfg(feature = "tee")]
pub struct EnclaveCommitter {
kms: crate::config::TeeKmsConfig,
store: Store,
running_did: Option<String>,
running_jwt: Option<[u8; 32]>,
}
#[cfg(feature = "tee")]
impl EnclaveCommitter {
async fn reserve_anchor(
&self,
did: &str,
live: Option<(u64, bool)>,
) -> Result<Option<vta_support::restore_stage::AnchorBinding>, AppError> {
if self.kms.anchor.is_none() {
return Ok(None);
}
if self.running_did.as_deref() == Some(did)
&& let Some((version, true)) = live
{
return Ok(Some(vta_support::restore_stage::AnchorBinding {
did: did.to_owned(),
version,
}));
}
let Some(counter) = build_anchor_counter(&self.kms, Some(did)).await? else {
return Ok(None);
};
let version = match counter.read().await? {
None => {
counter.init(0, [0u8; 32]).await?;
0
}
Some(n) => {
counter.set(n, n + 1, [0u8; 32]).await?;
n + 1
}
};
Ok(Some(vta_support::restore_stage::AnchorBinding {
did: did.to_owned(),
version,
}))
}
}
#[cfg(feature = "tee")]
#[async_trait::async_trait]
impl RestoreCommitter for EnclaveCommitter {
async fn prepare(&self, secrets: &RestoredSecrets<'_>) -> Result<PreparedCommit, AppError> {
let jwt = secrets.jwt_key.or(self.running_jwt).ok_or_else(|| {
AppError::Internal(
"neither the backup nor this enclave has a JWT signing key to seal".into(),
)
})?;
let row =
crate::tee::kms_bootstrap::seal_restored_secrets(&self.kms, secrets.seed, &jwt).await?;
let live = vti_common::integrity::seal_and_freeze_for_restore().await?;
let anchor = match secrets.vta_did {
Some(did) => match self.reserve_anchor(did, live).await {
Ok(anchor) => anchor,
Err(e) => {
vti_common::integrity::thaw_after_aborted_restore();
return Err(e);
}
},
None => None,
};
Ok(PreparedCommit {
tee_secrets_row: Some(row),
anchor,
})
}
async fn commit(
&self,
_secrets: &RestoredSecrets<'_>,
prepared: PreparedCommit,
) -> Result<(), AppError> {
let row = prepared
.tee_secrets_row
.ok_or_else(|| AppError::Internal("enclave commit without a sealed row".into()))?;
let bootstrap = self.store.keyspace(crate::keyspaces::BOOTSTRAP)?;
bootstrap
.insert_raw(vta_support::restore_stage::TEE_RESTORED_SECRETS_KEY, row)
.await?;
bootstrap.persist().await
}
async fn abort(&self) {
vti_common::integrity::thaw_after_aborted_restore();
}
}
#[cfg(feature = "tee")]
pub async fn build_anchor_counter(
kms: &crate::config::TeeKmsConfig,
vta_did: Option<&str>,
) -> Result<Option<std::sync::Arc<dyn vti_common::integrity::AnchorCounter>>, AppError> {
use base64::Engine;
let (Some(anchor_cfg), Some(vta_did)) = (kms.anchor.as_ref(), vta_did) else {
if kms.anchor.is_some() {
tracing::warn!(
"tee.kms.anchor is configured but vta_did is unset — booting \
manifest-only (P0.2a); the external rollback counter is disabled"
);
}
return Ok(None);
};
let writer = match anchor_cfg.writer_credential_ciphertext.as_ref() {
Some(b64) => {
let ct = base64::engine::general_purpose::STANDARD
.decode(b64)
.map_err(|e| {
AppError::Config(format!(
"tee.kms.anchor.writer_credential_ciphertext is not valid base64: {e}"
))
})?;
let pt = crate::tee::kms_bootstrap::attested_decrypt(kms, &ct).await?;
let creds: crate::tee::anchor::WriterCredentials = serde_json::from_slice(&pt)
.map_err(|e| {
AppError::Config(format!(
"anchor writer credential did not decrypt to \
{{access_key_id, secret_access_key}}: {e}"
))
})?;
info!("anchor writer credential unsealed (attestation-gated, P0.2c)");
Some(creds)
}
None => None,
};
Ok(Some(std::sync::Arc::new(
crate::tee::anchor::DynamoAnchorCounter::new(
&kms.region,
anchor_cfg.table_name.clone(),
vta_did.to_owned(),
writer,
)
.await,
)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_environment_follows_the_storage_key_and_the_enclave() {
assert_eq!(environment_of(None, false), BackupEnvironment::Plain);
assert_eq!(
environment_of(Some([1; 32]), false),
BackupEnvironment::Hardened
);
assert_eq!(environment_of(Some([1; 32]), true), BackupEnvironment::Tee);
}
#[cfg(feature = "tee")]
#[test]
fn restore_row_names_match_the_enclave_s() {
assert_eq!(
vta_backup::restore::TEE_VTA_DID_KEY,
crate::tee::did_autogen::VTA_DID_STORE_KEY
);
assert_eq!(
vta_backup::restore::TEE_DID_LOG_KEY,
crate::tee::did_autogen::DID_LOG_STORE_KEY
);
assert_eq!(
vta_backup::restore::TEE_CARVEOUT_CLOSED_KEY,
crate::tee::admin_bootstrap::BOOTSTRAP_CARVEOUT_CLOSED_KEY
);
}
#[tokio::test]
async fn a_restore_applied_at_boot_rebuilds_what_it_does_not_carry() {
use vta_persona::model::{Provenance, ValueType};
let open = || {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(&vti_common::config::StoreConfig {
data_dir: dir.path().into(),
})
.unwrap();
(dir, store)
};
let source_seed = [0x11u8; 32];
let (_sd, source) = open();
let persona = vta_persona::PersonaStore::new(
source.keyspace(crate::keyspaces::PERSONA).unwrap(),
persona_correlation_key(None),
);
let phone = |v: &str| {
vta_persona::store::new_attribute(
"phone.mobile",
ValueType::String,
serde_json::json!(v),
Provenance::SelfAsserted,
)
};
let a = phone("+61 400");
persona.put(a.clone(), None).await.unwrap();
persona.put(phone("+61 400"), None).await.unwrap();
let mut source_config: AppConfig = toml::from_str("").unwrap();
source_config.vta_did = Some("did:example:restored".into());
let seed_store = vta_backup_test_seed(&source_seed);
let envelope = vta_backup::ops::export_backup(
&BackupTarget {
store: &source,
storage_key: None,
environment: BackupEnvironment::Plain,
},
&seed_store,
&source_config,
&crate::test_support::super_admin_claims(),
"restore-glue-password",
false,
)
.await
.unwrap();
let payload = vta_backup::ops::decrypt_backup(&envelope, "restore-glue-password").unwrap();
let (td, target) = open();
let target_seed_store = vta_backup_test_seed(&[0x22u8; 32]);
let key_for = |seed: &[u8]| *crate::hardened_bootstrap::derive_storage_key(seed, "salt");
let before = BackupTarget {
store: &target,
storage_key: Some(key_for(&[0x22u8; 32])),
environment: BackupEnvironment::Hardened,
};
let mut config: AppConfig = toml::from_str("").unwrap();
config.config_path = td.path().join("config.toml");
let config_lock = tokio::sync::RwLock::new(config);
vta_backup::ops::stage_import(
payload,
vta_backup::ops::StageRequest {
target: &before,
config: &config_lock,
committer: &SeedStoreCommitter {
seed_store: &target_seed_store,
},
auth: &crate::test_support::super_admin_claims(),
replace_identity: false,
},
)
.await
.unwrap();
let seed = crate::keys::seed_store::SeedStore::get(&target_seed_store)
.await
.unwrap()
.unwrap();
assert_eq!(seed, source_seed, "the commit adopted the restored seed");
let key = key_for(&seed);
let storage_key = Some(key);
let mut config = config_lock.into_inner();
let provenance = apply_pending_restore(
&target,
storage_key,
BackupEnvironment::Hardened,
&seed,
&mut config,
)
.await
.unwrap()
.expect("a committed restore is applied");
assert_eq!(
provenance.source_did.as_deref(),
Some("did:example:restored")
);
let restored = vta_persona::PersonaStore::new(
target
.keyspace(crate::keyspaces::PERSONA)
.unwrap()
.with_encryption(key),
persona_correlation_key(storage_key),
);
assert_eq!(
restored
.correlation_count(&serde_json::json!("+61 400"), &a.attribute_id)
.await
.unwrap(),
1,
"the correlation index is rebuilt under the target's key"
);
let saved = std::fs::read_to_string(td.path().join("config.toml")).unwrap();
assert!(
saved.contains("did:example:restored"),
"the restored identity survives a process restart"
);
assert!(
apply_pending_restore(
&target,
storage_key,
BackupEnvironment::Hardened,
&seed,
&mut config
)
.await
.unwrap()
.is_none()
);
}
fn vta_backup_test_seed(seed: &[u8]) -> MemSeed {
MemSeed(std::sync::Mutex::new(seed.to_vec()))
}
struct MemSeed(std::sync::Mutex<Vec<u8>>);
impl crate::keys::seed_store::SeedStore for MemSeed {
fn get(
&self,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Option<Vec<u8>>, AppError>> + Send + '_>,
> {
let v = self.0.lock().unwrap().clone();
Box::pin(async move { Ok(Some(v)) })
}
fn set(
&self,
seed: &[u8],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), AppError>> + Send + '_>>
{
*self.0.lock().unwrap() = seed.to_vec();
Box::pin(async { Ok(()) })
}
}
#[test]
fn restore_row_names_match_the_hardened_bootstrap_s() {
assert_eq!(
vta_backup::restore::HARDENED_JWT_KEY,
crate::hardened_bootstrap::HARDENED_JWT_KEY
);
}
}