use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::{debug, info};
use crate::error::{AppError, tee_attestation_error};
use super::provider::{StructuralCheckOutcome, TeeProvider};
use super::types::{AttestationReport, TeeStatus, TeeType};
pub struct SevSnpProvider;
impl TeeProvider for SevSnpProvider {
fn tee_type(&self) -> TeeType {
TeeType::SevSnp
}
fn detect(&self) -> Result<TeeStatus, AppError> {
let detected = std::path::Path::new("/dev/sev-guest").exists();
let platform_version = if detected { read_sev_version() } else { None };
if detected {
info!(version = ?platform_version, "SEV-SNP guest device detected");
}
Ok(TeeStatus {
tee_type: TeeType::SevSnp,
detected,
platform_version,
})
}
fn attest(&self, user_data: &[u8], nonce: &[u8]) -> Result<AttestationReport, AppError> {
let report_data = build_report_data(user_data, nonce);
debug!(
user_data_len = user_data.len(),
nonce_len = nonce.len(),
"requesting SEV-SNP attestation report"
);
let evidence = request_snp_report(&report_data)?;
let policy = u64::from_le_bytes(
evidence[POLICY_OFFSET..POLICY_OFFSET + 8]
.try_into()
.unwrap_or_default(),
);
let guest_svn = u32::from_le_bytes(
evidence[GUEST_SVN_OFFSET..GUEST_SVN_OFFSET + 4]
.try_into()
.unwrap_or_default(),
);
debug!(
evidence_len = evidence.len(),
policy = format!("{policy:#018x}"),
guest_svn,
"SEV-SNP attestation report generated"
);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Ok(AttestationReport {
tee_type: TeeType::SevSnp,
evidence: BASE64.encode(&evidence),
nonce: hex::encode(nonce),
generated_at: now,
vta_did: None,
})
}
fn smoke_check_structure(
&self,
report: &AttestationReport,
) -> Result<StructuralCheckOutcome, AppError> {
if report.tee_type != TeeType::SevSnp {
return Ok(StructuralCheckOutcome::Malformed);
}
let evidence = BASE64
.decode(&report.evidence)
.map_err(|e| tee_attestation_error(format!("invalid evidence encoding: {e}")))?;
if evidence.len() != SNP_REPORT_SIZE {
debug!(
actual_len = evidence.len(),
expected_len = SNP_REPORT_SIZE,
"SNP report size mismatch"
);
return Ok(StructuralCheckOutcome::Malformed);
}
let version = u32::from_le_bytes(
evidence[VERSION_OFFSET..VERSION_OFFSET + 4]
.try_into()
.unwrap_or_default(),
);
if version != 2 {
debug!(version, "unexpected SNP report version (expected 2)");
return Ok(StructuralCheckOutcome::Malformed);
}
let sig_algo = u32::from_le_bytes(
evidence[SIG_ALGO_OFFSET..SIG_ALGO_OFFSET + 4]
.try_into()
.unwrap_or_default(),
);
if sig_algo != 1 {
debug!(
sig_algo,
"unexpected signature algorithm (expected 1 = ECDSA P-384)"
);
return Ok(StructuralCheckOutcome::Malformed);
}
let report_data = &evidence[REPORT_DATA_OFFSET..REPORT_DATA_OFFSET + 64];
if report_data.iter().all(|&b| b == 0) {
debug!("report_data is all zeros — may indicate empty attestation");
}
if !report.nonce.is_empty()
&& let Ok(nonce_bytes) = hex::decode(&report.nonce)
{
let nonce_in_report = &report_data[32..32 + nonce_bytes.len().min(32)];
let expected = &nonce_bytes[..nonce_bytes.len().min(32)];
if nonce_in_report[..expected.len()] != *expected {
debug!("nonce mismatch in report_data");
return Ok(StructuralCheckOutcome::Malformed);
}
}
debug!("SNP report structural smoke-check passed (shape + nonce)");
Ok(StructuralCheckOutcome::StructurallyValid)
}
}
const SNP_REPORT_SIZE: usize = 1184;
const VERSION_OFFSET: usize = 0;
const GUEST_SVN_OFFSET: usize = 4;
const POLICY_OFFSET: usize = 8;
const REPORT_DATA_OFFSET: usize = 80;
const SIG_ALGO_OFFSET: usize = 0x34;
fn build_report_data(user_data: &[u8], nonce: &[u8]) -> [u8; 64] {
use sha2::{Digest, Sha256};
let mut report_data = [0u8; 64];
let user_hash = Sha256::digest(user_data);
report_data[..32].copy_from_slice(&user_hash);
let nonce_len = nonce.len().min(32);
report_data[32..32 + nonce_len].copy_from_slice(&nonce[..nonce_len]);
report_data
}
fn read_sev_version() -> Option<String> {
let major = std::fs::read_to_string("/sys/firmware/sev/api_major").ok()?;
let minor = std::fs::read_to_string("/sys/firmware/sev/api_minor").ok()?;
let build = std::fs::read_to_string("/sys/firmware/sev/build")
.ok()
.map(|b| format!(" build {}", b.trim()));
Some(format!(
"{}.{}{}",
major.trim(),
minor.trim(),
build.unwrap_or_default()
))
}
fn request_snp_report(report_data: &[u8; 64]) -> Result<Vec<u8>, AppError> {
use std::fs::OpenOptions;
use std::os::unix::io::AsRawFd;
let dev = OpenOptions::new()
.read(true)
.write(true)
.open("/dev/sev-guest")
.map_err(|e| tee_attestation_error(format!("failed to open /dev/sev-guest: {e}")))?;
#[repr(C)]
struct SnpReportReq {
user_data: [u8; 64], vmpl: u32, rsvd: [u8; 28], }
#[repr(C)]
struct SnpReportResp {
status: u32, report_size: u32, rsvd: [u8; 24], report: [u8; SNP_REPORT_SIZE], }
#[repr(C)]
struct SnpGuestRequestIoctl {
msg_version: u8, req_data: u64, resp_data: u64, fw_err: u64, }
let mut req = SnpReportReq {
user_data: *report_data,
vmpl: 0,
rsvd: [0u8; 28],
};
let mut resp = SnpReportResp {
status: 0,
report_size: 0,
rsvd: [0u8; 24],
report: [0u8; SNP_REPORT_SIZE],
};
let mut guest_req = SnpGuestRequestIoctl {
msg_version: 1,
req_data: &mut req as *mut SnpReportReq as u64,
resp_data: &mut resp as *mut SnpReportResp as u64,
fw_err: 0,
};
const SNP_GET_REPORT: libc::c_ulong = 0xC020_5300;
let ret = unsafe {
libc::ioctl(
dev.as_raw_fd(),
SNP_GET_REPORT,
&mut guest_req as *mut SnpGuestRequestIoctl,
)
};
if ret != 0 {
let errno = std::io::Error::last_os_error();
return Err(tee_attestation_error(format!(
"SNP_GET_REPORT ioctl failed: {errno} (fw_err={:#x})",
guest_req.fw_err
)));
}
if resp.status != 0 {
return Err(tee_attestation_error(format!(
"SNP_GET_REPORT firmware error: status={}, fw_err={:#x}",
resp.status, guest_req.fw_err
)));
}
debug!(
report_size = resp.report_size,
"SEV-SNP report received from PSP"
);
Ok(resp.report.to_vec())
}