1use base64::Engine;
32use base64::engine::general_purpose::URL_SAFE_NO_PAD as BASE64;
33use chrono::{DateTime, Utc};
34use serde::{Deserialize, Serialize};
35use tracing::{info, warn};
36
37use vta_keys::KeyOrigin;
38use vta_sdk::protocols::backup_management::types::{BackupEnvironment, BackupPayload};
39use vta_support::restore_stage::{self, AnchorBinding, OpenedStage, StageMeta, StageState};
40use vti_common::error::AppError;
41
42use crate::BackupTarget;
43
44pub const PROVENANCE_KEY: &str = "restore:provenance";
48pub const REBASELINE_KEY: &str = "restore:rebaseline";
51
52pub const TEE_VTA_DID_KEY: &str = "tee:vta_did";
56pub const TEE_DID_LOG_KEY: &str = "tee:did_log";
58pub const TEE_CARVEOUT_CLOSED_KEY: &str = "tee:bootstrap-carveout-closed";
60pub const HARDENED_JWT_KEY: &str = "hardened:jwt_key";
62
63const INTERNAL_KEY_PREFIX: &str = "internal:";
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct RestoreProvenance {
68 pub restore_id: String,
69 pub applied_at: DateTime<Utc>,
70 pub staged_at: DateTime<Utc>,
71 pub staged_by: String,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub source_did: Option<String>,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub source_environment: Option<BackupEnvironment>,
76 pub target_environment: BackupEnvironment,
77 #[serde(default)]
79 pub internal_keys_lost: Vec<String>,
80 #[serde(default)]
83 pub hosted_dids_detached: Vec<String>,
84 #[serde(default)]
88 pub audited: bool,
89}
90
91pub async fn read_provenance(
93 target: &BackupTarget<'_>,
94) -> Result<Option<RestoreProvenance>, AppError> {
95 target
96 .keyspace(vta_keyspaces::KEYS)?
97 .get::<RestoreProvenance>(PROVENANCE_KEY)
98 .await
99}
100
101pub async fn mark_provenance_audited(target: &BackupTarget<'_>) -> Result<(), AppError> {
103 let keys = target.keyspace(vta_keyspaces::KEYS)?;
104 if let Some(mut p) = keys.get::<RestoreProvenance>(PROVENANCE_KEY).await? {
105 p.audited = true;
106 keys.insert(PROVENANCE_KEY, &p).await?;
107 }
108 Ok(())
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct RebaselineMarker {
114 pub restore_id: String,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub anchor: Option<AnchorBinding>,
117}
118
119pub async fn read_rebaseline_marker(
121 target: &BackupTarget<'_>,
122) -> Result<Option<RebaselineMarker>, AppError> {
123 target
124 .keyspace(vta_keyspaces::KEYS)?
125 .get::<RebaselineMarker>(REBASELINE_KEY)
126 .await
127}
128
129pub async fn clear_rebaseline_marker(target: &BackupTarget<'_>) -> Result<(), AppError> {
131 let keys = target.keyspace(vta_keyspaces::KEYS)?;
132 keys.remove(REBASELINE_KEY).await?;
133 keys.persist().await
134}
135
136pub enum PendingRestore {
138 None,
139 Discarded {
142 restore_id: String,
143 },
144 Ready(Box<ReadyRestore>),
145}
146
147pub struct ReadyRestore {
149 stage: OpenedStage,
150}
151
152pub async fn load_pending(
155 target: &BackupTarget<'_>,
156 seed: &[u8],
157) -> Result<PendingRestore, AppError> {
158 let bootstrap = target.bootstrap()?;
159 match restore_stage::open_stage(&bootstrap, seed).await? {
160 StageState::None => Ok(PendingRestore::None),
161 StageState::Uncommitted { restore_id } => {
162 warn!(
163 restore_id,
164 "discarding a staged restore that does not open under this VTA's seed — \
165 its import never committed, so nothing was changed"
166 );
167 restore_stage::clear_stage(&bootstrap).await?;
168 Ok(PendingRestore::Discarded { restore_id })
169 }
170 StageState::Committed(stage) => Ok(PendingRestore::Ready(Box::new(ReadyRestore {
171 stage: *stage,
172 }))),
173 }
174}
175
176pub struct AppliedRestore {
178 pub provenance: RestoreProvenance,
179 pub config_changed: bool,
181}
182
183impl ReadyRestore {
184 pub fn meta(&self) -> &StageMeta {
185 &self.stage.meta
186 }
187
188 pub async fn apply(
197 &self,
198 target: &BackupTarget<'_>,
199 config: &mut vta_config::AppConfig,
200 ) -> Result<AppliedRestore, AppError> {
201 let meta = &self.stage.meta;
202 let payload = &self.stage.payload;
203 if meta.target_environment != target.environment {
204 return Err(AppError::Config(format!(
209 "a restore ({}) was staged for a {} deployment but this VTA booted as {}. \
210 Boot it with the configuration it had when the import was committed to \
211 apply the restore.",
212 meta.restore_id, meta.target_environment, target.environment
213 )));
214 }
215 crate::ops::validate_payload(payload)?;
216 let seed = zeroize::Zeroizing::new(
217 hex::decode(&payload.active_seed_hex)
218 .map_err(|e| AppError::Validation(format!("backup seed is not hex: {e}")))?,
219 );
220
221 info!(
222 restore_id = %meta.restore_id,
223 source_did = payload.config.vta_did.as_deref().unwrap_or("unknown"),
224 environment = %target.environment,
225 "applying staged restore"
226 );
227
228 wipe(target).await?;
229 write_state(target, payload, &seed).await?;
230 let internal_keys_lost = reconcile_internal_keys(target).await?;
231 let hosted_dids_detached = if meta.cross_identity {
232 detach_hosted_dids(target).await?
233 } else {
234 Vec::new()
235 };
236 let config_changed = write_deployment_rows(target, meta, payload, config).await?;
237
238 let provenance = RestoreProvenance {
239 restore_id: meta.restore_id.clone(),
240 applied_at: Utc::now(),
241 staged_at: meta.staged_at,
242 staged_by: meta.staged_by.clone(),
243 source_did: payload.config.vta_did.clone(),
244 source_environment: payload.source_environment,
245 target_environment: target.environment,
246 internal_keys_lost,
247 hosted_dids_detached,
248 audited: false,
249 };
250 target
251 .keyspace(vta_keyspaces::KEYS)?
252 .insert(PROVENANCE_KEY, &provenance)
253 .await?;
254 target.store.persist().await?;
255
256 if !provenance.internal_keys_lost.is_empty() {
257 warn!(
258 keys = ?provenance.internal_keys_lost,
259 "restored internal-key records whose material was not in the backup — \
260 these keys cannot sign and cannot be recovered"
261 );
262 }
263 if !provenance.hosted_dids_detached.is_empty() {
264 warn!(
265 dids = ?provenance.hosted_dids_detached,
266 "the restore replaced a different identity, so hosted-DID registrations \
267 were detached; re-attach each with `did-mgmt dids register`"
268 );
269 }
270 Ok(AppliedRestore {
271 provenance,
272 config_changed,
273 })
274 }
275
276 pub async fn finish(self, target: &BackupTarget<'_>) -> Result<(), AppError> {
280 restore_stage::clear_stage(&target.bootstrap()?).await?;
281 info!(restore_id = %self.stage.meta.restore_id, "staged restore applied");
282 Ok(())
283 }
284}
285
286async fn wipe(target: &BackupTarget<'_>) -> Result<(), AppError> {
293 for name in vta_keyspaces::ALL {
294 if *name == vta_keyspaces::BOOTSTRAP || *name == vta_keyspaces::INTERNAL_KEYS {
295 continue;
296 }
297 let ks = target.store.keyspace(name)?;
298 for key in ks.prefix_keys(Vec::<u8>::new()).await? {
299 ks.remove(key).await?;
300 }
301 }
302 Ok(())
303}
304
305async fn write_state(
306 target: &BackupTarget<'_>,
307 payload: &BackupPayload,
308 seed: &[u8],
309) -> Result<(), AppError> {
310 if payload.keyspaces.is_empty() {
311 let keys = target.keyspace(vta_keyspaces::KEYS)?;
312 let acl = target.keyspace(vta_keyspaces::ACL)?;
313 let contexts = target.keyspace(vta_keyspaces::CONTEXTS)?;
314 let did_templates = target.keyspace(vta_keyspaces::DID_TEMPLATES)?;
315 let audit = target.keyspace(vta_keyspaces::AUDIT)?;
316 let imported = target.keyspace(vta_keyspaces::IMPORTED_SECRETS)?;
317 #[cfg(feature = "webvh")]
318 let webvh = target.keyspace(vta_keyspaces::WEBVH)?;
319 let ks = vta_keyspaces::Keyspaces {
320 keys: &keys,
321 acl: &acl,
322 contexts: &contexts,
323 did_templates: &did_templates,
324 audit: &audit,
325 imported: &imported,
326 #[cfg(feature = "webvh")]
327 webvh: &webvh,
328 };
329 return crate::ops::write_legacy_payload(payload, &ks, seed).await;
330 }
331 for dump in &payload.keyspaces {
332 let ks = target.keyspace(&dump.name)?;
333 for (k, v) in &dump.rows {
334 let key = BASE64
335 .decode(k)
336 .map_err(|e| AppError::Validation(format!("restored row key: {e}")))?;
337 if vta_keyspaces::is_environment_bound(&dump.name, &key) {
339 continue;
340 }
341 let value = zeroize::Zeroizing::new(
342 BASE64
343 .decode(v)
344 .map_err(|e| AppError::Validation(format!("restored row value: {e}")))?,
345 );
346 ks.insert_raw(key, value.to_vec()).await?;
347 }
348 }
349 Ok(())
350}
351
352async fn reconcile_internal_keys(target: &BackupTarget<'_>) -> Result<Vec<String>, AppError> {
357 let keys = target.keyspace(vta_keyspaces::KEYS)?;
358 let mut restored_internal = std::collections::BTreeSet::new();
359 for key in keys.prefix_keys("key:").await? {
360 let Some(bytes) = keys.get_raw(key).await? else {
361 continue;
362 };
363 if let Ok(record) = serde_json::from_slice::<vta_sdk::keys::KeyRecord>(&bytes)
364 && record.origin == KeyOrigin::Internal
365 {
366 restored_internal.insert(record.key_id);
367 }
368 }
369
370 let bare = target.store.keyspace(vta_keyspaces::INTERNAL_KEYS)?;
371 let sealed = target.keyspace(vta_keyspaces::INTERNAL_KEYS)?;
372 let mut kept = std::collections::BTreeSet::new();
373 for key in bare.prefix_keys(Vec::<u8>::new()).await? {
374 let id = key
375 .strip_prefix(INTERNAL_KEY_PREFIX.as_bytes())
376 .and_then(|id| std::str::from_utf8(id).ok())
377 .map(str::to_owned);
378 let keep = match &id {
379 Some(id) if restored_internal.contains(id) => {
380 matches!(sealed.get_raw(key.clone()).await, Ok(Some(_)))
381 }
382 _ => false,
383 };
384 if keep {
385 kept.extend(id);
386 } else {
387 bare.remove(key).await?;
388 }
389 }
390 Ok(restored_internal.difference(&kept).cloned().collect())
391}
392
393async fn detach_hosted_dids(target: &BackupTarget<'_>) -> Result<Vec<String>, AppError> {
398 let webvh = target.keyspace(vta_keyspaces::WEBVH)?;
399 let mut detached = Vec::new();
400 for key in webvh.prefix_keys("did:").await? {
401 let Some(mut record) = webvh
402 .get::<vta_sdk::webvh::WebvhDidRecord>(key.clone())
403 .await?
404 else {
405 continue;
406 };
407 if record.server_id == "serverless" {
408 continue;
409 }
410 record.server_id = "serverless".into();
411 record.mnemonic = String::new();
412 webvh.insert(key, &record).await?;
413 detached.push(record.did);
414 }
415 Ok(detached)
416}
417
418async fn write_deployment_rows(
421 target: &BackupTarget<'_>,
422 meta: &StageMeta,
423 payload: &BackupPayload,
424 config: &mut vta_config::AppConfig,
425) -> Result<bool, AppError> {
426 let keys = target.keyspace(vta_keyspaces::KEYS)?;
427 let restored = &payload.config;
428 match target.environment {
429 BackupEnvironment::Tee => {
430 if let Some(did) = &restored.vta_did {
431 keys.insert_raw(TEE_VTA_DID_KEY, did.as_bytes().to_vec())
435 .await?;
436 if let Some(log) = target
437 .keyspace(vta_keyspaces::WEBVH)?
438 .get_raw(format!("log:{did}"))
439 .await?
440 {
441 keys.insert_raw(TEE_DID_LOG_KEY, log.clone()).await?;
442 target.bootstrap()?.insert_raw(TEE_DID_LOG_KEY, log).await?;
444 }
445 config.vta_did = Some(did.clone());
446 }
447 keys.insert_raw(TEE_CARVEOUT_CLOSED_KEY, b"restored".to_vec())
451 .await?;
452 keys.insert(
453 REBASELINE_KEY,
454 &RebaselineMarker {
455 restore_id: meta.restore_id.clone(),
456 anchor: meta.anchor.clone(),
457 },
458 )
459 .await?;
460 Ok(restored.vta_did.is_some())
462 }
463 BackupEnvironment::Hardened | BackupEnvironment::Plain => {
464 if let Some(jwt) = &payload.jwt_signing_key {
465 if target.environment == BackupEnvironment::Hardened {
466 let key = BASE64
467 .decode(jwt)
468 .map_err(|e| AppError::Validation(format!("backup JWT key: {e}")))?;
469 keys.insert_raw(HARDENED_JWT_KEY, key).await?;
470 } else {
471 config.auth.jwt_signing_key = Some(jwt.clone());
472 }
473 }
474 if let Some(did) = &restored.vta_did {
475 config.vta_did = Some(did.clone());
476 }
477 if let Some(name) = &restored.vta_name {
478 config.vta_name = Some(name.clone());
479 }
480 if let Some(url) = &restored.public_url {
483 config.public_url = Some(url.clone());
484 }
485 if restored.mediator_url.is_some() || restored.mediator_did.is_some() {
486 let messaging =
487 config
488 .messaging
489 .get_or_insert_with(|| vti_common::config::MessagingConfig {
490 mediator_url: String::new(),
491 mediator_did: String::new(),
492 mediator_host: None,
493 setup_acl: false,
494 drain_inbox_on_start: false,
495 });
496 if let Some(url) = &restored.mediator_url {
497 messaging.mediator_url = url.clone();
498 }
499 if let Some(did) = &restored.mediator_did {
500 messaging.mediator_did = did.clone();
501 }
502 }
503 if !config.config_path.as_os_str().is_empty() {
506 config.save()?;
507 }
508 Ok(true)
509 }
510 }
511}