use super::tsm::{self, TsmRoot};
use super::{AttestationError, AttestationProvider};
use crate::verifier::submod;
use crate::{AttestationParams, EatClaimsSet};
use ciborium::Value;
use rustls_pki_types::pem::PemObject;
use rustls_pki_types::CertificateDer;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
pub use super::tsm::{report_data, CONFIGFS_TSM_REPORT, REPORT_DATA_LEN};
const DEVICE: &str = "/dev/sev-guest";
const TSM_PROVIDER_SEV: &str = "sev_guest";
pub const VCEK_ENV: &str = "TTK_SEV_SNP_VCEK";
const EAT_PROFILE: &str = "tag:lanetus.github.io,2026:sev-snp-nested-eat";
const REPORT_LEN: usize = 0x4A0;
const OFFSET_FLAGS: usize = 0x48;
const OFFSET_REPORT_DATA: usize = 0x50;
const VCEK_GUID: [u8; 16] = [
0x63, 0xda, 0x75, 0x8d, 0xe6, 0x64, 0x45, 0x64, 0xad, 0xc5, 0xf4, 0xb9, 0x3b, 0xe8, 0xac, 0xcd,
];
const CERT_TABLE_ENTRY_LEN: usize = 24;
#[derive(Debug)]
pub struct SevSnpSession {
tsm: TsmRoot,
vcek: Option<Vec<u8>>,
}
#[derive(Debug, Clone)]
pub struct SevSnpEvidence {
pub report: Vec<u8>,
pub vcek: Vec<u8>,
}
impl SevSnpSession {
pub fn open() -> Result<Self, AttestationError> {
let session = Self::open_at(CONFIGFS_TSM_REPORT)?;
match std::env::var_os(VCEK_ENV) {
Some(path) => session.with_vcek_file(path),
None => Ok(session),
}
}
pub fn open_at(report_root: impl Into<PathBuf>) -> Result<Self, AttestationError> {
Ok(Self {
tsm: TsmRoot::open_at(report_root, "an SEV-SNP")?,
vcek: None,
})
}
pub fn with_vcek_file(self, path: impl AsRef<Path>) -> Result<Self, AttestationError> {
let path = path.as_ref();
let vcek = std::fs::read(path).map_err(|e| {
AttestationError::InvalidInput(format!(
"failed to read the VCEK certificate {}: {e}",
path.display()
))
})?;
self.with_vcek(&vcek)
}
pub fn with_vcek(mut self, vcek: &[u8]) -> Result<Self, AttestationError> {
self.vcek = Some(parse_certificate(vcek)?);
Ok(self)
}
pub fn get_evidence(
&self,
report_data: &[u8; REPORT_DATA_LEN],
) -> Result<SevSnpEvidence, AttestationError> {
let response = self.tsm.request(TSM_PROVIDER_SEV, report_data, true)?;
evidence_from_response(response, report_data, self.vcek.as_deref())
}
}
impl AttestationProvider for SevSnpSession {
fn name(&self) -> &'static str {
"sev-snp"
}
fn is_available() -> bool {
Path::new(DEVICE).exists()
}
fn generate_document(
&self,
params: &AttestationParams,
) -> Result<EatClaimsSet, AttestationError> {
let evidence = self.get_evidence(&report_data(params)?)?;
Ok(wrap_evidence_as_eat(&evidence))
}
}
pub fn evidence_from_entry(
entry: &Path,
report_data: &[u8; REPORT_DATA_LEN],
vcek: Option<&[u8]>,
) -> Result<SevSnpEvidence, AttestationError> {
let response = tsm::request_in_entry(entry, TSM_PROVIDER_SEV, report_data, vcek.is_none())?;
evidence_from_response(response, report_data, vcek)
}
fn evidence_from_response(
response: tsm::TsmReport,
report_data: &[u8; REPORT_DATA_LEN],
vcek: Option<&[u8]>,
) -> Result<SevSnpEvidence, AttestationError> {
let report = response.outblob;
check_report(&report, report_data)?;
let vcek = match vcek {
Some(vcek) => vcek.to_vec(),
None => response
.auxblob
.as_deref()
.map(vcek_from_cert_table)
.transpose()?
.flatten()
.ok_or_else(|| {
AttestationError::UnexpectedResponse(format!(
"the host did not attach a VCEK certificate to the SEV-SNP report; set \
{VCEK_ENV} to this chip's VCEK from the AMD Key Distribution Service"
))
})?,
};
Ok(SevSnpEvidence { report, vcek })
}
fn check_report(
report: &[u8],
report_data: &[u8; REPORT_DATA_LEN],
) -> Result<(), AttestationError> {
if report.len() != REPORT_LEN {
return Err(AttestationError::DocumentDecodingFailed(format!(
"SEV-SNP report is {} bytes, expected {REPORT_LEN}",
report.len()
)));
}
if report[OFFSET_REPORT_DATA..OFFSET_REPORT_DATA + REPORT_DATA_LEN] != *report_data {
return Err(AttestationError::UnexpectedResponse(
"SEV-SNP report does not carry the requested REPORT_DATA".into(),
));
}
let flags = u32::from_le_bytes(
report[OFFSET_FLAGS..OFFSET_FLAGS + 4]
.try_into()
.expect("4-byte slice"),
);
if (flags >> 2) & 0b111 != 0 {
return Err(AttestationError::Unsupported(
"SEV-SNP report is signed by a VLEK, which the client does not support".into(),
));
}
Ok(())
}
pub fn vcek_from_cert_table(table: &[u8]) -> Result<Option<Vec<u8>>, AttestationError> {
let malformed = |msg: &str| {
AttestationError::DocumentDecodingFailed(format!("SEV-SNP certificate table {msg}"))
};
let vcek_guid_le = guid_to_le(&VCEK_GUID);
for entry in table.chunks(CERT_TABLE_ENTRY_LEN) {
if entry.len() < CERT_TABLE_ENTRY_LEN {
return Err(malformed("has a truncated entry"));
}
let guid = &entry[..16];
if guid.iter().all(|b| *b == 0) {
break;
}
if guid != VCEK_GUID && guid != vcek_guid_le {
continue;
}
let offset = u32::from_le_bytes(entry[16..20].try_into().expect("4-byte slice")) as usize;
let length = u32::from_le_bytes(entry[20..24].try_into().expect("4-byte slice")) as usize;
let cert = offset
.checked_add(length)
.and_then(|end| table.get(offset..end))
.ok_or_else(|| malformed("points outside the table"))?;
return parse_certificate(cert).map(Some);
}
Ok(None)
}
fn guid_to_le(guid: &[u8; 16]) -> [u8; 16] {
let mut le = *guid;
le[0..4].reverse();
le[4..6].reverse();
le[6..8].reverse();
le
}
fn parse_certificate(bytes: &[u8]) -> Result<Vec<u8>, AttestationError> {
let der = if bytes.starts_with(b"-----BEGIN") {
CertificateDer::from_pem_slice(bytes)
.map_err(|e| AttestationError::InvalidInput(format!("invalid VCEK PEM: {e:?}")))?
.to_vec()
} else {
let (rest, _) = x509_parser::parse_x509_certificate(bytes).map_err(|e| {
AttestationError::InvalidInput(format!("invalid VCEK certificate: {e}"))
})?;
bytes[..bytes.len() - rest.len()].to_vec()
};
Ok(der)
}
pub fn wrap_evidence_as_eat(evidence: &SevSnpEvidence) -> EatClaimsSet {
let iat = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or_default();
EatClaimsSet {
iat: Some(iat),
eat_profile: Some(EAT_PROFILE.to_string()),
submods: Some(Value::Map(vec![(
Value::Text(submod::SEV_SNP.to_string()),
Value::Map(vec![
(
Value::Text("report".into()),
Value::Bytes(evidence.report.clone()),
),
(
Value::Text("vcek".into()),
Value::Bytes(evidence.vcek.clone()),
),
]),
)])),
..EatClaimsSet::default()
}
}