a3s-box-runtime 0.8.0

MicroVM runtime engine — VM lifecycle, OCI images, attestation, networking
//! TEE attestation report types and guest-side report generation.
//!
//! This module defines the data structures for requesting and receiving
//! SNP attestation reports from a guest VM running in a TEE environment.
//! The report is generated by the CPU hardware and cannot be forged.

use serde::{Deserialize, Serialize};

/// Size of the SNP attestation report in bytes (AMD SEV-SNP spec v1.52).
pub const SNP_REPORT_SIZE: usize = 1184;

/// Size of the report_data / user_data field in the SNP report.
pub const SNP_USER_DATA_SIZE: usize = 64;

/// Size of the measurement field in the SNP report.
pub const SNP_MEASUREMENT_SIZE: usize = 48;

/// Request for an attestation report from the guest VM.
///
/// The host sends this to the guest agent, which calls the
/// `SNP_GET_REPORT` ioctl on `/dev/sev-guest` to obtain a
/// hardware-signed attestation report.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttestationRequest {
    /// Nonce provided by the verifier (included in report_data to prevent replay).
    /// Will be hashed together with any additional user_data before being placed
    /// into the report's `report_data` field.
    pub nonce: Vec<u8>,

    /// Optional additional data to bind into the report (e.g., a DH public key).
    /// Combined with the nonce via SHA-512 to produce the 64-byte report_data.
    #[serde(default)]
    pub user_data: Option<Vec<u8>>,
}

/// Attestation report returned from the guest VM.
///
/// Contains the raw hardware-signed SNP report and the certificate
/// chain needed to verify the report's signature.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttestationReport {
    /// Raw SNP attestation report bytes (1184 bytes, signed by VCEK).
    pub report: Vec<u8>,

    /// Certificate chain for verifying the report signature.
    pub cert_chain: CertificateChain,

    /// TEE platform information.
    pub platform: PlatformInfo,
}

/// Certificate chain for SNP report verification.
///
/// Verification order: report signature → VCEK → ASK → ARK (AMD root).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CertificateChain {
    /// VCEK (Versioned Chip Endorsement Key) certificate, DER-encoded.
    /// Signs the attestation report. Unique per chip + TCB version.
    #[serde(default)]
    pub vcek: Vec<u8>,

    /// ASK (AMD SEV Signing Key) certificate, DER-encoded.
    /// Signs the VCEK. Intermediate CA.
    #[serde(default)]
    pub ask: Vec<u8>,

    /// ARK (AMD Root Key) certificate, DER-encoded.
    /// Signs the ASK. AMD's root of trust.
    #[serde(default)]
    pub ark: Vec<u8>,
}

/// Platform information extracted from the SNP report.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PlatformInfo {
    /// SNP report version.
    pub version: u32,

    /// Guest Security Version Number.
    pub guest_svn: u32,

    /// Guest policy (debug, migration, etc.).
    pub policy: u64,

    /// Launch measurement (SHA-384 of initial guest memory).
    /// 48 bytes, hex-encoded for readability.
    pub measurement: String,

    /// Current TCB (Trusted Computing Base) version.
    pub tcb_version: TcbVersion,

    /// CPU chip ID (unique per physical processor), hex-encoded.
    pub chip_id: String,
}

/// TCB (Trusted Computing Base) version components.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TcbVersion {
    /// Boot loader SVN.
    pub boot_loader: u8,
    /// TEE (PSP) SVN.
    pub tee: u8,
    /// SNP firmware SVN.
    pub snp: u8,
    /// CPU microcode SVN.
    pub microcode: u8,
}

/// Parse platform info from raw SNP report bytes.
///
/// Field offsets are based on the AMD SEV-SNP ABI specification (Table 21).
pub fn parse_platform_info(report: &[u8]) -> Option<PlatformInfo> {
    if report.len() < SNP_REPORT_SIZE {
        return None;
    }

    let version = u32::from_le_bytes(report[0x00..0x04].try_into().ok()?);
    let guest_svn = u32::from_le_bytes(report[0x04..0x08].try_into().ok()?);
    let policy = u64::from_le_bytes(report[0x08..0x10].try_into().ok()?);

    // measurement is at offset 0x90, 48 bytes
    let measurement = hex::encode(&report[0x90..0xC0]);

    // current_tcb is at offset 0x38, 8 bytes
    let tcb = TcbVersion {
        boot_loader: report[0x38],
        tee: report[0x39],
        snp: report[0x3E],
        microcode: report[0x3F],
    };

    // chip_id is at offset 0x1A0, 64 bytes
    let chip_id = hex::encode(&report[0x1A0..0x1E0]);

    Some(PlatformInfo {
        version,
        guest_svn,
        policy,
        measurement,
        tcb_version: tcb,
        chip_id,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_attestation_request_serialization() {
        let req = AttestationRequest {
            nonce: vec![1, 2, 3, 4],
            user_data: Some(vec![5, 6, 7]),
        };
        let json = serde_json::to_string(&req).unwrap();
        let parsed: AttestationRequest = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.nonce, vec![1, 2, 3, 4]);
        assert_eq!(parsed.user_data, Some(vec![5, 6, 7]));
    }

    #[test]
    fn test_attestation_request_without_user_data() {
        let json = r#"{"nonce":[1,2,3]}"#;
        let req: AttestationRequest = serde_json::from_str(json).unwrap();
        assert_eq!(req.nonce, vec![1, 2, 3]);
        assert!(req.user_data.is_none());
    }

    #[test]
    fn test_attestation_report_serialization() {
        let report = AttestationReport {
            report: vec![0u8; SNP_REPORT_SIZE],
            cert_chain: CertificateChain::default(),
            platform: PlatformInfo::default(),
        };
        let json = serde_json::to_string(&report).unwrap();
        let parsed: AttestationReport = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.report.len(), SNP_REPORT_SIZE);
    }

    #[test]
    fn test_certificate_chain_default() {
        let chain = CertificateChain::default();
        assert!(chain.vcek.is_empty());
        assert!(chain.ask.is_empty());
        assert!(chain.ark.is_empty());
    }

    #[test]
    fn test_tcb_version_default() {
        let tcb = TcbVersion::default();
        assert_eq!(tcb.boot_loader, 0);
        assert_eq!(tcb.tee, 0);
        assert_eq!(tcb.snp, 0);
        assert_eq!(tcb.microcode, 0);
    }

    #[test]
    fn test_parse_platform_info_valid_report() {
        let mut report = vec![0u8; SNP_REPORT_SIZE];

        // version = 2 at offset 0x00
        report[0x00] = 2;
        // guest_svn = 1 at offset 0x04
        report[0x04] = 1;
        // policy at offset 0x08
        report[0x08] = 0x30; // SMT + migration allowed
                             // tcb at offset 0x38
        report[0x38] = 3; // boot_loader
        report[0x39] = 0; // tee
        report[0x3E] = 8; // snp
        report[0x3F] = 115; // microcode
                            // measurement at offset 0x90 (48 bytes)
        report[0x90] = 0xAB;
        report[0x91] = 0xCD;

        let info = parse_platform_info(&report).unwrap();
        assert_eq!(info.version, 2);
        assert_eq!(info.guest_svn, 1);
        assert_eq!(info.policy, 0x30);
        assert_eq!(info.tcb_version.boot_loader, 3);
        assert_eq!(info.tcb_version.snp, 8);
        assert_eq!(info.tcb_version.microcode, 115);
        assert!(info.measurement.starts_with("abcd"));
    }

    #[test]
    fn test_parse_platform_info_too_short() {
        let report = vec![0u8; 100]; // Too short
        assert!(parse_platform_info(&report).is_none());
    }

    #[test]
    fn test_snp_constants() {
        assert_eq!(SNP_REPORT_SIZE, 1184);
        assert_eq!(SNP_USER_DATA_SIZE, 64);
        assert_eq!(SNP_MEASUREMENT_SIZE, 48);
    }
}