Repository: coconut-svsm/svsm
Pull request #662: Add support for SVSM attest protocol
Author: login: deeglaze
Labels: name: in-review
description: PR is under active review and not yet approved
Created: 2025-04-02T01:41:32Z | Merged: 2025-04-18T04:29:21Z

This PR is based off PR #541 and the original author has agreed to share authorship with Co-Developed-by:.

The memory operations included in this PR should first be reviewed and merged in PR #653.

Notable differences from PR #541 are

* the use of the new memory operations
* the choice to change get_ekpub to a mutable method that will cache the EK on demand rather than on startup. This leaves the startup and self test obligation still with the firmware.
* the inclusion SVSM_ATTEST_SERVICES, which creates the GUID table of all services that produce manifests. This must be implemented before SVSM_CORE_QUERY_PROTOCOL can claim to support SVSM_ATTEST_PROTOCOL version 1.
* added results for SVSM_CORE_QUERY_PROTOCOL, so guest OS can ensure the protocol is available before using it.
* I've renamed the module from "attestation" to "attest" to match the name choice in the call identifiers in the SVSM specification (e.g., SVSM_ATTEST_SERVICES).

This series only provides a single selector "discriminator" for interpreting the manifest selector as a TPMT_PUBLIC input to TPM_CC_CREATEPRIMARY with certain boilerplate choices for the rest of the command.
Potential follow-up discriminator choices could be

* to provide the full CreatePrimary command, for e.g., tighter authorization policy
* to provide just an NV index to where a key template is stored, in order for the generated key to be more effectively cached.

I opted not to include these for expediency.

Update: removed proposed protocol extension.


Diff:
diff --git a/Cargo.lock b/Cargo.lock
index 08e40fea5b..5360393259 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1109,8 +1109,10 @@ dependencies = [
  "packit",
  "release",
  "rustc_version",
+ "sha2",
  "syscall",
  "test",
+ "uuid",
  "verify_external",
  "verify_proof",
  "vstd",
diff --git a/Cargo.toml b/Cargo.toml
index 599235ca9a..1ca87db881 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -57,8 +57,8 @@ intrusive-collections = "0.9.6"
 libfuzzer-sys = "0.4"
 log = "0.4.17"
 p384 = { version = "0.13.0" }
-sha2 = "0.10.8"
-uuid = "1.6.1"
+sha2 = { version = "0.10.8", default-features = false }
+uuid = { version = "1.6.1", default-features = false }
 # Add the derive feature by default because all crates use it.
 zerocopy = { version = "0.8.2", features = ["derive"] }
 
diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml
index 1385afea83..8778684bfc 100644
--- a/kernel/Cargo.toml
+++ b/kernel/Cargo.toml
@@ -36,6 +36,9 @@ packit.workspace = true
 libtcgtpm = { workspace = true, optional = true }
 zerocopy = { workspace = true, features = ["alloc", "derive"] }
 release.workspace = true
+# Need "force-soft", see https://github.com/RustCrypto/hashes/issues/446
+sha2 = { workspace = true, features = ["force-soft"] }
+uuid.workspace = true
 
 builtin = { workspace = true, optional = true }
 builtin_macros = { workspace = true }
diff --git a/kernel/src/crypto/mod.rs b/kernel/src/crypto/mod.rs
index 5138bb2208..23996abc0a 100644
--- a/kernel/src/crypto/mod.rs
+++ b/kernel/src/crypto/mod.rs
@@ -77,6 +77,27 @@ pub mod aead {
     pub struct Aes256Gcm;
 }
 
+pub mod digest {
+    //! API for message digests.
+
+    extern crate alloc;
+
+    use alloc::vec::Vec;
+
+    // Note the typical new/update/finish API is not initially supported due
+    // to our inability to hide an abstract implementation's state representation
+    // behind a type without any fields.
+
+    pub trait Algorithm {
+        /// Digests `input` into an output vector of size `OUTPUT_LEN`.
+        fn digest(input: &[u8]) -> Vec<u8>;
+    }
+
+    /// Sha512 type
+    #[derive(Copy, Clone, Debug)]
+    pub struct Sha512;
+}
+
 // Crypto implementations supported. Only one of them must be compiled-in.
 
 pub mod rustcrypto;
diff --git a/kernel/src/crypto/rustcrypto.rs b/kernel/src/crypto/rustcrypto.rs
index ceebf9d980..bbe3e066d6 100644
--- a/kernel/src/crypto/rustcrypto.rs
+++ b/kernel/src/crypto/rustcrypto.rs
@@ -6,15 +6,20 @@
 
 //! RustCrypto implementation
 
+extern crate alloc;
+
 use aes_gcm::{
     aead::{Aead, Payload},
     Aes256Gcm, Key, KeyInit, Nonce,
 };
+use alloc::vec::Vec;
+use sha2::{Digest, Sha512};
 
 use crate::{
     crypto::aead::{
         Aes256Gcm as CryptoAes256Gcm, Aes256GcmTrait as CryptoAes256GcmTrait, IV_SIZE, KEY_SIZE,
     },
+    crypto::digest::{Algorithm as CryptoHashTrait, Sha512 as CryptoSha512},
     protocols::errors::SvsmReqError,
 };
 
@@ -75,3 +80,9 @@ impl CryptoAes256GcmTrait for CryptoAes256Gcm {
         aes_gcm_do(AesGcmOperation::Decrypt, iv, key, aad, inbuf, outbuf)
     }
 }
+
+impl CryptoHashTrait for CryptoSha512 {
+    fn digest(input: &[u8]) -> Vec<u8> {
+        Sha512::digest(input).to_vec()
+    }
+}
diff --git a/kernel/src/error.rs b/kernel/src/error.rs
index 0791e76b3b..f2068bd03f 100644
--- a/kernel/src/error.rs
+++ b/kernel/src/error.rs
@@ -45,6 +45,18 @@ pub enum ApicError {
     Registration,
 }
 
+/// Errors related to Attestation handling. These may originate from multiple
+/// layers in the system. Added to protocol base for protocol error.
+#[repr(u64)]
+#[derive(Clone, Copy, Debug)]
+pub enum AttestError {
+    /// An error related to attestation report.
+    Report = 0,
+
+    /// An error related to attestation manifest.
+    Manifest = 1,
+}
+
 /// A generic error during SVSM operation.
 #[derive(Clone, Copy, Debug)]
 pub enum SvsmError {
@@ -100,6 +112,8 @@ pub enum SvsmError {
     NotSupported,
     /// Generic errors related to APIC emulation.
     Apic(ApicError),
+    /// Generic errors related to attestation handling.
+    Attestation(AttestError),
     /// Errors related to Hyper-V.
     HyperV(u16),
 }
@@ -116,6 +130,12 @@ impl From<ApicError> for SvsmError {
     }
 }
 
+impl From<AttestError> for SvsmError {
+    fn from(err: AttestError) -> Self {
+        Self::Attestation(err)
+    }
+}
+
 impl From<ObjError> for SvsmError {
     fn from(err: ObjError) -> Self {
         Self::Obj(err)
diff --git a/kernel/src/greq/pld_report.rs b/kernel/src/greq/pld_report.rs
index dc6158a3be..513e600715 100644
--- a/kernel/src/greq/pld_report.rs
+++ b/kernel/src/greq/pld_report.rs
@@ -8,7 +8,7 @@
 
 use core::mem::size_of;
 
-use zerocopy::{FromBytes, Immutable, KnownLayout};
+use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
 
 use crate::protocols::errors::SvsmReqError;
 
@@ -17,20 +17,20 @@ pub const USER_DATA_SIZE: usize = 64;
 
 /// MSG_REPORT_REQ payload format (AMD SEV-SNP spec. table 20)
 #[repr(C, packed)]
-#[derive(Clone, Copy, Debug, FromBytes, KnownLayout, Immutable)]
+#[derive(Clone, Copy, Debug, FromBytes, KnownLayout, Immutable, IntoBytes)]
 pub struct SnpReportRequest {
     /// Guest-provided data to be included in the attestation report
     /// REPORT_DATA (512 bits)
-    user_data: [u8; USER_DATA_SIZE],
+    pub user_data: [u8; USER_DATA_SIZE],
     /// The VMPL to put in the attestation report
-    vmpl: u32,
+    pub vmpl: u32,
     /// 31:2 - Reserved
     ///  1:0 - KEY_SEL. Selects which key to use for derivation
     ///        0: If VLEK is installed, sign with VLEK. Otherwise, sign with VCEK
     ///        1: Sign with VCEK
     ///        2: Sign with VLEK
     ///        3: Reserved
-    flags: u32,
+    pub flags: u32,
     /// Reserved, must be zero
     rsvd: [u8; 24],
 }
@@ -57,7 +57,7 @@ impl SnpReportRequest {
 
 ///  MSG_REPORT_RSP payload format (AMD SEV-SNP spec. table 23)
 #[repr(C, packed)]
-#[derive(Clone, Copy, Debug, FromBytes, KnownLayout, Immutable)]
+#[derive(Clone, Copy, Debug, FromBytes, KnownLayout, Immutable, IntoBytes)]
 pub struct SnpReportResponse {
     /// The status of the key derivation operation, see [SnpReportResponseStatus]
     status: u32,
@@ -66,7 +66,7 @@ pub struct SnpReportResponse {
     /// Reserved
     _reserved: [u8; 24],
     /// The attestation report generated by firmware
-    report: AttestationReport,
+    pub report: AttestationReport,
 }
 
 /// Supported values for SnpReportResponse.status
@@ -101,7 +101,7 @@ impl SnpReportResponse {
 /// component in the trusted computing base (TCB) of the SNP firmware.
 /// (AMD SEV-SNP spec. table 3)
 #[repr(C, packed)]
-#[derive(Clone, Copy, Debug, FromBytes, Immutable)]
+#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)]
 struct TcbVersion {
     /// Version of the Microcode, SNP firmware, PSP and boot loader
     raw: u64,
@@ -109,7 +109,7 @@ struct TcbVersion {
 
 /// Format for an ECDSA P-384 with SHA-384 signature (AMD SEV-SNP spec. table 115)
 #[repr(C, packed)]
-#[derive(Clone, Copy, Debug, FromBytes, KnownLayout, Immutable)]
+#[derive(Clone, Copy, Debug, FromBytes, KnownLayout, Immutable, IntoBytes)]
 struct Signature {
     /// R component of this signature
     r: [u8; 72],
@@ -121,7 +121,7 @@ struct Signature {
 
 /// ATTESTATION_REPORT format (AMD SEV-SNP spec. table 21)
 #[repr(C, packed)]
-#[derive(Clone, Copy, Debug, FromBytes, KnownLayout, Immutable)]
+#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes, KnownLayout)]
 pub struct AttestationReport {
     /// Version number of this attestation report
     version: u32,
diff --git a/kernel/src/protocols/attest.rs b/kernel/src/protocols/attest.rs
new file mode 100644
index 0000000000..c2b33b9c40
--- /dev/null
+++ b/kernel/src/protocols/attest.rs
@@ -0,0 +1,406 @@
+// SPDX-License-Identifier: MIT
+//
+// Copyright (c) 2025  Hewlett Packard Enterprise Development LP
+// Copyright (c) 2025 Coconut-SVSM Authors
+//
+
+//! Attest protocol implementation
+
+extern crate alloc;
+
+use crate::address::{Address, PhysAddr};
+use crate::crypto::digest::{Algorithm, Sha512};
+use crate::error::{AttestError, SvsmError};
+use crate::greq::{
+    pld_report::{SnpReportRequest, SnpReportResponse},
+    services::get_regular_report,
+};
+use crate::mm::guestmem::{copy_slice_to_guest, read_bytes_from_guest, read_from_guest};
+use crate::protocols::{errors::SvsmReqError, RequestParams};
+use crate::utils::MemoryRegion;
+#[cfg(all(feature = "vtpm", not(test)))]
+use crate::vtpm::vtpm_get_manifest;
+
+use alloc::{boxed::Box, vec::Vec};
+use uuid::{uuid, Uuid};
+use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
+
+pub const ATTEST_PROTOCOL_VERSION_MIN: u32 = 1;
+pub const ATTEST_PROTOCOL_VERSION_MAX: u32 = 1;
+
+const SERVICES_MANIFEST_GUID: Uuid = uuid!("63849ebb-3d92-4670-a1ff-58f9c94b87bb");
+const GUID_HEADER_ENTRY_SIZE: usize = 24;
+const SVSM_ATTEST_SERVICES: u32 = 0;
+const SVSM_ATTEST_SINGLE_SERVICE: u32 = 1;
+
+#[cfg(all(feature = "vtpm", not(test)))]
+const SVSM_ATTEST_VTPM_GUID: Uuid = uuid!("c476f1eb-0123-45a5-9641-b4e7dde5bfe3");
+
+// Attest services operation structure, as defined in Table 11 of Secure VM Service Module for
+// SEV-SNP Guests 58019 Rev, 1.00 July 2023
+#[repr(C, packed)]
+#[derive(FromBytes, IntoBytes, Immutable, KnownLayout, Clone, Copy, Debug)]
+pub struct AttestServicesOp {
+    report_gpa: u64,
+    report_size: u32,
+    reserved_1: [u8; 4],
+    nonce_gpa: u64,
+    nonce_size: u16,
+    reserved_2: [u8; 6],
+    manifest_gpa: u64,
+    manifest_size: u32,
+    reserved_3: [u8; 4],
+    certificate_gpa: u64,
+    certificate_size: u32,
+    reserved_4: [u8; 4],
+}
+
+impl AttestServicesOp {
+    /// Take a slice and return a reference for Self
+    pub fn try_from_as_ref(buffer: &[u8]) -> Result<&Self, SvsmReqError> {
+        let ops: &Self =
+            Self::ref_from_bytes(buffer).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !ops.is_reserved_clear() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok(ops)
+    }
+
+    /// Checks if reserved fields are all set to zero
+    pub fn is_reserved_clear(&self) -> bool {
+        self.reserved_1.iter().all(|&x| x == 0)
+            && self.reserved_2.iter().all(|&x| x == 0)
+            && self.reserved_3.iter().all(|&x| x == 0)
+            && self.reserved_4.iter().all(|&x| x == 0)
+    }
+
+    /// Returns the nonce
+    pub fn get_nonce(&self) -> Result<Vec<u8>, SvsmReqError> {
+        // Check that nonce is the right size and doesn't cross a page boundary.
+        // Nonce i.e. REPORT_DATA is 64 bytes per Table 22 of
+        // "SEV Secure Nested Paging Firmware ABI Specification, Revision 1.57"
+        let gpa = PhysAddr::from(self.nonce_gpa);
+        let nonce_size = self.nonce_size.into();
+        if nonce_size != 64 || gpa.crosses_page(nonce_size) {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        read_bytes_from_guest(gpa, nonce_size).map_err(|_| SvsmReqError::invalid_parameter())
+    }
+
+    /// Returns the manifest buffer gpa and size
+    /// Checks if gpa is page aligned and valid.
+    /// Manifest buffer size can be greater than 4k, so it can cross page boundary.
+    pub fn get_manifest_region(&self) -> Result<MemoryRegion<PhysAddr>, SvsmReqError> {
+        let gpa = PhysAddr::from(self.manifest_gpa);
+        let size =
+            usize::try_from(self.manifest_size).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !gpa.is_page_aligned() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok(MemoryRegion::new(gpa, size))
+    }
+
+    /// Returns the report buffer gpa and size
+    /// Checks if gpa is page aligned and valid.
+    /// Report buffer size can be greater than 4k, so it can cross page boundary.
+    pub fn get_report_region(&self) -> Result<MemoryRegion<PhysAddr>, SvsmReqError> {
+        let gpa = PhysAddr::from(self.report_gpa);
+        let size =
+            usize::try_from(self.report_size).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !gpa.is_page_aligned() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok(MemoryRegion::new(gpa, size))
+    }
+}
+
+#[derive(Clone)]
+struct GuidTableEntry {
+    guid: uuid::Uuid,
+    data: Vec<u8>,
+}
+
+struct GuidTable {
+    entries: Vec<GuidTableEntry>,
+}
+
+impl GuidTable {
+    fn new() -> Self {
+        Self {
+            entries: Vec::new(),
+        }
+    }
+
+    // Only used when features are enabled.
+    #[allow(dead_code)]
+    fn push(&mut self, guid: uuid::Uuid, data: Vec<u8>) {
+        self.entries.push(GuidTableEntry { guid, data })
+    }
+
+    fn header_size(&self) -> usize {
+        // The services manifest header is 24 bytes, followed by 24 bytes for each entry.
+        (1 + self.entries.len()) * GUID_HEADER_ENTRY_SIZE
+    }
+
+    /// Returns how many bytes the wire ABI representation takes.
+    fn len(&self) -> usize {
+        let init = self.header_size();
+        self.entries
+            .iter()
+            .fold(init, |acc, entry| acc + entry.data.len())
+    }
+
+    // Writes the GuidTable contents at the end of `data` or returns an error.
+    fn to_vec(&self) -> Result<Vec<u8>, SvsmError> {
+        let length = self.len();
+        let mut data: Vec<u8> = Vec::with_capacity(length);
+        data.extend_from_slice(&SERVICES_MANIFEST_GUID.to_bytes_le());
+        data.extend_from_slice(&(length as u32).to_le_bytes());
+        data.extend_from_slice(&(self.entries.len() as u32).to_le_bytes());
+
+        let mut data_cursor = self.header_size() as u32;
+        self.entries.iter().for_each(|entry| {
+            data.extend_from_slice(&entry.guid.to_bytes_le());
+            data.extend_from_slice(&data_cursor.to_le_bytes());
+            data.extend_from_slice(&(entry.data.len() as u32).to_le_bytes());
+            data_cursor += entry.data.len() as u32;
+        });
+        self.entries
+            .iter()
+            .for_each(|entry| data.extend_from_slice(entry.data.as_slice()));
+        Ok(data)
+    }
+}
+
+// Attest Single Service Operation structure, as defined in
+// Table 13 of Secure VM Service Module for SEV-SNP Guests
+// 58019 Rev. 1.00 July 2023
+#[repr(C, packed)]
+#[derive(FromBytes, IntoBytes, Immutable, KnownLayout, Clone, Copy, Debug)]
+pub struct AttestSingleServiceOp {
+    op: AttestServicesOp,
+    guid: [u8; 16],
+    manifest_ver: u32,
+    reserved_5: [u8; 4],
+}
+
+impl AttestSingleServiceOp {
+    /// Take a slice and return a reference for Self
+    pub fn try_from_as_ref(buffer: &[u8]) -> Result<&Self, SvsmReqError> {
+        let ops: &Self =
+            Self::ref_from_bytes(buffer).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !ops.is_reserved_clear() || !ops.is_manifest_version_valid() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok(ops)
+    }
+
+    /// Checks if reserved fields are all set to zero
+    pub fn is_reserved_clear(&self) -> bool {
+        self.op.is_reserved_clear() && self.reserved_5.iter().all(|&x| x == 0)
+    }
+
+    /// Returns the nonce
+    pub fn get_nonce(&self) -> Result<Vec<u8>, SvsmReqError> {
+        self.op.get_nonce()
+    }
+
+    /// Returns the manifest buffer gpa and size
+    /// Checks if gpa is page aligned and valid.
+    /// Manifest buffer size can be greater than 4k, so it can cross page boundary.
+    pub fn get_manifest_region(&self) -> Result<MemoryRegion<PhysAddr>, SvsmReqError> {
+        self.op.get_manifest_region()
+    }
+
+    /// Returns the report buffer gpa and size
+    /// Checks if gpa is page aligned and valid.
+    /// Report buffer size can be greater than 4k, so it can cross page boundary.
+    pub fn get_report_region(&self) -> Result<MemoryRegion<PhysAddr>, SvsmReqError> {
+        self.op.get_report_region()
+    }
+
+    pub fn get_manifest_version(&self) -> u32 {
+        self.manifest_ver
+    }
+
+    fn is_manifest_version_valid(&self) -> bool {
+        // Currently only manifest version 0 is supported
+        self.manifest_ver == 0
+    }
+
+    /// Returns the guid
+    pub fn get_guid(&self) -> uuid::Uuid {
+        Uuid::from_bytes_le(self.guid)
+    }
+}
+
+fn get_attestation_report(nonce: &[u8]) -> Result<Box<SnpReportResponse>, SvsmReqError> {
+    let mut resp = SnpReportResponse::new_box_zeroed()
+        .map_err(|_| SvsmReqError::FatalError(SvsmError::Mem))?;
+    let resp_buffer = resp.as_mut_bytes();
+    // Cast error is infallibly discarded.
+    let (report_req, _) = SnpReportRequest::mut_from_prefix(resp_buffer)
+        .map_err(|_| SvsmReqError::invalid_parameter())?;
+    // Zero initialized, so
+    // vmpl=0
+    // flags=0: Use VLEK if installed, otherwise VCEK.
+    report_req.user_data = nonce
+        .try_into()
+        .map_err(|_| SvsmReqError::invalid_parameter())?;
+    let _response_size = get_regular_report(resp_buffer)?;
+
+    Ok(resp)
+}
+
+fn write_report_and_manifest(
+    manifest: &[u8],
+    params: &mut RequestParams,
+    ops: &AttestServicesOp,
+    report: &[u8],
+) -> Result<(), SvsmReqError> {
+    // Get attestation report buffer's gPA from call's Attest Single Service Operation structure.
+    // The buffer is required to be page aligned but can be bigger than 4K so can cross pages.
+    // If it is bigger than 4K, it must be physically contiguous.
+    let report_region = ops.get_report_region()?;
+
+    // Get manifest buffer's GPA from call's Attest Single Service Operation structure
+    // The buffer is required to be page aligned but can be bigger than 4K so can cross pages.
+    // If it is bigger than 4K, it must be physically contiguous.
+    let manifest_region = ops.get_manifest_region()?;
+
+    // Check that the manifest will fit in the buffer by checking that the length of the manifest
+    // is less than the size of the buffer. The size of the buffer was used to create the guard,
+    // so can not be tricked into writing outside the buffer.
+    // If the manifest is larger than the buffer, it is either a malformed manifest or buffer too
+    // small. In either case, return an error.
+    if manifest.len() > manifest_region.len() {
+        return Err(SvsmError::Attestation(AttestError::Manifest).into());
+    }
+
+    // Check that the attestation report will fit in the buffer by checking that the length of the
+    // report is less than the size of the buffer. The size of the buffer was used to create the
+    // guard, so can not be tricked into writing outside the buffer.
+    // If the report is larger than the buffer, it is either a malformed report or buffer too small.
+    // In either case, return an error.
+    if report.len() > report_region.len() {
+        return Err(SvsmError::Attestation(AttestError::Report).into());
+    }
+
+    copy_slice_to_guest(report, report_region.start())?;
+
+    // Set report size in bytes in r8 register
+    params.r8 = report
+        .len()
+        .try_into()
+        .map_err(|_| SvsmError::Attestation(AttestError::Report))?;
+
+    copy_slice_to_guest(manifest, manifest_region.start())?;
+
+    // Set the manifest size in bytes in rcx register
+    params.rcx = manifest
+        .len()
+        .try_into()
+        .map_err(|_| SvsmError::Attestation(AttestError::Manifest))?;
+
+    // Does not support certificate currently, so setting certificate size to 0
+    params.rdx = 0;
+
+    Ok(())
+}
+
+#[allow(dead_code)]
+fn attest_single_service(
+    manifest: &[u8],
+    params: &mut RequestParams,
+    ops: &AttestSingleServiceOp,
+) -> Result<(), SvsmReqError> {
+    let nonce = ops.get_nonce()?;
+
+    // Concatenate nonce and manifest and hash per page 29 of
+    // "Secure VM Service Module for SEV-SNP Guests 58019 Rev. 1.00".
+    let nonce_and_manifest = [&nonce[..], manifest].concat();
+    let hash = Sha512::digest(&nonce_and_manifest);
+
+    // Get attestation report from PSP with Sha512(nonce||manifest) as REPORT_DATA.
+    let resp = get_attestation_report(hash.as_slice())?;
+
+    write_report_and_manifest(manifest, params, &ops.op, resp.report.as_bytes())
+}
+
+#[cfg(all(feature = "vtpm", not(test)))]
+fn attest_single_vtpm(
+    params: &mut RequestParams,
+    ops: &AttestSingleServiceOp,
+) -> Result<(), SvsmReqError> {
+    attest_single_service(vtpm_get_manifest()?.as_slice(), params, ops)
+}
+
+fn attest_multiple_services(params: &mut RequestParams) -> Result<(), SvsmReqError> {
+    let gpa = PhysAddr::from(params.rcx);
+
+    let attest_op =
+        read_from_guest::<AttestServicesOp>(gpa).map_err(|_| SvsmReqError::invalid_parameter())?;
+
+    // Attest multiple services is expected to return a GUID table (mixed endian ordering) of the
+    // enumerated active services' attestation manifest. A service that does not have its own
+    // manifest is still enumerated, but with an empty data blob.
+    #[allow(unused_mut)]
+    let mut services = GuidTable::new();
+
+    #[cfg(all(feature = "vtpm", not(test)))]
+    services.push(SVSM_ATTEST_VTPM_GUID, vtpm_get_manifest()?);
+
+    let manifest = services.to_vec()?;
+    let mut nonce_and_manifest = attest_op.get_nonce()?;
+    nonce_and_manifest.extend_from_slice(manifest.as_slice());
+
+    // Concatenate nonce and manifest and hash per page 29 of
+    // "Secure VM Service Module for SEV-SNP Guests 58019 Rev. 1.00".
+    let hash = Sha512::digest(&nonce_and_manifest);
+
+    // Get attestation report from PSP with Sha512(nonce||manifest) as REPORT_DATA.
+    let resp = get_attestation_report(hash.as_slice())?;
+
+    write_report_and_manifest(
+        manifest.as_slice(),
+        params,
+        &attest_op,
+        resp.report.as_bytes(),
+    )
+}
+
+#[allow(clippy::needless_pass_by_ref_mut)]
+fn attest_single_service_handler(params: &mut RequestParams) -> Result<(), SvsmReqError> {
+    // Get the gpa of Attest Single Service Operation structure
+    let gpa = PhysAddr::from(params.rcx);
+
+    let attest_op = read_from_guest::<AttestSingleServiceOp>(gpa)
+        .map_err(|_| SvsmReqError::invalid_parameter())?;
+
+    // Extract the GUID from the Attest Single Service Operation structure.
+    // The GUID is used to determine the specific service to be attested.
+    // Currently, only the VTPM service with the GUID 0xebf176c4_2301a545_9641b4e7_dde5bfe3
+    // is supported, see 8.3.1 of the spec "Secure VM Service Module for SEV-SNP Guests
+    // 58019 Rev. 1.00" for more details.
+    match attest_op.get_guid() {
+        #[cfg(all(feature = "vtpm", not(test)))]
+        SVSM_ATTEST_VTPM_GUID => attest_single_vtpm(params, &attest_op),
+        _ => Err(SvsmReqError::unsupported_protocol()),
+    }
+}
+
+pub fn attest_protocol_request(
+    request: u32,
+    params: &mut RequestParams,
+) -> Result<(), SvsmReqError> {
+    match request {
+        SVSM_ATTEST_SERVICES => attest_multiple_services(params),
+        SVSM_ATTEST_SINGLE_SERVICE => attest_single_service_handler(params),
+        _ => Err(SvsmReqError::unsupported_protocol()),
+    }
+}
diff --git a/kernel/src/protocols/core.rs b/kernel/src/protocols/core.rs
index a4e1535530..63e4fee4ba 100644
--- a/kernel/src/protocols/core.rs
+++ b/kernel/src/protocols/core.rs
@@ -14,8 +14,11 @@ use crate::mm::virtualrange::{VIRT_ALIGN_2M, VIRT_ALIGN_4K};
 use crate::mm::PerCPUPageMappingGuard;
 use crate::mm::{valid_phys_address, writable_phys_addr, GuestPtr};
 use crate::protocols::apic::{APIC_PROTOCOL_VERSION_MAX, APIC_PROTOCOL_VERSION_MIN};
+use crate::protocols::attest::{ATTEST_PROTOCOL_VERSION_MAX, ATTEST_PROTOCOL_VERSION_MIN};
 use crate::protocols::errors::SvsmReqError;
-use crate::protocols::{RequestParams, SVSM_APIC_PROTOCOL, SVSM_CORE_PROTOCOL};
+use crate::protocols::{
+    RequestParams, SVSM_APIC_PROTOCOL, SVSM_ATTEST_PROTOCOL, SVSM_CORE_PROTOCOL,
+};
 use crate::requests::SvsmCaa;
 use crate::sev::utils::{
     pvalidate, rmp_clear_guest_vmsa, rmp_grant_guest_access, rmp_revoke_guest_access,
@@ -223,6 +226,11 @@ fn core_query_protocol(params: &mut RequestParams) -> Result<(), SvsmReqError> {
                 0
             }
         }
+        SVSM_ATTEST_PROTOCOL => protocol_supported(
+            version,
+            ATTEST_PROTOCOL_VERSION_MIN,
+            ATTEST_PROTOCOL_VERSION_MAX,
+        ),
         _ => 0,
     };
 
diff --git a/kernel/src/protocols/errors.rs b/kernel/src/protocols/errors.rs
index 70f4d4bbd1..c6e104c4e2 100644
--- a/kernel/src/protocols/errors.rs
+++ b/kernel/src/protocols/errors.rs
@@ -82,6 +82,7 @@ impl From<SvsmError> for SvsmReqError {
                 ApicError::Emulation => Self::invalid_parameter(),
                 ApicError::Registration => Self::protocol(SVSM_ERR_APIC_CANNOT_REGISTER),
             },
+            SvsmError::Attestation(e) => Self::protocol(e as u64),
             // Use a fatal error for now
             _ => Self::FatalError(err),
         }
diff --git a/kernel/src/protocols/mod.rs b/kernel/src/protocols/mod.rs
index ccadb987f5..80fd2f3a75 100644
--- a/kernel/src/protocols/mod.rs
+++ b/kernel/src/protocols/mod.rs
@@ -5,6 +5,7 @@
 // Author: Dov Murik <dovmurik@linux.ibm.com>
 
 pub mod apic;
+pub mod attest;
 pub mod core;
 pub mod errors;
 #[cfg(all(feature = "vtpm", not(test)))]
@@ -14,6 +15,7 @@ use cpuarch::vmsa::{GuestVMExit, VMSA};
 
 // SVSM protocols
 pub const SVSM_CORE_PROTOCOL: u32 = 0;
+pub const SVSM_ATTEST_PROTOCOL: u32 = 1;
 pub const SVSM_VTPM_PROTOCOL: u32 = 2;
 pub const SVSM_APIC_PROTOCOL: u32 = 3;
 
diff --git a/kernel/src/requests.rs b/kernel/src/requests.rs
index 03b58ed4af..0048c27c6d 100644
--- a/kernel/src/requests.rs
+++ b/kernel/src/requests.rs
@@ -14,9 +14,12 @@ use crate::protocols::errors::{SvsmReqError, SvsmResultCode};
 use crate::sev::ghcb::switch_to_vmpl;
 use crate::task::go_idle;
 
+use crate::protocols::attest::attest_protocol_request;
 #[cfg(all(feature = "vtpm", not(test)))]
 use crate::protocols::{vtpm::vtpm_protocol_request, SVSM_VTPM_PROTOCOL};
-use crate::protocols::{RequestParams, SVSM_APIC_PROTOCOL, SVSM_CORE_PROTOCOL};
+use crate::protocols::{
+    RequestParams, SVSM_APIC_PROTOCOL, SVSM_ATTEST_PROTOCOL, SVSM_CORE_PROTOCOL,
+};
 use crate::sev::vmsa::VMSAControl;
 use crate::types::GUEST_VMPL;
 use cpuarch::vmsa::GuestVMExit;
@@ -113,6 +116,7 @@ fn request_loop_once(
 
     match protocol {
         SVSM_CORE_PROTOCOL => core_protocol_request(request, params).map(|_| true),
+        SVSM_ATTEST_PROTOCOL => attest_protocol_request(request, params).map(|_| true),
         #[cfg(all(feature = "vtpm", not(test)))]
         SVSM_VTPM_PROTOCOL => vtpm_protocol_request(request, params).map(|_| true),
         SVSM_APIC_PROTOCOL => apic_protocol_request(request, params).map(|_| true),
diff --git a/kernel/src/vtpm/mod.rs b/kernel/src/vtpm/mod.rs
index ef7dd906ca..04962b6180 100644
--- a/kernel/src/vtpm/mod.rs
+++ b/kernel/src/vtpm/mod.rs
@@ -10,6 +10,10 @@
 /// TPM 2.0 Reference Implementation
 pub mod tcgtpm;
 
+extern crate alloc;
+
+use alloc::vec::Vec;
+
 use crate::vtpm::tcgtpm::TcgTpm as Vtpm;
 use crate::{locking::LockGuard, protocols::vtpm::TpmPlatformCommand};
 use crate::{locking::SpinLock, protocols::errors::SvsmReqError};
@@ -58,6 +62,27 @@ pub trait TcgTpmSimulatorInterface: VtpmProtocolInterface {
     fn signal_nvon(&self) -> Result<(), SvsmReqError>;
 }
 
+#[derive(Debug)]
+pub enum SvsmVTpmError {
+    ReqError(SvsmReqError),
+    CommandError(u32),
+}
+
+impl From<SvsmReqError> for SvsmVTpmError {
+    fn from(err: SvsmReqError) -> Self {
+        SvsmVTpmError::ReqError(err)
+    }
+}
+
+impl From<SvsmVTpmError> for SvsmReqError {
+    fn from(err: SvsmVTpmError) -> Self {
+        match err {
+            SvsmVTpmError::ReqError(e) => e,
+            SvsmVTpmError::CommandError(_) => SvsmReqError::invalid_request(),
+        }
+    }
+}
+
 /// Basic TPM driver services
 pub trait VtpmInterface: TcgTpmSimulatorInterface {
     /// Check if the TPM is powered on.
@@ -66,6 +91,11 @@ pub trait VtpmInterface: TcgTpmSimulatorInterface {
     /// Prepare the TPM to be used for the first time. At this stage,
     /// the TPM is manufactured.
     fn init(&mut self) -> Result<(), SvsmReqError>;
+
+    /// Returns the cached EK public key if it exists, otherwise it returns an error indicating
+    /// that the EK public key does not exist.
+    /// Needs mutability to cache the key.
+    fn get_ekpub(&mut self) -> Result<Vec<u8>, SvsmReqError>;
 }
 
 static VTPM: SpinLock<Vtpm> = SpinLock::new(Vtpm::new());
@@ -84,3 +114,10 @@ pub fn vtpm_init() -> Result<(), SvsmReqError> {
 pub fn vtpm_get_locked<'a>() -> LockGuard<'a, Vtpm> {
     VTPM.lock()
 }
+
+/// Get the TPM manifest i.e the EK public key by calling the get_ekpub() implementation of the
+/// [`VtpmInterface`]
+pub fn vtpm_get_manifest() -> Result<Vec<u8>, SvsmReqError> {
+    let mut vtpm = VTPM.lock();
+    vtpm.get_ekpub()
+}
diff --git a/kernel/src/vtpm/tcgtpm/ek_templates.rs b/kernel/src/vtpm/tcgtpm/ek_templates.rs
new file mode 100644
index 0000000000..c31bdfbd82
--- /dev/null
+++ b/kernel/src/vtpm/tcgtpm/ek_templates.rs
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: MIT
+//
+// Copyright (C) Coconut-SVSM authors
+//
+// Author: Dionna Glaze <dionnaglaze@google.com>
+//
+
+// All constants are defined in TCG Trusted Platform Module Library Part 2 - Structures
+// with the names stated in comments.
+//
+// Table 9. definition of TPM_ALG_ID constants:
+// TPM_ALG_RSA     (0x0001)
+// TPM_ALG_AES     (0x0006)
+// TPM_ALG_SHA256  (0x000B)
+// TPM_ALG_NULL    (0x0010)
+// TPM_ALG_CFB     (0x0043)
+//
+// Table 33. TPMA_OBJECT bits
+// fixedTPM(1)
+// fixedParent(4)
+// sensitiveDataOrigin(5)
+// adminWithPolicy(7)
+// restricted(16)
+// decrypt(17)
+
+// TPMT_PUBLIC with TCG default EK template,
+// see Table 2: Default EK Template (TPMT_PUBLIC) L-1: RSA 2048 (Storage)
+// of TCG EK Credential Profile For TPM Family 2.0; Level 0 Version 2.5 Revision 2
+pub const DEFAULT_PUBLIC_AREA: [u8; 314] = [
+    0x00, 0x01, // type TPM_ALG_RSA
+    0x00, 0x0B, // nameAlg TPM_ALG_SHA256
+    0x00, 0x03, 0x00, 0xb2, // objectAttributes { decrypt restricted adminWithPolicy
+    // sensitiveDataOrigin fixedParent fixedTpm }
+    0x00, 0x20, // authPolicy PolicyA_SHA256 (EK Credential profile)
+    0x83, 0x71, 0x97, 0x67, 0x44, 0x84, 0xb3, 0xf8, 0x1a, 0x90, 0xcc, 0x8d, 0x46, 0xa5, 0xd7, 0x24,
+    0xfd, 0x52, 0xd7, 0x6e, 0x06, 0x52, 0x0b, 0x64, 0xf2, 0xa1, 0xda, 0x1b, 0x33, 0x14, 0x69, 0xaa,
+    // TPMS_RSA_PARMS {
+    0x00, 0x06, // symmetric { algorithm TPM_ALG_AES
+    0x00, 0x80, // keyBits 128
+    0x00, 0x43, // mode TPM_ALG_CFB }
+    0x00, 0x10, // scheme TPM_ALG_NULL
+    0x08, 0x00, // keyBits 2048
+    0x00, 0x00, 0x00, 0x00, // exponent (0) } TPMS_RSA_PARMS
+    0x01, 0x00, // unique
+    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, 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,
+    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, 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,
+    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,
+];
diff --git a/kernel/src/vtpm/tcgtpm/mod.rs b/kernel/src/vtpm/tcgtpm/mod.rs
index 3e8d2a445f..292ae26edc 100644
--- a/kernel/src/vtpm/tcgtpm/mod.rs
+++ b/kernel/src/vtpm/tcgtpm/mod.rs
@@ -11,9 +11,13 @@
 #[cfg(not(any(test, fuzzing)))]
 mod wrapper;
 
+pub mod ek_templates;
+mod tss;
+
 extern crate alloc;
 
 use alloc::vec::Vec;
+
 use core::ffi::c_void;
 use libtcgtpm::bindings::{
     TPM_Manufacture, TPM_TearDown, _plat__LocalitySet, _plat__NVDisable, _plat__NVEnable,
@@ -24,18 +28,23 @@ use crate::{
     address::VirtAddr,
     protocols::{errors::SvsmReqError, vtpm::TpmPlatformCommand},
     types::PAGE_SIZE,
-    vtpm::{TcgTpmSimulatorInterface, VtpmInterface, VtpmProtocolInterface},
+    vtpm::{
+        tcgtpm::ek_templates::DEFAULT_PUBLIC_AREA, TcgTpmSimulatorInterface, VtpmInterface,
+        VtpmProtocolInterface,
+    },
 };
 
-#[derive(Debug, Copy, Clone, Default)]
+#[derive(Debug, Clone, Default)]
 pub struct TcgTpm {
     is_powered_on: bool,
+    ekpub: Option<Vec<u8>>,
 }
 
 impl TcgTpm {
     pub const fn new() -> TcgTpm {
         TcgTpm {
             is_powered_on: false,
+            ekpub: None,
         }
     }
 
@@ -164,6 +173,13 @@ impl TcgTpmSimulatorInterface for TcgTpm {
 }
 
 impl VtpmInterface for TcgTpm {
+    fn get_ekpub(&mut self) -> Result<Vec<u8>, SvsmReqError> {
+        if self.ekpub.is_none() {
+            self.ekpub = Some(tss::create_ek(self, &DEFAULT_PUBLIC_AREA[..])?);
+        }
+        self.ekpub.clone().ok_or_else(SvsmReqError::invalid_request)
+    }
+
     fn is_powered_on(&self) -> bool {
         self.is_powered_on
     }
diff --git a/kernel/src/vtpm/tcgtpm/tss.rs b/kernel/src/vtpm/tcgtpm/tss.rs
new file mode 100644
index 0000000000..38c7386805
--- /dev/null
+++ b/kernel/src/vtpm/tcgtpm/tss.rs
@@ -0,0 +1,141 @@
+// SPDX-License-Identifier: MIT
+//
+// Copyright (c) 2025  Hewlett Packard Enterprise Development LP
+// Copyright (c) Coconut-SVSM authors
+//
+
+// This module is an incomplete software stack for constructing commands to send to the TPM.
+// It is not fully general for expressing all inputs to a command.
+
+extern crate alloc;
+
+use crate::protocols::errors::SvsmReqError;
+use crate::vtpm::{
+    tcgtpm::{TcgTpmSimulatorInterface, TPM_BUFFER_MAX_SIZE},
+    SvsmVTpmError,
+};
+use alloc::vec::Vec;
+
+pub const TPM_RC_SUCCESS: u32 = 0;
+
+// PREREQUISITE: CMD must be at least 10 bytes long.
+// A TPM command result contains
+//
+// Byte offset | Size | Description
+// ---
+// 0x00        | 2    | u16 ST tag
+// 0x02        | 4    | u32 response size
+// 0x06        | 4    | u32 response code
+fn tpm_cmd_rc(cmd: &[u8]) -> u32 {
+    u32::from_be_bytes(cmd[6..10].try_into().unwrap())
+}
+
+fn extend_empty_auth(buf: &mut Vec<u8>) {
+    // TPM_RS_PW(4) + nonce(2) + attributes(1) + pw(2)
+    buf.extend_from_slice(&[
+        0x00, 0x00, 0x00, 0x09, // Size
+        // TPM_RS_PW
+        0x40, 0x00, 0x00, 0x09, // nonce == empty buffer
+        0x00, 0x00, // session attributes = continueSession = 0x01
+        0x01, // password = empty buffer
+        0x00, 0x00,
+    ]);
+}
+
+fn create_mtauth_ek_cmd(tpmt_public: &[u8]) -> Vec<u8> {
+    let mut cmd = Vec::<u8>::with_capacity(TPM_BUFFER_MAX_SIZE);
+
+    // TPM Command header
+    cmd.extend_from_slice(&[
+        0x80, 0x02, // TPM_ST_SESSIONS
+        0x00, 0x00, 0x00, 0x00, // Placeholder for command size
+        0x00, 0x00, 0x01, 0x31, // TPM_CC_CREATEPRIMARY
+        0x40, 0x00, 0x00, 0x0B, // TPM_RH_ENDORSEMENT
+    ]);
+
+    // Authorization block
+    extend_empty_auth(&mut cmd);
+
+    // inSensitive parameter
+    //
+    // TPM2B_SENSITIVE_CREATE structure is defined in
+    // Table 132 — Definition of TPM2B_SENSITIVE_CREATE Structure,
+    // Trusted Platform Module Library Part 2: Structures
+    cmd.extend_from_slice(&[
+        0x00, 0x04, // sensitive data size
+        0x00, 0x00, 0x00, 0x00, // user auth
+    ]);
+
+    // inPublic parameter
+    // parameters size
+    cmd.extend_from_slice(&(tpmt_public.len() as u16).to_be_bytes());
+    // parameters
+    cmd.extend_from_slice(tpmt_public);
+
+    cmd.extend_from_slice(&[
+        0x00, 0x00, 0x00, 0x00, // outsideInfo parameter
+        0x00, 0x00, // pcr selection
+    ]);
+
+    // Update command size
+    let command_size = cmd.len();
+    cmd[2..6].copy_from_slice(&(command_size as u32).to_be_bytes());
+
+    cmd.resize(TPM_BUFFER_MAX_SIZE, 0);
+    cmd
+}
+
+/// Sends `cmd` to `vtpm` and returns the interpretation of its error mode.
+///
+/// Arguments:
+///
+/// * `vtpm`: An implementation of [`TcgTpmSimulatorInterface`] to send `cmd` to.
+/// * `cmd`: A command buffer large enough to receive the command response.
+/// * `set_len`: If true, sets the command length in the command header to `cmd.len()` before
+///              sending the command.
+pub fn checked_send<T: TcgTpmSimulatorInterface>(
+    vtpm: &T,
+    cmd: &mut [u8],
+    set_len: bool,
+) -> Result<(), SvsmVTpmError> {
+    let mut command_size;
+    if set_len {
+        command_size = cmd.len();
+        cmd[2..6].copy_from_slice(&(command_size as u32).to_be_bytes());
+    } else {
+        command_size = u32::from_be_bytes(cmd[2..6].try_into().unwrap()) as usize;
+    }
+    vtpm.send_tpm_command(cmd, &mut command_size, 0)
+        .map_err(|_| SvsmVTpmError::ReqError(SvsmReqError::invalid_request()))?;
+    let rc = tpm_cmd_rc(cmd);
+    if rc != TPM_RC_SUCCESS {
+        return Err(SvsmVTpmError::CommandError(rc));
+    }
+    Ok(())
+}
+
+/// Uses `vtpm` to create an a primary key on the endorsement hierarchy.
+///
+/// The key has no authorization policy.
+///
+/// Arguments:
+///
+/// * `vtpm`: An implementation of [`TcgTpmSimulatorInterface`] to send `cmd` to.
+/// * `tpmt_public`: A marshaled TPMT_PUBLIC to use as the key creation template.
+///
+/// Returns:
+///
+/// A TPMT_PUBLIC of the key created from the template.
+pub fn create_ek<T: TcgTpmSimulatorInterface>(
+    vtpm: &T,
+    tpmt_public: &[u8],
+) -> Result<Vec<u8>, SvsmVTpmError> {
+    let mut cmd = create_mtauth_ek_cmd(tpmt_public);
+
+    checked_send(vtpm, &mut cmd, /*set_len=*/ false)?;
+
+    // Get size (UINT16) of TPMT_PUBLIC at offset 18.
+    // Note this is output from the TPM, so its value is trusted.
+    let size_of_tpmt_public = u16::from_be_bytes([cmd[18], cmd[19]]);
+    Ok(cmd[20..(20 + size_of_tpmt_public) as usize].to_vec())
+}


Reviews:
user: login: stefano-garzarella
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T17:01:24Z
user: login: stefano-garzarella
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T17:10:59Z
user: login: deeglaze
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T18:06:53Z
user: login: deeglaze
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T21:34:12Z
user: login: stefano-garzarella
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T07:29:03Z
user: login: IT302
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T12:00:42Z
user: login: IT302
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T12:01:13Z
user: login: deeglaze
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T15:49:19Z
user: login: deeglaze
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T22:55:31Z
user: login: IT302
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T12:56:33Z
user: login: IT302
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T13:00:21Z
user: login: IT302
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T13:10:58Z
user: login: deeglaze
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T14:23:22Z
user: login: IT302
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:56:27Z
user: login: deeglaze
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T15:07:24Z
user: login: deeglaze
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T15:07:27Z
user: login: IT302
body: Looks good to me!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:36:33Z
user: login: deeglaze
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-16T21:33:59Z
user: login: joergroedel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T08:02:59Z
user: login: deeglaze
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T14:55:27Z
user: login: deeglaze
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T16:31:02Z
user: login: deeglaze
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T16:37:55Z
user: login: deeglaze
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T22:23:05Z

Review comments:
diff_hunk: @@ -428,6 +431,74 @@ pub fn copy_to_user(src: &[u8], dst: VirtAddr) -> Result<(), SvsmError> {
     }
 }
 
+fn checked_guest_region(start: PhysAddr, size: usize) -> Result<MemoryRegion<PhysAddr>, SvsmError> {
+    let region = MemoryRegion::checked_new(start, size).ok_or(SvsmError::Mem)?;
+    if !valid_phys_region(&region) {
+        return Err(SvsmError::InvalidAddress);
+    }
+    Ok(region)
+}
+
+/// Reads a slice of bytes from a physical address region outside of SVSM use.
+///
+/// # Arguments
+///
+/// * `src`: The physical address designating the start of continguous physical
+///   memory to read from.
+/// * `dst`: The pointer to the beginning of the guest memory range to populate.
+/// * `size`: The number of bytes to write from src to dst.
+///
+/// # Returns
+///
+/// This function returns a `Result` that indicates the success or failure of the operation.
+/// If the physical address region cannot be mapped, it returns `Err(SvsmError::Mem)`.
+/// If the physical address region cannot be read, it returns `Err(SvsmError::Fault)`.
+pub fn copy_from_guest(src: PhysAddr, dst: *mut u8, size: usize) -> Result<(), SvsmError> {
+    let size = dst.len();
path: kernel/src/mm/guestmem.rs
user: login: stefano-garzarella
body: What is the point of `size` parameter if we alias it as first step of this function.
Also how can `dst.len()` works on a raw pointer?
created_at: 2025-04-09T16:57:22Z
author_association: MEMBER
diff_hunk: @@ -454,8 +454,7 @@ fn checked_guest_region(start: PhysAddr, size: usize) -> Result<MemoryRegion<Phy
 /// If the physical address region cannot be mapped, it returns `Err(SvsmError::Mem)`.
 /// If the physical address region cannot be read, it returns `Err(SvsmError::Fault)`.
 pub fn copy_from_guest(src: PhysAddr, dst: *mut u8, size: usize) -> Result<(), SvsmError> {
-    let size = dst.len();
-    let region = checked_guest_region(src, dst.len())?;
+    let region = checked_guest_region(src, size)?;
path: kernel/src/mm/guestmem.rs
user: login: stefano-garzarella
body: Okay, now I see. I think you should move this change to the previous patch that without it, it shouldn't build.

That said, should this function be marked `unsafe`? Who is checking that `buf` is at least `size` bytes?

Otherwise, can we use a `buf` slice like for `copy_to_guest`?
created_at: 2025-04-09T17:01:18Z
author_association: MEMBER
diff_hunk: @@ -454,8 +454,7 @@ fn checked_guest_region(start: PhysAddr, size: usize) -> Result<MemoryRegion<Phy
 /// If the physical address region cannot be mapped, it returns `Err(SvsmError::Mem)`.
 /// If the physical address region cannot be read, it returns `Err(SvsmError::Fault)`.
 pub fn copy_from_guest(src: PhysAddr, dst: *mut u8, size: usize) -> Result<(), SvsmError> {
-    let size = dst.len();
-    let region = checked_guest_region(src, dst.len())?;
+    let region = checked_guest_region(src, size)?;
path: kernel/src/mm/guestmem.rs
user: login: stefano-garzarella
body: > Otherwise, can we use a buf slice like for copy_to_guest?

I meant something like this:
```diff
diff --git a/kernel/src/mm/guestmem.rs b/kernel/src/mm/guestmem.rs
index 873b3824..9a311862 100644
--- a/kernel/src/mm/guestmem.rs
+++ b/kernel/src/mm/guestmem.rs
@@ -453,12 +453,13 @@ fn checked_guest_region(start: PhysAddr, size: usize) -> Result<MemoryRegion<Phy
 /// This function returns a `Result` that indicates the success or failure of the operation.
 /// If the physical address region cannot be mapped, it returns `Err(SvsmError::Mem)`.
 /// If the physical address region cannot be read, it returns `Err(SvsmError::Fault)`.
-pub fn copy_from_guest(src: PhysAddr, dst: *mut u8, size: usize) -> Result<(), SvsmError> {
+pub fn copy_from_guest(src: PhysAddr, dst: &mut [u8]) -> Result<(), SvsmError> {
+    let size = dst.len();
     let region = checked_guest_region(src, size)?;
     let start = region.start().page_align();
     let offset = region.start().page_offset();
     let end = region.end().page_align_up();
-    let destination = dst as usize;
+    let destination = dst.as_ptr() as usize;
 
     // SAFETY: Only reads data from a region outside the SVSM. The Uninit->Init transmute
     // is safe since the entirety of the slice is populated.
@@ -513,13 +514,8 @@ pub fn copy_to_guest(src: &[u8], dst: PhysAddr) -> Result<(), SvsmError> {
 /// If the physical address region cannot be mapped, it returns `Err(SvsmError::Mem)`.
 /// If the physical address region cannot be read, it returns `Err(SvsmError::Fault)`.
 pub fn read_bytes_from_guest(src: PhysAddr, size: usize) -> Result<Vec<u8>, SvsmError> {
-    let mut result = Vec::with_capacity(size);
-    // SAFETY: The vector's capacity, `size` has been populated by copy_from_guest.
-    // The translation of bytes to T are checked.
-    unsafe {
-        copy_from_guest(src, result.as_mut_ptr(), size)?;
-        result.set_len(size);
-    }
+    let mut result = vec![0u8; size];
+    copy_from_guest(src, result.as_mut_bytes())?;
     Ok(result)
 }
 
@@ -540,7 +536,7 @@ pub fn read_from_guest<T: FromBytes + Sized>(src: PhysAddr) -> Result<T, SvsmErr
     // Should be stack allocatable, but using size_of in array bounds is not possible
     // in current Rust. The zero, copy, read sequence could be better optimized too.
     let mut result_bytes = vec![0u8; size_of::<T>()];
-    copy_from_guest(src, result_bytes.as_mut_ptr(), size_of::<T>())?;
+    copy_from_guest(src, result_bytes.as_mut_bytes())?;
     T::read_from_bytes(&result_bytes).map_err(|_| SvsmError::InvalidBytes)
 }
```
created_at: 2025-04-09T17:10:59Z
author_association: MEMBER
diff_hunk: @@ -454,8 +454,7 @@ fn checked_guest_region(start: PhysAddr, size: usize) -> Result<MemoryRegion<Phy
 /// If the physical address region cannot be mapped, it returns `Err(SvsmError::Mem)`.
 /// If the physical address region cannot be read, it returns `Err(SvsmError::Fault)`.
 pub fn copy_from_guest(src: PhysAddr, dst: *mut u8, size: usize) -> Result<(), SvsmError> {
-    let size = dst.len();
-    let region = checked_guest_region(src, dst.len())?;
+    let region = checked_guest_region(src, size)?;
path: kernel/src/mm/guestmem.rs
user: login: deeglaze
body: Can we do the safe memory review in https://github.com/coconut-svsm/svsm/pull/653 so I can just rebase on that?
created_at: 2025-04-09T18:06:53Z
author_association: COLLABORATOR
diff_hunk: @@ -454,8 +454,7 @@ fn checked_guest_region(start: PhysAddr, size: usize) -> Result<MemoryRegion<Phy
 /// If the physical address region cannot be mapped, it returns `Err(SvsmError::Mem)`.
 /// If the physical address region cannot be read, it returns `Err(SvsmError::Fault)`.
 pub fn copy_from_guest(src: PhysAddr, dst: *mut u8, size: usize) -> Result<(), SvsmError> {
-    let size = dst.len();
-    let region = checked_guest_region(src, dst.len())?;
+    let region = checked_guest_region(src, size)?;
path: kernel/src/mm/guestmem.rs
user: login: deeglaze
body: I specifically avoided using slices to allow for a better-performing copyless implementation.
created_at: 2025-04-09T21:34:11Z
author_association: COLLABORATOR
diff_hunk: @@ -454,8 +454,7 @@ fn checked_guest_region(start: PhysAddr, size: usize) -> Result<MemoryRegion<Phy
 /// If the physical address region cannot be mapped, it returns `Err(SvsmError::Mem)`.
 /// If the physical address region cannot be read, it returns `Err(SvsmError::Fault)`.
 pub fn copy_from_guest(src: PhysAddr, dst: *mut u8, size: usize) -> Result<(), SvsmError> {
-    let size = dst.len();
-    let region = checked_guest_region(src, dst.len())?;
+    let region = checked_guest_region(src, size)?;
path: kernel/src/mm/guestmem.rs
user: login: stefano-garzarella
body: > Can we do the safe memory review in #653 so I can just rebase on that?

@deeglaze sorry, GH handle dependencies very badly, I didn't get they come from that PR. Sure, I'll review there.

> I specifically avoided using slices to allow for a better-performing copyless implementation.

I see, but we can use `std::slice::from_raw_parts_mut(result.as_mut_ptr(), size)` for that. Anyway, I'll comment in that PR.
created_at: 2025-04-10T07:29:03Z
author_association: MEMBER
diff_hunk: @@ -428,6 +431,74 @@ pub fn copy_to_user(src: &[u8], dst: VirtAddr) -> Result<(), SvsmError> {
     }
 }
 
+fn checked_guest_region(start: PhysAddr, size: usize) -> Result<MemoryRegion<PhysAddr>, SvsmError> {
path: kernel/src/mm/guestmem.rs
user: login: IT302
body: I was thinking, maybe we could change the name to get_checked_guest_region to make it a bit clearer what it does?
created_at: 2025-04-14T12:00:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -428,6 +431,74 @@ pub fn copy_to_user(src: &[u8], dst: VirtAddr) -> Result<(), SvsmError> {
     }
 }
 
+fn checked_guest_region(start: PhysAddr, size: usize) -> Result<MemoryRegion<PhysAddr>, SvsmError> {
+    let region = MemoryRegion::checked_new(start, size).ok_or(SvsmError::Mem)?;
+    if !valid_phys_region(&region) {
+        return Err(SvsmError::InvalidAddress);
+    }
+    Ok(region)
+}
+
+/// Reads a slice of bytes from a physical address region outside of SVSM use.
+///
+/// # Arguments
+///
+/// * `src`: The physical address designating the start of continguous physical
+///   memory to read from.
+/// * `dst`: The pointer to the beginning of the guest memory range to populate.
+/// * `size`: The number of bytes to write from src to dst.
+///
+/// # Returns
+///
+/// This function returns a `Result` that indicates the success or failure of the operation.
+/// If the physical address region cannot be mapped, it returns `Err(SvsmError::Mem)`.
+/// If the physical address region cannot be read, it returns `Err(SvsmError::Fault)`.
+pub fn copy_from_guest(src: PhysAddr, dst: *mut u8, size: usize) -> Result<(), SvsmError> {
+    let size = dst.len();
+    let region = checked_guest_region(src, dst.len())?;
+    let start = region.start().page_align();
+    let offset = region.start().page_offset();
+    let end = region.end().page_align_up();
+    let destination = dst as usize;
+
+    // SAFETY: Only reads data from a region outside the SVSM. The Uninit->Init transmute
+    // is safe since the entirety of the slice is populated.
+    unsafe {
+        let guard = PerCPUPageMappingGuard::create(start, end, 0)?;
+        let source = guard.virt_addr().bits() + offset;
+        copy_bytes(source, destination, size)
+    }
+}
+
+/// Writes a slice of bytes to a physical address region outside of SVSM use.
+///
+/// # Arguments
+///
+/// * `src`: The byte slice to write to guest memory.
+/// * `dst`: The physical address designating the start of continguous physical
+///   memory to write to.
+///
+/// # Returns
+///
+/// This function returns a `Result` that indicates the success or failure of the operation.
+/// If the physical address region cannot be mapped, it returns `Err(SvsmError::Mem)`.
+/// If the physical address region cannot be read, it returns `Err(SvsmError::Fault)`.
+pub fn copy_to_guest(src: &[u8], dst: PhysAddr) -> Result<(), SvsmError> {
+    let size = src.len();
+    let region = checked_guest_region(dst, src.len())?;
+    let start = region.start().page_align();
+    let offset = region.start().page_offset();
+    let end = region.end().page_align_up();
+    let source = src.as_ptr() as usize;
+
+    // SAFETY: Only reads data from a region outside the SVSM.
+    unsafe {
+        let guard = PerCPUPageMappingGuard::create(start, end, 0)?;
+        let destination = guard.virt_addr().bits() + offset;
+        copy_bytes(source, destination, size)
path: kernel/src/mm/guestmem.rs
user: login: IT302
body: Rust-for-Linux tends to use core::ptr::copy_nonoverlapping(). I am wondering if it going to be lowered into something faster than copy_bytes().
created_at: 2025-04-14T12:01:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -428,6 +431,74 @@ pub fn copy_to_user(src: &[u8], dst: VirtAddr) -> Result<(), SvsmError> {
     }
 }
 
+fn checked_guest_region(start: PhysAddr, size: usize) -> Result<MemoryRegion<PhysAddr>, SvsmError> {
+    let region = MemoryRegion::checked_new(start, size).ok_or(SvsmError::Mem)?;
+    if !valid_phys_region(&region) {
+        return Err(SvsmError::InvalidAddress);
+    }
+    Ok(region)
+}
+
+/// Reads a slice of bytes from a physical address region outside of SVSM use.
+///
+/// # Arguments
+///
+/// * `src`: The physical address designating the start of continguous physical
+///   memory to read from.
+/// * `dst`: The pointer to the beginning of the guest memory range to populate.
+/// * `size`: The number of bytes to write from src to dst.
+///
+/// # Returns
+///
+/// This function returns a `Result` that indicates the success or failure of the operation.
+/// If the physical address region cannot be mapped, it returns `Err(SvsmError::Mem)`.
+/// If the physical address region cannot be read, it returns `Err(SvsmError::Fault)`.
+pub fn copy_from_guest(src: PhysAddr, dst: *mut u8, size: usize) -> Result<(), SvsmError> {
+    let size = dst.len();
+    let region = checked_guest_region(src, dst.len())?;
+    let start = region.start().page_align();
+    let offset = region.start().page_offset();
+    let end = region.end().page_align_up();
+    let destination = dst as usize;
+
+    // SAFETY: Only reads data from a region outside the SVSM. The Uninit->Init transmute
+    // is safe since the entirety of the slice is populated.
+    unsafe {
+        let guard = PerCPUPageMappingGuard::create(start, end, 0)?;
+        let source = guard.virt_addr().bits() + offset;
+        copy_bytes(source, destination, size)
+    }
+}
+
+/// Writes a slice of bytes to a physical address region outside of SVSM use.
+///
+/// # Arguments
+///
+/// * `src`: The byte slice to write to guest memory.
+/// * `dst`: The physical address designating the start of continguous physical
+///   memory to write to.
+///
+/// # Returns
+///
+/// This function returns a `Result` that indicates the success or failure of the operation.
+/// If the physical address region cannot be mapped, it returns `Err(SvsmError::Mem)`.
+/// If the physical address region cannot be read, it returns `Err(SvsmError::Fault)`.
+pub fn copy_to_guest(src: &[u8], dst: PhysAddr) -> Result<(), SvsmError> {
+    let size = src.len();
+    let region = checked_guest_region(dst, src.len())?;
+    let start = region.start().page_align();
+    let offset = region.start().page_offset();
+    let end = region.end().page_align_up();
+    let source = src.as_ptr() as usize;
+
+    // SAFETY: Only reads data from a region outside the SVSM.
+    unsafe {
+        let guard = PerCPUPageMappingGuard::create(start, end, 0)?;
+        let destination = guard.virt_addr().bits() + offset;
+        copy_bytes(source, destination, size)
path: kernel/src/mm/guestmem.rs
user: login: deeglaze
body: I can change that in the safemem PR after the upgrade to 1.84.0, since then I can use `src: VirtAddress` as a constant pointer using `core::ptr::with_exposed_provenance(src.bits())`.
created_at: 2025-04-14T15:49:19Z
author_association: COLLABORATOR
diff_hunk: @@ -428,6 +431,74 @@ pub fn copy_to_user(src: &[u8], dst: VirtAddr) -> Result<(), SvsmError> {
     }
 }
 
+fn checked_guest_region(start: PhysAddr, size: usize) -> Result<MemoryRegion<PhysAddr>, SvsmError> {
path: kernel/src/mm/guestmem.rs
user: login: deeglaze
body: That feels a bit redundant, since you get a result from checked_guest_region.
created_at: 2025-04-14T22:55:31Z
author_association: COLLABORATOR
diff_hunk: @@ -5,13 +5,287 @@
 
 //! Attest protocol implementation
 
+extern crate alloc;
+
+use crate::address::{Address, PhysAddr};
+use crate::error::{AttestError, SvsmError};
+use crate::greq::{pld_report::{USER_DATA_SIZE, SnpReportResponse}, services::get_regular_report};
+use crate::mm::guestmem::{read_bytes_from_guest, read_from_guest, copy_to_guest};
 use crate::protocols::{errors::SvsmReqError, RequestParams};
 
+use alloc::vec::Vec;
+use sha2::{Digest, Sha512};
+use uuid::{uuid, Uuid};
+use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
+
+const SERVICES_MANIFEST_GUID: Uuid = uuid!("63849ebb-3d92-4670-a1ff-58f9c94b87bb");
+const GUID_HEADER_ENTRY_SIZE: usize = 24;
+const SVSM_ATTEST_SERVICES: u32 = 0;
+
+// Attest services operation structure, as defined in Table 11 of Secure VM Service Module for
+// SEV-SNP Guests 58019 Rev, 1.00 July 2023
+#[repr(C, packed)]
+#[derive(FromBytes, IntoBytes, Immutable, KnownLayout, Clone, Copy, Debug)]
+pub struct AttestServicesOp {
+    report_gpa: u64,
+    report_size: u32,
+    reserved_1: [u8; 4],
+    nonce_gpa: u64,
+    nonce_size: u16,
+    reserved_2: [u8; 6],
+    manifest_gpa: u64,
+    manifest_size: u32,
+    reserved_3: [u8; 4],
+    certificate_gpa: u64,
+    certificate_size: u32,
+    reserved_4: [u8; 4],
+}
+
+impl AttestServicesOp {
+    /// Take a slice and return a reference for Self
+    pub fn try_from_as_ref(buffer: &[u8]) -> Result<&Self, SvsmReqError> {
+        let ops: &Self =
+            Self::ref_from_bytes(buffer).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !ops.is_reserved_clear() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok(ops)
+    }
+
+    /// Checks if reserved fields are all set to zero
+    pub fn is_reserved_clear(&self) -> bool {
+        self.reserved_1.iter().all(|&x| x == 0)
+            && self.reserved_2.iter().all(|&x| x == 0)
+            && self.reserved_3.iter().all(|&x| x == 0)
+            && self.reserved_4.iter().all(|&x| x == 0)
+    }
+
+    /// Returns the nonce
+    pub fn get_nonce(&self) -> Result<Vec<u8>, SvsmReqError> {
path: kernel/src/protocols/attest.rs
user: login: IT302
body: I think we missed to check if nonce crosses a 4 KB boundary. The spec says we must check and return SVSM_ERR_INVALID_PARAMETER  if it does.
created_at: 2025-04-15T12:56:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -166,6 +173,69 @@ impl GuidTable {
     }
 }
 
+// Attest Single Service Operation structure, as defined in
+// Table 13 of Secure VM Service Module for SEV-SNP Guests
+// 58019 Rev. 1.00 July 2023
+#[repr(C, packed)]
+#[derive(FromBytes, IntoBytes, Immutable, KnownLayout, Clone, Copy, Debug)]
+pub struct AttestSingleServiceOp {
+    op: AttestServicesOp,
+    guid: [u8; 16],
+    manifest_ver: u32,
+    reserved_5: [u8; 4],
+}
+
+impl AttestSingleServiceOp {
+    /// Take a slice and return a reference for Self
+    pub fn try_from_as_ref(buffer: &[u8]) -> Result<&Self, SvsmReqError> {
+        let ops: &Self =
+            Self::ref_from_bytes(buffer).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !ops.is_reserved_clear() || !ops.is_manifest_version_valid() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok(ops)
+    }
+
+    /// Checks if reserved fields are all set to zero
+    pub fn is_reserved_clear(&self) -> bool {
+        self.op.is_reserved_clear() && self.reserved_5.iter().all(|&x| x == 0)
+    }
+
+    /// Returns the nonce
+    pub fn get_nonce(&self) -> Result<Vec<u8>, SvsmReqError> {
path: kernel/src/protocols/attest.rs
user: login: IT302
body: I think we missed to check if nonce crosses a 4 KB boundary. The spec says we must check and return SVSM_ERR_INVALID_PARAMETER if it does.
created_at: 2025-04-15T13:00:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,13 +5,287 @@
 
 //! Attest protocol implementation
 
+extern crate alloc;
+
+use crate::address::{Address, PhysAddr};
+use crate::error::{AttestError, SvsmError};
+use crate::greq::{pld_report::{USER_DATA_SIZE, SnpReportResponse}, services::get_regular_report};
+use crate::mm::guestmem::{read_bytes_from_guest, read_from_guest, copy_to_guest};
 use crate::protocols::{errors::SvsmReqError, RequestParams};
 
+use alloc::vec::Vec;
+use sha2::{Digest, Sha512};
+use uuid::{uuid, Uuid};
+use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
+
+const SERVICES_MANIFEST_GUID: Uuid = uuid!("63849ebb-3d92-4670-a1ff-58f9c94b87bb");
+const GUID_HEADER_ENTRY_SIZE: usize = 24;
+const SVSM_ATTEST_SERVICES: u32 = 0;
+
+// Attest services operation structure, as defined in Table 11 of Secure VM Service Module for
+// SEV-SNP Guests 58019 Rev, 1.00 July 2023
+#[repr(C, packed)]
+#[derive(FromBytes, IntoBytes, Immutable, KnownLayout, Clone, Copy, Debug)]
+pub struct AttestServicesOp {
+    report_gpa: u64,
+    report_size: u32,
+    reserved_1: [u8; 4],
+    nonce_gpa: u64,
+    nonce_size: u16,
+    reserved_2: [u8; 6],
+    manifest_gpa: u64,
+    manifest_size: u32,
+    reserved_3: [u8; 4],
+    certificate_gpa: u64,
+    certificate_size: u32,
+    reserved_4: [u8; 4],
+}
+
+impl AttestServicesOp {
+    /// Take a slice and return a reference for Self
+    pub fn try_from_as_ref(buffer: &[u8]) -> Result<&Self, SvsmReqError> {
+        let ops: &Self =
+            Self::ref_from_bytes(buffer).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !ops.is_reserved_clear() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok(ops)
+    }
+
+    /// Checks if reserved fields are all set to zero
+    pub fn is_reserved_clear(&self) -> bool {
+        self.reserved_1.iter().all(|&x| x == 0)
+            && self.reserved_2.iter().all(|&x| x == 0)
+            && self.reserved_3.iter().all(|&x| x == 0)
+            && self.reserved_4.iter().all(|&x| x == 0)
+    }
+
+    /// Returns the nonce
+    pub fn get_nonce(&self) -> Result<Vec<u8>, SvsmReqError> {
+        // Check that nonce is the right size.
+        // Nonce i.e. REPORT_DATA is 64 bytes per Table 22 of
+        // "SEV Secure Nested Paging Firmware ABI Specification, Revision 1.57"
+        let nonce_size = self.nonce_size.into();
+        if nonce_size != 64 {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        let gpa = PhysAddr::from(self.nonce_gpa);
+
+        read_bytes_from_guest(gpa, nonce_size).map_err(|_| SvsmReqError::invalid_parameter())
+    }
+
+    /// Returns the manifest buffer gpa and size
+    /// Checks if gpa is page aligned and valid.
+    /// Manifest buffer size can be greater than 4k, so it can cross page boundary.
+    pub fn get_manifest_gpa_and_size(&self) -> Result<(PhysAddr, usize), SvsmReqError> {
+        let gpa = PhysAddr::from(self.manifest_gpa);
+        let size =
+            usize::try_from(self.manifest_size).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !gpa.is_page_aligned() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok((gpa, size))
+    }
+
+    /// Returns the report buffer gpa and size
+    /// Checks if gpa is page aligned and valid.
+    /// Report buffer size can be greater than 4k, so it can cross page boundary.
+    pub fn get_report_gpa_and_size(&self) -> Result<(PhysAddr, usize), SvsmReqError> {
+        let gpa = PhysAddr::from(self.report_gpa);
+        let size =
+            usize::try_from(self.report_size).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !gpa.is_page_aligned() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok((gpa, size))
+    }
+}
+
+#[derive(Clone)]
+struct GuidTableEntry {
+    guid: uuid::Uuid,
+    data: Vec<u8>,
+}
+
+struct GuidTable {
+    entries: Vec<GuidTableEntry>,
+}
+
+impl GuidTable {
+    fn new() -> Self {
+        Self {
+            entries: Vec::new(),
+        }
+    }
+
+    // Only used when features are enabled.
+    #[allow(dead_code)]
+    fn push(&mut self, guid: uuid::Uuid, data: Vec<u8>) {
+        self.entries.push(GuidTableEntry { guid, data })
+    }
+
+    fn header_size(&self) -> usize {
+        // The services manifest header is 24 bytes, followed by 24 bytes for each entry.
+        (1 + self.entries.len()) * GUID_HEADER_ENTRY_SIZE
+    }
+
+    /// Returns how many bytes the wire ABI representation takes.
+    fn len(&self) -> usize {
+        let init = self.header_size();
+        self.entries
+            .iter()
+            .fold(init, |acc, entry| acc + entry.data.len())
+    }
+
+    // Writes the GuidTable contents at the end of `data` or returns an error.
+    fn to_vec(&self) -> Result<Vec<u8>, SvsmError> {
path: kernel/src/protocols/attest.rs
user: login: IT302
body: Question about byte ordering of GuidTable. I thought GuidTable is written out in network byte order (big endian) but  I notice we're using to_bytes_le() and to_le_bytes(). Am I missing something obvious here?
created_at: 2025-04-15T13:10:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,13 +5,287 @@
 
 //! Attest protocol implementation
 
+extern crate alloc;
+
+use crate::address::{Address, PhysAddr};
+use crate::error::{AttestError, SvsmError};
+use crate::greq::{pld_report::{USER_DATA_SIZE, SnpReportResponse}, services::get_regular_report};
+use crate::mm::guestmem::{read_bytes_from_guest, read_from_guest, copy_to_guest};
 use crate::protocols::{errors::SvsmReqError, RequestParams};
 
+use alloc::vec::Vec;
+use sha2::{Digest, Sha512};
+use uuid::{uuid, Uuid};
+use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
+
+const SERVICES_MANIFEST_GUID: Uuid = uuid!("63849ebb-3d92-4670-a1ff-58f9c94b87bb");
+const GUID_HEADER_ENTRY_SIZE: usize = 24;
+const SVSM_ATTEST_SERVICES: u32 = 0;
+
+// Attest services operation structure, as defined in Table 11 of Secure VM Service Module for
+// SEV-SNP Guests 58019 Rev, 1.00 July 2023
+#[repr(C, packed)]
+#[derive(FromBytes, IntoBytes, Immutable, KnownLayout, Clone, Copy, Debug)]
+pub struct AttestServicesOp {
+    report_gpa: u64,
+    report_size: u32,
+    reserved_1: [u8; 4],
+    nonce_gpa: u64,
+    nonce_size: u16,
+    reserved_2: [u8; 6],
+    manifest_gpa: u64,
+    manifest_size: u32,
+    reserved_3: [u8; 4],
+    certificate_gpa: u64,
+    certificate_size: u32,
+    reserved_4: [u8; 4],
+}
+
+impl AttestServicesOp {
+    /// Take a slice and return a reference for Self
+    pub fn try_from_as_ref(buffer: &[u8]) -> Result<&Self, SvsmReqError> {
+        let ops: &Self =
+            Self::ref_from_bytes(buffer).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !ops.is_reserved_clear() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok(ops)
+    }
+
+    /// Checks if reserved fields are all set to zero
+    pub fn is_reserved_clear(&self) -> bool {
+        self.reserved_1.iter().all(|&x| x == 0)
+            && self.reserved_2.iter().all(|&x| x == 0)
+            && self.reserved_3.iter().all(|&x| x == 0)
+            && self.reserved_4.iter().all(|&x| x == 0)
+    }
+
+    /// Returns the nonce
+    pub fn get_nonce(&self) -> Result<Vec<u8>, SvsmReqError> {
+        // Check that nonce is the right size.
+        // Nonce i.e. REPORT_DATA is 64 bytes per Table 22 of
+        // "SEV Secure Nested Paging Firmware ABI Specification, Revision 1.57"
+        let nonce_size = self.nonce_size.into();
+        if nonce_size != 64 {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        let gpa = PhysAddr::from(self.nonce_gpa);
+
+        read_bytes_from_guest(gpa, nonce_size).map_err(|_| SvsmReqError::invalid_parameter())
+    }
+
+    /// Returns the manifest buffer gpa and size
+    /// Checks if gpa is page aligned and valid.
+    /// Manifest buffer size can be greater than 4k, so it can cross page boundary.
+    pub fn get_manifest_gpa_and_size(&self) -> Result<(PhysAddr, usize), SvsmReqError> {
+        let gpa = PhysAddr::from(self.manifest_gpa);
+        let size =
+            usize::try_from(self.manifest_size).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !gpa.is_page_aligned() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok((gpa, size))
+    }
+
+    /// Returns the report buffer gpa and size
+    /// Checks if gpa is page aligned and valid.
+    /// Report buffer size can be greater than 4k, so it can cross page boundary.
+    pub fn get_report_gpa_and_size(&self) -> Result<(PhysAddr, usize), SvsmReqError> {
+        let gpa = PhysAddr::from(self.report_gpa);
+        let size =
+            usize::try_from(self.report_size).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !gpa.is_page_aligned() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok((gpa, size))
+    }
+}
+
+#[derive(Clone)]
+struct GuidTableEntry {
+    guid: uuid::Uuid,
+    data: Vec<u8>,
+}
+
+struct GuidTable {
+    entries: Vec<GuidTableEntry>,
+}
+
+impl GuidTable {
+    fn new() -> Self {
+        Self {
+            entries: Vec::new(),
+        }
+    }
+
+    // Only used when features are enabled.
+    #[allow(dead_code)]
+    fn push(&mut self, guid: uuid::Uuid, data: Vec<u8>) {
+        self.entries.push(GuidTableEntry { guid, data })
+    }
+
+    fn header_size(&self) -> usize {
+        // The services manifest header is 24 bytes, followed by 24 bytes for each entry.
+        (1 + self.entries.len()) * GUID_HEADER_ENTRY_SIZE
+    }
+
+    /// Returns how many bytes the wire ABI representation takes.
+    fn len(&self) -> usize {
+        let init = self.header_size();
+        self.entries
+            .iter()
+            .fold(init, |acc, entry| acc + entry.data.len())
+    }
+
+    // Writes the GuidTable contents at the end of `data` or returns an error.
+    fn to_vec(&self) -> Result<Vec<u8>, SvsmError> {
path: kernel/src/protocols/attest.rs
user: login: deeglaze
body: Thomas sent to the mailing list that SVSM is using the mixed-endian GUID encoding, and I had a couple responses that resulted in me ultimately agreeing.
created_at: 2025-04-15T14:23:22Z
author_association: COLLABORATOR
diff_hunk: @@ -5,13 +5,287 @@
 
 //! Attest protocol implementation
 
+extern crate alloc;
+
+use crate::address::{Address, PhysAddr};
+use crate::error::{AttestError, SvsmError};
+use crate::greq::{pld_report::{USER_DATA_SIZE, SnpReportResponse}, services::get_regular_report};
+use crate::mm::guestmem::{read_bytes_from_guest, read_from_guest, copy_to_guest};
 use crate::protocols::{errors::SvsmReqError, RequestParams};
 
+use alloc::vec::Vec;
+use sha2::{Digest, Sha512};
+use uuid::{uuid, Uuid};
+use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
+
+const SERVICES_MANIFEST_GUID: Uuid = uuid!("63849ebb-3d92-4670-a1ff-58f9c94b87bb");
+const GUID_HEADER_ENTRY_SIZE: usize = 24;
+const SVSM_ATTEST_SERVICES: u32 = 0;
+
+// Attest services operation structure, as defined in Table 11 of Secure VM Service Module for
+// SEV-SNP Guests 58019 Rev, 1.00 July 2023
+#[repr(C, packed)]
+#[derive(FromBytes, IntoBytes, Immutable, KnownLayout, Clone, Copy, Debug)]
+pub struct AttestServicesOp {
+    report_gpa: u64,
+    report_size: u32,
+    reserved_1: [u8; 4],
+    nonce_gpa: u64,
+    nonce_size: u16,
+    reserved_2: [u8; 6],
+    manifest_gpa: u64,
+    manifest_size: u32,
+    reserved_3: [u8; 4],
+    certificate_gpa: u64,
+    certificate_size: u32,
+    reserved_4: [u8; 4],
+}
+
+impl AttestServicesOp {
+    /// Take a slice and return a reference for Self
+    pub fn try_from_as_ref(buffer: &[u8]) -> Result<&Self, SvsmReqError> {
+        let ops: &Self =
+            Self::ref_from_bytes(buffer).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !ops.is_reserved_clear() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok(ops)
+    }
+
+    /// Checks if reserved fields are all set to zero
+    pub fn is_reserved_clear(&self) -> bool {
+        self.reserved_1.iter().all(|&x| x == 0)
+            && self.reserved_2.iter().all(|&x| x == 0)
+            && self.reserved_3.iter().all(|&x| x == 0)
+            && self.reserved_4.iter().all(|&x| x == 0)
+    }
+
+    /// Returns the nonce
+    pub fn get_nonce(&self) -> Result<Vec<u8>, SvsmReqError> {
+        // Check that nonce is the right size.
+        // Nonce i.e. REPORT_DATA is 64 bytes per Table 22 of
+        // "SEV Secure Nested Paging Firmware ABI Specification, Revision 1.57"
+        let nonce_size = self.nonce_size.into();
+        if nonce_size != 64 {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        let gpa = PhysAddr::from(self.nonce_gpa);
+
+        read_bytes_from_guest(gpa, nonce_size).map_err(|_| SvsmReqError::invalid_parameter())
+    }
+
+    /// Returns the manifest buffer gpa and size
+    /// Checks if gpa is page aligned and valid.
+    /// Manifest buffer size can be greater than 4k, so it can cross page boundary.
+    pub fn get_manifest_gpa_and_size(&self) -> Result<(PhysAddr, usize), SvsmReqError> {
+        let gpa = PhysAddr::from(self.manifest_gpa);
+        let size =
+            usize::try_from(self.manifest_size).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !gpa.is_page_aligned() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok((gpa, size))
+    }
+
+    /// Returns the report buffer gpa and size
+    /// Checks if gpa is page aligned and valid.
+    /// Report buffer size can be greater than 4k, so it can cross page boundary.
+    pub fn get_report_gpa_and_size(&self) -> Result<(PhysAddr, usize), SvsmReqError> {
+        let gpa = PhysAddr::from(self.report_gpa);
+        let size =
+            usize::try_from(self.report_size).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !gpa.is_page_aligned() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok((gpa, size))
+    }
+}
+
+#[derive(Clone)]
+struct GuidTableEntry {
+    guid: uuid::Uuid,
+    data: Vec<u8>,
+}
+
+struct GuidTable {
+    entries: Vec<GuidTableEntry>,
+}
+
+impl GuidTable {
+    fn new() -> Self {
+        Self {
+            entries: Vec::new(),
+        }
+    }
+
+    // Only used when features are enabled.
+    #[allow(dead_code)]
+    fn push(&mut self, guid: uuid::Uuid, data: Vec<u8>) {
+        self.entries.push(GuidTableEntry { guid, data })
+    }
+
+    fn header_size(&self) -> usize {
+        // The services manifest header is 24 bytes, followed by 24 bytes for each entry.
+        (1 + self.entries.len()) * GUID_HEADER_ENTRY_SIZE
+    }
+
+    /// Returns how many bytes the wire ABI representation takes.
+    fn len(&self) -> usize {
+        let init = self.header_size();
+        self.entries
+            .iter()
+            .fold(init, |acc, entry| acc + entry.data.len())
+    }
+
+    // Writes the GuidTable contents at the end of `data` or returns an error.
+    fn to_vec(&self) -> Result<Vec<u8>, SvsmError> {
path: kernel/src/protocols/attest.rs
user: login: IT302
body: So the comment on line 261 about big endian ordering no longer applies?
created_at: 2025-04-15T14:56:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,13 +5,287 @@
 
 //! Attest protocol implementation
 
+extern crate alloc;
+
+use crate::address::{Address, PhysAddr};
+use crate::error::{AttestError, SvsmError};
+use crate::greq::{pld_report::{USER_DATA_SIZE, SnpReportResponse}, services::get_regular_report};
+use crate::mm::guestmem::{read_bytes_from_guest, read_from_guest, copy_to_guest};
 use crate::protocols::{errors::SvsmReqError, RequestParams};
 
+use alloc::vec::Vec;
+use sha2::{Digest, Sha512};
+use uuid::{uuid, Uuid};
+use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
+
+const SERVICES_MANIFEST_GUID: Uuid = uuid!("63849ebb-3d92-4670-a1ff-58f9c94b87bb");
+const GUID_HEADER_ENTRY_SIZE: usize = 24;
+const SVSM_ATTEST_SERVICES: u32 = 0;
+
+// Attest services operation structure, as defined in Table 11 of Secure VM Service Module for
+// SEV-SNP Guests 58019 Rev, 1.00 July 2023
+#[repr(C, packed)]
+#[derive(FromBytes, IntoBytes, Immutable, KnownLayout, Clone, Copy, Debug)]
+pub struct AttestServicesOp {
+    report_gpa: u64,
+    report_size: u32,
+    reserved_1: [u8; 4],
+    nonce_gpa: u64,
+    nonce_size: u16,
+    reserved_2: [u8; 6],
+    manifest_gpa: u64,
+    manifest_size: u32,
+    reserved_3: [u8; 4],
+    certificate_gpa: u64,
+    certificate_size: u32,
+    reserved_4: [u8; 4],
+}
+
+impl AttestServicesOp {
+    /// Take a slice and return a reference for Self
+    pub fn try_from_as_ref(buffer: &[u8]) -> Result<&Self, SvsmReqError> {
+        let ops: &Self =
+            Self::ref_from_bytes(buffer).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !ops.is_reserved_clear() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok(ops)
+    }
+
+    /// Checks if reserved fields are all set to zero
+    pub fn is_reserved_clear(&self) -> bool {
+        self.reserved_1.iter().all(|&x| x == 0)
+            && self.reserved_2.iter().all(|&x| x == 0)
+            && self.reserved_3.iter().all(|&x| x == 0)
+            && self.reserved_4.iter().all(|&x| x == 0)
+    }
+
+    /// Returns the nonce
+    pub fn get_nonce(&self) -> Result<Vec<u8>, SvsmReqError> {
+        // Check that nonce is the right size.
+        // Nonce i.e. REPORT_DATA is 64 bytes per Table 22 of
+        // "SEV Secure Nested Paging Firmware ABI Specification, Revision 1.57"
+        let nonce_size = self.nonce_size.into();
+        if nonce_size != 64 {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        let gpa = PhysAddr::from(self.nonce_gpa);
+
+        read_bytes_from_guest(gpa, nonce_size).map_err(|_| SvsmReqError::invalid_parameter())
+    }
+
+    /// Returns the manifest buffer gpa and size
+    /// Checks if gpa is page aligned and valid.
+    /// Manifest buffer size can be greater than 4k, so it can cross page boundary.
+    pub fn get_manifest_gpa_and_size(&self) -> Result<(PhysAddr, usize), SvsmReqError> {
+        let gpa = PhysAddr::from(self.manifest_gpa);
+        let size =
+            usize::try_from(self.manifest_size).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !gpa.is_page_aligned() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok((gpa, size))
+    }
+
+    /// Returns the report buffer gpa and size
+    /// Checks if gpa is page aligned and valid.
+    /// Report buffer size can be greater than 4k, so it can cross page boundary.
+    pub fn get_report_gpa_and_size(&self) -> Result<(PhysAddr, usize), SvsmReqError> {
+        let gpa = PhysAddr::from(self.report_gpa);
+        let size =
+            usize::try_from(self.report_size).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !gpa.is_page_aligned() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok((gpa, size))
+    }
+}
+
+#[derive(Clone)]
+struct GuidTableEntry {
+    guid: uuid::Uuid,
+    data: Vec<u8>,
+}
+
+struct GuidTable {
+    entries: Vec<GuidTableEntry>,
+}
+
+impl GuidTable {
+    fn new() -> Self {
+        Self {
+            entries: Vec::new(),
+        }
+    }
+
+    // Only used when features are enabled.
+    #[allow(dead_code)]
+    fn push(&mut self, guid: uuid::Uuid, data: Vec<u8>) {
+        self.entries.push(GuidTableEntry { guid, data })
+    }
+
+    fn header_size(&self) -> usize {
+        // The services manifest header is 24 bytes, followed by 24 bytes for each entry.
+        (1 + self.entries.len()) * GUID_HEADER_ENTRY_SIZE
+    }
+
+    /// Returns how many bytes the wire ABI representation takes.
+    fn len(&self) -> usize {
+        let init = self.header_size();
+        self.entries
+            .iter()
+            .fold(init, |acc, entry| acc + entry.data.len())
+    }
+
+    // Writes the GuidTable contents at the end of `data` or returns an error.
+    fn to_vec(&self) -> Result<Vec<u8>, SvsmError> {
path: kernel/src/protocols/attest.rs
user: login: deeglaze
body: Ah. Updated.
created_at: 2025-04-15T15:07:23Z
author_association: COLLABORATOR
diff_hunk: @@ -166,6 +173,69 @@ impl GuidTable {
     }
 }
 
+// Attest Single Service Operation structure, as defined in
+// Table 13 of Secure VM Service Module for SEV-SNP Guests
+// 58019 Rev. 1.00 July 2023
+#[repr(C, packed)]
+#[derive(FromBytes, IntoBytes, Immutable, KnownLayout, Clone, Copy, Debug)]
+pub struct AttestSingleServiceOp {
+    op: AttestServicesOp,
+    guid: [u8; 16],
+    manifest_ver: u32,
+    reserved_5: [u8; 4],
+}
+
+impl AttestSingleServiceOp {
+    /// Take a slice and return a reference for Self
+    pub fn try_from_as_ref(buffer: &[u8]) -> Result<&Self, SvsmReqError> {
+        let ops: &Self =
+            Self::ref_from_bytes(buffer).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !ops.is_reserved_clear() || !ops.is_manifest_version_valid() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok(ops)
+    }
+
+    /// Checks if reserved fields are all set to zero
+    pub fn is_reserved_clear(&self) -> bool {
+        self.op.is_reserved_clear() && self.reserved_5.iter().all(|&x| x == 0)
+    }
+
+    /// Returns the nonce
+    pub fn get_nonce(&self) -> Result<Vec<u8>, SvsmReqError> {
path: kernel/src/protocols/attest.rs
user: login: deeglaze
body: Done.
created_at: 2025-04-15T15:07:27Z
author_association: COLLABORATOR
diff_hunk: @@ -164,6 +173,13 @@ impl TcgTpmSimulatorInterface for TcgTpm {
 }
 
 impl VtpmInterface for TcgTpm {
+    fn get_ekpub(&mut self) -> Result<Vec<u8>, SvsmReqError> {
+        if self.ekpub.is_none() {
path: kernel/src/vtpm/tcgtpm/mod.rs
user: login: deeglaze
body: @AdamCDunlap notes that this is reimplementing `get_or_insert_with`.
created_at: 2025-04-16T21:33:59Z
author_association: COLLABORATOR
diff_hunk: @@ -428,6 +431,74 @@ pub fn copy_to_user(src: &[u8], dst: VirtAddr) -> Result<(), SvsmError> {
     }
 }
 
+fn checked_guest_region(start: PhysAddr, size: usize) -> Result<MemoryRegion<PhysAddr>, SvsmError> {
+    let region = MemoryRegion::checked_new(start, size).ok_or(SvsmError::Mem)?;
+    if !valid_phys_region(&region) {
+        return Err(SvsmError::InvalidAddress);
+    }
+    Ok(region)
+}
+
+/// Reads a slice of bytes from a physical address region outside of SVSM use.
+///
+/// # Arguments
+///
+/// * `src`: The physical address designating the start of continguous physical
+///   memory to read from.
+/// * `dst`: The pointer to the beginning of the guest memory range to populate.
+/// * `size`: The number of bytes to write from src to dst.
+///
+/// # Returns
+///
+/// This function returns a `Result` that indicates the success or failure of the operation.
+/// If the physical address region cannot be mapped, it returns `Err(SvsmError::Mem)`.
+/// If the physical address region cannot be read, it returns `Err(SvsmError::Fault)`.
+pub fn copy_from_guest(src: PhysAddr, dst: *mut u8, size: usize) -> Result<(), SvsmError> {
+    let size = dst.len();
+    let region = checked_guest_region(src, dst.len())?;
+    let start = region.start().page_align();
+    let offset = region.start().page_offset();
+    let end = region.end().page_align_up();
+    let destination = dst as usize;
+
+    // SAFETY: Only reads data from a region outside the SVSM. The Uninit->Init transmute
+    // is safe since the entirety of the slice is populated.
+    unsafe {
+        let guard = PerCPUPageMappingGuard::create(start, end, 0)?;
+        let source = guard.virt_addr().bits() + offset;
+        copy_bytes(source, destination, size)
+    }
+}
+
+/// Writes a slice of bytes to a physical address region outside of SVSM use.
+///
+/// # Arguments
+///
+/// * `src`: The byte slice to write to guest memory.
+/// * `dst`: The physical address designating the start of continguous physical
+///   memory to write to.
+///
+/// # Returns
+///
+/// This function returns a `Result` that indicates the success or failure of the operation.
+/// If the physical address region cannot be mapped, it returns `Err(SvsmError::Mem)`.
+/// If the physical address region cannot be read, it returns `Err(SvsmError::Fault)`.
+pub fn copy_to_guest(src: &[u8], dst: PhysAddr) -> Result<(), SvsmError> {
+    let size = src.len();
+    let region = checked_guest_region(dst, src.len())?;
+    let start = region.start().page_align();
+    let offset = region.start().page_offset();
+    let end = region.end().page_align_up();
+    let source = src.as_ptr() as usize;
+
+    // SAFETY: Only reads data from a region outside the SVSM.
+    unsafe {
+        let guard = PerCPUPageMappingGuard::create(start, end, 0)?;
+        let destination = guard.virt_addr().bits() + offset;
+        copy_bytes(source, destination, size)
path: kernel/src/mm/guestmem.rs
user: login: joergroedel
body: The copying needs SVSM-specific functions which have the annotations to catch any exception that might happen during the copy.
created_at: 2025-04-17T08:02:59Z
author_association: MEMBER
diff_hunk: @@ -428,6 +431,74 @@ pub fn copy_to_user(src: &[u8], dst: VirtAddr) -> Result<(), SvsmError> {
     }
 }
 
+fn checked_guest_region(start: PhysAddr, size: usize) -> Result<MemoryRegion<PhysAddr>, SvsmError> {
+    let region = MemoryRegion::checked_new(start, size).ok_or(SvsmError::Mem)?;
+    if !valid_phys_region(&region) {
+        return Err(SvsmError::InvalidAddress);
+    }
+    Ok(region)
+}
+
+/// Reads a slice of bytes from a physical address region outside of SVSM use.
+///
+/// # Arguments
+///
+/// * `src`: The physical address designating the start of continguous physical
+///   memory to read from.
+/// * `dst`: The pointer to the beginning of the guest memory range to populate.
+/// * `size`: The number of bytes to write from src to dst.
+///
+/// # Returns
+///
+/// This function returns a `Result` that indicates the success or failure of the operation.
+/// If the physical address region cannot be mapped, it returns `Err(SvsmError::Mem)`.
+/// If the physical address region cannot be read, it returns `Err(SvsmError::Fault)`.
+pub fn copy_from_guest(src: PhysAddr, dst: *mut u8, size: usize) -> Result<(), SvsmError> {
+    let size = dst.len();
path: kernel/src/mm/guestmem.rs
user: login: deeglaze
body: Fixed in guest memory PR.
created_at: 2025-04-17T14:55:27Z
author_association: COLLABORATOR
diff_hunk: @@ -164,6 +173,13 @@ impl TcgTpmSimulatorInterface for TcgTpm {
 }
 
 impl VtpmInterface for TcgTpm {
+    fn get_ekpub(&mut self) -> Result<Vec<u8>, SvsmReqError> {
+        if self.ekpub.is_none() {
path: kernel/src/vtpm/tcgtpm/mod.rs
user: login: deeglaze
body: Closing since the create_ek error result cannot be bubbled out. 
created_at: 2025-04-17T16:31:02Z
author_association: COLLABORATOR
diff_hunk: @@ -428,6 +431,74 @@ pub fn copy_to_user(src: &[u8], dst: VirtAddr) -> Result<(), SvsmError> {
     }
 }
 
+fn checked_guest_region(start: PhysAddr, size: usize) -> Result<MemoryRegion<PhysAddr>, SvsmError> {
+    let region = MemoryRegion::checked_new(start, size).ok_or(SvsmError::Mem)?;
+    if !valid_phys_region(&region) {
+        return Err(SvsmError::InvalidAddress);
+    }
+    Ok(region)
+}
+
+/// Reads a slice of bytes from a physical address region outside of SVSM use.
+///
+/// # Arguments
+///
+/// * `src`: The physical address designating the start of continguous physical
+///   memory to read from.
+/// * `dst`: The pointer to the beginning of the guest memory range to populate.
+/// * `size`: The number of bytes to write from src to dst.
+///
+/// # Returns
+///
+/// This function returns a `Result` that indicates the success or failure of the operation.
+/// If the physical address region cannot be mapped, it returns `Err(SvsmError::Mem)`.
+/// If the physical address region cannot be read, it returns `Err(SvsmError::Fault)`.
+pub fn copy_from_guest(src: PhysAddr, dst: *mut u8, size: usize) -> Result<(), SvsmError> {
+    let size = dst.len();
+    let region = checked_guest_region(src, dst.len())?;
+    let start = region.start().page_align();
+    let offset = region.start().page_offset();
+    let end = region.end().page_align_up();
+    let destination = dst as usize;
+
+    // SAFETY: Only reads data from a region outside the SVSM. The Uninit->Init transmute
+    // is safe since the entirety of the slice is populated.
+    unsafe {
+        let guard = PerCPUPageMappingGuard::create(start, end, 0)?;
+        let source = guard.virt_addr().bits() + offset;
+        copy_bytes(source, destination, size)
+    }
+}
+
+/// Writes a slice of bytes to a physical address region outside of SVSM use.
+///
+/// # Arguments
+///
+/// * `src`: The byte slice to write to guest memory.
+/// * `dst`: The physical address designating the start of continguous physical
+///   memory to write to.
+///
+/// # Returns
+///
+/// This function returns a `Result` that indicates the success or failure of the operation.
+/// If the physical address region cannot be mapped, it returns `Err(SvsmError::Mem)`.
+/// If the physical address region cannot be read, it returns `Err(SvsmError::Fault)`.
+pub fn copy_to_guest(src: &[u8], dst: PhysAddr) -> Result<(), SvsmError> {
+    let size = src.len();
+    let region = checked_guest_region(dst, src.len())?;
+    let start = region.start().page_align();
+    let offset = region.start().page_offset();
+    let end = region.end().page_align_up();
+    let source = src.as_ptr() as usize;
+
+    // SAFETY: Only reads data from a region outside the SVSM.
+    unsafe {
+        let guard = PerCPUPageMappingGuard::create(start, end, 0)?;
+        let destination = guard.virt_addr().bits() + offset;
+        copy_bytes(source, destination, size)
path: kernel/src/mm/guestmem.rs
user: login: deeglaze
body: Yes. I've moved this aspect to a different PR.
created_at: 2025-04-17T16:37:55Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,405 @@
+// SPDX-License-Identifier: MIT
+//
+// Copyright (c) 2025  Hewlett Packard Enterprise Development LP
+// Copyright (c) 2025 Coconut-SVSM Authors
+//
+
+//! Attest protocol implementation
+
+extern crate alloc;
+
+use crate::address::{Address, PhysAddr};
+use crate::crypto::digest::{Algorithm, Sha512};
+use crate::error::{AttestError, SvsmError};
+use crate::greq::{
+    pld_report::{SnpReportRequest, SnpReportResponse},
+    services::get_regular_report,
+};
+use crate::mm::guestmem::{copy_slice_to_guest, read_bytes_from_guest, read_from_guest};
+use crate::protocols::{errors::SvsmReqError, RequestParams};
+use crate::utils::MemoryRegion;
+#[cfg(all(feature = "vtpm", not(test)))]
+use crate::vtpm::vtpm_get_manifest;
+
+use alloc::{vec, vec::Vec};
+use uuid::{uuid, Uuid};
+use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
+
+pub const ATTEST_PROTOCOL_VERSION_MIN: u32 = 1;
+pub const ATTEST_PROTOCOL_VERSION_MAX: u32 = 1;
+
+const SERVICES_MANIFEST_GUID: Uuid = uuid!("63849ebb-3d92-4670-a1ff-58f9c94b87bb");
+const GUID_HEADER_ENTRY_SIZE: usize = 24;
+const SVSM_ATTEST_SERVICES: u32 = 0;
+const SVSM_ATTEST_SINGLE_SERVICE: u32 = 1;
+
+#[cfg(all(feature = "vtpm", not(test)))]
+const SVSM_ATTEST_VTPM_GUID: Uuid = uuid!("c476f1eb-0123-45a5-9641-b4e7dde5bfe3");
+
+// Attest services operation structure, as defined in Table 11 of Secure VM Service Module for
+// SEV-SNP Guests 58019 Rev, 1.00 July 2023
+#[repr(C, packed)]
+#[derive(FromBytes, IntoBytes, Immutable, KnownLayout, Clone, Copy, Debug)]
+pub struct AttestServicesOp {
+    report_gpa: u64,
+    report_size: u32,
+    reserved_1: [u8; 4],
+    nonce_gpa: u64,
+    nonce_size: u16,
+    reserved_2: [u8; 6],
+    manifest_gpa: u64,
+    manifest_size: u32,
+    reserved_3: [u8; 4],
+    certificate_gpa: u64,
+    certificate_size: u32,
+    reserved_4: [u8; 4],
+}
+
+impl AttestServicesOp {
+    /// Take a slice and return a reference for Self
+    pub fn try_from_as_ref(buffer: &[u8]) -> Result<&Self, SvsmReqError> {
+        let ops: &Self =
+            Self::ref_from_bytes(buffer).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !ops.is_reserved_clear() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok(ops)
+    }
+
+    /// Checks if reserved fields are all set to zero
+    pub fn is_reserved_clear(&self) -> bool {
+        self.reserved_1.iter().all(|&x| x == 0)
+            && self.reserved_2.iter().all(|&x| x == 0)
+            && self.reserved_3.iter().all(|&x| x == 0)
+            && self.reserved_4.iter().all(|&x| x == 0)
+    }
+
+    /// Returns the nonce
+    pub fn get_nonce(&self) -> Result<Vec<u8>, SvsmReqError> {
+        // Check that nonce is the right size and doesn't cross a page boundary.
+        // Nonce i.e. REPORT_DATA is 64 bytes per Table 22 of
+        // "SEV Secure Nested Paging Firmware ABI Specification, Revision 1.57"
+        let gpa = PhysAddr::from(self.nonce_gpa);
+        let nonce_size = self.nonce_size.into();
+        if nonce_size != 64 || gpa.crosses_page(nonce_size) {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        read_bytes_from_guest(gpa, nonce_size).map_err(|_| SvsmReqError::invalid_parameter())
+    }
+
+    /// Returns the manifest buffer gpa and size
+    /// Checks if gpa is page aligned and valid.
+    /// Manifest buffer size can be greater than 4k, so it can cross page boundary.
+    pub fn get_manifest_region(&self) -> Result<MemoryRegion<PhysAddr>, SvsmReqError> {
+        let gpa = PhysAddr::from(self.manifest_gpa);
+        let size =
+            usize::try_from(self.manifest_size).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !gpa.is_page_aligned() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok(MemoryRegion::new(gpa, size))
+    }
+
+    /// Returns the report buffer gpa and size
+    /// Checks if gpa is page aligned and valid.
+    /// Report buffer size can be greater than 4k, so it can cross page boundary.
+    pub fn get_report_region(&self) -> Result<MemoryRegion<PhysAddr>, SvsmReqError> {
+        let gpa = PhysAddr::from(self.report_gpa);
+        let size =
+            usize::try_from(self.report_size).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !gpa.is_page_aligned() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok(MemoryRegion::new(gpa, size))
+    }
+}
+
+#[derive(Clone)]
+struct GuidTableEntry {
+    guid: uuid::Uuid,
+    data: Vec<u8>,
+}
+
+struct GuidTable {
+    entries: Vec<GuidTableEntry>,
+}
+
+impl GuidTable {
+    fn new() -> Self {
+        Self {
+            entries: Vec::new(),
+        }
+    }
+
+    // Only used when features are enabled.
+    #[allow(dead_code)]
+    fn push(&mut self, guid: uuid::Uuid, data: Vec<u8>) {
+        self.entries.push(GuidTableEntry { guid, data })
+    }
+
+    fn header_size(&self) -> usize {
+        // The services manifest header is 24 bytes, followed by 24 bytes for each entry.
+        (1 + self.entries.len()) * GUID_HEADER_ENTRY_SIZE
+    }
+
+    /// Returns how many bytes the wire ABI representation takes.
+    fn len(&self) -> usize {
+        let init = self.header_size();
+        self.entries
+            .iter()
+            .fold(init, |acc, entry| acc + entry.data.len())
+    }
+
+    // Writes the GuidTable contents at the end of `data` or returns an error.
+    fn to_vec(&self) -> Result<Vec<u8>, SvsmError> {
+        let length = self.len();
+        let mut data: Vec<u8> = Vec::with_capacity(length);
+        data.extend_from_slice(&SERVICES_MANIFEST_GUID.to_bytes_le());
+        data.extend_from_slice(&(length as u32).to_le_bytes());
+        data.extend_from_slice(&(self.entries.len() as u32).to_le_bytes());
+
+        let mut data_cursor = self.header_size() as u32;
+        self.entries.iter().for_each(|entry| {
+            data.extend_from_slice(&entry.guid.to_bytes_le());
+            data.extend_from_slice(&data_cursor.to_le_bytes());
+            data.extend_from_slice(&(entry.data.len() as u32).to_le_bytes());
+            data_cursor += entry.data.len() as u32;
+        });
+        self.entries
+            .iter()
+            .for_each(|entry| data.extend_from_slice(entry.data.as_slice()));
+        Ok(data)
+    }
+}
+
+// Attest Single Service Operation structure, as defined in
+// Table 13 of Secure VM Service Module for SEV-SNP Guests
+// 58019 Rev. 1.00 July 2023
+#[repr(C, packed)]
+#[derive(FromBytes, IntoBytes, Immutable, KnownLayout, Clone, Copy, Debug)]
+pub struct AttestSingleServiceOp {
+    op: AttestServicesOp,
+    guid: [u8; 16],
+    manifest_ver: u32,
+    reserved_5: [u8; 4],
+}
+
+impl AttestSingleServiceOp {
+    /// Take a slice and return a reference for Self
+    pub fn try_from_as_ref(buffer: &[u8]) -> Result<&Self, SvsmReqError> {
+        let ops: &Self =
+            Self::ref_from_bytes(buffer).map_err(|_| SvsmReqError::invalid_parameter())?;
+        if !ops.is_reserved_clear() || !ops.is_manifest_version_valid() {
+            return Err(SvsmReqError::invalid_parameter());
+        }
+
+        Ok(ops)
+    }
+
+    /// Checks if reserved fields are all set to zero
+    pub fn is_reserved_clear(&self) -> bool {
+        self.op.is_reserved_clear() && self.reserved_5.iter().all(|&x| x == 0)
+    }
+
+    /// Returns the nonce
+    pub fn get_nonce(&self) -> Result<Vec<u8>, SvsmReqError> {
+        self.op.get_nonce()
+    }
+
+    /// Returns the manifest buffer gpa and size
+    /// Checks if gpa is page aligned and valid.
+    /// Manifest buffer size can be greater than 4k, so it can cross page boundary.
+    pub fn get_manifest_region(&self) -> Result<MemoryRegion<PhysAddr>, SvsmReqError> {
+        self.op.get_manifest_region()
+    }
+
+    /// Returns the report buffer gpa and size
+    /// Checks if gpa is page aligned and valid.
+    /// Report buffer size can be greater than 4k, so it can cross page boundary.
+    pub fn get_report_region(&self) -> Result<MemoryRegion<PhysAddr>, SvsmReqError> {
+        self.op.get_report_region()
+    }
+
+    pub fn get_manifest_version(&self) -> u32 {
+        self.manifest_ver
+    }
+
+    fn is_manifest_version_valid(&self) -> bool {
+        // Currently only manifest version 0 is supported
+        self.manifest_ver == 0
+    }
+
+    /// Returns the guid
+    pub fn get_guid(&self) -> uuid::Uuid {
+        Uuid::from_bytes_le(self.guid)
+    }
+}
+
+fn get_attestation_report(nonce: &[u8]) -> Result<Vec<u8>, SvsmReqError> {
+    let mut buffer = vec![0; size_of::<SnpReportResponse>()];
path: kernel/src/protocols/attest.rs
user: login: deeglaze
body: Should this be SnpReportResponse::new_box_zeroed() and the function returns the box with a public report field? This is pretty big for the stack, as mentioned in the call.
created_at: 2025-04-17T22:23:05Z
author_association: COLLABORATOR

PR comments:
user: login: deeglaze
created_at: 2025-04-02T05:50:10Z
body: In splitting this into several patches, I appear to have broken something. 

Here is my testing script

```
# Setup
echo -n 53534D32ebf176c42301a5459641b4e7dde5bfe300000000 | xxd -p -r > prefix.bin

head -c 64 /dev/zero > nonce.bin

# discriminator
echo -n 0100000000000000 > selector
# The size is automatically added. This is a TPMT_PUBLIC, not a TPM2B_PUBLIC.
# type_ecdsa
echo -n 0023 >> selector
# namealg_sha256
echo -n 000b >> selector
# object_attributes
echo -n 00050072 >> selector
# empty_auth
echo -n 0000 >> selector
# symmetric_null
echo -n 0010 >> selector
# scheme_ecdsa
echo -n 0018 >> selector
# details_sha256
echo -n 000b >> selector
# NIST_P256
echo -n 0003 >> selector
# null_kdf
echo -n 0010 >> selector
# Xcoord length
echo -n 0020 >> selector
head -c 32 /dev/urandom | xxd -p -c 32 >> selector
# Ycoord length
echo -n 0020 >> selector
head -c 32 /dev/urandom | xxd -p -c 32 >> selector

cat selector | xxd -p -r > selector.bin

# Report creation
R=/sys/kernel/config/tsm/report/r0

mkdir $R

cat nonce.bin > $R/inblob
echo svsm > $R/service_provider
echo c476f1eb-0123-45a5-9641-b4e7dde5bfe3 > $R/service_guid
cat $R/privlevel_floor > $R/privlevel
cat selector.bin > $R/manifest_selector
cat $R/outblob > report.bin

cat $R/manifestblob > manifest.bin

# Check report
wantrdata=$(sha512sum <(cat nonce.bin prefix.bin selector.bin manifest.bin) | cut -d' ' -f1)
# the report data is at offset 80 and is 64 bytes long
gotrdata=$(head -c 144 report.bin | tail -c 64 | xxd -p -c 64)
echo ${wantrdata}
echo ${gotrdata}

# Error testing
R2=/sys/kernel/config/tsm/report/r2

mkdir $R2
ls $R2
cat nonce.bin > $R2/inblob
echo svsm > $R2/service_provider
echo c476f1eb-0123-45a5-9641-b4e7dde5bfe3 > $R2/service_guid
cat $R2/privlevel_floor > $R2/privlevel
echo selector-bad > $R2/manifest_selector
cat $R2/outblob > /dev/null
cat $R2/selector_error


cat nonce.bin > $R1/inblob
echo svsm > $R1/service_provider
echo c476f1eb-0123-45a5-9641-b4e7dde5bfe3 > $R1/service_guid
cat $R1/privlevel_floor > $R1/privlevel
cat $R1/outblob > report.bin
cat $R1/manifestblob > manifest.bin


# L-1 testing
R1=/sys/kernel/config/tsm/report/r1
mkdir $R1

cat nonce.bin > $R1/inblob
echo svsm > $R1/service_provider
echo c476f1eb-0123-45a5-9641-b4e7dde5bfe3 > $R1/service_guid
cat $R1/privlevel_floor > $R1/privlevel
cat $R1/outblob > report.bin
cat $R1/manifestblob > manifest.bin
```

This is still experiencing issues with the service_error attribute patches in https://github.com/deeglaze/amdese-linux/tree/atssex
author_association: COLLABORATOR
user: login: deeglaze
created_at: 2025-04-09T16:25:53Z
body: Note to self to remove the extension from this PR to expedite its merge.
author_association: COLLABORATOR
user: login: deeglaze
created_at: 2025-04-16T18:15:35Z
body: I've added a simple digest interface to address feedback during the developers' meeting.
author_association: COLLABORATOR