use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use tokio::sync::RwLock;
use tracing::{debug, warn};
use crate::config::AppConfig;
use crate::error::{AppError, tee_attestation_error};
use crate::tee::TeeState;
use crate::tee::provider::StructuralCheckOutcome;
use crate::tee::types::{AttestationReport, TeeStatus};
use vta_sdk::attestation_report::ConfigAttestationReport;
pub fn get_tee_status(tee_state: &TeeState) -> TeeStatus {
tee_state.status.clone()
}
pub async fn generate_attestation_report(
tee_state: &TeeState,
config: &Arc<RwLock<AppConfig>>,
nonce: &str,
) -> Result<AttestationReport, AppError> {
let nonce_bytes = hex::decode(nonce)
.map_err(|e| AppError::Validation(format!("nonce must be hex-encoded: {e}")))?;
if nonce_bytes.is_empty() || nonce_bytes.len() > 64 {
return Err(AppError::Validation(
"nonce must be 1-64 bytes (2-128 hex chars)".into(),
));
}
let vta_did = config.read().await.vta_did.clone();
let user_data = vta_did.as_deref().unwrap_or("").as_bytes();
debug!(
nonce_len = nonce_bytes.len(),
"generating attestation report"
);
let mut report = tee_state.provider.attest(user_data, &nonce_bytes)?;
report.vta_did = vta_did;
match tee_state.provider.smoke_check_structure(&report)? {
StructuralCheckOutcome::StructurallyValid => {}
StructuralCheckOutcome::Malformed => {
warn!(
tee_type = %report.tee_type,
"attestation evidence failed structural smoke-check — \
returning anyway, consumer must verify cryptographically"
);
}
}
Ok(report)
}
pub async fn get_cached_report(
tee_state: &TeeState,
config: &Arc<RwLock<AppConfig>>,
) -> Result<AttestationReport, AppError> {
let cache_ttl = {
#[cfg(feature = "tee")]
{
config.read().await.tee.attestation_cache_ttl
}
#[cfg(not(feature = "tee"))]
{
let _ = config;
300u64
}
};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let time_bucket = now / cache_ttl;
let nonce = hex::encode(time_bucket.to_be_bytes());
generate_attestation_report(tee_state, config, &nonce).await
}
pub async fn generate_config_attestation(
tee_state: &TeeState,
config: &Arc<RwLock<AppConfig>>,
nonce: &str,
) -> Result<ConfigAttestationReport, AppError> {
let nonce_bytes = hex::decode(nonce)
.map_err(|e| AppError::Validation(format!("nonce must be hex-encoded: {e}")))?;
if nonce_bytes.is_empty() || nonce_bytes.len() > 64 {
return Err(AppError::Validation(
"nonce must be 1-64 bytes (2-128 hex chars)".into(),
));
}
let (digest, view) = {
let cfg = config.read().await;
let digest = cfg.effective_config_digest.clone().ok_or_else(|| {
tee_attestation_error(
"config attestation is not available on this build — no effective \
config digest was captured at boot (the enclave front-end captures it)",
)
})?;
let view = cfg.effective_config_view.clone().ok_or_else(|| {
tee_attestation_error(
"config attestation is not available on this build — no effective \
config view was captured at boot (the enclave front-end captures it)",
)
})?;
(digest, view)
};
debug!(
nonce_len = nonce_bytes.len(),
"generating config attestation report"
);
let report = tee_state.provider.attest(digest.as_slice(), &nonce_bytes)?;
Ok(ConfigAttestationReport {
config_digest_sha384: BASE64.encode(&digest),
config_view: BASE64.encode(&view),
nonce: report.nonce,
tee_type: report.tee_type.to_string(),
evidence: report.evidence,
generated_at: report.generated_at,
})
}
static MNEMONIC_EXPORT_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
pub async fn export_mnemonic_sealed(
state: &crate::server::AppState,
auth: &crate::auth::AuthClaims,
req: vta_sdk::sealed_transfer::BootstrapRequest,
channel: &str,
) -> Result<vta_sdk::protocols::attestation_management::MnemonicExportResultBody, AppError> {
use sha2::{Digest, Sha256};
use vta_sdk::sealed_transfer::{
AssertionProof, AttestationQuoteAssertion, ProducerAssertion, SealedPayloadV1,
SeedMnemonicBundle, armor, bundle_digest, generate_ed25519_keypair, seal_payload,
};
auth.require_super_admin()?;
crate::operations::keys::ensure_may_export(&state.acl_ks, auth, "attestation/mnemonic-export")
.await?;
if req.version != 1 {
return Err(AppError::Validation(format!(
"unsupported request version: {}",
req.version
)));
}
let client_ed25519_pub = req
.decode_client_ed25519_pub()
.map_err(|e| AppError::Validation(format!("invalid client_did: {e}")))?;
let client_x25519_pub = req
.decode_client_x25519_pub()
.map_err(|e| AppError::Validation(format!("invalid client_did: {e}")))?;
let bundle_id = req
.decode_nonce()
.map_err(|e| AppError::Validation(format!("invalid nonce: {e}")))?;
let tee = state.tee.as_ref().ok_or_else(|| {
tee_attestation_error("mnemonic export not available (TEE mode not active)")
})?;
let guard = tee.mnemonic_guard.as_ref().ok_or_else(|| {
tee_attestation_error(
"mnemonic export not available (TEE mode not active or no KMS bootstrap)",
)
})?;
let _serial = MNEMONIC_EXPORT_LOCK.lock().await;
let reservation = guard.reserve()?;
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 assertion = ProducerAssertion {
producer_did,
proof: AssertionProof::Attested(AttestationQuoteAssertion {
format: format!("{}", report.tee_type),
quote_b64: report.evidence,
}),
};
let vta_did = state.config.read().await.vta_did.clone();
let payload = SealedPayloadV1::SeedMnemonic(Box::new(SeedMnemonicBundle {
mnemonic: reservation.mnemonic().to_string(),
vta_did,
}));
let nonce_store =
crate::sealed_nonce_store::PersistentNonceStore::new(state.sealed_nonces_ks.clone());
let sealed = seal_payload(
&client_x25519_pub,
bundle_id,
assertion,
&payload,
&nonce_store,
)
.await;
drop(payload);
let bundle =
sealed.map_err(|e| AppError::Internal(format!("sealed-transfer seal failed: {e}")))?;
let digest = bundle_digest(&bundle);
crate::audit::record_with_detail(
&state.audit_sink,
"seed.mnemonic_export",
&auth.did,
Some(&req.client_did),
"success",
Some(channel),
None,
Some(&format!("bundle_sha256:{digest}")),
)
.await
.map_err(|e| {
tracing::error!(
target: vta_audit::AUDIT_WRITE_FAILURE_TARGET,
error = %e, actor = %auth.did,
"mnemonic export refused: its audit row could not be written"
);
AppError::Internal(
"the mnemonic was not released: the export could not be recorded in the audit \
trail, and an unrecorded export is not permitted (VTI-VTA-003)"
.into(),
)
})?;
let window_remaining_secs = reservation.window_remaining_secs();
reservation.commit();
Ok(
vta_sdk::protocols::attestation_management::MnemonicExportResultBody {
bundle: armor::encode(&bundle),
digest,
window_remaining_secs,
},
)
}
#[cfg(test)]
mod tests {
use super::BASE64;
use base64::Engine;
use sha2::{Digest, Sha384};
#[test]
fn config_digest_is_deterministic_sha384_b64() {
let input = b"resolver_url = \"ws://127.0.0.1:4445/did/v1/ws\"\n";
let a = BASE64.encode(Sha384::digest(input));
let b = BASE64.encode(Sha384::digest(input));
assert_eq!(a, b, "digest must be deterministic");
assert_eq!(
BASE64.decode(&a).unwrap().len(),
48,
"SHA-384 digest is 48 bytes"
);
}
}