use async_trait::async_trait;
use sha2::{Digest, Sha384};
use std::collections::HashMap;
use uuid::Uuid;
use tenzro_types::tee::*;
use crate::attestation::{self, ParsedCertificate};
use crate::certs;
use crate::error::{Result, TeeError};
use crate::traits::TeeProvider;
#[cfg(target_os = "linux")]
const SEV_IOCTL_MAGIC: u8 = b'S';
#[cfg(target_os = "linux")]
const SNP_GET_REPORT_NR: u8 = 0;
#[cfg(target_os = "linux")]
const SNP_GET_DERIVED_KEY_NR: u8 = 1;
#[cfg(target_os = "linux")]
const SNP_REPORT_USER_DATA_SIZE: usize = 64;
const SNP_REPORT_SIZE: usize = 1184;
pub const SNP_DERIVED_KEY_SIZE: usize = 64;
pub mod guest_field_select {
pub const GUEST_POLICY: u64 = 1 << 0;
pub const IMAGE_ID: u64 = 1 << 1;
pub const FAMILY_ID: u64 = 1 << 2;
pub const MEASUREMENT: u64 = 1 << 3;
pub const GUEST_SVN: u64 = 1 << 4;
pub const TCB_VERSION: u64 = 1 << 5;
}
pub mod report_offsets {
pub const VERSION: usize = 0x000; pub const GUEST_SVN: usize = 0x004; pub const POLICY: usize = 0x008; pub const FAMILY_ID: usize = 0x010; pub const IMAGE_ID: usize = 0x020; pub const VMPL: usize = 0x030; pub const SIGNATURE_ALGO: usize = 0x034; pub const PLATFORM_VERSION: usize = 0x038; pub const PLATFORM_INFO: usize = 0x040; pub const FLAGS: usize = 0x048; pub const REPORT_DATA: usize = 0x050; pub const MEASUREMENT: usize = 0x090; pub const HOST_DATA: usize = 0x0C0; pub const ID_KEY_DIGEST: usize = 0x0E0; pub const AUTHOR_KEY_DIGEST: usize = 0x110; pub const REPORT_ID: usize = 0x140; pub const REPORT_ID_MA: usize = 0x160; pub const REPORTED_TCB: usize = 0x180; pub const CHIP_ID: usize = 0x1A0; pub const COMMITTED_TCB: usize = 0x1E0;
pub const SIGNATURE: usize = 0x2A0; pub const SIGNATURE_R: usize = 0x2A0; pub const SIGNATURE_S: usize = 0x2E8;
pub const SIGNED_BODY_LEN: usize = 0x2A0; }
#[derive(Debug, Clone)]
struct SnpReport {
version: u32,
guest_svn: u32,
policy: u64,
vmpl: u32,
platform_version: u64,
reported_tcb: u64,
committed_tcb: u64,
measurement: Vec<u8>,
report_data: Vec<u8>,
host_data: Vec<u8>,
chip_id: Vec<u8>,
report_id: Vec<u8>,
signature_r: Vec<u8>,
signature_s: Vec<u8>,
raw: Vec<u8>,
simulated: bool,
}
#[derive(Clone)]
pub struct AmdSevSnpProvider {
keystore: std::sync::Arc<crate::enclave_keystore::EnclaveKeystore>,
available: bool,
simulate: bool,
}
impl std::fmt::Debug for AmdSevSnpProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AmdSevSnpProvider")
.field("available", &self.available)
.field("simulate", &self.simulate)
.finish_non_exhaustive()
}
}
impl AmdSevSnpProvider {
pub fn new() -> Self {
let simulate = is_simulation_mode();
let available = if simulate {
tracing::debug!("AMD SEV-SNP running in simulation mode");
true
} else {
Self::detect_sev_snp_hardware()
};
Self {
keystore: std::sync::Arc::new(crate::enclave_keystore::EnclaveKeystore::new("amd-sev-snp")),
available,
simulate,
}
}
pub fn bind_platform_measurement(&self, measurement: &[u8]) {
use sha2::{Digest, Sha256};
let hash = Sha256::digest(measurement);
let mut m = [0u8; 32];
m.copy_from_slice(&hash);
crate::enclave_crypto::set_platform_measurement(m);
tracing::info!("Enclave key derivation bound to SEV-SNP platform measurement");
}
fn detect_sev_snp_hardware() -> bool {
if std::path::Path::new("/dev/sev-guest").exists() {
tracing::info!("AMD SEV-SNP hardware detected at /dev/sev-guest");
return true;
}
if std::path::Path::new("/sys/kernel/config/tsm/report").exists() {
if let Ok(provider) = std::fs::read_to_string("/sys/kernel/config/tsm/report/provider")
&& provider.trim() == "sev_guest"
{
tracing::info!("AMD SEV-SNP detected via configfs-tsm");
return true;
}
}
if std::path::Path::new("/dev/sev").exists() {
tracing::info!("AMD SEV hardware detected at /dev/sev (may not support SNP)");
return true;
}
tracing::warn!("AMD SEV-SNP hardware not available");
false
}
fn generate_snp_report_real(&self, user_data: &[u8]) -> Result<Vec<u8>> {
#[cfg(target_os = "linux")]
{
use std::fs::OpenOptions;
use std::os::unix::io::AsRawFd;
let mut req = vec![0u8; 96];
let copy_len = user_data.len().min(SNP_REPORT_USER_DATA_SIZE);
req[..copy_len].copy_from_slice(&user_data[..copy_len]);
req[64..68].copy_from_slice(&0u32.to_le_bytes());
const SNP_REPORT_RESP_SIZE: usize = 4000;
let mut resp = vec![0u8; SNP_REPORT_RESP_SIZE];
const SNP_GUEST_REQUEST_IOCTL_SIZE: usize = 32;
let mut ioctl_buf = vec![0u8; SNP_GUEST_REQUEST_IOCTL_SIZE];
ioctl_buf[0] = 1; let req_ptr = req.as_ptr() as u64;
ioctl_buf[8..16].copy_from_slice(&req_ptr.to_ne_bytes());
let resp_ptr = resp.as_mut_ptr() as u64;
ioctl_buf[16..24].copy_from_slice(&resp_ptr.to_ne_bytes());
let file = OpenOptions::new()
.read(true)
.write(true)
.open("/dev/sev-guest")
.map_err(|e| TeeError::AttestationGenerationFailed(
format!("Failed to open /dev/sev-guest: {}", e)
))?;
let ioctl_nr = build_ioctl_rw(
SEV_IOCTL_MAGIC,
SNP_GET_REPORT_NR,
SNP_GUEST_REQUEST_IOCTL_SIZE as u32,
);
let ret = unsafe {
libc::ioctl(file.as_raw_fd(), ioctl_nr as libc::c_ulong, ioctl_buf.as_mut_ptr())
};
if ret != 0 {
let errno = std::io::Error::last_os_error();
let fw_error = u32::from_le_bytes([
ioctl_buf[24], ioctl_buf[25], ioctl_buf[26], ioctl_buf[27]
]);
let vmm_error = u32::from_le_bytes([
ioctl_buf[28], ioctl_buf[29], ioctl_buf[30], ioctl_buf[31]
]);
return Err(TeeError::AttestationGenerationFailed(format!(
"SNP_GET_REPORT ioctl failed: {} (fw_error: 0x{:X}, vmm_error: 0x{:X})",
errno, fw_error, vmm_error
)));
}
let report_start = 32; if resp.len() >= report_start + SNP_REPORT_SIZE {
let report = resp[report_start..report_start + SNP_REPORT_SIZE].to_vec();
tracing::info!("SNP attestation report generated ({} bytes)", report.len());
Ok(report)
} else {
tracing::warn!("SNP response smaller than expected, returning raw");
Ok(resp)
}
}
#[cfg(not(target_os = "linux"))]
{
let _ = user_data;
Err(TeeError::not_available(
"AMD SEV-SNP requires Linux (ioctl to /dev/sev-guest)"
))
}
}
async fn generate_snp_report(&self, user_data: &[u8]) -> Result<Vec<u8>> {
if self.simulate {
return self.generate_simulated_report(user_data);
}
self.generate_snp_report_real(user_data)
}
fn generate_simulated_report(&self, user_data: &[u8]) -> Result<Vec<u8>> {
let measurement = Sha384::digest(b"simulated-sev-snp-vm-measurement");
let mut report_data = [0u8; 64];
let copy_len = user_data.len().min(64);
report_data[..copy_len].copy_from_slice(&user_data[..copy_len]);
let report = serde_json::json!({
"version": 2,
"type": "SEV_SNP_REPORT_SIMULATED",
"simulated": true,
"guest_svn": 1,
"vmpl": 0,
"policy": {
"abi_minor": 0,
"abi_major": 1,
"smt_allowed": false,
"migration_agent_allowed": false,
"debug_allowed": false,
},
"report_data": hex::encode(report_data),
"measurement": hex::encode(measurement.as_slice()),
"host_data": hex::encode([0u8; 32]),
"id_key_digest": hex::encode(Sha384::digest(b"simulated-id-key").as_slice()),
"author_key_digest": hex::encode(Sha384::digest(b"simulated-author-key").as_slice()),
"report_id": hex::encode([0xABu8; 32]),
"report_id_ma": hex::encode([0xCDu8; 32]),
"reported_tcb": {
"boot_loader": 3,
"tee": 0,
"snp": 12,
"microcode": 209,
"raw": "0300000C00D10000",
},
"committed_tcb": {
"boot_loader": 3,
"tee": 0,
"snp": 12,
"microcode": 209,
},
"chip_id": hex::encode([0xEFu8; 64]),
"signature_algo": 1, });
Ok(serde_json::to_vec(&report)?)
}
fn parse_report(&self, data: &[u8]) -> Result<SnpReport> {
if let Ok(json) = serde_json::from_slice::<serde_json::Value>(data)
&& json.get("simulated").and_then(|v| v.as_bool()).unwrap_or(false)
{
return self.parse_simulated_report(&json, data);
}
self.parse_binary_report(data)
}
fn parse_simulated_report(&self, json: &serde_json::Value, raw: &[u8]) -> Result<SnpReport> {
let get_hex = |key: &str| -> Vec<u8> {
json.get(key)
.and_then(|v| v.as_str())
.and_then(|s| hex::decode(s).ok())
.unwrap_or_default()
};
let tcb = &json["reported_tcb"];
let reported_tcb = tcb.get("raw")
.and_then(|v| v.as_str())
.and_then(|s| hex::decode(s).ok())
.map(|b| {
if b.len() >= 8 {
u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
} else {
0
}
})
.unwrap_or(0);
Ok(SnpReport {
version: json.get("version").and_then(|v| v.as_u64()).unwrap_or(2) as u32,
guest_svn: json.get("guest_svn").and_then(|v| v.as_u64()).unwrap_or(1) as u32,
policy: json.get("policy").and_then(|p| p.get("abi_major")).and_then(|v| v.as_u64()).unwrap_or(1),
vmpl: json.get("vmpl").and_then(|v| v.as_u64()).unwrap_or(0) as u32,
platform_version: 0,
reported_tcb,
committed_tcb: 0,
measurement: get_hex("measurement"),
report_data: get_hex("report_data"),
host_data: get_hex("host_data"),
chip_id: get_hex("chip_id"),
report_id: get_hex("report_id"),
signature_r: vec![],
signature_s: vec![],
raw: raw.to_vec(),
simulated: true,
})
}
fn parse_binary_report(&self, data: &[u8]) -> Result<SnpReport> {
if data.len() < SNP_REPORT_SIZE {
return Err(TeeError::InvalidAttestationReport(format!(
"SNP report too short: {} bytes (need {})", data.len(), SNP_REPORT_SIZE
)));
}
let version = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
if version != 2 {
return Err(TeeError::InvalidAttestationReport(format!(
"Unsupported SNP report version: {} (expected 2)", version
)));
}
let guest_svn = u32::from_le_bytes(
data[report_offsets::GUEST_SVN..report_offsets::GUEST_SVN + 4].try_into().unwrap()
);
let policy = u64::from_le_bytes(
data[report_offsets::POLICY..report_offsets::POLICY + 8].try_into().unwrap()
);
let vmpl = u32::from_le_bytes(
data[report_offsets::VMPL..report_offsets::VMPL + 4].try_into().unwrap()
);
let platform_version = u64::from_le_bytes(
data[report_offsets::PLATFORM_VERSION..report_offsets::PLATFORM_VERSION + 8].try_into().unwrap()
);
let reported_tcb = u64::from_le_bytes(
data[report_offsets::REPORTED_TCB..report_offsets::REPORTED_TCB + 8].try_into().unwrap()
);
let committed_tcb = u64::from_le_bytes(
data[report_offsets::COMMITTED_TCB..report_offsets::COMMITTED_TCB + 8].try_into().unwrap()
);
let sig_r_padded = &data[report_offsets::SIGNATURE_R..report_offsets::SIGNATURE_R + 72];
let sig_s_padded = &data[report_offsets::SIGNATURE_S..report_offsets::SIGNATURE_S + 72];
let signature_r = sig_r_padded[..48].to_vec();
let signature_s = sig_s_padded[..48].to_vec();
Ok(SnpReport {
version,
guest_svn,
policy,
vmpl,
platform_version,
reported_tcb,
committed_tcb,
measurement: data[report_offsets::MEASUREMENT..report_offsets::MEASUREMENT + 48].to_vec(),
report_data: data[report_offsets::REPORT_DATA..report_offsets::REPORT_DATA + 64].to_vec(),
host_data: data[report_offsets::HOST_DATA..report_offsets::HOST_DATA + 32].to_vec(),
chip_id: data[report_offsets::CHIP_ID..report_offsets::CHIP_ID + 64].to_vec(),
report_id: data[report_offsets::REPORT_ID..report_offsets::REPORT_ID + 32].to_vec(),
signature_r,
signature_s,
raw: data.to_vec(),
simulated: false,
})
}
fn decode_tcb(tcb: u64) -> HashMap<String, u8> {
let bytes = tcb.to_le_bytes();
HashMap::from([
("boot_loader".to_string(), bytes[0]),
("tee".to_string(), bytes[1]),
("reserved0".to_string(), bytes[2]),
("reserved1".to_string(), bytes[3]),
("snp".to_string(), bytes[4]),
("microcode".to_string(), bytes[5]),
("reserved2".to_string(), bytes[6]),
("reserved3".to_string(), bytes[7]),
])
}
#[cfg(feature = "amd-sev-snp")]
async fn fetch_vcek_certificate(&self, chip_id: &[u8], reported_tcb: u64) -> Result<Vec<u8>> {
if chip_id.len() != 64 {
return Err(TeeError::InvalidAttestationReport(
format!("Invalid chip_id length: {} (expected 64)", chip_id.len())
));
}
let tcb = Self::decode_tcb(reported_tcb);
let chip_id_hex = hex::encode(chip_id).to_lowercase();
let product_names = ["Milan", "Genoa"];
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| TeeError::AttestationGenerationFailed(format!("Failed to build HTTP client: {}", e)))?;
for product_name in &product_names {
let url = format!(
"https://kdsintf.amd.com/vcek/v1/{}/{}?blSPL={}&teeSPL={}&snpSPL={}&ucodeSPL={}",
product_name,
chip_id_hex,
tcb.get("boot_loader").unwrap_or(&0),
tcb.get("tee").unwrap_or(&0),
tcb.get("snp").unwrap_or(&0),
tcb.get("microcode").unwrap_or(&0),
);
tracing::info!("Fetching VCEK from AMD KDS: {}", url);
match client.get(&url).send().await {
Ok(resp) if resp.status().is_success() => {
let vcek_der = resp.bytes().await
.map_err(|e| TeeError::AttestationGenerationFailed(format!("Failed to read VCEK: {}", e)))?
.to_vec();
tracing::info!("Fetched VCEK certificate from AMD KDS ({} bytes, product={})", vcek_der.len(), product_name);
return Ok(vcek_der);
}
Ok(resp) => {
tracing::debug!("AMD KDS VCEK request failed for {}: {}", product_name, resp.status());
}
Err(e) => {
tracing::debug!("AMD KDS VCEK request error for {}: {}", product_name, e);
}
}
}
Err(TeeError::AttestationGenerationFailed(
format!("Failed to fetch VCEK from AMD KDS for chip_id={}", chip_id_hex)
))
}
#[cfg(not(feature = "amd-sev-snp"))]
async fn fetch_vcek_certificate(&self, _chip_id: &[u8], _reported_tcb: u64) -> Result<Vec<u8>> {
tracing::warn!("AMD KDS VCEK fetching requires reqwest (enable amd-sev-snp feature)");
Err(TeeError::not_available("AMD KDS VCEK fetching not available"))
}
#[cfg(feature = "amd-sev-snp")]
async fn fetch_cert_chain(&self, product_name: &str) -> Result<Vec<Vec<u8>>> {
let url = format!("https://kdsintf.amd.com/vcek/v1/{}/cert_chain", product_name);
tracing::info!("Fetching ASK+ARK cert chain from AMD KDS: {}", url);
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| TeeError::AttestationGenerationFailed(format!("Failed to build HTTP client: {}", e)))?;
let resp = client.get(&url)
.send()
.await
.map_err(|e| TeeError::AttestationGenerationFailed(format!("Failed to fetch cert chain: {}", e)))?;
if !resp.status().is_success() {
return Err(TeeError::AttestationGenerationFailed(
format!("AMD KDS cert_chain request failed: {}", resp.status())
));
}
let pem_data = resp.text().await
.map_err(|e| TeeError::AttestationGenerationFailed(format!("Failed to read cert chain: {}", e)))?;
let mut certs = Vec::new();
for cert_pem in pem_data.split("-----END CERTIFICATE-----") {
if let Some(start) = cert_pem.find("-----BEGIN CERTIFICATE-----") {
let full_pem = format!("{}-----END CERTIFICATE-----", &cert_pem[start..]);
match certs::pem_to_der(&full_pem) {
Ok(der) => certs.push(der),
Err(e) => tracing::warn!("Failed to parse certificate from cert_chain: {}", e),
}
}
}
tracing::info!("Fetched {} certificates from AMD KDS cert_chain", certs.len());
Ok(certs)
}
#[cfg(not(feature = "amd-sev-snp"))]
async fn fetch_cert_chain(&self, _product_name: &str) -> Result<Vec<Vec<u8>>> {
tracing::warn!("AMD KDS cert_chain fetching requires reqwest (enable amd-sev-snp feature)");
Ok(vec![])
}
async fn verify_snp_report(&self, report_data: &[u8], certificates: &[Vec<u8>]) -> Result<AttestationResult> {
let report = self.parse_report(report_data)?;
let tcb_components = if report.simulated {
if let Ok(json) = serde_json::from_slice::<serde_json::Value>(report_data) {
let tcb = &json["reported_tcb"];
format!(
"BL:{}.TEE:{}.SNP:{}.UCODE:{}",
tcb["boot_loader"].as_u64().unwrap_or(0),
tcb["tee"].as_u64().unwrap_or(0),
tcb["snp"].as_u64().unwrap_or(0),
tcb["microcode"].as_u64().unwrap_or(0),
)
} else {
"unknown".to_string()
}
} else {
let tcb = Self::decode_tcb(report.reported_tcb);
format!(
"BL:{}.TEE:{}.SNP:{}.UCODE:{}",
tcb.get("boot_loader").unwrap_or(&0),
tcb.get("tee").unwrap_or(&0),
tcb.get("snp").unwrap_or(&0),
tcb.get("microcode").unwrap_or(&0),
)
};
let mut details = HashMap::from([
("vmpl".to_string(), report.vmpl.to_string()),
("guest_svn".to_string(), report.guest_svn.to_string()),
("version".to_string(), report.version.to_string()),
("policy".to_string(), format!("0x{:016X}", report.policy)),
("platform_version".to_string(), format!("0x{:016X}", report.platform_version)),
("committed_tcb".to_string(), format!("0x{:016X}", report.committed_tcb)),
("report_data".to_string(), hex::encode(&report.report_data)),
("host_data".to_string(), hex::encode(&report.host_data)),
("report_id".to_string(), hex::encode(&report.report_id)),
]);
let mut cert_chain_valid = false;
let mut signature_valid = false;
if report.simulated {
details.insert("simulated".to_string(), "true".to_string());
details.insert("type".to_string(), "simulated".to_string());
tracing::warn!(
"Verifying SIMULATED AMD SEV-SNP report — AttestationResult.valid \
will be false. Simulated reports carry no cryptographic authority."
);
} else {
details.insert("type".to_string(), "real".to_string());
details.insert("chip_id".to_string(), hex::encode(&report.chip_id));
tracing::info!("Verifying real AMD SEV-SNP report");
if !report.signature_r.is_empty() && !report.signature_s.is_empty() {
tracing::debug!(
"SNP report signature: R={} bytes, S={} bytes",
report.signature_r.len(), report.signature_s.len()
);
}
if !certificates.is_empty() {
cert_chain_valid = self.verify_amd_cert_chain(certificates)?;
if !report.signature_r.is_empty() && !report.signature_s.is_empty() {
match self.verify_report_signature(&report, &certificates[0]) {
Ok(true) => {
tracing::info!("SNP report signature verified successfully");
details.insert("signature_verified".to_string(), "true".to_string());
signature_valid = true;
}
Ok(false) => {
tracing::warn!("SNP report signature verification failed");
details.insert("signature_verified".to_string(), "false".to_string());
}
Err(e) => {
tracing::warn!("SNP report signature verification error: {}", e);
details.insert("signature_verified".to_string(), format!("error: {}", e));
}
}
}
}
if report.raw.len() >= report_offsets::SIGNED_BODY_LEN {
let signed_body = &report.raw[..report_offsets::SIGNED_BODY_LEN];
let body_hash = Sha384::digest(signed_body);
details.insert("body_hash".to_string(), hex::encode(body_hash.as_slice()));
}
}
let measurements = vec![
Measurement {
index: 0,
algorithm: "SHA384".to_string(),
value: report.measurement.clone(),
register: "MEASUREMENT".to_string(),
description: Some("Initial VM state measurement".to_string()),
},
];
let valid = !report.simulated && signature_valid && cert_chain_valid;
Ok(AttestationResult {
valid,
vendor: TeeVendor::AmdSevSnp,
tcb_version: tcb_components,
measurements,
cert_chain_valid,
details,
verified_at: tenzro_types::Timestamp::now(),
..Default::default()
})
}
fn verify_report_signature(&self, report: &SnpReport, vcek_der: &[u8]) -> Result<bool> {
let vcek_cert = attestation::parse_x509_certificate(vcek_der)?;
if report.raw.len() < report_offsets::SIGNED_BODY_LEN {
return Err(TeeError::InvalidAttestationReport(
"Report too short for signature verification".to_string()
));
}
let signed_body = &report.raw[..report_offsets::SIGNED_BODY_LEN];
let mut signature_r_be = report.signature_r.clone();
let mut signature_s_be = report.signature_s.clone();
signature_r_be.reverse();
signature_s_be.reverse();
let mut signature_bytes = Vec::with_capacity(96);
signature_bytes.extend_from_slice(&signature_r_be);
signature_bytes.extend_from_slice(&signature_s_be);
attestation::verify_ecdsa_p384_signature(&vcek_cert.spki_der, signed_body, &signature_bytes)
}
fn verify_amd_cert_chain(&self, certificates: &[Vec<u8>]) -> Result<bool> {
let root_der = certs::pem_to_der(certs::AMD_ARK_MILAN_PEM)
.map_err(|e| TeeError::CertificateValidationFailed(
format!("Failed to decode AMD ARK: {}", e)
))?;
let root_cert = attestation::parse_x509_certificate(&root_der)?;
let mut chain: Vec<ParsedCertificate> = Vec::new();
for cert_der in certificates {
match attestation::parse_x509_certificate(cert_der) {
Ok(cert) => chain.push(cert),
Err(e) => {
tracing::warn!("Failed to parse certificate in AMD chain: {}", e);
}
}
}
if chain.is_empty() {
return Ok(false);
}
let last = chain.last().unwrap();
if last.issuer_cn == root_cert.subject_cn || last.subject_cn == root_cert.subject_cn {
let verified = attestation::verify_certificate_signature(last, &root_cert.spki_der)?;
if verified {
tracing::info!("AMD SEV-SNP certificate chain verified against pinned ARK");
}
Ok(verified)
} else {
let genoa_der = certs::pem_to_der(certs::AMD_ARK_GENOA_PEM)
.map_err(|e| TeeError::CertificateValidationFailed(
format!("Failed to decode AMD ARK Genoa: {}", e)
))?;
let genoa_cert = attestation::parse_x509_certificate(&genoa_der)?;
if last.issuer_cn == genoa_cert.subject_cn || last.subject_cn == genoa_cert.subject_cn {
let verified = attestation::verify_certificate_signature(last, &genoa_cert.spki_der)?;
if verified {
tracing::info!("AMD SEV-SNP certificate chain verified against Genoa ARK");
}
Ok(verified)
} else {
tracing::warn!(
"AMD chain does not terminate at ARK: last issuer='{}', Milan ARK='{}', Genoa ARK='{}'",
last.issuer_cn, root_cert.subject_cn, genoa_cert.subject_cn
);
Ok(false)
}
}
}
}
impl AmdSevSnpProvider {
pub fn derived_key(
&self,
root_key_select: u32,
guest_field_select: u64,
vmpl: u32,
guest_svn: u32,
tcb_version: u64,
) -> Result<[u8; SNP_DERIVED_KEY_SIZE]> {
#[cfg(target_os = "linux")]
{
use std::fs::OpenOptions;
use std::os::unix::io::AsRawFd;
const SNP_DERIVED_KEY_REQ_SIZE: usize = 32;
let mut req = vec![0u8; SNP_DERIVED_KEY_REQ_SIZE];
req[0..4].copy_from_slice(&root_key_select.to_le_bytes());
req[8..16].copy_from_slice(&guest_field_select.to_le_bytes());
req[16..20].copy_from_slice(&vmpl.to_le_bytes());
req[20..24].copy_from_slice(&guest_svn.to_le_bytes());
req[24..32].copy_from_slice(&tcb_version.to_le_bytes());
const SNP_DERIVED_KEY_RESP_SIZE: usize = 4000;
let mut resp = vec![0u8; SNP_DERIVED_KEY_RESP_SIZE];
const SNP_GUEST_REQUEST_IOCTL_SIZE: usize = 32;
let mut ioctl_buf = vec![0u8; SNP_GUEST_REQUEST_IOCTL_SIZE];
ioctl_buf[0] = 1; let req_ptr = req.as_ptr() as u64;
ioctl_buf[8..16].copy_from_slice(&req_ptr.to_ne_bytes());
let resp_ptr = resp.as_mut_ptr() as u64;
ioctl_buf[16..24].copy_from_slice(&resp_ptr.to_ne_bytes());
let file = OpenOptions::new()
.read(true)
.write(true)
.open("/dev/sev-guest")
.map_err(|e| TeeError::not_available(format!(
"Failed to open /dev/sev-guest: {}", e
)))?;
let ioctl_nr = build_ioctl_rw(
SEV_IOCTL_MAGIC,
SNP_GET_DERIVED_KEY_NR,
SNP_GUEST_REQUEST_IOCTL_SIZE as u32,
);
let ret = unsafe {
libc::ioctl(file.as_raw_fd(), ioctl_nr as libc::c_ulong, ioctl_buf.as_mut_ptr())
};
if ret != 0 {
let errno = std::io::Error::last_os_error();
let fw_error = u32::from_le_bytes([
ioctl_buf[24], ioctl_buf[25], ioctl_buf[26], ioctl_buf[27]
]);
let vmm_error = u32::from_le_bytes([
ioctl_buf[28], ioctl_buf[29], ioctl_buf[30], ioctl_buf[31]
]);
return Err(TeeError::AttestationGenerationFailed(format!(
"SNP_GET_DERIVED_KEY ioctl failed: {} (fw_error: 0x{:X}, vmm_error: 0x{:X})",
errno, fw_error, vmm_error
)));
}
let key_start = 32;
if resp.len() < key_start + SNP_DERIVED_KEY_SIZE {
return Err(TeeError::AttestationGenerationFailed(
"SNP_GET_DERIVED_KEY response truncated".to_string()
));
}
let mut out = [0u8; SNP_DERIVED_KEY_SIZE];
out.copy_from_slice(&resp[key_start..key_start + SNP_DERIVED_KEY_SIZE]);
tracing::debug!(
"SNP derived key obtained (root_key_select={}, guest_field_select=0x{:X})",
root_key_select, guest_field_select
);
Ok(out)
}
#[cfg(not(target_os = "linux"))]
{
let _ = (root_key_select, guest_field_select, vmpl, guest_svn, tcb_version);
Err(TeeError::not_available(
"SNP_GET_DERIVED_KEY requires Linux (ioctl to /dev/sev-guest)"
))
}
}
}
impl Default for AmdSevSnpProvider {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl TeeProvider for AmdSevSnpProvider {
fn vendor(&self) -> TeeVendor {
TeeVendor::AmdSevSnp
}
async fn is_available(&self) -> Result<bool> {
Ok(self.available)
}
async fn generate_attestation(&self, user_data: &[u8]) -> Result<AttestationReport> {
if !self.available {
return Err(TeeError::not_available("AMD SEV-SNP not available"));
}
let attestation_data = self.generate_snp_report(user_data).await?;
let mut metadata = HashMap::from([
("sev_snp_version".to_string(), "1.55".to_string()),
("firmware_version".to_string(), "1.55.25".to_string()),
]);
if self.simulate {
metadata.insert("simulated".to_string(), "true".to_string());
}
let certificates = if !self.simulate && attestation_data.len() >= SNP_REPORT_SIZE {
match self.parse_binary_report(&attestation_data) {
Ok(report) => {
self.bind_platform_measurement(&report.measurement);
let mut certs = Vec::new();
match self.fetch_vcek_certificate(&report.chip_id, report.reported_tcb).await {
Ok(vcek) => {
tracing::info!("Fetched VCEK certificate ({} bytes)", vcek.len());
certs.push(vcek);
for product_name in &["Milan", "Genoa"] {
match self.fetch_cert_chain(product_name).await {
Ok(chain) if !chain.is_empty() => {
tracing::info!("Fetched {} certs from AMD KDS cert_chain ({})", chain.len(), product_name);
certs.extend(chain);
break;
}
Ok(_) => {
tracing::debug!("No certs in AMD KDS cert_chain for {}", product_name);
}
Err(e) => {
tracing::debug!("Failed to fetch cert_chain for {}: {}", product_name, e);
}
}
}
}
Err(e) => {
tracing::warn!("Failed to fetch VCEK certificate: {}", e);
}
}
certs
}
Err(e) => {
tracing::warn!("Failed to parse SNP report for cert fetching: {}", e);
vec![]
}
}
} else {
vec![]
};
Ok(AttestationReport {
id: Uuid::new_v4(),
vendor: TeeVendor::AmdSevSnp,
user_data: user_data.to_vec(),
attestation_data,
certificates,
timestamp: tenzro_types::Timestamp::now(),
metadata,
..Default::default()
})
}
async fn verify_attestation(&self, report: &AttestationReport) -> Result<AttestationResult> {
if report.vendor != TeeVendor::AmdSevSnp {
return Err(TeeError::InvalidAttestationReport(
"Report is not from AMD SEV-SNP".to_string(),
));
}
self.verify_snp_report(&report.attestation_data, &report.certificates).await
}
async fn execute_in_enclave(&self, request: EnclaveRequest) -> Result<EnclaveResponse> {
if !self.available {
return Err(TeeError::not_available("AMD SEV-SNP not available"));
}
tracing::info!("Executing in SEV-SNP VM: {:?}", request.operation);
Ok(EnclaveResponse {
request_id: request.id,
success: true,
data: request.params,
error: None,
attestation: None,
})
}
async fn enclave_keygen(&self, params: KeyGenParams) -> Result<EnclaveKeyHandle> {
if !self.available {
return Err(TeeError::not_available("AMD SEV-SNP not available"));
}
if self.simulate {
return Err(TeeError::not_available(
"AMD SEV-SNP simulation mode cannot supply real SNP_GET_DERIVED_KEY IKM",
));
}
const ROOT_KEY_SELECT: u32 = 0;
const GUEST_FIELD_SELECT: u64 = 0b101; let ikm = self.derived_key(ROOT_KEY_SELECT, GUEST_FIELD_SELECT, 0, 0, 0)?;
let handle = self.keystore.keygen(params, &ikm).await?;
tracing::info!(
key_id = %handle.id,
algorithm = ?handle.algorithm,
"Generated key in SEV-SNP VM keystore"
);
Ok(handle)
}
async fn enclave_sign(&self, key: &EnclaveKeyHandle, data: &[u8]) -> Result<Vec<u8>> {
if !self.available {
return Err(TeeError::not_available("AMD SEV-SNP not available"));
}
self.keystore.sign(key, data).await
}
async fn enclave_encrypt(&self, key: &EnclaveKeyHandle, plaintext: &[u8]) -> Result<Vec<u8>> {
if !self.available {
return Err(TeeError::not_available("AMD SEV-SNP not available"));
}
self.keystore.encrypt(key, plaintext).await
}
async fn enclave_decrypt(&self, key: &EnclaveKeyHandle, ciphertext: &[u8]) -> Result<Vec<u8>> {
if !self.available {
return Err(TeeError::not_available("AMD SEV-SNP not available"));
}
self.keystore.decrypt(key, ciphertext).await
}
}
fn is_simulation_mode() -> bool {
std::env::var("TENZRO_SIMULATE_SEV")
.or_else(|_| std::env::var("TENZRO_SIMULATE_SEV_SNP"))
.unwrap_or_else(|_| "0".to_string()) == "1"
}
#[cfg(target_os = "linux")]
fn build_ioctl_rw(magic: u8, nr: u8, size: u32) -> u32 {
let dir: u32 = 3; (dir << 30) | ((size & 0x3FFF) << 16) | ((magic as u32) << 8) | (nr as u32)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_sev_snp_provider_creation() {
let provider = AmdSevSnpProvider::new();
assert_eq!(provider.vendor(), TeeVendor::AmdSevSnp);
}
#[test]
fn test_derived_key_off_hardware_returns_not_available() {
let provider = AmdSevSnpProvider::new();
let result = provider.derived_key(
0,
guest_field_select::MEASUREMENT | guest_field_select::IMAGE_ID,
0,
0,
0,
);
match result {
Err(TeeError::NotAvailable(_)) => { }
Err(TeeError::AttestationGenerationFailed(_)) => {
}
Ok(_) => {
assert!(std::path::Path::new("/dev/sev-guest").exists());
}
Err(other) => panic!("unexpected error variant: {:?}", other),
}
}
#[test]
fn test_guest_field_select_bits_match_amd_spec() {
assert_eq!(guest_field_select::GUEST_POLICY, 0x01);
assert_eq!(guest_field_select::IMAGE_ID, 0x02);
assert_eq!(guest_field_select::FAMILY_ID, 0x04);
assert_eq!(guest_field_select::MEASUREMENT, 0x08);
assert_eq!(guest_field_select::GUEST_SVN, 0x10);
assert_eq!(guest_field_select::TCB_VERSION, 0x20);
}
#[tokio::test]
async fn test_sev_snp_simulation_mode() {
unsafe { std::env::set_var("TENZRO_SIMULATE_SEV_SNP", "1"); }
let provider = AmdSevSnpProvider::new();
assert!(provider.simulate);
assert!(provider.available);
}
#[tokio::test]
async fn test_sev_snp_generate_simulated_report() {
unsafe { std::env::set_var("TENZRO_SIMULATE_SEV_SNP", "1"); }
let provider = AmdSevSnpProvider::new();
let user_data = b"tenzro-sev-test";
let report = provider.generate_attestation(user_data).await;
assert!(report.is_ok());
let report = report.unwrap();
assert_eq!(report.vendor, TeeVendor::AmdSevSnp);
assert_eq!(report.user_data, user_data);
assert!(!report.attestation_data.is_empty());
assert_eq!(report.metadata.get("simulated"), Some(&"true".to_string()));
}
#[tokio::test]
async fn test_sev_snp_verify_simulated_report_is_invalid() {
unsafe { std::env::set_var("TENZRO_SIMULATE_SEV_SNP", "1"); }
let provider = AmdSevSnpProvider::new();
let report = provider.generate_attestation(b"test").await.unwrap();
let result = provider.verify_attestation(&report).await.unwrap();
assert!(
!result.valid,
"simulated SEV-SNP reports must never report valid=true"
);
assert_eq!(result.vendor, TeeVendor::AmdSevSnp);
assert_eq!(result.details.get("simulated"), Some(&"true".to_string()));
assert!(!result.measurements.is_empty());
assert_eq!(result.measurements[0].algorithm, "SHA384");
assert_eq!(result.measurements[0].register, "MEASUREMENT");
}
#[tokio::test]
async fn test_sev_snp_keygen_in_simulation_returns_not_available() {
unsafe { std::env::set_var("TENZRO_SIMULATE_SEV_SNP", "1"); }
let provider = AmdSevSnpProvider::new();
let params = KeyGenParams {
algorithm: KeyAlgorithm::Secp256k1,
purpose: KeyPurpose::Signing,
exportable: false,
params: HashMap::new(),
};
let err = provider.enclave_keygen(params).await.unwrap_err();
assert!(
matches!(err, TeeError::NotAvailable(_)),
"expected NotAvailable, got {err:?}"
);
}
#[tokio::test]
async fn test_sev_snp_keystore_real_secp256k1_recovers() {
use k256::ecdsa::{
RecoveryId, Signature as K256Sig, VerifyingKey as Secp256k1Verifying,
};
use k256::elliptic_curve::sec1::ToSec1Point;
use sha2::{Digest, Sha256};
let ks = crate::enclave_keystore::EnclaveKeystore::new("amd-sev-snp-test");
let ikm: Vec<u8> = (0u8..64).collect();
let params = KeyGenParams {
algorithm: KeyAlgorithm::Secp256k1,
purpose: KeyPurpose::Signing,
exportable: false,
params: HashMap::new(),
};
let handle = ks.keygen(params, &ikm).await.unwrap();
let pk_uncompressed = handle.public_key.clone().unwrap();
let msg = b"amd-sev-snp real Secp256k1";
let sig = ks.sign(&handle, msg).await.unwrap();
let mut h = Sha256::new();
h.update(msg);
let digest = h.finalize();
let r_s: [u8; 64] = sig[..64].try_into().unwrap();
let v = sig[64];
let parsed = K256Sig::from_slice(&r_s).unwrap();
let rec = RecoveryId::from_byte(v).unwrap();
let recovered = Secp256k1Verifying::recover_from_prehash(&digest, &parsed, rec).unwrap();
let recovered_encoded =
k256::PublicKey::from(&recovered).to_sec1_point(false);
assert_eq!(recovered_encoded.as_bytes(), pk_uncompressed.as_slice());
}
#[tokio::test]
async fn test_sev_snp_wrong_vendor_rejected() {
unsafe { std::env::set_var("TENZRO_SIMULATE_SEV_SNP", "1"); }
let provider = AmdSevSnpProvider::new();
let mut report = provider.generate_attestation(b"test").await.unwrap();
report.vendor = TeeVendor::IntelTdx;
let result = provider.verify_attestation(&report).await;
assert!(result.is_err());
}
#[test]
fn test_report_offsets() {
assert_eq!(report_offsets::REPORT_DATA, 0x050);
assert_eq!(report_offsets::MEASUREMENT, 0x090);
assert_eq!(report_offsets::CHIP_ID, 0x1A0);
assert_eq!(report_offsets::SIGNATURE, 0x2A0);
assert_eq!(report_offsets::SIGNED_BODY_LEN, 0x2A0);
}
#[test]
fn test_decode_tcb() {
let tcb: u64 = 0x0000_D10C_0000_0003;
let components = AmdSevSnpProvider::decode_tcb(tcb);
assert_eq!(*components.get("boot_loader").unwrap(), 3);
}
#[tokio::test]
async fn test_sev_snp_parse_simulated_report() {
unsafe { std::env::set_var("TENZRO_SIMULATE_SEV_SNP", "1"); }
let provider = AmdSevSnpProvider::new();
let data = provider.generate_simulated_report(b"hello").unwrap();
let report = provider.parse_report(&data).unwrap();
assert!(report.simulated);
assert_eq!(report.version, 2);
assert_eq!(report.vmpl, 0);
assert!(!report.measurement.is_empty());
}
}