vta_tee/admin_bootstrap.rs
1//! Auto-bootstrap a super-admin credential on first TEE boot.
2//!
3//! Two paths:
4//!
5//! 1. **`admin_did` configured** — create the ACL entry for the operator's
6//! known DID and close the first-boot carve-out. The operator keeps the
7//! corresponding private key off-enclave.
8//! 2. **No `admin_did` configured** — leave the first-boot carve-out OPEN.
9//! The operator completes the swap via the sealed-bootstrap flow
10//! (`POST /bootstrap/request` with attestation, Phase 3). The first
11//! successful swap closes the carve-out.
12//!
13//! Legacy behavior pre-Phase-3 was to auto-generate a random admin
14//! credential on first boot and store it under `tee:admin_credential` for
15//! retrieval via `GET /attestation/admin-credential`. That endpoint is
16//! gone; startup migrates any stored row out of the store.
17
18use tracing::info;
19
20use vta_config::AppConfig;
21use vta_support::contexts;
22use vti_common::acl::{AclEntry, Role, store_acl_entry};
23use vti_common::error::AppError;
24use vti_common::store::{KeyspaceHandle, Store};
25
26/// Sentinel indicating the TEE first-boot carve-out has been closed. Written
27/// either by `maybe_bootstrap_admin` (when an operator DID is configured) or
28/// by `POST /bootstrap/request` after a successful Mode B swap. When present,
29/// any subsequent Mode B attempt is rejected.
30pub const BOOTSTRAP_CARVEOUT_CLOSED_KEY: &str = "tee:bootstrap-carveout-closed";
31
32/// Legacy store key for the pre-Phase-3 auto-generated admin credential.
33/// No longer written; cleaned up on first startup after the upgrade.
34pub const LEGACY_ADMIN_CREDENTIAL_KEY: &str = "tee:admin_credential";
35
36/// Bootstrap a super-admin credential on first boot.
37///
38/// - If an admin credential already exists in the store, this is a no-op.
39/// - Otherwise: creates the admin context, generates a `did:key`, creates
40/// an ACL entry, encodes a `CredentialBundle`, and writes it to both
41/// the encrypted keys keyspace and the unencrypted bootstrap keyspace.
42///
43/// Returns `Ok(())` on success or if bootstrap is not needed.
44pub async fn maybe_bootstrap_admin(
45 config: &AppConfig,
46 store: &Store,
47 storage_encryption_key: Option<[u8; 32]>,
48) -> Result<(), AppError> {
49 // Guard: no KMS config means no TEE bootstrap
50 let kms_config = match &config.tee.kms {
51 Some(kms) => kms,
52 None => return Ok(()),
53 };
54
55 // Open keyspaces
56 let apply_enc = |ks: KeyspaceHandle| -> KeyspaceHandle {
57 if let Some(key) = storage_encryption_key {
58 ks.with_encryption(key)
59 } else {
60 ks
61 }
62 };
63 let keys_ks = apply_enc(store.keyspace(vta_keyspaces::KEYS)?);
64 let contexts_ks = apply_enc(store.keyspace(vta_keyspaces::CONTEXTS)?);
65 let acl_ks = apply_enc(store.keyspace(vta_keyspaces::ACL)?);
66
67 // One-time migration: if the legacy pre-Phase-3 credential row is still in
68 // the store, the old endpoint retrieving it is gone. Move any operator
69 // copies they already have, then retire the row so the carve-out reflects
70 // real state.
71 if keys_ks
72 .get_raw(LEGACY_ADMIN_CREDENTIAL_KEY)
73 .await?
74 .is_some()
75 {
76 info!("migrating legacy tee:admin_credential row — carve-out now closed");
77 keys_ks.remove(LEGACY_ADMIN_CREDENTIAL_KEY).await?;
78 // Old row might also be mirrored in the bootstrap keyspace.
79 if let Ok(bootstrap_ks) = store.keyspace(vta_keyspaces::BOOTSTRAP) {
80 let _ = bootstrap_ks.remove(LEGACY_ADMIN_CREDENTIAL_KEY).await;
81 }
82 keys_ks
83 .insert_raw(BOOTSTRAP_CARVEOUT_CLOSED_KEY, b"legacy-migrated".to_vec())
84 .await?;
85 store.persist().await?;
86 return Ok(());
87 }
88
89 // Carve-out already closed by a prior boot (operator DID path) or a
90 // prior Mode B swap.
91 if keys_ks
92 .get_raw(BOOTSTRAP_CARVEOUT_CLOSED_KEY)
93 .await?
94 .is_some()
95 {
96 info!("tee first-boot carve-out already closed — skipping");
97 return Ok(());
98 }
99
100 let context_id = &kms_config.admin_context_id;
101
102 // Create admin context if it doesn't exist
103 let _ctx = match contexts::get_context(&contexts_ks, context_id).await? {
104 Some(ctx) => ctx,
105 None => contexts::create_context(&contexts_ks, context_id, "Default Admin Context")
106 .await
107 .map_err(|e| AppError::Internal(format!("failed to create admin context: {e}")))?,
108 };
109
110 // Use the operator-provided admin DID if configured, otherwise generate one
111 if let Some(ref admin_did) = kms_config.admin_did {
112 // Operator-provided DID — just create the ACL entry. Carve-out is
113 // closed immediately because the admin identity is already known.
114 info!(did = %admin_did, context_id, "bootstrapping super-admin from config admin_did");
115
116 let entry = AclEntry::new(admin_did.clone(), Role::Admin, "tee:bootstrap")
117 .with_label(Some("TEE bootstrap admin".to_string()));
118 store_acl_entry(&acl_ks, &entry).await?;
119
120 keys_ks
121 .insert_raw(BOOTSTRAP_CARVEOUT_CLOSED_KEY, admin_did.as_bytes().to_vec())
122 .await?;
123
124 store.persist().await?;
125
126 info!(
127 did = %admin_did,
128 context_id,
129 "super-admin ACL created — connect using the private key for this DID"
130 );
131 } else {
132 // No admin_did configured — leave the first-boot carve-out OPEN.
133 // The operator completes the swap by running
134 // `pnm bootstrap connect --vta-url <URL>` against this VTA (no token),
135 // which triggers the Mode B attestation branch and closes the carve-out.
136 info!(
137 context_id,
138 "no admin_did configured — first-boot carve-out remains open for \
139 sealed-bootstrap Mode B"
140 );
141 store.persist().await?;
142 }
143
144 Ok(())
145}