Skip to main content

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}