use axum::Json;
use axum::extract::State;
use axum::response::Response;
use crate::auth::SuperAdminAuth;
use crate::error::{AppError, tee_attestation_error};
use crate::operations;
use crate::server::AppState;
use crate::tee::mnemonic_guard::MnemonicExportStatus;
use crate::tee::types::{AttestationReport, AttestationRequest, TeeStatus};
use vta_sdk::attestation_report::ConfigAttestationReport;
#[utoipa::path(
get, path = "/attestation/status", tag = "attestation",
responses(
(status = 200, description = "TEE detection status", body = TeeStatus),
(status = 503, description = "TEE attestation not enabled"),
),
)]
pub async fn status(State(state): State<AppState>) -> Result<Json<TeeStatus>, AppError> {
let tee_state = state
.tee
.as_ref()
.map(|tc| &tc.state)
.ok_or_else(|| tee_attestation_error("TEE attestation is not enabled on this VTA"))?;
Ok(Json(operations::attestation::get_tee_status(tee_state)))
}
#[utoipa::path(
post, path = "/attestation/report", tag = "attestation",
request_body = AttestationRequest,
responses(
(status = 200, description = "Fresh attestation report", body = AttestationReport),
(status = 503, description = "TEE attestation not enabled"),
),
)]
pub async fn generate_report(
State(state): State<AppState>,
Json(body): Json<AttestationRequest>,
) -> Result<Json<AttestationReport>, AppError> {
let tee_state = state
.tee
.as_ref()
.map(|tc| &tc.state)
.ok_or_else(|| tee_attestation_error("TEE attestation is not enabled on this VTA"))?;
let response =
operations::attestation::generate_attestation_report(tee_state, &state.config, &body.nonce)
.await?;
Ok(Json(response))
}
#[utoipa::path(
post, path = "/attestation/config-report", tag = "attestation",
request_body = AttestationRequest,
responses(
(status = 200, description = "Fresh config attestation report", body = ConfigAttestationReport),
(status = 503, description = "TEE attestation not enabled, or this build captured no effective-config snapshot at boot (only the enclave front-end does)"),
),
)]
pub async fn config_report(
State(state): State<AppState>,
Json(body): Json<AttestationRequest>,
) -> Result<Json<ConfigAttestationReport>, AppError> {
let tee_state = state
.tee
.as_ref()
.map(|tc| &tc.state)
.ok_or_else(|| tee_attestation_error("TEE attestation is not enabled on this VTA"))?;
let response =
operations::attestation::generate_config_attestation(tee_state, &state.config, &body.nonce)
.await?;
Ok(Json(response))
}
#[utoipa::path(
get, path = "/attestation/report", tag = "attestation",
responses(
(status = 200, description = "Cached attestation report", body = AttestationReport),
(status = 503, description = "TEE attestation not enabled"),
),
)]
pub async fn cached_report(
State(state): State<AppState>,
) -> Result<Json<AttestationReport>, AppError> {
let tee_state = state
.tee
.as_ref()
.map(|tc| &tc.state)
.ok_or_else(|| tee_attestation_error("TEE attestation is not enabled on this VTA"))?;
let response = operations::attestation::get_cached_report(tee_state, &state.config).await?;
Ok(Json(response))
}
#[utoipa::path(
get, path = "/attestation/did-log", tag = "attestation",
responses(
(status = 200, description = "Auto-generated did.jsonl", content_type = "text/jsonl"),
(status = 304, description = "Not modified: If-None-Match names the current ETag"),
(status = 429, description = "Rate limited by the VTA (`x-rate-limit-source: vta`)"),
(status = 404, description = "No auto-generated DID log"),
),
)]
pub async fn did_log(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
) -> Result<Response, AppError> {
let log_bytes = state
.keys_ks
.get_raw(crate::tee::did_autogen::DID_LOG_STORE_KEY)
.await?
.ok_or_else(|| {
AppError::NotFound(
"no auto-generated DID log found — the VTA may not have \
been configured with a vta_did_template"
.into(),
)
})?;
let log = String::from_utf8(log_bytes)
.map_err(|e| AppError::Internal(format!("DID log is not valid UTF-8: {e}")))?;
Ok(super::self_hosted_did::did_log_response(&headers, log))
}
#[utoipa::path(
get, path = "/attestation/mnemonic", tag = "attestation",
security(("bearer_jwt" = [])),
responses(
(status = 200, description = "Mnemonic export window status", body = MnemonicExportStatus),
(status = 401, description = "Missing or invalid bearer token"),
(status = 403, description = "Caller is not a super-admin"),
(status = 503, description = "Mnemonic export not available"),
),
)]
pub async fn mnemonic_status(
_auth: SuperAdminAuth,
State(state): State<AppState>,
) -> Result<Json<MnemonicExportStatus>, AppError> {
let guard = state
.tee
.as_ref()
.and_then(|tc| tc.mnemonic_guard.as_ref())
.ok_or_else(|| {
tee_attestation_error(
"mnemonic export not available (TEE mode not active or no KMS bootstrap)",
)
})?;
Ok(Json(guard.status()))
}
#[utoipa::path(
post, path = "/attestation/mnemonic", tag = "attestation",
security(("bearer_jwt" = [])),
responses(
(status = 401, description = "Missing or invalid bearer token"),
(status = 403, description = "Always: the caller is not a super admin with key-export, or \
the export was asked for over REST, which is hop-by-hop. Send \
spec/vta/attestation/mnemonic-export/1.0 over DIDComm or TSP, or \
at first boot as a Trust Task signed by the caller with clientDid \
set to the caller's own DID"),
),
)]
pub async fn mnemonic_export(
SuperAdminAuth(auth): SuperAdminAuth,
State(state): State<AppState>,
) -> Result<Json<()>, AppError> {
crate::operations::keys::ensure_may_export(&state.acl_ks, &auth, "attestation/mnemonic")
.await?;
Err(AppError::Forbidden(
"the mnemonic export is refused over REST: a bearer token alone never releases it. \
Send spec/vta/attestation/mnemonic-export/1.0 over DIDComm or TSP, or at first boot \
as a Trust Task signed by the caller with clientDid set to the caller's own DID"
.into(),
))
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
#[tokio::test]
async fn the_rest_mnemonic_export_is_refused() {
let (mut state, _dir) = crate::test_support::build_signing_test_app_state().await;
let guard = Arc::new(crate::tee::mnemonic_guard::MnemonicExportGuard::new(
[0x42; 32], 60,
));
let tee = crate::tee::init_tee(&crate::config::TeeConfig {
mode: crate::config::TeeMode::Simulated,
..Default::default()
})
.unwrap()
.unwrap();
state.tee = Some(crate::server::TeeContext {
state: tee,
mnemonic_guard: Some(guard.clone()),
});
let err = mnemonic_export(
SuperAdminAuth(crate::test_support::super_admin_claims()),
State(state),
)
.await
.unwrap_err();
assert!(
matches!(&err, AppError::Forbidden(m) if m.contains("DIDComm or TSP")),
"{err:?}"
);
assert!(!guard.status().already_exported);
}
}