diff --git a/src/certs/snp/mod.rs b/src/certs/snp/mod.rs index d52f7e65..5e750883 100644 --- a/src/certs/snp/mod.rs +++ b/src/certs/snp/mod.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 -/// ECDSA signatures. -pub mod ecdsa; +/// Attestation report signature parsing and verification helpers. +pub mod signature; #[cfg(any(feature = "openssl", feature = "crypto_nossl"))] /// Certificate Authority (CA) certificates. diff --git a/src/certs/snp/ecdsa/mod.rs b/src/certs/snp/signature/ecdsa.rs similarity index 78% rename from src/certs/snp/ecdsa/mod.rs rename to src/certs/snp/signature/ecdsa.rs index f7a2fb3a..252d6114 100644 --- a/src/certs/snp/ecdsa/mod.rs +++ b/src/certs/snp/signature/ecdsa.rs @@ -1,26 +1,19 @@ // SPDX-License-Identifier: Apache-2.0 -#[cfg(any(feature = "openssl", feature = "crypto_nossl"))] use super::*; -use crate::{ - parser::{ByteParser, Decoder, Encoder}, - util::{ - hexline::HexLine, - parser_helper::{ReadExt, WriteExt}, - }, -}; +use crate::{parser::ByteParser, util::hexline::HexLine}; use std::io::{Read, Result, Write}; #[cfg(feature = "openssl")] -use crate::certs::snp::{AsLeBytes, FromLe}; +use crate::certs::snp::{AsLeBytes, Certificate, FromLe}; #[cfg(any(feature = "openssl", feature = "crypto_nossl"))] use std::convert::TryFrom; #[cfg(feature = "openssl")] -use openssl::{bn, ecdsa}; +use openssl::{bn, ecdsa, ecdsa::EcdsaSig, sha::Sha384}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -68,6 +61,8 @@ impl Decoder<()> for Signature { fn decode(reader: &mut impl Read, _: ()) -> Result { let r = reader.read_bytes()?; let s = reader.read_bytes()?; + // Firmware signature field is 0x200 bytes; after r and s, the remaining bytes are reserved/padding. + reader.skip_bytes::<368>()?; Ok(Self { r, s }) } } @@ -129,16 +124,6 @@ impl From for Signature { } } -#[cfg(feature = "openssl")] -impl TryFrom<&[u8]> for Signature { - type Error = Error; - - #[inline] - fn try_from(value: &[u8]) -> Result { - Ok(ecdsa::EcdsaSig::from_der(value)?.into()) - } -} - #[cfg(feature = "openssl")] impl TryFrom<&Signature> for ecdsa::EcdsaSig { type Error = Error; @@ -166,23 +151,57 @@ impl TryFrom<&Signature> for p384::ecdsa::Signature { GenericArray::clone_from_slice(&s_big_endian), ) .map_err(|e| { - Error::new( - ErrorKind::Other, - format!("failed to deserialize signature from scalars: {e:?}"), - ) + Error::other(format!( + "failed to deserialize signature from scalars: {e:?}" + )) }) } } #[cfg(feature = "openssl")] -impl TryFrom<&Signature> for Vec { - type Error = Error; +/// Verify ECDSA signature on attestation report using VEK certificate +pub fn verify_ecdsa_signature(body: &[u8], signature: &[u8], vek: &Certificate) -> Result<()> { + let sev_sig = Signature::from_bytes(signature)?; - #[inline] - fn try_from(value: &Signature) -> Result { - Ok(ecdsa::EcdsaSig::try_from(value)?.to_der()?) + let sig: EcdsaSig = EcdsaSig::try_from(&sev_sig)?; + + let mut hasher = Sha384::new(); + hasher.update(body); + let base_digest = hasher.finish(); + + let ec = vek.public_key()?.ec_key()?; + let signed = sig.verify(&base_digest, &ec)?; + match signed { + true => Ok(()), + false => Err(Error::other("VEK does not sign the attestation report")), } } + +#[cfg(feature = "crypto_nossl")] +/// Verify ECDSA signature on attestation report using VEK certificate +pub fn verify_ecdsa_signature(body: &[u8], signature: &[u8], vek: &Certificate) -> Result<()> { + let sev_sig = Signature::from_bytes(signature)?; + + let sig = p384::ecdsa::Signature::try_from(&sev_sig).map_err(|e| { + std::io::Error::other(format!( + "failed to generate signature from raw bytes: {e:?}" + )) + })?; + + use sha2::Digest; + let base_digest = sha2::Sha384::new_with_prefix(body); + let verifying_key = + p384::ecdsa::VerifyingKey::from_sec1_bytes(vek.public_key_sec1()).map_err(|e| { + std::io::Error::other(format!( + "failed to deserialize public key from sec1 bytes: {e:?}" + )) + })?; + use p384::ecdsa::signature::DigestVerifier; + verifying_key.verify_digest(base_digest, &sig).map_err(|e| { + std::io::Error::other(format!("VEK does not sign the attestation report: {e:?}")) + }) +} + #[cfg(test)] mod tests { use super::*; @@ -261,17 +280,6 @@ mod tests { assert_ne!(sig.s(), &[0u8; 72]); } - #[test] - fn test_try_from_bytes() { - let r = BigNum::from_dec_str("123").unwrap(); - let s = BigNum::from_dec_str("456").unwrap(); - let ecdsa_sig = ecdsa::EcdsaSig::from_private_components(r, s).unwrap(); - let der = ecdsa_sig.to_der().unwrap(); - let sig = Signature::try_from(der.as_slice()).unwrap(); - assert_ne!(sig.r(), &[0u8; 72]); - assert_ne!(sig.s(), &[0u8; 72]); - } - #[test] fn test_try_into_ecdsa_sig() { let sig: Signature = Default::default(); @@ -279,13 +287,6 @@ mod tests { assert_eq!(ecdsa_sig.r().to_vec(), vec![]); assert_eq!(ecdsa_sig.s().to_vec(), vec![]); } - - #[test] - fn test_try_into_vec() { - let sig: Signature = Default::default(); - let der: Vec = (&sig).try_into().unwrap(); - assert!(!der.is_empty()); - } } #[cfg(feature = "crypto_nossl")] diff --git a/src/certs/snp/signature/mod.rs b/src/certs/snp/signature/mod.rs new file mode 100644 index 00000000..4afd19a0 --- /dev/null +++ b/src/certs/snp/signature/mod.rs @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + convert::TryFrom, + fmt::Display, + io::{Read, Write}, +}; + +#[cfg(any(feature = "openssl", feature = "crypto_nossl"))] +use std::io::Error; + +use crate::{ + parser::{Decoder, Encoder}, + util::parser_helper::{ReadExt, WriteExt}, +}; + +#[cfg(any(feature = "openssl", feature = "crypto_nossl"))] +use crate::certs::snp::Certificate; + +/// ECDSA algorithm signature +pub mod ecdsa; + +/// Signature algorithms that firmware may use to sign the SEV-SNP attestation report. +/// +/// The algorithm identifier is encoded in the report body and is therefore derived +/// from **untrusted bytes**. It MUST NOT be treated as a trust signal on its own. +/// Authenticity is only established by successfully verifying the report signature. +/// +/// This enum is intentionally explicit: unknown values are rejected during decoding. +#[repr(u32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum SignatureAlgorithm { + /// ECDSA with SECP384R1 curve + EcdsaSecp384r1 = 1, +} + +/// Creates a [`SignatureAlgorithm`] from the numeric algorithm identifier encoded +/// in the report body. +/// +/// # Errors +/// +/// Returns `InvalidData` if `v` does not correspond to a supported algorithm. +impl TryFrom for SignatureAlgorithm { + type Error = std::io::Error; + + fn try_from(v: u32) -> Result { + match v { + 1 => Ok(SignatureAlgorithm::EcdsaSecp384r1), + v => Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Unsupported signature algorithm: {}", v), + )), + } + } +} + +impl SignatureAlgorithm { + /// Verify the report signature over `body` using `vek` and this algorithm. + /// + /// This validates the signature bytes against the signed body bytes; it does + /// not parse or validate any fields inside the body. + #[cfg(any(feature = "openssl", feature = "crypto_nossl"))] + pub fn verify( + &self, + body: &[u8], + signature: &[u8], + vek: &Certificate, + ) -> Result<(), std::io::Error> { + match self { + SignatureAlgorithm::EcdsaSecp384r1 => { + ecdsa::verify_ecdsa_signature(body, signature, vek) + } + } + } +} + +impl Encoder<()> for SignatureAlgorithm { + fn encode(&self, writer: &mut impl Write, _: ()) -> Result<(), std::io::Error> { + match self { + SignatureAlgorithm::EcdsaSecp384r1 => writer.write_bytes(1u32, ())?, + }; + Ok(()) + } +} + +impl Decoder<()> for SignatureAlgorithm { + fn decode(reader: &mut impl Read, _: ()) -> Result { + let algo: u32 = reader.read_bytes()?; + Self::try_from(algo) + } +} + +impl Display for SignatureAlgorithm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SignatureAlgorithm::EcdsaSecp384r1 => write!(f, "ECDSA with SECP384R1"), + } + } +} diff --git a/src/firmware/guest/types/snp.rs b/src/firmware/guest/types/snp.rs index a84eaa28..6269c739 100644 --- a/src/firmware/guest/types/snp.rs +++ b/src/firmware/guest/types/snp.rs @@ -4,13 +4,12 @@ use crate::certs::snp::{Certificate, Chain, Verifiable}; use crate::{ - certs::snp::ecdsa::Signature, - error::AttestationReportError, + certs::snp::signature::SignatureAlgorithm, firmware::host::TcbVersion, parser::{ByteParser, Decoder, Encoder}, util::{ hexline::HexLine, - parser_helper::{ReadExt, WriteExt}, + parser_helper::{validate_reserved, ReadExt, WriteExt}, }, Generation, }; @@ -21,27 +20,13 @@ use serde::{Deserialize, Serialize}; use serde_big_array::BigArray; use std::{ + convert::TryFrom, fmt::Display, - io::{Cursor, Read, Write}, - ops::Range, + io::{Read, Write}, }; -#[cfg(any(feature = "openssl", feature = "crypto_nossl"))] -use std::convert::TryFrom; - -#[cfg(feature = "openssl")] -use std::io::Error; - use bitfield::bitfield; -#[cfg(feature = "openssl")] -use openssl::{ecdsa::EcdsaSig, sha::Sha384}; - -const ATT_REP_FW_LEN: usize = 1184; -const CHIP_ID_RANGE: Range = 0x1A0..0x1E0; -const CPUID_FAMILY_ID_BYTES: usize = 0x188; -const CPUID_MODEL_ID_BYTES: usize = 0x189; - /// Structure of required data for fetching the derived key. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct DerivedKey { @@ -189,16 +174,115 @@ impl ByteParser<()> for Version { const EXPECTED_LEN: Option = Some(3); } -/// Possible variants of the attestation report. +/// Identifies the firmware-defined format version of an SEV-SNP attestation report. +/// +/// `ReportVariant` corresponds to the *report layout version* as emitted by +/// platform firmware. The variant determines: +/// +/// - Which fields are present in the report body +/// - How certain fields are interpreted (e.g. TCB layout) +/// - How platform generation is inferred +/// +/// This enum is intentionally **narrow and explicit**: only variants currently +/// understood by the library are represented. Any unknown or future report +/// versions will be rejected during decoding. +/// +/// --- +/// +/// # Version Semantics +/// +/// | Variant | Firmware Versions | Notes | +/// |--------:|-------------------|-------| +/// | `V2` | 2 | Pre-CPUID reports. Platform generation is inferred from the CHIP_ID field. | +/// | `V3` | 3, 4 | Introduces CPUID fields used for platform identification. | +/// | `V5` | 5 | Adds mitigation vector fields and additional reserved regions. | +/// +/// Firmware version values `3` and `4` are treated equivalently and both map to +/// [`ReportVariant::V3`], as they share an identical report layout. +/// +/// --- +/// +/// # Security Considerations +/// +/// `ReportVariant` only describes the *format* of the report. It does **not** +/// imply that the report is authentic or trustworthy. +/// +/// A parsed `ReportVariant` must not be used as a trust signal on its own. +/// Authenticity is only established after successful cryptographic verification +/// of the report signature. +/// +/// --- +/// +/// # Parsing and Validation +/// +/// `ReportVariant` is decoded from the first 4 bytes of the report body and +/// validated during parsing. Unsupported or unknown version values will cause +/// parsing to fail with an error. +/// +/// This ensures forward compatibility is explicit and prevents accidental +/// acceptance of report formats the library does not understand. +/// +/// --- +/// +/// # Correct Usage +/// +/// `ReportVariant` is primarily consumed internally during report parsing to +/// drive generation inference and conditional field handling. +/// +/// Consumers should not attempt to construct `ReportVariant` values manually +/// from untrusted inputs; instead, rely on decoding via [`ReportBody`] or +/// verified [`Report`] processing. +/// +/// --- +/// +/// # Example +/// +/// ```ignore +/// let variant = ReportVariant::decode(&mut reader, ())?; +/// +/// match variant { +/// ReportVariant::V2 => { /* CHIP_ID-based inference */ } +/// ReportVariant::V3 => { /* CPUID-based inference */ } +/// ReportVariant::V5 => { /* mitigation vector fields present */ } +/// } +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[repr(u32)] pub enum ReportVariant { - /// Version 2 of the Attestation Report. - V2, + /// Version 2 of the attestation report format. + /// + /// This variant predates CPUID-based platform identification. Platform + /// generation is inferred heuristically from the CHIP_ID field. + V2 = 2, - /// Version 3 of the Attestation Report for PreTurin CPUs. - V3, + /// Version 3 (and firmware version 4) of the attestation report format. + /// + /// Introduces CPUID family, model, and stepping fields, enabling explicit + /// platform identification. Firmware versions `3` and `4` share the same + /// report layout and are represented by this variant. + V3 = 3, - /// Version 5 of the Attestation Report - V5, + /// Version 5 of the attestation report format. + /// + /// Extends the V3 layout with mitigation vector fields and additional + /// reserved regions. Used by newer firmware revisions. + V5 = 5, +} + +impl TryFrom for ReportVariant { + type Error = std::io::Error; + + fn try_from(v: u32) -> Result { + match v { + 2 => Ok(ReportVariant::V2), + 3 | 4 => Ok(ReportVariant::V3), + 5 => Ok(ReportVariant::V5), + unknown_variant => Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("unsupported report variant: {}", unknown_variant), + )), + } + } } impl Encoder<()> for ReportVariant { @@ -215,13 +299,7 @@ impl Encoder<()> for ReportVariant { impl Decoder<()> for ReportVariant { fn decode(reader: &mut impl Read, _: ()) -> Result { let version: u32 = reader.read_bytes()?; - Ok(match version { - 0 | 1 => return Err(std::io::ErrorKind::Unsupported.into()), - 2 => Self::V2, - 3 | 4 => Self::V3, - 5 => Self::V5, - _ => Self::V5, - }) + Self::try_from(version) } } @@ -230,290 +308,442 @@ impl ByteParser<()> for ReportVariant { const EXPECTED_LEN: Option = Some(4); } -/// Attestation Report for SEV-SNP guests. -/// These are all the possible fields in the attestation report. -/// Optional fields are set to none or a value depending on the version the system has. -/// Added in version 3: -/// The CPUID Family, Model and Stepping fields -/// The Alias_Check_Complete field in the PlatformInfo field -/// Added in version 5: -/// The launch_mit_vector and current_mit_vector fields -/// The page_swap_disabled field in the GuestPolicy field -/// The SEV-TIO field in the PlatformInfo field -#[repr(C)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +impl Display for ReportVariant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ReportVariant::V2 => write!(f, "V2"), + ReportVariant::V3 => write!(f, "V3"), + ReportVariant::V5 => write!(f, "V5"), + } + } +} + +/// A zero-copy view of a raw SEV-SNP attestation report. +/// +/// This type splits the report into two byte slices: +/// - `body`: the bytes covered by the report signature +/// - `signature`: the firmware-provided signature bytes +/// +/// `Report` does **not** imply authenticity or integrity. It is just a view over +/// untrusted bytes. Consumers should verify the signature (using [`Verifiable`]) +/// before interpreting any fields from the body. +/// +/// This design supports a two-phase workflow: +/// 1) Parse the outer framing to locate the signed body and signature. +/// 2) Verify the signature over `body`, then parse the verified body into +/// [`ReportBody`] for typed access. +/// +/// # Notes +/// - `Report` borrows from the input buffer (`'a`), so the input bytes must +/// outlive the `Report`. +/// - The offsets used by [`Report::from_bytes`] assume the current fixed +/// firmware report layout and size (1184 bytes). +#[derive(Debug, Clone, Copy)] +pub struct Report<'a> { + /// The signature algorithm used to sign the attestation report + pub algorithm: SignatureAlgorithm, + /// The bytes covered by the report signature (bytes 0x00 to 0x2A0). + pub body: &'a [u8], + /// The signature bytes (0x2A0..0x4A0). + pub signature: &'a [u8], +} + +impl<'a> Report<'a> { + const REPORT_LEN: usize = 0x4A0; // 1184 + const BODY_LEN: usize = 0x2A0; // bytes 0x000..=0x29F + const SIG_OFF: usize = 0x2A0; + const SIG_LEN: usize = 0x200; // bytes 0x2A0..=0x49F + const SIG_ALGO_OFF: usize = 0x34; + const SIG_ALGO_LEN: usize = 0x4; + /// Parse a raw attestation report into body and signature slices. + /// + /// This function performs **framing only**: + /// - validates the total report length + /// - returns borrowed slices for the signed body and signature + /// + /// It does **not** verify the signature or validate reserved fields. + /// Use [`ReportBody::try_from`] (with a certificate or chain) to obtain a + /// verified [`ReportBody`]. + pub fn from_bytes(report: &'a [u8]) -> std::io::Result { + if report.len() != Self::REPORT_LEN { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Bad report length", + )); + }; + + let algorithm = SignatureAlgorithm::decode( + &mut &report[Self::SIG_ALGO_OFF..Self::SIG_ALGO_OFF + Self::SIG_ALGO_LEN], + (), + )?; + + Ok(Self { + algorithm, + body: &report[..Self::BODY_LEN], + signature: &report[Self::SIG_OFF..Self::SIG_OFF + Self::SIG_LEN], + }) + } +} + +#[cfg(any(feature = "openssl", feature = "crypto_nossl"))] +impl Verifiable for (&Certificate, &Report<'_>) { + type Output = (); + + fn verify(self) -> Result { + let (vek, report) = self; + + let algo = report.algorithm; + + algo.verify(report.body, report.signature, vek) + } +} + +#[cfg(any(feature = "openssl", feature = "crypto_nossl"))] +impl Verifiable for (&Chain, &Report<'_>) { + type Output = (); + + fn verify(self) -> Result<(), std::io::Error> { + let (chain, report) = self; + let vek = chain.verify()?; + (vek, report).verify() + } +} + +#[cfg(any(feature = "openssl", feature = "crypto_nossl"))] +/// Verifies `report` with `vek` and returns a parsed [`ReportBody`]. +impl<'a> TryFrom<(&Report<'a>, &Certificate)> for ReportBody<'a> { + type Error = std::io::Error; + + fn try_from((report, vek): (&Report<'a>, &Certificate)) -> Result { + (vek, report).verify()?; + ReportBody::from_bytes(report.body) + } +} + +#[cfg(any(feature = "openssl", feature = "crypto_nossl"))] +/// Verifies `report` with `chain` and returns a parsed [`ReportBody`]. +/// +/// This is the **recommended** way to obtain a `ReportBody`, because it +/// enforces signature verification before parsing typed fields. +impl<'a> TryFrom<(&Report<'a>, &Chain)> for ReportBody<'a> { + type Error = std::io::Error; + + fn try_from((report, chain): (&Report<'a>, &Chain)) -> Result { + (chain, report).verify()?; + ReportBody::from_bytes(report.body) + } +} + +/// A zero-copy view of the attestation report body. +/// All byte-arrayfields are borrowed from the original report body slice, so the input bytes must outlive this struct. +/// +/// This struct contains fully typed and parsed fields from the attestation report body. +/// All fields are parsed to their final types at [`ReportBody::from_bytes`] time, including +/// TCB version parsing with generation-aware layout selection. +/// +/// The correct method to generate [`ReportBody`] is from a [`Report`]: +/// ```ignore +/// let report = Report::from_bytes(&raw_bytes); +/// let body = ReportBody::try_from((&report, &vek)) +/// ``` +/// +/// This will verify the signature and body of the report before parsing it into fully typed fields. +/// +/// [`ReportBody::from_bytes`] can be used to parse the body from raw bytes, but this should be done for debugging purposes. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct AttestationReport { - /// Version number of this attestation report. Set to 2h for this specification. - pub version: u32, - /// The guest SVN. +pub struct ReportBody<'a> { + /// Version number of this attestation report. + pub version: ReportVariant, + + /// Guest Security Version Number (SVN). pub guest_svn: u32, - /// The guest policy. + + /// Guest policy governing hypervisor restrictions. pub policy: GuestPolicy, - /// The family ID provided at launch. - pub family_id: [u8; 16], - /// The image ID provided at launch. - pub image_id: [u8; 16], - /// The request VMPL for the attestation report. + + /// Family ID provided at launch (128 bits). + pub family_id: &'a [u8; 16], + + /// Image ID provided at launch (128 bits). + pub image_id: &'a [u8; 16], + + /// Virtual Machine Privilege Level (VMPL) of the attestation request. pub vmpl: u32, - /// The signature algorithm used to sign this report. - pub sig_algo: u32, - /// Current TCB. See SNPTcbVersion + + /// Signature algorithm used to sign this report. + pub sig_algo: SignatureAlgorithm, + + /// Current TCB (Trusted Computing Base) version, parsed for the inferred generation. pub current_tcb: TcbVersion, - /// Information about the platform. See PlatformInfo + + /// Platform information flags. pub plat_info: PlatformInfo, - /// Information related to signing keys in the report. See KeyInfo + + /// Key and signing information. pub key_info: KeyInfo, - /// Guest-provided 512 Bits of Data - #[cfg_attr(feature = "serde", serde(with = "BigArray"))] - pub report_data: [u8; 64], - - /// The measurement calculated at launch. - #[cfg_attr(feature = "serde", serde(with = "BigArray"))] - pub measurement: [u8; 48], - - /// Data provided by the hypervisor at launch. - pub host_data: [u8; 32], - - /// SHA-384 digest of the ID public key that signed the ID block provided - /// in SNP_LANUNCH_FINISH. - #[cfg_attr(feature = "serde", serde(with = "BigArray"))] - pub id_key_digest: [u8; 48], - - /// SHA-384 digest of the Author public key that certified the ID key, - /// if provided in SNP_LAUNCH_FINSIH. Zeroes if AUTHOR_KEY_EN is 1. - #[cfg_attr(feature = "serde", serde(with = "BigArray"))] - pub author_key_digest: [u8; 48], - /// Report ID of this guest. - pub report_id: [u8; 32], - /// Report ID of this guest's migration agent (if applicable). - pub report_id_ma: [u8; 32], - /// Reported TCB version used to derive the VCEK that signed this report. + /// Guest-provided data (512 bits). + pub report_data: &'a [u8; 64], + + /// Launch measurement (SHA-384). + pub measurement: &'a [u8; 48], + + /// Host-provided data (256 bits). + pub host_data: &'a [u8; 32], + + /// SHA-384 digest of the ID public key. + pub id_key_digest: &'a [u8; 48], + + /// SHA-384 digest of the Author public key. + pub author_key_digest: &'a [u8; 48], + + /// Report ID of this guest (256 bits). + pub report_id: &'a [u8; 32], + + /// Report ID of this guest's migration agent (256 bits). + pub report_id_ma: &'a [u8; 32], + + /// Reported TCB version used to derive the VCEK, parsed for the inferred generation. pub reported_tcb: TcbVersion, - /// CPUID Familiy ID (Combined Extended Family ID and Family ID) + + /// CPUID Family ID - present in report version 3+. pub cpuid_fam_id: Option, - /// CPUID Model (Combined Extended Model and Model fields) + + /// CPUID Model ID - present in report version 3+. pub cpuid_mod_id: Option, - /// CPUID Stepping + + /// CPUID Stepping - present in report version 3+. pub cpuid_step: Option, - /// If MaskChipId is set to 0, Identifier unique to the chip. - /// Otherwise set to 0h. - #[cfg_attr(feature = "serde", serde(with = "BigArray"))] - pub chip_id: [u8; 64], - /// CommittedTCB + /// Chip identifier (512 bits). Zero if MaskChipId was set during launch. + pub chip_id: &'a [u8; 64], + + /// Committed TCB version, parsed for the inferred generation. pub committed_tcb: TcbVersion, - /// The build number of CurrentVersion + + /// Current firmware version (major.minor.build). pub current: Version, - /// The build number of CommittedVersion + + /// Committed firmware version (major.minor.build). pub committed: Version, - /// The CurrentTcb at the time the guest was launched or imported. + + /// Launch TCB version, parsed for the inferred generation. pub launch_tcb: TcbVersion, - /// The verified mitigation vecor value at the time the guest was launched (LaunchMitVector). + + /// Launch mitigation vector - present in report version 5+. pub launch_mit_vector: Option, - /// Value is set to the current verified mitigation vectore value (CurrentMitVector). + + /// Current mitigation vector - present in report version 5+. pub current_mit_vector: Option, - /// Signature of bytes 0 to 0x29F inclusive of this report. - /// The format of the signature is found within Signature. - pub signature: Signature, } -impl Default for AttestationReport { - fn default() -> Self { - Self { - version: Default::default(), - guest_svn: Default::default(), - policy: Default::default(), - family_id: Default::default(), - image_id: Default::default(), - vmpl: Default::default(), - sig_algo: Default::default(), - current_tcb: Default::default(), - plat_info: Default::default(), - key_info: Default::default(), - report_data: [0u8; 64], - measurement: [0u8; 48], - host_data: Default::default(), - id_key_digest: [0u8; 48], - author_key_digest: [0u8; 48], - report_id: Default::default(), - report_id_ma: Default::default(), - reported_tcb: Default::default(), - cpuid_fam_id: Default::default(), - cpuid_mod_id: Default::default(), - cpuid_step: Default::default(), - chip_id: [0u8; 64], - committed_tcb: Default::default(), - current: Default::default(), - committed: Default::default(), - launch_tcb: Default::default(), - launch_mit_vector: Default::default(), - current_mit_vector: Default::default(), - signature: Default::default(), +impl<'a> ReportBody<'a> { + /// The expected length of the report body (bytes 0x00 to 0x2A0). + pub const BODY_LEN: usize = 0x2A0; + + /// Parses a raw attestation report body into a typed [`ReportBody`]. + /// + /// Security Warning + /// + /// This function **does not perform any cryptographic verification**. + /// It only parses the provided byte slice according to the SEV-SNP + /// attestation report layout. + /// + /// Calling this method directly means the caller is responsible for + /// ensuring that the input bytes are authentic and have not been + /// tampered with. + /// + /// --- + /// + /// # Correct Usage + /// + /// The **recommended and correct way** to obtain a [`ReportBody`] is via + /// the `TryFrom` implementation that verifies the report signature first: + /// + /// ```ignore + /// let report = Report::from_bytes(&raw_bytes)?; + /// let body = ReportBody::try_from((&report, &certificate))?; + /// ``` + /// + /// or + /// + /// ```ignore + /// let report = Report::from_bytes(&raw_bytes)?; + /// let body = ReportBody::try_from((&report, &chain))?; + /// ``` + /// + /// These conversion paths: + /// + /// 1. Verify the report signature using the provided VEK or certificate chain. + /// 2. Only after successful verification, parse the signed body bytes + /// into a typed [`ReportBody`]. + /// + /// This ensures that parsed fields such as TCB versions, policy flags, + /// measurements, and identifiers are cryptographically authenticated. + /// + /// --- + /// + /// # Intended Use of `from_bytes` + /// + /// This method exists primarily for: + /// + /// - Internal parsing after successful verification + /// - Debugging and inspection of raw report bytes + /// - Unit tests that validate layout and field decoding logic + /// + /// It should **not** be used in security-sensitive paths where the + /// authenticity of the report matters. + /// + /// --- + /// + /// # Errors + /// + /// Returns an error if: + /// + /// - The body length is incorrect + /// - The report version is unsupported + /// - Reserved fields are non-zero + /// - The chip ID is masked (for V2 reports) + /// - Generation inference fails + /// + /// This function validates structural correctness, but **not** + /// authenticity. + /// + pub fn from_bytes(body: &'a [u8]) -> Result { + if body.len() != Self::BODY_LEN { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!( + "Report body length incorrect: expected {} bytes, got {}", + Self::BODY_LEN, + body.len() + ), + )); } - } -} -impl Encoder<()> for AttestationReport { - fn encode(&self, writer: &mut impl Write, _: ()) -> Result<(), std::io::Error> { - // Determine the variant based on version and CPUID step - let variant = match self.version { - 2 => ReportVariant::V2, - 3 | 4 => ReportVariant::V3, - _ => ReportVariant::V5, + // Parse version to determine variant and generation + let version = ReportVariant::decode(&mut &body[0x00..0x04], ())?; + + // Parse firmware version early (needed for policy validation) + let current = Version { + build: body[0x1E8], + minor: body[0x1E9], + major: body[0x1EA], }; - let generation = match variant { - ReportVariant::V2 => { - if Self::chip_id_is_turin_like(&self.chip_id)? { - Generation::Turin - } else { - Generation::Genoa - } - } - _ => { - let family = self.cpuid_fam_id.unwrap_or(0); - let model = self.cpuid_mod_id.unwrap_or(0); - Generation::identify_cpu(family, model)? + // Infer generation from chip_id (V2) or CPUID fields (V3+) + let generation = if version >= ReportVariant::V3 { + // V3+ uses CPUID fields + let family = body[0x188]; + let model = body[0x189]; + Generation::identify_cpu(family, model)? + } else { + // V2 uses chip_id heuristic + let chip_id_bytes = &body[0x1A0..0x1E0]; + if chip_id_bytes == &[0u8; 64][..] { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Chip ID is masked", + )); + } else if chip_id_bytes[8..] == [0u8; 56] { + // Turin-like: first 8 bytes non-zero, rest zero + Generation::Turin + } else { + // Genoa-like: full 64 bytes used + Generation::Genoa } }; - // Write version (common to all variants) - writer.write_bytes(self.version, ())?; - writer.write_bytes(self.guest_svn, ())?; - writer.write_bytes(self.policy, ())?; - writer.write_bytes(self.family_id, ())?; - writer.write_bytes(self.image_id, ())?; - writer.write_bytes(self.vmpl, ())?; - writer.write_bytes(self.sig_algo, ())?; - writer.write_bytes(self.current_tcb, generation)?; - writer.write_bytes(self.plat_info, ())?; - writer.write_bytes(self.key_info, ())?; - writer - .skip_bytes::<4>()? - .write_bytes(self.report_data, ())?; - writer.write_bytes(self.measurement, ())?; - writer.write_bytes(self.host_data, ())?; - writer.write_bytes(self.id_key_digest, ())?; - writer.write_bytes(self.author_key_digest, ())?; - writer.write_bytes(self.report_id, ())?; - writer.write_bytes(self.report_id_ma, ())?; - writer.write_bytes(self.reported_tcb, generation)?; - - // Write CPUID fields based on variant - match variant { - ReportVariant::V2 => { - // V2 doesn't have CPUID fields - writer.skip_bytes::<24>()?.write_bytes(self.chip_id, ())?; - } - _ => { - // Write CPUID fields for V3 and V4 - writer.write_bytes(self.cpuid_fam_id.unwrap_or(0), ())?; - writer.write_bytes(self.cpuid_mod_id.unwrap_or(0), ())?; - writer.write_bytes(self.cpuid_step.unwrap_or(0), ())?; - writer.skip_bytes::<21>()?.write_bytes(self.chip_id, ())?; - } - } + let guest_svn = u32::decode(&mut &body[0x04..0x08], ())?; - // Write committed TCB based on variant - writer.write_bytes(self.committed_tcb, generation)?; - writer.write_bytes(self.current, ())?; - writer.skip_bytes::<1>()?.write_bytes(self.committed, ())?; - writer - .skip_bytes::<1>()? - .write_bytes(self.launch_tcb, generation)?; - - // Write launch and current mitigation vectors based on variant - match variant { - ReportVariant::V2 | ReportVariant::V3 => { - writer - .skip_bytes::<168>()? - .write_bytes(self.signature, ())?; - } - _ => { - writer.write_bytes(self.launch_mit_vector.unwrap_or(0), ())?; - writer.write_bytes(self.current_mit_vector.unwrap_or(0), ())?; - writer - .skip_bytes::<152>()? - .write_bytes(self.signature, ())?; - } - } + let policy = GuestPolicy::decode(&mut &body[0x08..0x10], current)?; - Ok(()) - } -} + let family_id: &'a [u8; 16] = <&[u8; 16]>::try_from(&body[0x10..0x20]) + .map_err(|e| std::io::Error::other(format!("Failed TryFrom Operation: {e}")))?; -impl Decoder<()> for AttestationReport { - fn decode(reader: &mut impl Read, _: ()) -> Result { - let mut bytes = vec![0u8; ATT_REP_FW_LEN]; - reader.read_exact(&mut bytes)?; - - let variant = ReportVariant::from_bytes(&bytes[0..4])?; - - let generation = match variant { - ReportVariant::V2 => { - if Self::chip_id_is_turin_like(&bytes[CHIP_ID_RANGE])? { - Generation::Turin - } else { - Generation::Genoa - } - } - _ => { - let family = &bytes[CPUID_FAMILY_ID_BYTES]; - let model = &bytes[CPUID_MODEL_ID_BYTES]; - Generation::identify_cpu(*family, *model)? - } - }; + let image_id: &'a [u8; 16] = <&[u8; 16]>::try_from(&body[0x20..0x30]) + .map_err(|e| std::io::Error::other(format!("Failed TryFrom Operation: {e}")))?; + + let vmpl = u32::decode(&mut &body[0x30..0x34], ())?; - let mut stepper = Cursor::new(&bytes[..]); - - let version = stepper.read_bytes()?; - let guest_svn = stepper.read_bytes()?; - let policy = stepper.read_bytes()?; - let family_id = stepper.read_bytes()?; - let image_id = stepper.read_bytes()?; - let vmpl = stepper.read_bytes()?; - let sig_algo = stepper.read_bytes()?; - - let current_tcb = stepper.read_bytes_with(generation)?; - let plat_info = stepper.read_bytes()?; - let key_info = stepper.read_bytes()?; - let report_data = stepper.skip_bytes::<4>()?.read_bytes()?; - let measurement = stepper.read_bytes()?; - let host_data = stepper.read_bytes()?; - let id_key_digest = stepper.read_bytes()?; - let author_key_digest = stepper.read_bytes()?; - let report_id = stepper.read_bytes()?; - let report_id_ma = stepper.read_bytes()?; - let reported_tcb = stepper.read_bytes_with(generation)?; - - // CPUID fields were added in V3 and later. - let (cpuid_fam_id, cpuid_mod_id, cpuid_step, chip_id) = match variant { - ReportVariant::V2 => (None, None, None, stepper.skip_bytes::<24>()?.read_bytes()?), - _ => ( - Some(stepper.read_bytes()?), - Some(stepper.read_bytes()?), - Some(stepper.read_bytes()?), - stepper.skip_bytes::<21>()?.read_bytes()?, - ), + let sig_algo = SignatureAlgorithm::decode(&mut &body[0x34..0x38], ())?; + + let current_tcb = TcbVersion::decode(&mut &body[0x38..0x40], generation)?; + + let plat_info = PlatformInfo::decode(&mut &body[0x40..0x48], current)?; + + let key_info = KeyInfo::decode(&mut &body[0x48..0x4C], current)?; + + // Reserved 0x4C - 0x50 + validate_reserved(&body[0x4C..0x50], 0x4C)?; + + let report_data: &'a [u8; 64] = <&[u8; 64]>::try_from(&body[0x50..0x90]) + .map_err(|e| std::io::Error::other(format!("Failed TryFrom Operation: {e}")))?; + + let measurement: &'a [u8; 48] = <&[u8; 48]>::try_from(&body[0x90..0xC0]) + .map_err(|e| std::io::Error::other(format!("Failed TryFrom Operation: {e}")))?; + + let host_data: &'a [u8; 32] = <&[u8; 32]>::try_from(&body[0xC0..0xE0]) + .map_err(|e| std::io::Error::other(format!("Failed TryFrom Operation: {e}")))?; + + let id_key_digest: &'a [u8; 48] = <&[u8; 48]>::try_from(&body[0xE0..0x110]) + .map_err(|e| std::io::Error::other(format!("Failed TryFrom Operation: {e}")))?; + + let author_key_digest: &'a [u8; 48] = <&[u8; 48]>::try_from(&body[0x110..0x140]) + .map_err(|e| std::io::Error::other(format!("Failed TryFrom Operation: {e}")))?; + + let report_id: &'a [u8; 32] = <&[u8; 32]>::try_from(&body[0x140..0x160]) + .map_err(|e| std::io::Error::other(format!("Failed TryFrom Operation: {e}")))?; + + let report_id_ma: &'a [u8; 32] = <&[u8; 32]>::try_from(&body[0x160..0x180]) + .map_err(|e| std::io::Error::other(format!("Failed TryFrom Operation: {e}")))?; + + let reported_tcb = TcbVersion::decode(&mut &body[0x180..0x188], generation)?; + + // Parse CPUID fields (V3+) or extract raw values + let (cpuid_fam_id, cpuid_mod_id, cpuid_step) = if version >= ReportVariant::V3 { + // Reserved 0x18B - 0x1A0 + validate_reserved(&body[0x18B..0x1A0], 0x18B)?; + (Some(body[0x188]), Some(body[0x189]), Some(body[0x18A])) + } else { + // Reserved 0x188 - 0x1A0 + validate_reserved(&body[0x188..0x1A0], 0x188)?; + (None, None, None) }; - let committed_tcb = stepper.read_bytes_with(generation)?; - let current = stepper.read_bytes()?; - let committed = stepper.skip_bytes::<1>()?.read_bytes()?; - let launch_tcb = stepper.skip_bytes::<1>()?.read_bytes_with(generation)?; + let chip_id: &'a [u8; 64] = <&[u8; 64]>::try_from(&body[0x1A0..0x1E0]) + .map_err(|e| std::io::Error::other(format!("Failed TryFrom Operation: {e}")))?; - // mit vecor fields were added in V5 and later. - let (launch_mit_vector, current_mit_vector, signature) = match variant { - ReportVariant::V2 | ReportVariant::V3 => { - (None, None, stepper.skip_bytes::<168>()?.read_bytes()?) - } - _ => ( - Some(stepper.read_bytes()?), - Some(stepper.read_bytes()?), - stepper.skip_bytes::<152>()?.read_bytes()?, - ), + let committed_tcb = TcbVersion::decode(&mut &body[0x1E0..0x1E8], generation)?; + + // current firmware version already parsed earlier for policy validation + + // Reserved 0x1EB + validate_reserved(&body[0x1EB..0x1EC], 0x1EB)?; + + // Parse committed firmware version + let committed = Version { + build: body[0x1EC], + minor: body[0x1ED], + major: body[0x1EE], + }; + + // Reserved 0x1EF + validate_reserved(&body[0x1EF..0x1F0], 0x1EF)?; + + let launch_tcb = TcbVersion::decode(&mut &body[0x1F0..0x1F8], generation)?; + + // Parse mitigation vector fields (V5+) + let (launch_mit_vector, current_mit_vector) = if version >= ReportVariant::V5 { + // Reserved 0x208 - 0x2A0 + validate_reserved(&body[0x208..0x2A0], 0x208)?; + let launch = u64::decode(&mut &body[0x1F8..0x200], ())?; + let current = u64::decode(&mut &body[0x200..0x208], ())?; + (Some(launch), Some(current)) + } else { + // Reserved 0x1F8 - 0x2A0 + validate_reserved(&body[0x1F8..0x2A0], 0x1F8)?; + (None, None) }; Ok(Self { @@ -545,32 +775,11 @@ impl Decoder<()> for AttestationReport { launch_tcb, launch_mit_vector, current_mit_vector, - signature, }) } } -impl ByteParser<()> for AttestationReport { - type Bytes = [u8; ATT_REP_FW_LEN]; - const EXPECTED_LEN: Option = Some(ATT_REP_FW_LEN); -} - -impl AttestationReport { - #[inline(always)] - /// Checks if the MaskChipId is set to 1. If not, then it will check if - /// the CHIP_ID is Turin-like. - fn chip_id_is_turin_like(bytes: &[u8]) -> Result { - // Chip ID -> 0x1A0-0x1E0 - if bytes == [0; 64] { - return Err(AttestationReportError::MaskedChipId); - } - - // Last 8 bytes of CHIP_ID are zero, then it is Turin Like. - Ok(bytes[8..] == [0; 56]) - } -} - -impl Display for AttestationReport { +impl Display for ReportBody<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, @@ -639,25 +848,24 @@ Launch TCB: Launch Mitigation Vector: {} Current Mitigation Vector: {} - -{}"#, +"#, self.version, self.guest_svn, - self.policy, - HexLine(&self.family_id), - HexLine(&self.image_id), + self.policy.display_for_version(self.current), + HexLine(self.family_id), + HexLine(self.image_id), self.vmpl, self.sig_algo, self.current_tcb, - self.plat_info, - self.key_info, - HexLine(&self.report_data), - HexLine(&self.measurement), - HexLine(&self.host_data), - HexLine(&self.id_key_digest), - HexLine(&self.author_key_digest), - HexLine(&self.report_id), - HexLine(&self.report_id_ma), + self.plat_info.display_for_version(self.current), + self.key_info.display_for_version(self.current), + HexLine(self.report_data), + HexLine(self.measurement), + HexLine(self.host_data), + HexLine(self.id_key_digest), + HexLine(self.author_key_digest), + HexLine(self.report_id), + HexLine(self.report_id_ma), self.reported_tcb, self.cpuid_fam_id .map_or("None".to_string(), |fam| fam.to_string()), @@ -665,7 +873,7 @@ Current Mitigation Vector: {} .map_or("None".to_string(), |model| model.to_string()), self.cpuid_step .map_or("None".to_string(), |step| step.to_string()), - HexLine(&self.chip_id), + HexLine(self.chip_id), self.committed_tcb, self.current, self.committed, @@ -674,127 +882,10 @@ Current Mitigation Vector: {} .map_or("None".to_string(), |lmv| lmv.to_string()), self.current_mit_vector .map_or("None".to_string(), |cmv| cmv.to_string()), - self.signature ) } } -#[cfg(feature = "openssl")] -impl Verifiable for (&Chain, &AttestationReport) { - type Output = (); - - fn verify(self) -> std::io::Result { - let vek = self.0.verify()?; - - let sig = EcdsaSig::try_from(&self.1.signature)?; - - let raw_report_bytes = self.1.to_bytes()?; - - let measurable_bytes: &[u8] = &raw_report_bytes[..0x2a0]; - - let mut hasher = Sha384::new(); - hasher.update(measurable_bytes); - let base_digest = hasher.finish(); - - let ec = vek.public_key()?.ec_key()?; - let signed = sig.verify(&base_digest, &ec)?; - match signed { - true => Ok(()), - false => Err(Error::other("VEK does not sign the attestation report")), - } - } -} - -#[cfg(feature = "openssl")] -impl Verifiable for (&Certificate, &AttestationReport) { - type Output = (); - - fn verify(self) -> std::io::Result { - let vek = self.0; - - let sig = EcdsaSig::try_from(&self.1.signature)?; - let raw_report_bytes = self.1.to_bytes()?; - - let measurable_bytes: &[u8] = &raw_report_bytes[..0x2a0]; - - let mut hasher = Sha384::new(); - hasher.update(measurable_bytes); - let base_digest = hasher.finish(); - - let ec = vek.public_key()?.ec_key()?; - let signed = sig.verify(&base_digest, &ec)?; - match signed { - true => Ok(()), - false => Err(Error::other("VEK does not sign the attestation report")), - } - } -} - -#[cfg(feature = "crypto_nossl")] -impl Verifiable for (&Chain, &AttestationReport) { - type Output = (); - - fn verify(self) -> std::io::Result { - // According to Chapter 3 of the Versioned Chip Endorsement Key (VCEK) Certificate and the Versioned Loaded Endorsement Key (VLEK) - // Certificate specifications, both Versioned Endorsement Key certificates certify an ECDSA public key on curve P-384, - // with the signature hash algorithm being SHA-384. - - let vek = self.0.verify()?; - - let sig = p384::ecdsa::Signature::try_from(&self.1.signature)?; - - let raw_report_bytes = self.1.to_bytes()?; - - let measurable_bytes: &[u8] = &raw_report_bytes[..0x2a0]; - - use sha2::Digest; - let base_digest = sha2::Sha384::new_with_prefix(measurable_bytes); - let verifying_key = p384::ecdsa::VerifyingKey::from_sec1_bytes(vek.public_key_sec1()) - .map_err(|e| { - std::io::Error::other(format!( - "failed to deserialize public key from sec1 bytes: {e:?}" - )) - })?; - use p384::ecdsa::signature::DigestVerifier; - verifying_key.verify_digest(base_digest, &sig).map_err(|e| { - std::io::Error::other(format!("VEK does not sign the attestation report: {e:?}")) - }) - } -} - -#[cfg(feature = "crypto_nossl")] -impl Verifiable for (&Certificate, &AttestationReport) { - type Output = (); - - fn verify(self) -> std::io::Result { - // According to Chapter 3 of the [Versioned Chip Endorsement Key (VCEK) Certificate and - // KDS Interface Specification][spec], the VCEK certificate certifies an ECDSA public key on curve P-384, - // and the signature hash algorithm is sha384. - // [spec]: https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/specifications/57230.pdf - - let vek = self.0; - - let sig = p384::ecdsa::Signature::try_from(&self.1.signature)?; - - let raw_report_bytes = self.1.to_bytes()?; - - let measurable_bytes: &[u8] = &raw_report_bytes[..0x2a0]; - - use sha2::Digest; - let base_digest = sha2::Sha384::new_with_prefix(measurable_bytes); - let verifying_key = p384::ecdsa::VerifyingKey::from_sec1_bytes(vek.public_key_sec1()) - .map_err(|e| { - std::io::Error::other(format!( - "failed to deserialize public key from sec1 bytes: {e:?}" - )) - })?; - use p384::ecdsa::signature::DigestVerifier; - verifying_key.verify_digest(base_digest, &sig).map_err(|e| { - std::io::Error::other(format!("VEK does not sign the attestation report: {e:?}")) - }) - } -} - bitfield! { /// The firmware associates each guest with a guest policy that the guest owner provides. The /// firmware restricts what actions the hypervisor can take on this guest according to the guest policy. @@ -850,9 +941,181 @@ bitfield! { /// option is selected to disable these Page Move commands, then these commands will return POLICY_FAILURE. /// 0: Do not disable Guest support for the commands. /// 1: Disable Guest support for the commands. + /// + /// **Since:** Report v5+ pub page_swap_disabled, set_page_swap_disabled: 25; } +impl GuestPolicy { + const RMB1_BIT_17: u64 = 1u64 << 17; + const RESERVED_MBZ_MASK_26_63: u64 = (!0u64) << 26; // bits 26..63 + + // Bit 21: CXL_ALLOW (added in v1.55) + const CXL_ALLOW_BIT_21: u64 = 1u64 << 21; + // Bit 22: MEM_AES_256_XTS (added in v1.55) + const MEM_AES_256_XTS_BIT_22: u64 = 1u64 << 22; + // Bit 23: RAPL_DIS (added in v1.55) + const RAPL_DIS_BIT_23: u64 = 1u64 << 23; + // Bit 24: CIPHERTEXT_HIDING (added in v1.55) + const CIPHERTEXT_HIDING_BIT_24: u64 = 1u64 << 24; + // Bit 25: PAGE_SWAP_DISABLE (added in v1.58) + const PAGE_SWAP_DISABLE_BIT_25: u64 = 1u64 << 25; + + // Version 1.55: Added CXL, AES-256-XTS, RAPL, CIPHERTEXT_HIDING (bits 21-24) + const VERSION_1_55: Version = Version { + major: 1, + minor: 55, + build: 0, + }; + // Version 1.58: Added PAGE_SWAP_DISABLE (bit 25) + const VERSION_1_58: Version = Version { + major: 1, + minor: 58, + build: 0, + }; + + fn validate_reserved_bits(self, version: Version) -> std::io::Result<()> { + let raw = self.0; + + // bit 17 must be 1 (RMB1) + if (raw & Self::RMB1_BIT_17) == 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("GuestPolicy bit 17 must be 1 (raw=0x{raw:016x})"), + )); + } + + // bits 26..63 must be zero (MBZ) + if (raw & Self::RESERVED_MBZ_MASK_26_63) != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("GuestPolicy reserved bits 26..63 must be zero (raw=0x{raw:016x})"), + )); + } + + // bit 21 (CXL_ALLOW) is only defined for firmware v1.55+ + if version < Self::VERSION_1_55 && (raw & Self::CXL_ALLOW_BIT_21) != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!( + "GuestPolicy bit 21 (CXL_ALLOW) is only valid for firmware v1.55+ (raw=0x{raw:016x})" + ), + )); + } + + // bit 22 (MEM_AES_256_XTS) is only defined for firmware v1.55+ + if version < Self::VERSION_1_55 && (raw & Self::MEM_AES_256_XTS_BIT_22) != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!( + "GuestPolicy bit 22 (MEM_AES_256_XTS) is only valid for firmware v1.55+ (raw=0x{raw:016x})" + ), + )); + } + + // bit 23 (RAPL_DIS) is only defined for firmware v1.55+ + if version < Self::VERSION_1_55 && (raw & Self::RAPL_DIS_BIT_23) != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!( + "GuestPolicy bit 23 (RAPL_DIS) is only valid for firmware v1.55+ (raw=0x{raw:016x})" + ), + )); + } + + // bit 24 (CIPHERTEXT_HIDING) is only defined for firmware v1.55+ + if version < Self::VERSION_1_55 && (raw & Self::CIPHERTEXT_HIDING_BIT_24) != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!( + "GuestPolicy bit 24 (CIPHERTEXT_HIDING) is only valid for firmware v1.55+ (raw=0x{raw:016x})" + ), + )); + } + + // bit 25 (PAGE_SWAP_DISABLE) is only defined for firmware v1.58+ + if version < Self::VERSION_1_58 && (raw & Self::PAGE_SWAP_DISABLE_BIT_25) != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!( + "GuestPolicy bit 25 (PAGE_SWAP_DISABLE) is only valid for firmware v1.58+ (raw=0x{raw:016x})" + ), + )); + } + + Ok(()) + } + + /// Formats the guest policy with version-aware display. + /// + /// Policy bits that are not defined for the given firmware version + /// will be displayed as "None" instead of their actual value. + /// + /// # Arguments + /// * `version` - The firmware version to use for determining which bits are valid + /// + /// # Returns + /// A formatted string representation of the guest policy + pub fn display_for_version(&self, version: Version) -> String { + let cxl_allowed = if version >= Self::VERSION_1_55 { + format!("{}", self.cxl_allowed()) + } else { + "None".to_string() + }; + + let mem_aes_256_xts = if version >= Self::VERSION_1_55 { + format!("{}", self.mem_aes_256_xts()) + } else { + "None".to_string() + }; + + let rapl_dis = if version >= Self::VERSION_1_55 { + format!("{}", self.rapl_dis()) + } else { + "None".to_string() + }; + + let ciphertext_hiding = if version >= Self::VERSION_1_55 { + format!("{}", self.ciphertext_hiding()) + } else { + "None".to_string() + }; + + let page_swap_disabled = if version >= Self::VERSION_1_58 { + format!("{}", self.page_swap_disabled()) + } else { + "None".to_string() + }; + + format!( + r#"Guest Policy (0x{:x}): + ABI Major: {} + ABI Minor: {} + SMT Allowed: {} + Migrate MA: {} + Debug Allowed: {} + Single Socket: {} + CXL Allowed: {} + AES 256 XTS: {} + RAPL Disabled: {} + Ciphertext Hiding: {} + Page Swap Disable: {}"#, + self.0, + self.abi_major(), + self.abi_minor(), + self.smt_allowed(), + self.migrate_ma_allowed(), + self.debug_allowed(), + self.single_socket_required(), + cxl_allowed, + mem_aes_256_xts, + rapl_dis, + ciphertext_hiding, + page_swap_disabled + ) + } +} + impl Encoder<()> for GuestPolicy { fn encode(&self, writer: &mut impl Write, _: ()) -> Result<(), std::io::Error> { writer.write_bytes(self.0, ())?; @@ -860,6 +1123,8 @@ impl Encoder<()> for GuestPolicy { } } +// No checking in case policy is being parsed outside attestation report (e.g. id-block) +// Assumes latest version for all bits, since older firmware should ignore unknown bits and newer firmware should require reserved bits to be set to 1 impl Decoder<()> for GuestPolicy { fn decode(reader: &mut impl Read, _: ()) -> Result { let policy = reader.read_bytes()?; @@ -867,7 +1132,17 @@ impl Decoder<()> for GuestPolicy { } } -impl ByteParser<()> for GuestPolicy { +// Checking reserved bytes according to known reserved bytes in attestation report +impl Decoder for GuestPolicy { + fn decode(reader: &mut impl Read, version: Version) -> Result { + let raw: u64 = reader.read_bytes()?; + let policy = GuestPolicy(raw); + policy.validate_reserved_bits(version)?; + Ok(policy) + } +} + +impl ByteParser for GuestPolicy { type Bytes = [u8; 8]; const EXPECTED_LEN: Option = Some(8); } @@ -956,6 +1231,172 @@ bitfield! { } +impl PlatformInfo { + // Bit 2: ECC_ENABLED (added in v1.55) + const ECC_BIT_2: u64 = 1u64 << 2; + // Bit 3: RAPL_DISABLED (added in v1.55) + const RAPL_BIT_3: u64 = 1u64 << 3; + // Bit 4: CIPHERTEXT_HIDING_ENABLED (added in v1.55) + const CIPHERTEXT_HIDING_BIT_4: u64 = 1u64 << 4; + // Bit 5: ALIAS_CHECK_COMPLETE (added in v1.57) + const ALIAS_CHECK_BIT_5: u64 = 1u64 << 5; + // Bit 6: Reserved (always MBZ) + const RESERVED_BIT_6: u64 = 1u64 << 6; + // Bit 7: TIO_ENABLED (added in v1.56) + const TIO_BIT_7: u64 = 1u64 << 7; + // Bits 8-63: Reserved (always MBZ) + const RESERVED_BITS_8_63: u64 = (!0u64) << 8; + + // Version constants + const VERSION_1_55: Version = Version { + major: 1, + minor: 55, + build: 0, + }; + const VERSION_1_56: Version = Version { + major: 1, + minor: 56, + build: 0, + }; + const VERSION_1_57: Version = Version { + major: 1, + minor: 57, + build: 0, + }; + + fn validate_reserved_bits(self, version: Version) -> std::io::Result<()> { + let raw = self.0; + + // Bit 6 and bits 8-63 are always reserved + if (raw & Self::RESERVED_BIT_6) != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("PlatformInfo bit 6 is reserved and must be zero (raw=0x{raw:016x})"), + )); + } + + if (raw & Self::RESERVED_BITS_8_63) != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("PlatformInfo bits 8-63 are reserved and must be zero (raw=0x{raw:016x})"), + )); + } + + // Bit 2 (ECC_ENABLED) is only defined for firmware v1.55+ + if version < Self::VERSION_1_55 && (raw & Self::ECC_BIT_2) != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!( + "PlatformInfo bit 2 (ECC_ENABLED) is only valid for firmware v1.55+ (raw=0x{raw:016x})" + ), + )); + } + + // Bit 3 (RAPL_DISABLED) is only defined for firmware v1.55+ + if version < Self::VERSION_1_55 && (raw & Self::RAPL_BIT_3) != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!( + "PlatformInfo bit 3 (RAPL_DISABLED) is only valid for firmware v1.55+ (raw=0x{raw:016x})" + ), + )); + } + + // Bit 4 (CIPHERTEXT_HIDING_ENABLED) is only defined for firmware v1.55+ + if version < Self::VERSION_1_55 && (raw & Self::CIPHERTEXT_HIDING_BIT_4) != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!( + "PlatformInfo bit 4 (CIPHERTEXT_HIDING_ENABLED) is only valid for firmware v1.55+ (raw=0x{raw:016x})" + ), + )); + } + + // Bit 5 (ALIAS_CHECK_COMPLETE) is only defined for firmware v1.57+ + if version < Self::VERSION_1_57 && (raw & Self::ALIAS_CHECK_BIT_5) != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!( + "PlatformInfo bit 5 (ALIAS_CHECK_COMPLETE) is only valid for firmware v1.57+ (raw=0x{raw:016x})" + ), + )); + } + + // Bit 7 (TIO_ENABLED) is only defined for firmware v1.56+ + if version < Self::VERSION_1_56 && (raw & Self::TIO_BIT_7) != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!( + "PlatformInfo bit 7 (TIO_ENABLED) is only valid for firmware v1.56+ (raw=0x{raw:016x})" + ), + )); + } + + Ok(()) + } + + /// Formats the platform info with version-aware display. + /// + /// Platform info bits that are not defined for the given firmware version + /// will be displayed as "None" instead of their actual value. + /// + /// # Arguments + /// * `version` - The firmware version to use for determining which bits are valid + /// + /// # Returns + /// A formatted string representation of the platform info + pub fn display_for_version(&self, version: Version) -> String { + let ecc_enabled = if version >= Self::VERSION_1_55 { + format!("{}", self.ecc_enabled()) + } else { + "None".to_string() + }; + + let rapl_disabled = if version >= Self::VERSION_1_55 { + format!("{}", self.rapl_disabled()) + } else { + "None".to_string() + }; + + let ciphertext_hiding_enabled = if version >= Self::VERSION_1_55 { + format!("{}", self.ciphertext_hiding_enabled()) + } else { + "None".to_string() + }; + + let alias_check_complete = if version >= Self::VERSION_1_57 { + format!("{}", self.alias_check_complete()) + } else { + "None".to_string() + }; + + let tio_enabled = if version >= Self::VERSION_1_56 { + format!("{}", self.tio_enabled()) + } else { + "None".to_string() + }; + + format!( + r#"Platform Info ({}): + SMT Enabled: {} + TSME Enabled: {} + ECC Enabled: {} + RAPL Disabled: {} + Ciphertext Hiding Enabled: {} + Alias Check Complete: {} + SEV-TIO Enabled: {}"#, + self.0, + self.smt_enabled(), + self.tsme_enabled(), + ecc_enabled, + rapl_disabled, + ciphertext_hiding_enabled, + alias_check_complete, + tio_enabled + ) + } +} + impl Display for PlatformInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( @@ -999,10 +1440,21 @@ impl Encoder<()> for PlatformInfo { } } +// No checking in case platform info is being parsed outside attestation report impl Decoder<()> for PlatformInfo { fn decode(reader: &mut impl Read, _: ()) -> Result { - let info = reader.read_bytes()?; - Ok(Self(info)) + let raw: u64 = reader.read_bytes()?; + Ok(PlatformInfo(raw)) + } +} + +// Checking reserved bytes according to known reserved bytes in attestation report +impl Decoder for PlatformInfo { + fn decode(reader: &mut impl Read, version: Version) -> Result { + let raw: u64 = reader.read_bytes()?; + let info = PlatformInfo(raw); + info.validate_reserved_bits(version)?; + Ok(info) } } @@ -1011,6 +1463,11 @@ impl ByteParser<()> for PlatformInfo { const EXPECTED_LEN: Option = Some(8); } +impl ByteParser for PlatformInfo { + type Bytes = [u8; 8]; + const EXPECTED_LEN: Option = Some(8); +} + bitfield! { /// When an attestation report is requested, the user can request to have the report to not be signed, or sign with different keys. The user may also /// pass in the author key when launching the guest. This field provides that information and will be present in the attestation report. @@ -1041,6 +1498,108 @@ bitfield! { } +impl KeyInfo { + // Bits 0..4 are defined, bits 5..31 must be zero. + const RESERVED_MASK: u32 = !0x1F; // 0xFFFF_FFE0 + + // Bit 1: MASK_CHIP_KEY (added in v1.53) + const MASK_CHIP_KEY_BIT_1: u32 = 1u32 << 1; + + // SIGNING_KEY field: bits 2-4 + const SIGNING_KEY_MASK: u32 = 0b111 << 2; + + // Version constants + const VERSION_1_53: Version = Version { + major: 1, + minor: 53, + build: 0, + }; + const VERSION_1_54: Version = Version { + major: 1, + minor: 54, + build: 0, + }; + + fn validate_reserved_bits(self, version: Version) -> std::io::Result<()> { + let raw: u32 = self.0; + + // Bits 5-31 must always be zero + if (raw & Self::RESERVED_MASK) != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("KeyInfo reserved bits 5-31 must be zero (raw=0x{raw:08x})"), + )); + } + + // Bit 1 (MASK_CHIP_KEY) is only defined for firmware v1.53+ + if version < Self::VERSION_1_53 && (raw & Self::MASK_CHIP_KEY_BIT_1) != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!( + "KeyInfo bit 1 (MASK_CHIP_KEY) is only valid for firmware v1.53+ (raw=0x{raw:08x})" + ), + )); + } + + // SIGNING_KEY = VLEK (value 1) is only defined for firmware v1.54+ + let signing_key = (raw & Self::SIGNING_KEY_MASK) >> 2; + if version < Self::VERSION_1_54 && signing_key == 1 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!( + "KeyInfo SIGNING_KEY=VLEK (1) is only valid for firmware v1.54+ (raw=0x{raw:08x})" + ), + )); + } + + Ok(()) + } + + /// Formats the key info with version-aware display. + /// + /// Key info fields that are not defined for the given firmware version + /// will be displayed as "None" instead of their actual value. + /// + /// # Arguments + /// * `version` - The firmware version to use for determining which fields are valid + /// + /// # Returns + /// A formatted string representation of the key info + pub fn display_for_version(&self, version: Version) -> String { + let mask_chip_key = if version >= Self::VERSION_1_53 { + format!("{}", self.mask_chip_key()) + } else { + "None".to_string() + }; + + let signing_key = if version >= Self::VERSION_1_54 { + match self.signing_key() { + 0 => "vcek".to_string(), + 1 => "vlek".to_string(), + 7 => "none".to_string(), + v => format!("unknown ({v})"), + } + } else { + // Pre v1.54 only supports VCEK (0) and NONE (7) + match self.signing_key() { + 0 => "vcek".to_string(), + 7 => "none".to_string(), + v => format!("unknown ({v})"), + } + }; + + format!( + r#"Key Information: + author key enabled: {} + mask chip key: {} + signing key: {}"#, + self.author_key_en(), + mask_chip_key, + signing_key + ) + } +} + impl Encoder<()> for KeyInfo { fn encode(&self, writer: &mut impl Write, _: ()) -> Result<(), std::io::Error> { writer.write_bytes(self.0.to_le_bytes(), ())?; @@ -1048,10 +1607,21 @@ impl Encoder<()> for KeyInfo { } } +// No checking in case key info is being parsed outside attestation report impl Decoder<()> for KeyInfo { fn decode(reader: &mut impl Read, _: ()) -> Result { - let info = reader.read_bytes()?; - Ok(Self(info)) + let raw: u32 = reader.read_bytes()?; + Ok(KeyInfo(raw)) + } +} + +// Checking reserved bytes according to known reserved bytes in attestation report +impl Decoder for KeyInfo { + fn decode(reader: &mut impl Read, version: Version) -> Result { + let raw: u32 = reader.read_bytes()?; + let info = KeyInfo(raw); + info.validate_reserved_bits(version)?; + Ok(info) } } @@ -1060,6 +1630,11 @@ impl ByteParser<()> for KeyInfo { const EXPECTED_LEN: Option = Some(4); } +impl ByteParser for KeyInfo { + type Bytes = [u8; 4]; + const EXPECTED_LEN: Option = Some(4); +} + impl From for KeyInfo { fn from(value: u32) -> Self { KeyInfo(value) @@ -1097,8 +1672,10 @@ impl Display for KeyInfo { #[cfg(test)] mod tests { + use std::ops::Range; + use super::*; - use std::{convert::TryInto, io::Write}; + const CHIP_ID_RANGE: Range = 0x1A0..0x1E0; #[test] fn test_derive_key_new() { @@ -1162,25 +1739,44 @@ mod tests { } #[test] - fn test_attestation_report_fmt() { + fn test_report_body_fmt_v2_zero() { + // Build a full firmware report (1184) with v2 and a non-masked chip_id. + let mut bytes = vec![0u8; Report::REPORT_LEN]; + + // version: u32 LE at 0x00..0x04 + bytes[0x00..0x04].copy_from_slice(&2u32.to_le_bytes()); + + // sig algo + bytes[0x34..0x38].copy_from_slice(&1u32.to_le_bytes()); + + // policy u64 LE at 0x08..0x10 + let policy_raw = 1u64 << 17; // RMB1 bit + bytes[0x08..0x10].copy_from_slice(&policy_raw.to_le_bytes()); + + // Make chip_id non-zero so v2 parsing doesn't error out + bytes[CHIP_ID_RANGE.start] = 1; + + let report = Report::from_bytes(&bytes).unwrap(); + let body = ReportBody::from_bytes(report.body).unwrap(); + let expected: &str = r#"Attestation Report: -Version: 0 +Version: V2 Guest SVN: 0 -Guest Policy (0x0): - ABI Major: 0 - ABI Minor: 0 - SMT Allowed: false - Migrate MA: false - Debug Allowed: false - Single Socket: false - CXL Allowed: false - AEX 256 XTS: false - RAPL Allowed: false - Ciphertext hiding: false - Page Swap Disable: false +Guest Policy (0x20000): + ABI Major: 0 + ABI Minor: 0 + SMT Allowed: false + Migrate MA: false + Debug Allowed: false + Single Socket: false + CXL Allowed: None + AES 256 XTS: None + RAPL Disabled: None + Ciphertext Hiding: None + Page Swap Disable: None Family ID: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 @@ -1190,7 +1786,7 @@ Image ID: VMPL: 0 -Signature Algorithm: 0 +Signature Algorithm: ECDSA with SECP384R1 Current TCB: @@ -1199,20 +1795,20 @@ TCB Version: SNP: 0 TEE: 0 Boot Loader: 0 - FMC: None + FMC: 0 Platform Info (0): SMT Enabled: false TSME Enabled: false - ECC Enabled: false - RAPL Disabled: false - Ciphertext Hiding Enabled: false - Alias Check Complete: false - SEV-TIO Enabled: false + ECC Enabled: None + RAPL Disabled: None + Ciphertext Hiding Enabled: None + Alias Check Complete: None + SEV-TIO Enabled: None Key Information: author key enabled: false - mask chip key: false + mask chip key: None signing key: vcek Report Data: @@ -1255,7 +1851,7 @@ TCB Version: SNP: 0 TEE: 0 Boot Loader: 0 - FMC: None + FMC: 0 CPUID Family ID: None @@ -1264,7 +1860,7 @@ CPUID Model ID: None CPUID Stepping: None Chip ID: -00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 @@ -1276,7 +1872,7 @@ TCB Version: SNP: 0 TEE: 0 Boot Loader: 0 - FMC: None + FMC: 0 Current Version: 0.0.0 @@ -1289,35 +1885,30 @@ TCB Version: SNP: 0 TEE: 0 Boot Loader: 0 - FMC: None + FMC: 0 Launch Mitigation Vector: None Current Mitigation Vector: None +"#; -Signature: - R: -00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -00 00 00 00 00 00 00 00 - S: -00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -00 00 00 00 00 00 00 00"#; - assert_eq!(expected, AttestationReport::default().to_string()) + assert_eq!(expected, body.to_string()); } #[test] - fn test_attestation_report_copy() { - let expected: AttestationReport = AttestationReport::default(); + fn test_report_copy() { + let mut bytes = vec![0u8; Report::REPORT_LEN]; + + // Setting sig algo + bytes[0x34..0x38].copy_from_slice(&1u32.to_le_bytes()); - let copy: AttestationReport = expected; + let report = Report::from_bytes(&bytes).unwrap(); + let copy = report; - assert_eq!(expected, copy); + assert_eq!(report.body.as_ptr(), copy.body.as_ptr()); + assert_eq!(report.body.len(), copy.body.len()); + assert_eq!(report.signature.as_ptr(), copy.signature.as_ptr()); + assert_eq!(report.signature.len(), copy.signature.len()); } #[test] @@ -1511,10 +2102,11 @@ Signature: #[test] fn test_platform_info_v2_serialization() { - let original = PlatformInfo(0b11111); - // Test encoding and decoding - let buffer = original.to_bytes().unwrap(); - let decoded = PlatformInfo::from_bytes(&buffer).unwrap(); + let original = PlatformInfo(0b11); + // Test encoding and decoding with basic bits (SMT, TSME) only + let mut buffer = [0u8; 8]; + original.encode(&mut buffer.as_mut_slice(), ()).unwrap(); + let decoded = PlatformInfo::decode(&mut buffer.as_slice(), ()).unwrap(); assert_eq!(original, decoded); } @@ -1524,8 +2116,9 @@ Signature: let original = KeyInfo(0b11111); // Test encoding and decoding - let buffer = original.to_bytes().unwrap(); - let decoded = KeyInfo::from_bytes(&buffer).unwrap(); + let mut buffer = [0u8; 4]; + original.encode(&mut buffer.as_mut_slice(), ()).unwrap(); + let decoded = KeyInfo::decode(&mut buffer.as_slice(), ()).unwrap(); assert_eq!(original, decoded); assert!(decoded.author_key_en()); @@ -1535,15 +2128,15 @@ Signature: #[test] fn test_guest_policy_serialization() { - let mut original: GuestPolicy = Default::default(); + let mut original: GuestPolicy = GuestPolicy::from(0u64); original.set_abi_major(2); original.set_abi_minor(1); original.set_smt_allowed(true); original.set_debug_allowed(true); - // Test bincode let buffer = original.to_bytes().unwrap(); - let decoded = GuestPolicy::from_bytes(&buffer).unwrap(); + // Use a recent firmware version that supports all policy bits + let decoded = GuestPolicy::from_bytes_with(&buffer, Version::new(1, 58, 0)).unwrap(); assert_eq!(original, decoded); } @@ -1558,28 +2151,6 @@ Signature: assert_eq!(version, 3); } - #[test] - fn test_boundary_value_serialization() { - // Test max values - let platform_info = PlatformInfo(u64::MAX); - let key_info = KeyInfo(u32::MAX); - let guest_policy = GuestPolicy(u64::MAX); - - // Verify serialization/deserialization preserves max values - assert_eq!( - platform_info, - PlatformInfo::from_bytes(&platform_info.to_bytes().unwrap()).unwrap() - ); - assert_eq!( - key_info, - KeyInfo::from_bytes(&key_info.to_bytes().unwrap()).unwrap() - ); - assert_eq!( - guest_policy, - GuestPolicy::from_bytes(&guest_policy.to_bytes().unwrap()).unwrap() - ); - } - #[test] fn test_guest_field_select_operations() { let mut field = GuestFieldSelect::default(); @@ -1624,17 +2195,38 @@ Signature: } #[test] - fn test_attestation_report_fields() { - let report: AttestationReport = AttestationReport { - version: 2, - guest_svn: 1, - vmpl: 3, - ..Default::default() - }; - assert_eq!(report.version, 2); - assert_eq!(report.guest_svn, 1); - assert_eq!(report.vmpl, 3); - assert_eq!(report.measurement, [0; 48]); + fn test_report_body_selected_fields() { + let mut bytes = vec![0u8; Report::REPORT_LEN]; + + // v2: version u32 LE at 0x00..0x04 + bytes[0x00..0x04].copy_from_slice(&2u32.to_le_bytes()); + + // v2 requires chip_id not fully masked (not all zeros) + bytes[CHIP_ID_RANGE.start] = 1; + + // guest_svn at 0x04..0x08 + bytes[0x04..0x08].copy_from_slice(&1u32.to_le_bytes()); + + // vmpl at 0x30..0x34 + bytes[0x30..0x34].copy_from_slice(&3u32.to_le_bytes()); + + // signature algorithm + bytes[0x34..0x38].copy_from_slice(&1u32.to_le_bytes()); + + // policy u64 LE at 0x08..0x10 + let policy_raw = 1u64 << 17; // RMB1 bit + bytes[0x08..0x10].copy_from_slice(&policy_raw.to_le_bytes()); + + let report = Report::from_bytes(&bytes).unwrap(); + + // NOTE: parsing-only test: do NOT use TryFrom/verify path here + let body = ReportBody::from_bytes(report.body).unwrap(); + + assert_eq!(body.version, ReportVariant::V2); + assert_eq!(body.guest_svn, 1); + assert_eq!(body.vmpl, 3); + assert_eq!(body.measurement, &[0u8; 48]); + assert_eq!(body.sig_algo, SignatureAlgorithm::EcdsaSecp384r1) } #[test] @@ -1690,79 +2282,59 @@ Signature: } #[test] - fn test_attestation_report_from_bytes() { - // Create a valid attestation report bytes minus one byte. - let mut bytes: Vec = vec![0; 1183]; + fn test_report_from_bytes_ok() { + let mut bytes = vec![0u8; Report::REPORT_LEN]; + bytes[0x00..0x04].copy_from_slice(&2u32.to_le_bytes()); // v2 + bytes[CHIP_ID_RANGE.start] = 1; // unmask chip id - // Push the version byte at the beginning. - bytes.insert(0, 2); + // signature algorithm + bytes[0x34..0x38].copy_from_slice(&1u32.to_le_bytes()); - let vcek = [ - 0xD4, 0x95, 0x54, 0xEC, 0x71, 0x7F, 0x4E, 0x5B, 0x0F, 0xE6, 0xB1, 0x43, 0xBC, 0xF0, - 0x40, 0x5B, 0xD7, 0xAE, 0x30, 0x47, 0x27, 0xED, 0xF4, 0x66, 0x03, 0xF2, 0xA7, 0x6A, - 0xEF, 0x6A, 0x3A, 0xBC, 0x15, 0xD7, 0xAF, 0x38, 0xDB, 0x75, 0x70, 0x39, 0x02, 0x9F, - 0x0E, 0xFA, 0xCF, 0xD0, 0x8E, 0x24, 0x43, 0x24, 0x88, 0x47, 0x38, 0xC7, 0x2B, 0x08, - 0x2E, 0x2F, 0x87, 0xA4, 0x4D, 0x54, 0x1E, 0xB6, - ]; + // policy u64 LE at 0x08..0x10 + let policy_raw = 1u64 << 17; // RMB1 bit + bytes[0x08..0x10].copy_from_slice(&policy_raw.to_le_bytes()); - bytes[0x1A8..0x1E0].copy_from_slice(&vcek[..(0x1E0 - 0x1A8)]); + let report = Report::from_bytes(bytes.as_slice()); + assert!(report.is_ok()); - // Test valid input - let result = AttestationReport::from_bytes(bytes.as_slice()); - assert!(result.is_ok()); + // Also ensure the body can be parsed + let report = report.unwrap(); + assert!(ReportBody::from_bytes(report.body).is_ok()); } #[test] - #[should_panic] - fn test_attestation_report_from_invalid_bytes() { - // Create a valid attestation report bytes minus one byte. - let mut bytes: Vec = vec![0; 1183]; - - // Push the version byte at the beginning. - bytes.insert(0, 2); - - // Test invalid input (too short) - AttestationReport::from_bytes(bytes[..100].try_into().unwrap()).unwrap(); + fn test_report_from_bytes_rejects_bad_len() { + let bytes = vec![0u8; Report::REPORT_LEN - 1]; + let err = Report::from_bytes(&bytes).unwrap_err(); + assert_eq!(err.kind(), std::io::ErrorKind::InvalidData); } #[test] - fn test_attestation_report_parse_and_write_bytes() { - let report = AttestationReport { - version: 2, - guest_svn: Default::default(), - policy: Default::default(), - family_id: Default::default(), - image_id: Default::default(), - chip_id: [ - 0xD4, 0x95, 0x54, 0xEC, 0x71, 0x7F, 0x4E, 0x5B, 0x0F, 0xE6, 0xB1, 0x43, 0xBC, 0xF0, - 0x40, 0x5B, 0xD7, 0xAE, 0x30, 0x47, 0x27, 0xED, 0xF4, 0x66, 0x03, 0xF2, 0xA7, 0x6A, - 0xEF, 0x6A, 0x3A, 0xBC, 0x15, 0xD7, 0xAF, 0x38, 0xDB, 0x75, 0x70, 0x39, 0x02, 0x9F, - 0x0E, 0xFA, 0xCF, 0xD0, 0x8E, 0x24, 0x43, 0x24, 0x88, 0x47, 0x38, 0xC7, 0x2B, 0x08, - 0x2E, 0x2F, 0x87, 0xA4, 0x4D, 0x54, 0x1E, 0xB6, - ], - ..Default::default() - }; + fn test_report_body_parse_roundtrip_like() { + let mut bytes = vec![0u8; Report::REPORT_LEN]; + bytes[0x00..0x04].copy_from_slice(&2u32.to_le_bytes()); // v2 + bytes[CHIP_ID_RANGE.start] = 1; // unmask - // Test successful write - let result = report.to_bytes(); - assert!(result.is_ok()); + // family_id and image_id + bytes[0x10..0x20].copy_from_slice(&[0xAA; 16]); + bytes[0x20..0x30].copy_from_slice(&[0xBB; 16]); - // Test writing to a failing writer - struct FailingWriter; - impl Write for FailingWriter { - fn write(&mut self, _buf: &[u8]) -> std::io::Result { - Err(std::io::Error::other("test error")) - } - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } - } + // guest_svn + bytes[0x04..0x08].copy_from_slice(&1u32.to_le_bytes()); + + // signature algorithm + bytes[0x34..0x38].copy_from_slice(&1u32.to_le_bytes()); - let mut writer = FailingWriter; + // policy u64 LE at 0x08..0x10 + let policy_raw = 1u64 << 17; // RMB1 bit + bytes[0x08..0x10].copy_from_slice(&policy_raw.to_le_bytes()); - let result = report.encode(&mut FailingWriter, ()); - assert!(result.is_err()); - assert!(writer.flush().is_ok()); + let report = Report::from_bytes(&bytes).unwrap(); + let body = ReportBody::from_bytes(report.body).unwrap(); + + assert_eq!(body.family_id, &[0xAA; 16]); + assert_eq!(body.image_id, &[0xBB; 16]); + assert_eq!(body.guest_svn, 1); } #[test] @@ -1802,80 +2374,6 @@ Signature: assert_eq!(original.to_bytes().unwrap(), cloned.to_bytes().unwrap()); } - #[test] - fn test_attestation_report_complex_write() { - let report = AttestationReport { - version: 2, - guest_svn: 1, - policy: GuestPolicy::from(0xFF), - family_id: [0xAA; 16], - image_id: [0xBB; 16], - chip_id: [ - 0xD4, 0x95, 0x54, 0xEC, 0x71, 0x7F, 0x4E, 0x5B, 0x0F, 0xE6, 0xB1, 0x43, 0xBC, 0xF0, - 0x40, 0x5B, 0xD7, 0xAE, 0x30, 0x47, 0x27, 0xED, 0xF4, 0x66, 0x03, 0xF2, 0xA7, 0x6A, - 0xEF, 0x6A, 0x3A, 0xBC, 0x15, 0xD7, 0xAF, 0x38, 0xDB, 0x75, 0x70, 0x39, 0x02, 0x9F, - 0x0E, 0xFA, 0xCF, 0xD0, 0x8E, 0x24, 0x43, 0x24, 0x88, 0x47, 0x38, 0xC7, 0x2B, 0x08, - 0x2E, 0x2F, 0x87, 0xA4, 0x4D, 0x54, 0x1E, 0xB6, - ], - ..Default::default() - }; - - let buffer = report.to_bytes(); - assert!(buffer.is_ok()); - - // Read back and verify - let read_back = AttestationReport::from_bytes(&buffer.unwrap()).unwrap(); - assert_eq!(read_back.version, 2); - assert_eq!(read_back.guest_svn, 1); - assert_eq!(read_back.family_id, [0xAA; 16]); - assert_eq!(read_back.image_id, [0xBB; 16]); - } - - #[test] - fn test_write_with_limited_writer() { - let report = AttestationReport { - version: 2, - guest_svn: Default::default(), - policy: Default::default(), - family_id: Default::default(), - image_id: Default::default(), - chip_id: [ - 0xD4, 0x95, 0x54, 0xEC, 0x71, 0x7F, 0x4E, 0x5B, 0x0F, 0xE6, 0xB1, 0x43, 0xBC, 0xF0, - 0x40, 0x5B, 0xD7, 0xAE, 0x30, 0x47, 0x27, 0xED, 0xF4, 0x66, 0x03, 0xF2, 0xA7, 0x6A, - 0xEF, 0x6A, 0x3A, 0xBC, 0x15, 0xD7, 0xAF, 0x38, 0xDB, 0x75, 0x70, 0x39, 0x02, 0x9F, - 0x0E, 0xFA, 0xCF, 0xD0, 0x8E, 0x24, 0x43, 0x24, 0x88, 0x47, 0x38, 0xC7, 0x2B, 0x08, - 0x2E, 0x2F, 0x87, 0xA4, 0x4D, 0x54, 0x1E, 0xB6, - ], - ..Default::default() - }; - - // Writer that can only write small chunks - struct LimitedWriter { - data: Vec, - max_write: usize, - } - - impl Write for LimitedWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - let write_size = std::cmp::min(self.max_write, buf.len()); - self.data.extend_from_slice(&buf[..write_size]); - Ok(write_size) - } - - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } - } - - let mut writer = LimitedWriter { - data: Vec::new(), - max_write: 16, // Only write 16 bytes at a time - }; - - assert!(report.encode(&mut writer, ()).is_ok()); - assert!(writer.flush().is_ok()); - } - #[test] fn test_platform_v2_info_from_u64() { let value: u64 = 0xFFFF; @@ -1937,8 +2435,11 @@ Signature: } #[test] - fn test_turin_like_chip_id_milan_chip_id() { - // Valid Milan CHIP_ID + fn test_chip_id_v2_genoa_like_allowed() { + let mut bytes = vec![0u8; Report::REPORT_LEN]; + bytes[0x00..0x04].copy_from_slice(&2u32.to_le_bytes()); // v2 + + // Genoa-like: full 64 bytes used (i.e., not "first 8 nonzero then rest zero") let vcek_bytes = [ 0xD4, 0x95, 0x54, 0xEC, 0x71, 0x7F, 0x4E, 0x5B, 0x0F, 0xE6, 0xB1, 0x43, 0xBC, 0xF0, 0x40, 0x5B, 0xD7, 0xAE, 0x30, 0x47, 0x27, 0xED, 0xF4, 0x66, 0x03, 0xF2, 0xA7, 0x6A, @@ -1947,20 +2448,101 @@ Signature: 0x2E, 0x2F, 0x87, 0xA4, 0x4D, 0x54, 0x1E, 0xB6, ]; - assert!(!AttestationReport::chip_id_is_turin_like(&vcek_bytes).unwrap()); + bytes[0x34..0x38].copy_from_slice(&1u32.to_le_bytes()); + + // policy u64 LE at 0x08..0x10 + let policy_raw = 1u64 << 17; // RMB1 bit + bytes[0x08..0x10].copy_from_slice(&policy_raw.to_le_bytes()); + + bytes[CHIP_ID_RANGE.clone()].copy_from_slice(&vcek_bytes); + + let report = Report::from_bytes(&bytes).unwrap(); + let body = ReportBody::from_bytes(report.body).unwrap(); + + // Should have cpuid fields absent in v2 + assert_eq!(body.cpuid_fam_id, None); + assert_eq!(body.cpuid_mod_id, None); + assert_eq!(body.cpuid_step, None); + + // Should preserve chip_id bytes + assert_eq!(body.chip_id, &vcek_bytes); } #[test] - fn test_turin_like_chip_id_turin_chip_id() { - // Valid Turin CHIP_ID - let vcek_bytes = [ - 0xD4, 0x95, 0x54, 0xEC, 0x71, 0x7F, 0x4E, 0x5B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ]; + fn test_chip_id_v2_turin_like_allowed() { + let mut bytes = vec![0u8; Report::REPORT_LEN]; + bytes[0x00..0x04].copy_from_slice(&2u32.to_le_bytes()); // v2 + + let mut chip = [0u8; 64]; + chip[0..8].copy_from_slice(&[0xD4, 0x95, 0x54, 0xEC, 0x71, 0x7F, 0x4E, 0x5B]); + // rest remains zero + bytes[CHIP_ID_RANGE.clone()].copy_from_slice(&chip); + + bytes[0x34..0x38].copy_from_slice(&1u32.to_le_bytes()); + + // policy u64 LE at 0x08..0x10 + let policy_raw = 1u64 << 17; // RMB1 bit + bytes[0x08..0x10].copy_from_slice(&policy_raw.to_le_bytes()); + + let report = Report::from_bytes(&bytes).unwrap(); + let body = ReportBody::from_bytes(report.body).unwrap(); + + assert_eq!(body.chip_id, &chip); + } + + #[test] + fn test_chip_id_v2_masked_rejected() { + let mut bytes = vec![0u8; Report::REPORT_LEN]; + bytes[0x00..0x04].copy_from_slice(&2u32.to_le_bytes()); // v2 + // chip_id left as all zeros + + // Setting sig algo + bytes[0x34..0x38].copy_from_slice(&1u32.to_le_bytes()); + + let report = Report::from_bytes(&bytes).unwrap(); + let err = ReportBody::from_bytes(report.body).unwrap_err(); + assert!(err.to_string().contains("Chip ID is masked")); + } + + #[test] + fn test_report_from_bytes_splits_body_and_signature() { + let mut bytes = vec![0u8; Report::REPORT_LEN]; + + // signature algorithm + bytes[0x34..0x38].copy_from_slice(&1u32.to_le_bytes()); + + let r = Report::from_bytes(&bytes).unwrap(); + + assert_eq!(r.body.len(), 0x2a0); + assert_eq!(r.signature.len(), 0x49f + 1 - 0x2a0); + assert_eq!(r.body.as_ptr(), bytes.as_ptr()); + assert_eq!( + r.signature.as_ptr() as usize - bytes.as_ptr() as usize, + 0x2a0 + ); + } + + #[test] + fn test_report_variant_tryfrom() { + assert_eq!(ReportVariant::try_from(2).unwrap(), ReportVariant::V2); + assert_eq!(ReportVariant::try_from(3).unwrap(), ReportVariant::V3); + assert_eq!(ReportVariant::try_from(4).unwrap(), ReportVariant::V3); + assert_eq!(ReportVariant::try_from(5).unwrap(), ReportVariant::V5); + assert!(ReportVariant::try_from(99).is_err()); + } + + #[test] + fn test_report_body_rejects_nonzero_reserved_0x4c() { + let mut bytes = vec![0u8; Report::REPORT_LEN]; + bytes[0x00..0x04].copy_from_slice(&2u32.to_le_bytes()); + bytes[CHIP_ID_RANGE.start] = 1; + + // Setting sig algo + bytes[0x34..0x38].copy_from_slice(&1u32.to_le_bytes()); + + bytes[0x4C] = 1; // reserved byte non-zero - assert!(AttestationReport::chip_id_is_turin_like(&vcek_bytes).unwrap()); + let r = Report::from_bytes(&bytes).unwrap(); + assert!(ReportBody::from_bytes(r.body).is_err()); } } diff --git a/src/firmware/host/types/snp.rs b/src/firmware/host/types/snp.rs index 1f442c9f..72d90a26 100644 --- a/src/firmware/host/types/snp.rs +++ b/src/firmware/host/types/snp.rs @@ -10,7 +10,7 @@ use crate::{ parser::{ByteParser, Decoder, Encoder}, util::{ hexline::HexLine, - parser_helper::{ReadExt, WriteExt}, + parser_helper::{validate_reserved, ReadExt, WriteExt}, }, Generation, }; @@ -543,10 +543,10 @@ impl Decoder for TcbVersion { fn decode(reader: &mut impl Read, generation: Generation) -> Result { match generation { Generation::Milan | Generation::Genoa => { - Ok(TcbVersion::from_legacy_bytes(&reader.read_bytes()?)) + TcbVersion::try_from_legacy_bytes(&reader.read_bytes()?) } Generation::Turin | Generation::Venice => { - Ok(TcbVersion::from_turin_bytes(&reader.read_bytes()?)) + TcbVersion::try_from_turin_bytes(&reader.read_bytes()?) } _ => Err(std::io::Error::new( std::io::ErrorKind::Unsupported, @@ -562,14 +562,15 @@ impl ByteParser for TcbVersion { } impl TcbVersion { - pub(crate) fn from_legacy_bytes(bytes: &[u8; 8]) -> Self { - Self { + pub(crate) fn try_from_legacy_bytes(bytes: &[u8; 8]) -> Result { + validate_reserved(&bytes[2..6], 2)?; + Ok(Self { fmc: None, bootloader: bytes[0], tee: bytes[1], snp: bytes[6], microcode: bytes[7], - } + }) } pub(crate) fn to_legacy_bytes(self) -> [u8; 8] { @@ -585,14 +586,15 @@ impl TcbVersion { ] } - pub(crate) fn from_turin_bytes(bytes: &[u8; 8]) -> Self { - Self { + pub(crate) fn try_from_turin_bytes(bytes: &[u8; 8]) -> Result { + validate_reserved(&bytes[4..7], 4)?; + Ok(Self { fmc: Some(bytes[0]), bootloader: bytes[1], tee: bytes[2], snp: bytes[3], microcode: bytes[7], - } + }) } pub(crate) fn to_turin_bytes(self) -> [u8; 8] { @@ -1488,7 +1490,7 @@ mod tests { let tcb = TcbVersion::new(None, 1, 2, 3, 4); let serialized = tcb.to_legacy_bytes(); - let deserialized = TcbVersion::from_legacy_bytes(&serialized); + let deserialized = TcbVersion::try_from_legacy_bytes(&serialized).unwrap(); assert_eq!(tcb, deserialized); } diff --git a/src/util/parser_helper/mod.rs b/src/util/parser_helper/mod.rs index 11fe04b9..6bf3617a 100644 --- a/src/util/parser_helper/mod.rs +++ b/src/util/parser_helper/mod.rs @@ -4,6 +4,10 @@ mod read_ext; mod write_ext; +mod reserved; + pub(crate) use read_ext::ReadExt; pub(crate) use write_ext::WriteExt; + +pub(crate) use reserved::validate_reserved; diff --git a/src/util/parser_helper/reserved.rs b/src/util/parser_helper/reserved.rs new file mode 100644 index 00000000..71409bff --- /dev/null +++ b/src/util/parser_helper/reserved.rs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 + +/// Validates that reserved bytes at a known offset are all zero. +/// +/// This function checks that all bytes in the provided slice contain only zeros, +/// which is required by the SNP attestation report specification for reserved fields. +/// +/// # Arguments +/// +/// * `reserved` - A byte slice containing the reserved bytes to validate +/// * `offset` - The starting offset of the slice in the original buffer (for error messages) +/// +/// # Returns +/// +/// Returns `Ok(())` if all bytes in the slice are zero, or an `Err` with a descriptive +/// message if any reserved byte is non-zero, including the absolute offset. +/// +/// # Errors +/// +/// Returns an error if any byte in the slice is non-zero, including the absolute offsets +/// and values of the non-zero bytes. +/// +/// # Example +/// +/// ```ignore +/// let body = &buffer[0..0x2A0]; +/// validate_reserved(&body[0x4C..0x50], 0x4C)?; // Reports absolute offsets 0x4C-0x4F +/// ``` +pub fn validate_reserved(reserved: &[u8], offset: usize) -> Result<(), std::io::Error> { + // Collect indices and values of non-zero bytes + let non_zero: Vec<(usize, u8)> = reserved + .iter() + .enumerate() + .filter(|(_, &b)| b != 0) + .map(|(i, &b)| (offset + i, b)) + .collect(); + + if !non_zero.is_empty() { + // Format the non-zero bytes for the error message with absolute offsets + let details: Vec = non_zero + .iter() + .map(|(idx, val)| format!("[0x{:x}]=0x{:02x}", idx, val)) + .collect(); + + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("reserved bytes are non-zero: {}", details.join(", ")), + )); + } + + Ok(()) +} diff --git a/tests/certs.rs b/tests/certs.rs index 54f393d8..873685e9 100644 --- a/tests/certs.rs +++ b/tests/certs.rs @@ -25,8 +25,9 @@ mod sev { #[cfg(all(feature = "snp", any(feature = "openssl", feature = "crypto_nossl")))] mod snp { + use std::convert::TryFrom; + use sev::certs::snp::{builtin::milan, ca, Certificate, Chain, Verifiable}; - use sev::parser::ByteParser; const TEST_MILAN_VCEK_DER: &[u8] = include_bytes!("certs_data/vcek_milan.der"); @@ -76,7 +77,7 @@ mod snp { #[test] fn milan_report() { - use sev::firmware::guest::AttestationReport; + use sev::firmware::guest::{Report, ReportBody}; let ark = milan::ark().unwrap(); let ask = milan::ask().unwrap(); @@ -87,14 +88,18 @@ mod snp { let chain = Chain { ca, vek: vcek }; let report_bytes = hex::decode(TEST_MILAN_ATTESTATION_REPORT).unwrap(); - let report = AttestationReport::from_bytes(report_bytes.as_slice()).unwrap(); - - assert_eq!((&chain, &report).verify().ok(), Some(())); + let report = Report::from_bytes(report_bytes.as_slice()).unwrap(); + let _body = ReportBody::try_from((&report, &chain)) + .map_err(|e| { + println!("report verification failed: {e}"); + e + }) + .unwrap(); } #[test] fn milan_report_invalid() { - use sev::firmware::guest::AttestationReport; + use sev::firmware::guest::{Report, ReportBody}; let ark = milan::ark().unwrap(); let ask = milan::ask().unwrap(); @@ -106,9 +111,8 @@ mod snp { let mut report_bytes = hex::decode(TEST_MILAN_ATTESTATION_REPORT).unwrap(); report_bytes[21] ^= 0x80; - let report = AttestationReport::from_bytes(report_bytes.as_slice()).unwrap(); - - assert_eq!((&chain, &report).verify().ok(), None); + let report = Report::from_bytes(report_bytes.as_slice()).unwrap(); + assert!(ReportBody::try_from((&report, &chain)).is_err()); } #[cfg(feature = "openssl")]