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,
})
}
#[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"
);
}
}