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 let memory = target.keyspace(vta_keyspaces::MEMORY)?;
318 let app_state = target.keyspace(vta_keyspaces::APP_STATE)?;
319 let internal = target.keyspace(vta_keyspaces::INTERNAL_KEYS)?;
320 let vault = target.keyspace(vta_keyspaces::VAULT)?;
321 let consent = target.keyspace(vta_keyspaces::CONSENT)?;
322 let consent_approvers = target.keyspace(vta_keyspaces::CONSENT_APPROVERS)?;
323 let persona = target.keyspace(vta_keyspaces::PERSONA)?;
324 let pcs_engine = target.keyspace(vta_keyspaces::PCS_ENGINE)?;
325 let notifications = target.keyspace(vta_keyspaces::NOTIFICATIONS)?;
326 let external_accounts = target.keyspace(vta_keyspaces::EXTERNAL_ACCOUNTS)?;
327 let external_secrets = target.keyspace(vta_keyspaces::EXTERNAL_SECRETS)?;
328 let collection = target.keyspace(vta_keyspaces::COLLECTION)?;
329 #[cfg(feature = "webvh")]
330 let webvh = target.keyspace(vta_keyspaces::WEBVH)?;
331 let ks = vta_keyspaces::Keyspaces {
332 keys: &keys,
333 acl: &acl,
334 contexts: &contexts,
335 did_templates: &did_templates,
336 audit: &audit,
337 imported: &imported,
338 memory: &memory,
339 app_state: &app_state,
340 internal: &internal,
341 vault: &vault,
342 consent: &consent,
343 consent_approvers: &consent_approvers,
344 persona: &persona,
345 pcs_engine: &pcs_engine,
346 notifications: ¬ifications,
347 external_accounts: &external_accounts,
348 external_secrets: &external_secrets,
349 collection: &collection,
350 #[cfg(feature = "webvh")]
351 webvh: &webvh,
352 };
353 return crate::ops::write_legacy_payload(payload, &ks, seed).await;
354 }
355 for dump in &payload.keyspaces {
356 let ks = target.keyspace(&dump.name)?;
357 for (k, v) in &dump.rows {
358 let key = BASE64
359 .decode(k)
360 .map_err(|e| AppError::Validation(format!("restored row key: {e}")))?;
361 if vta_keyspaces::is_environment_bound(&dump.name, &key) {
363 continue;
364 }
365 let value = zeroize::Zeroizing::new(
366 BASE64
367 .decode(v)
368 .map_err(|e| AppError::Validation(format!("restored row value: {e}")))?,
369 );
370 ks.insert_raw(key, value.to_vec()).await?;
371 }
372 }
373 Ok(())
374}
375
376async fn reconcile_internal_keys(target: &BackupTarget<'_>) -> Result<Vec<String>, AppError> {
381 let keys = target.keyspace(vta_keyspaces::KEYS)?;
382 let mut restored_internal = std::collections::BTreeSet::new();
383 for key in keys.prefix_keys("key:").await? {
384 let Some(bytes) = keys.get_raw(key).await? else {
385 continue;
386 };
387 if let Ok(record) = serde_json::from_slice::<vta_sdk::keys::KeyRecord>(&bytes)
388 && record.origin == KeyOrigin::Internal
389 {
390 restored_internal.insert(record.key_id);
391 }
392 }
393
394 let bare = target.store.keyspace(vta_keyspaces::INTERNAL_KEYS)?;
395 let sealed = target.keyspace(vta_keyspaces::INTERNAL_KEYS)?;
396 let mut kept = std::collections::BTreeSet::new();
397 for key in bare.prefix_keys(Vec::<u8>::new()).await? {
398 let id = key
399 .strip_prefix(INTERNAL_KEY_PREFIX.as_bytes())
400 .and_then(|id| std::str::from_utf8(id).ok())
401 .map(str::to_owned);
402 let keep = match &id {
403 Some(id) if restored_internal.contains(id) => {
404 matches!(sealed.get_raw(key.clone()).await, Ok(Some(_)))
405 }
406 _ => false,
407 };
408 if keep {
409 kept.extend(id);
410 } else {
411 bare.remove(key).await?;
412 }
413 }
414 Ok(restored_internal.difference(&kept).cloned().collect())
415}
416
417async fn detach_hosted_dids(target: &BackupTarget<'_>) -> Result<Vec<String>, AppError> {
422 let webvh = target.keyspace(vta_keyspaces::WEBVH)?;
423 let mut detached = Vec::new();
424 for key in webvh.prefix_keys("did:").await? {
425 let Some(mut record) = webvh
426 .get::<vta_sdk::webvh::WebvhDidRecord>(key.clone())
427 .await?
428 else {
429 continue;
430 };
431 if record.server_id == "serverless" {
432 continue;
433 }
434 record.server_id = "serverless".into();
435 record.mnemonic = String::new();
436 webvh.insert(key, &record).await?;
437 detached.push(record.did);
438 }
439 Ok(detached)
440}
441
442async fn write_deployment_rows(
445 target: &BackupTarget<'_>,
446 meta: &StageMeta,
447 payload: &BackupPayload,
448 config: &mut vta_config::AppConfig,
449) -> Result<bool, AppError> {
450 let keys = target.keyspace(vta_keyspaces::KEYS)?;
451 let restored = &payload.config;
452 match target.environment {
453 BackupEnvironment::Tee => {
454 if let Some(did) = &restored.vta_did {
455 keys.insert_raw(TEE_VTA_DID_KEY, did.as_bytes().to_vec())
459 .await?;
460 if let Some(log) = target
461 .keyspace(vta_keyspaces::WEBVH)?
462 .get_raw(format!("log:{did}"))
463 .await?
464 {
465 keys.insert_raw(TEE_DID_LOG_KEY, log.clone()).await?;
466 target.bootstrap()?.insert_raw(TEE_DID_LOG_KEY, log).await?;
468 }
469 config.vta_did = Some(did.clone());
470 }
471 keys.insert_raw(TEE_CARVEOUT_CLOSED_KEY, b"restored".to_vec())
475 .await?;
476 keys.insert(
477 REBASELINE_KEY,
478 &RebaselineMarker {
479 restore_id: meta.restore_id.clone(),
480 anchor: meta.anchor.clone(),
481 },
482 )
483 .await?;
484 Ok(restored.vta_did.is_some())
486 }
487 BackupEnvironment::Hardened | BackupEnvironment::Plain => {
488 if let Some(jwt) = &payload.jwt_signing_key {
489 if target.environment == BackupEnvironment::Hardened {
490 let key = BASE64
491 .decode(jwt)
492 .map_err(|e| AppError::Validation(format!("backup JWT key: {e}")))?;
493 keys.insert_raw(HARDENED_JWT_KEY, key).await?;
494 } else {
495 config.auth.jwt_signing_key = Some(jwt.clone());
496 }
497 }
498 if let Some(did) = &restored.vta_did {
499 config.vta_did = Some(did.clone());
500 }
501 if let Some(name) = &restored.vta_name {
502 config.vta_name = Some(name.clone());
503 }
504 if let Some(url) = &restored.public_url {
507 config.public_url = Some(url.clone());
508 }
509 if restored.mediator_url.is_some() || restored.mediator_did.is_some() {
510 let messaging =
511 config
512 .messaging
513 .get_or_insert_with(|| vti_common::config::MessagingConfig {
514 mediator_url: String::new(),
515 mediator_did: String::new(),
516 mediator_host: None,
517 setup_acl: false,
518 drain_inbox_on_start: false,
519 });
520 if let Some(url) = &restored.mediator_url {
521 messaging.mediator_url = url.clone();
522 }
523 if let Some(did) = &restored.mediator_did {
524 messaging.mediator_did = did.clone();
525 }
526 }
527 if !config.config_path.as_os_str().is_empty() {
530 config.save()?;
531 }
532 Ok(true)
533 }
534 }
535}