use axum::Json;
use axum::extract::State;
use axum::response::IntoResponse;
#[cfg(feature = "tee")]
use base64::Engine;
#[cfg(feature = "tee")]
use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64URL;
use serde::{Deserialize, Serialize};
#[cfg(feature = "tee")]
use sha2::{Digest, Sha256};
#[cfg(feature = "tee")]
use tracing::info;
#[cfg(feature = "tee")]
use vta_sdk::credentials::CredentialBundle;
#[cfg(feature = "tee")]
use vta_sdk::sealed_transfer::{
AssertionProof, AttestationQuoteAssertion, ProducerAssertion, SealedPayloadV1, armor,
bundle_digest, generate_ed25519_keypair, seal_payload,
};
#[cfg(feature = "tee")]
use crate::acl::delete_acl_entry;
#[cfg(feature = "tee")]
use crate::acl::store_acl_entry;
#[cfg(feature = "tee")]
use crate::acl::{AclEntry, Role};
#[cfg(feature = "tee")]
use crate::audit::audit;
use crate::auth::session::now_epoch;
use crate::error::AppError;
#[cfg(feature = "tee")]
use crate::sealed_nonce_store::PersistentNonceStore;
use crate::server::AppState;
const MAX_LABEL_LEN: usize = 256;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
#[derive(utoipa::ToSchema)]
pub struct BootstrapRequestBody {
pub version: u8,
pub client_did: String,
pub nonce: String,
#[serde(default, deserialize_with = "deserialize_bounded_label")]
#[cfg_attr(not(feature = "tee"), allow(dead_code))]
pub label: Option<String>,
}
fn deserialize_bounded_label<'de, D>(de: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let s: Option<String> = Option::deserialize(de)?;
if let Some(label) = &s
&& label.len() > MAX_LABEL_LEN
{
return Err(serde::de::Error::custom(format!(
"label exceeds {MAX_LABEL_LEN} bytes"
)));
}
Ok(s)
}
#[derive(Debug, Serialize, utoipa::ToSchema)]
pub struct BootstrapResponseBody {
pub bundle: String,
pub digest: String,
}
#[utoipa::path(
post, path = "/bootstrap/request", tag = "bootstrap",
request_body = BootstrapRequestBody,
responses(
(status = 200, description = "Armored sealed admin bundle + digest", body = BootstrapResponseBody),
(status = 400, description = "Unsupported version or malformed client_did/nonce"),
(status = 403, description = "TEE first-boot unavailable on this VTA build"),
(status = 410, description = "Carve-out already used — this VTA already has an admin"),
),
)]
pub async fn request(
State(state): State<AppState>,
Json(req): Json<BootstrapRequestBody>,
) -> Result<Json<BootstrapResponseBody>, AppError> {
if req.version != 1 {
return Err(AppError::Validation(format!(
"unsupported bootstrap request version: {}",
req.version
)));
}
let client_ed25519_pub = decode_client_did(&req.client_did)?;
let bundle_id = decode_nonce(&req.nonce)?;
let now = now_epoch();
#[cfg(feature = "tee")]
let bundle = mint_mode_b(&state, &client_ed25519_pub, bundle_id, now).await?;
#[cfg(not(feature = "tee"))]
{
let _ = (state, client_ed25519_pub, bundle_id, now);
Err(AppError::Forbidden(
"bootstrap request requires TEE first-boot attestation, which is not available on \
this VTA build. Non-TEE VTAs use the `pnm setup` temp-did:key + ACL flow instead."
.into(),
))
}
#[cfg(feature = "tee")]
{
let digest = bundle_digest(&bundle);
let armored = armor::encode(&bundle);
info!(
client_label_hash = %label_hash_prefix(req.label.as_deref()),
"TEE first-boot bootstrap completed"
);
audit!(
"bootstrap.swap",
actor = "bootstrap-endpoint",
resource = "bootstrap",
outcome = "success"
);
let _ = crate::audit::record(
&state.audit_sink,
"bootstrap.swap",
"bootstrap-endpoint",
None,
"success",
Some("rest"),
None,
)
.await;
Ok(Json(BootstrapResponseBody {
bundle: armored,
digest,
}))
}
}
#[cfg(feature = "tee")]
static MODE_B_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
#[cfg(feature = "tee")]
async fn mint_mode_b(
state: &AppState,
client_ed25519_pub: &[u8; 32],
bundle_id: [u8; 16],
now: u64,
) -> Result<vta_sdk::sealed_transfer::SealedBundle, AppError> {
use crate::tee::admin_bootstrap::{BOOTSTRAP_CARVEOUT_CLOSED_KEY, LEGACY_ADMIN_CREDENTIAL_KEY};
let tee_state =
state.tee.as_ref().map(|tc| &tc.state).ok_or_else(|| {
AppError::Forbidden("TEE first-boot is not available on this VTA".into())
})?;
let _carve_out_guard = MODE_B_LOCK.lock().await;
if state
.keys_ks
.get_raw(BOOTSTRAP_CARVEOUT_CLOSED_KEY)
.await?
.is_some()
|| state
.keys_ks
.get_raw(LEGACY_ADMIN_CREDENTIAL_KEY)
.await?
.is_some()
{
return Err(carve_out_closed_error());
}
let cfg = state.config.read().await;
let vta_did = cfg
.vta_did
.as_ref()
.ok_or_else(|| AppError::Internal("VTA DID not configured".into()))?
.clone();
let vta_url = cfg.public_url.clone();
drop(cfg);
let (_producer_seed, producer_ed_pub) = generate_ed25519_keypair();
let producer_did = affinidi_crypto::did_key::ed25519_pub_to_did_key(&producer_ed_pub);
let mut hasher = Sha256::new();
hasher.update(client_ed25519_pub);
hasher.update(bundle_id);
hasher.update(producer_ed_pub);
let user_data = hasher.finalize();
let report = tee_state
.provider
.attest(user_data.as_slice(), &bundle_id)
.map_err(|e| AppError::Internal(format!("tee attest failed: {e}")))?;
let (did, private_key_multibase) = crate::auth::credentials::generate_did_key();
let credential = CredentialBundle {
did: did.clone(),
private_key_multibase,
vta_did,
vta_url,
};
let assertion = ProducerAssertion {
producer_did,
proof: AssertionProof::Attested(AttestationQuoteAssertion {
format: format!("{}", report.tee_type),
quote_b64: report.evidence,
}),
};
let client_x25519_pub =
affinidi_crypto::did_key::ed25519_pub_to_x25519_bytes(client_ed25519_pub)
.map_err(|e| AppError::Internal(format!("client_did X25519 derivation: {e}")))?;
let nonce_store = PersistentNonceStore::new(state.sealed_nonces_ks.clone());
let payload = SealedPayloadV1::AdminCredential(Box::new(credential));
let bundle = seal_payload(
&client_x25519_pub,
bundle_id,
assertion,
&payload,
&nonce_store,
)
.await
.map_err(|e| AppError::Internal(format!("sealed-transfer seal failed: {e}")))?;
let entry = AclEntry::new(did.clone(), Role::Admin, "tee:mode-b")
.with_label(Some("TEE first-boot admin".to_string()))
.with_created_at(now);
store_acl_entry(&state.acl_ks, &entry).await?;
if !state
.keys_ks
.insert_raw_if_absent(BOOTSTRAP_CARVEOUT_CLOSED_KEY, did.as_bytes().to_vec())
.await?
{
let _ = delete_acl_entry(&state.acl_ks, &did).await;
return Err(carve_out_closed_error());
}
state.keys_ks.persist().await?;
vti_common::integrity::reseal_if_active().await?;
info!("TEE first-boot carve-out consumed — closed for good");
Ok(bundle)
}
#[cfg(feature = "tee")]
fn carve_out_closed_error() -> AppError {
AppError::Gone("TEE first-boot carve-out has already been used".into())
}
fn decode_client_did(did: &str) -> Result<[u8; 32], AppError> {
affinidi_crypto::did_key::did_key_to_ed25519_pub(did)
.map_err(|e| AppError::Validation(format!("invalid client_did: {e}")))
}
#[cfg(feature = "tee")]
fn label_hash_prefix(label: Option<&str>) -> String {
match label {
Some(s) => {
let digest = Sha256::digest(s.as_bytes());
let mut hex = String::with_capacity(16);
for b in &digest[..8] {
hex.push_str(&format!("{b:02x}"));
}
hex
}
None => "none".to_string(),
}
}
#[cfg(feature = "tee")]
fn decode_nonce(s: &str) -> Result<[u8; 16], AppError> {
let raw = B64URL
.decode(s)
.map_err(|e| AppError::Validation(format!("invalid nonce base64: {e}")))?;
raw.try_into()
.map_err(|_| AppError::Validation("nonce must be 16 bytes".into()))
}
#[cfg(not(feature = "tee"))]
fn decode_nonce(_s: &str) -> Result<[u8; 16], AppError> {
Err(AppError::Forbidden(
"bootstrap request requires TEE first-boot attestation".into(),
))
}
impl IntoResponse for BootstrapResponseBody {
fn into_response(self) -> axum::response::Response {
Json(self).into_response()
}
}
#[cfg(all(test, feature = "tee"))]
mod tests {
use super::*;
#[test]
fn label_hash_prefix_is_stable_and_truncated() {
let a1 = label_hash_prefix(Some("alice@example.com"));
let a2 = label_hash_prefix(Some("alice@example.com"));
let b = label_hash_prefix(Some("bob@example.com"));
assert_eq!(a1, a2, "same label produces same hash");
assert_ne!(a1, b, "different labels produce different hashes");
assert_eq!(a1.len(), 16, "prefix is 16 hex chars (64 bits)");
assert!(
a1.chars().all(|c| c.is_ascii_hexdigit()),
"hash prefix is hex"
);
assert_eq!(label_hash_prefix(None), "none");
}
#[test]
fn label_hash_prefix_does_not_leak_raw_label() {
let raw = "glenn's iphone";
let hashed = label_hash_prefix(Some(raw));
for substr_len in 3..=raw.len() {
for start in 0..=(raw.len() - substr_len) {
let needle = &raw[start..start + substr_len];
assert!(
!hashed.contains(needle),
"hashed output '{hashed}' must not contain raw substring '{needle}'"
);
}
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn carveout_claim_admits_exactly_one_concurrent_minter() {
use crate::tee::admin_bootstrap::BOOTSTRAP_CARVEOUT_CLOSED_KEY;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use vti_common::config::StoreConfig;
use vti_common::store::Store;
let dir = tempfile::tempdir().expect("tempdir");
let store_config = StoreConfig {
data_dir: dir.path().to_path_buf(),
};
let store = Store::open(&store_config).expect("open store");
let keys_ks = store.keyspace(crate::keyspaces::KEYS).expect("keyspace");
let n_tasks: usize = 16;
let successes = std::sync::Arc::new(AtomicUsize::new(0));
let mut handles = Vec::with_capacity(n_tasks);
for i in 0..n_tasks {
let ks = keys_ks.clone();
let successes = std::sync::Arc::clone(&successes);
handles.push(tokio::spawn(async move {
let _guard = MODE_B_LOCK.lock().await;
tokio::time::sleep(Duration::from_millis(2)).await;
let claimed = ks
.insert_raw_if_absent(
BOOTSTRAP_CARVEOUT_CLOSED_KEY,
format!("admin-{i}").into_bytes(),
)
.await
.expect("claim sentinel");
if claimed {
ks.persist().await.expect("persist carve-out");
successes.fetch_add(1, Ordering::SeqCst);
}
}));
}
for h in handles {
h.await.expect("task joined");
}
assert_eq!(
successes.load(Ordering::SeqCst),
1,
"exactly one task may claim the carve-out sentinel; got {} successes",
successes.load(Ordering::SeqCst),
);
assert!(
keys_ks
.get_raw(BOOTSTRAP_CARVEOUT_CLOSED_KEY)
.await
.unwrap()
.is_some(),
"sentinel must be set after the run"
);
}
#[tokio::test]
async fn consumed_carveout_returns_410_gone_not_403() {
use crate::config::{TeeConfig, TeeMode};
use crate::server::TeeContext;
let (_app, ctx) = crate::test_support::build_test_app().await;
let tee_state = crate::tee::init_tee(&TeeConfig {
mode: TeeMode::Simulated,
..Default::default()
})
.expect("init simulated tee")
.expect("simulated tee state present");
let mut state = ctx.state.clone();
state.tee = Some(TeeContext {
state: tee_state,
mnemonic_guard: None,
});
let now = now_epoch();
let (_seed_a, pub_a) = generate_ed25519_keypair();
mint_mode_b(&state, &pub_a, [1u8; 16], now)
.await
.expect("first bootstrap mints an admin");
let (_seed_b, pub_b) = generate_ed25519_keypair();
let err = mint_mode_b(&state, &pub_b, [2u8; 16], now)
.await
.expect_err("second bootstrap must be refused");
assert!(
matches!(err, AppError::Gone(_)),
"consumed carve-out must map to AppError::Gone, got {err:?}"
);
assert_eq!(
err.into_response().status(),
axum::http::StatusCode::GONE,
"consumed carve-out must render as HTTP 410, not 403/409"
);
}
}