From 4d8d22e23a8e334962600f6b38f61a761611e350 Mon Sep 17 00:00:00 2001 From: Magnus Kulke Date: Tue, 28 Nov 2023 19:55:44 +0100 Subject: [PATCH] Consolidate snp and tdx crates (#39) Since Azure TDX CVMs are in public preview now, this PR consolidates both crates. There is shared logic, which has been organized into a `az-cvm-vtpm` crate that is imported by both `az-snp-vtpm` and `az-tdx-vtpm`. This constitutes a breaking change in the API, hence the version bump to 0.4 for all crates. Signed-off-by: Magnus Kulke --- az-cvm-vtpm/Cargo.toml | 47 ++++- az-cvm-vtpm/README.md | 2 +- az-cvm-vtpm/az-snp-vtpm/Cargo.toml | 26 +-- az-cvm-vtpm/az-snp-vtpm/example/Cargo.toml | 3 +- az-cvm-vtpm/az-snp-vtpm/example/src/main.rs | 29 +-- az-cvm-vtpm/az-snp-vtpm/src/certs.rs | 2 +- az-cvm-vtpm/az-snp-vtpm/src/hcl.rs | 199 ------------------ az-cvm-vtpm/az-snp-vtpm/src/lib.rs | 35 +-- az-cvm-vtpm/az-snp-vtpm/src/main.rs | 13 +- az-cvm-vtpm/az-snp-vtpm/src/report.rs | 44 +++- az-cvm-vtpm/az-tdx-vtpm/Cargo.toml | 28 ++- az-cvm-vtpm/az-tdx-vtpm/README.md | 5 +- az-cvm-vtpm/az-tdx-vtpm/src/imds.rs | 12 +- az-cvm-vtpm/az-tdx-vtpm/src/lib.rs | 67 ++++-- az-cvm-vtpm/az-tdx-vtpm/src/main.rs | 18 +- az-cvm-vtpm/az-tdx-vtpm/src/report.rs | 28 +++ az-cvm-vtpm/az-tdx-vtpm/src/tdx.rs | 77 ------- az-cvm-vtpm/az-tdx-vtpm/src/verify.rs | 42 ---- .../src/hcl.rs => src/hcl/mod.rs} | 103 +++++++-- az-cvm-vtpm/src/lib.rs | 6 + az-cvm-vtpm/src/tdx/mod.rs | 69 ++++++ .../src/vtpm.rs => src/vtpm/mod.rs} | 99 ++------- az-cvm-vtpm/src/vtpm/verify.rs | 84 ++++++++ az-cvm-vtpm/{az-snp-vtpm => }/test/akpub.pem | 0 az-cvm-vtpm/{az-snp-vtpm => }/test/certs.pem | 0 .../hcl-report-snp.bin} | Bin .../hcl-report-tdx.bin} | Bin az-cvm-vtpm/{az-snp-vtpm => }/test/quote_msg | Bin az-cvm-vtpm/{az-snp-vtpm => }/test/quote_sig | Bin .../{az-snp-vtpm => }/test/var-data.bin | 0 30 files changed, 516 insertions(+), 522 deletions(-) delete mode 100644 az-cvm-vtpm/az-snp-vtpm/src/hcl.rs create mode 100644 az-cvm-vtpm/az-tdx-vtpm/src/report.rs delete mode 100644 az-cvm-vtpm/az-tdx-vtpm/src/tdx.rs delete mode 100644 az-cvm-vtpm/az-tdx-vtpm/src/verify.rs rename az-cvm-vtpm/{az-tdx-vtpm/src/hcl.rs => src/hcl/mod.rs} (58%) create mode 100644 az-cvm-vtpm/src/lib.rs create mode 100644 az-cvm-vtpm/src/tdx/mod.rs rename az-cvm-vtpm/{az-snp-vtpm/src/vtpm.rs => src/vtpm/mod.rs} (62%) create mode 100644 az-cvm-vtpm/src/vtpm/verify.rs rename az-cvm-vtpm/{az-snp-vtpm => }/test/akpub.pem (100%) rename az-cvm-vtpm/{az-snp-vtpm => }/test/certs.pem (100%) rename az-cvm-vtpm/{az-snp-vtpm/test/hcl-report.bin => test/hcl-report-snp.bin} (100%) rename az-cvm-vtpm/{az-tdx-vtpm/test/hcl_report.bin => test/hcl-report-tdx.bin} (100%) rename az-cvm-vtpm/{az-snp-vtpm => }/test/quote_msg (100%) rename az-cvm-vtpm/{az-snp-vtpm => }/test/quote_sig (100%) rename az-cvm-vtpm/{az-snp-vtpm => }/test/var-data.bin (100%) diff --git a/az-cvm-vtpm/Cargo.toml b/az-cvm-vtpm/Cargo.toml index 51bbed9..020901a 100644 --- a/az-cvm-vtpm/Cargo.toml +++ b/az-cvm-vtpm/Cargo.toml @@ -1,7 +1,50 @@ +[package] +name = "az-cvm-vtpm" +version = "0.4.0" +edition = "2021" +repository = "https://github.com/kinvolk/azure-cvm-tooling/" +license = "MIT" +keywords = ["azure", "tpm", "sev-snp", "tdx"] +categories = ["cryptography", "virtualization"] +description = "Package with shared code for Azure Confidential VMs" + [workspace] members = [ "az-snp-vtpm", "az-tdx-vtpm", - "az-snp-vtpm/example", + "az-snp-vtpm/example", ] -resolver = "2" + +[lib] +path = "src/lib.rs" + +[dependencies] +bincode.workspace = true +jsonwebkey = { version = "0.3.5", features = ["pkcs-convert"] } +memoffset = "0.9.0" +openssl = { workspace = true, optional = true } +rsa = { version = "0.8.2", features = ["pkcs5", "sha2"] } +serde.workspace = true +serde_json.workspace = true +serde-big-array = "0.5.1" +sev.workspace = true +sha2 = "0.10.8" +thiserror.workspace = true +tss-esapi = "7.4" +zerocopy.workspace = true + +[features] +default = ["attester", "verifier"] +attester = [] +verifier = ["openssl", "sev/openssl"] + +[workspace.dependencies] +bincode = "1.3.1" +clap = { version = "4", features = ["derive"] } +openssl = "0.10" +serde = { version = "1.0.189", features = ["derive"] } +serde_json = "1.0.107" +thiserror = "1.0.38" +sev = "1.2.0" +ureq = { version = "2.6.2", default-features = false, features = ["json"] } +zerocopy = { version = "0.7.26", features = ["derive"] } diff --git a/az-cvm-vtpm/README.md b/az-cvm-vtpm/README.md index 0351d6b..b47b669 100644 --- a/az-cvm-vtpm/README.md +++ b/az-cvm-vtpm/README.md @@ -8,4 +8,4 @@ Attestation Library for Azure AMD SEV-SNP Confidential Virtual Machines. ## az-tdx-vtpm -Attestation Library for Azure Intel TDX Confidential Virtual Machines (Limited Preview). +Attestation Library for Azure Intel TDX Confidential Virtual Machines. diff --git a/az-cvm-vtpm/az-snp-vtpm/Cargo.toml b/az-cvm-vtpm/az-snp-vtpm/Cargo.toml index ac8a411..634dad5 100644 --- a/az-cvm-vtpm/az-snp-vtpm/Cargo.toml +++ b/az-cvm-vtpm/az-snp-vtpm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "az-snp-vtpm" -version = "0.3.0" +version = "0.4.0" edition = "2021" repository = "https://github.com/kinvolk/azure-cvm-tooling/" license = "MIT" @@ -17,22 +17,16 @@ path = "src/main.rs" required-features = ["attester", "verifier"] [dependencies] -bincode = "1" -clap = { version = "4", features = ["derive"] } -jsonwebkey = { version = "0.3.5", features = ["pkcs-convert"] } -memoffset = "0.8.0" -openssl = { version = "0.10", optional = true } -rsa = { version = "0.8.2", features = ["pkcs5", "sha2"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -sev = "1.2.0" -sha2 = "0.10.6" -static_assertions = "^1.1.0" -thiserror = "1.0.38" -tss-esapi = "7.4" -ureq = { version = "2.6.2", default-features = false, features = ["json"] } +az-cvm-vtpm = { path = "..", version = "0.4.0" } +bincode.workspace = true +clap.workspace = true +openssl = { workspace = true, optional = true } +serde.workspace = true +sev.workspace = true +thiserror.workspace = true +ureq.workspace = true [features] default = ["attester", "verifier"] attester = [] -verifier = ["openssl", "sev/openssl", "ureq/tls"] +verifier = ["az-cvm-vtpm/openssl", "openssl", "ureq/tls"] diff --git a/az-cvm-vtpm/az-snp-vtpm/example/Cargo.toml b/az-cvm-vtpm/az-snp-vtpm/example/Cargo.toml index 3088086..c76c477 100644 --- a/az-cvm-vtpm/az-snp-vtpm/example/Cargo.toml +++ b/az-cvm-vtpm/az-snp-vtpm/example/Cargo.toml @@ -6,4 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -az-snp-vtpm = { path = "../" } +az-snp-vtpm.path = "../" +openssl.workspace = true diff --git a/az-cvm-vtpm/az-snp-vtpm/example/src/main.rs b/az-cvm-vtpm/az-snp-vtpm/example/src/main.rs index c7e0692..e9b586a 100644 --- a/az-cvm-vtpm/az-snp-vtpm/example/src/main.rs +++ b/az-cvm-vtpm/az-snp-vtpm/example/src/main.rs @@ -1,13 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use az_snp_vtpm::amd_kds; use az_snp_vtpm::certs::Vcek; -use az_snp_vtpm::hcl; -use az_snp_vtpm::imds; -use az_snp_vtpm::report::Validateable; -use az_snp_vtpm::vtpm; -use az_snp_vtpm::vtpm::VerifyVTpmQuote; +use az_snp_vtpm::hcl::HclReport; +use az_snp_vtpm::report::{AttestationReport, Validateable}; +use az_snp_vtpm::{amd_kds, imds, vtpm}; +use openssl::pkey::PKey; use std::error::Error; struct Evidence { @@ -36,8 +34,12 @@ struct Verifier; impl Verifier { fn verify(nonce: &[u8], evidence: &Evidence) -> Result<(), Box> { - let hcl_data: hcl::HclData = evidence.report[..].try_into()?; - let snp_report = hcl_data.report().snp_report(); + let Evidence { quote, report, .. } = evidence; + + let hcl_report = HclReport::new(report.clone())?; + let var_data_hash = hcl_report.var_data_sha256(); + let ak_pub = hcl_report.ak_pub()?; + let snp_report: AttestationReport = hcl_report.try_into()?; let cert_chain = amd_kds::get_cert_chain()?; let vcek = Vcek::from_pem(&evidence.certs.vcek)?; @@ -46,11 +48,12 @@ impl Verifier { vcek.validate(&cert_chain)?; snp_report.validate(&vcek)?; - let var_data = hcl_data.var_data(); - hcl_data.report().verify_report_data(var_data)?; - - let ak_pub = var_data.ak_pub()?; - ak_pub.verify_quote(&evidence.quote, nonce)?; + if var_data_hash != snp_report.report_data[..32] { + return Err("var_data_hash mismatch".into()); + } + let der = ak_pub.key.try_to_der()?; + let pub_key = PKey::public_key_from_der(&der)?; + quote.verify(&pub_key, nonce)?; Ok(()) } diff --git a/az-cvm-vtpm/az-snp-vtpm/src/certs.rs b/az-cvm-vtpm/az-snp-vtpm/src/certs.rs index 0f2b240..900327a 100644 --- a/az-cvm-vtpm/az-snp-vtpm/src/certs.rs +++ b/az-cvm-vtpm/az-snp-vtpm/src/certs.rs @@ -88,7 +88,7 @@ mod tests { #[test] fn test_validate_certificates() { - let bytes = include_bytes!("../test/certs.pem"); + let bytes = include_bytes!("../../test/certs.pem"); let certs = X509::stack_from_pem(bytes).unwrap(); let (vcek, ask, ark) = (certs[0].clone(), certs[1].clone(), certs[2].clone()); let vcek = Vcek(vcek); diff --git a/az-cvm-vtpm/az-snp-vtpm/src/hcl.rs b/az-cvm-vtpm/az-snp-vtpm/src/hcl.rs deleted file mode 100644 index cdeee4e..0000000 --- a/az-cvm-vtpm/az-snp-vtpm/src/hcl.rs +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use memoffset::offset_of; -#[cfg(feature = "verifier")] -use openssl::pkey::{PKey, Public}; -use serde::{Deserialize, Serialize}; -use sev::firmware::guest::AttestationReport; -use sha2::{Digest, Sha256}; -use static_assertions::const_assert; -use std::convert::TryFrom; -use thiserror::Error; - -const HCL_AKPUB_KEY_ID: &str = "HCLAkPub"; - -#[derive(Deserialize, Debug)] -struct VarDataKeys { - keys: Vec, -} - -#[repr(C)] -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] -pub enum IgvmHashType { - Invalid = 0, - Sha256 = 1, - Sha384 = 2, - Sha512 = 3, -} - -#[repr(C)] -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] -pub enum IgvmReportType { - Invalid = 0, - Reserved = 1, - Snp = 2, - Tvm = 3, -} - -#[allow(dead_code)] -const HCL_ATTESTATION_SIGNATURE: u32 = 0x414C4348; - -#[allow(dead_code)] -const HCL_ATTESTATION_VERSION: u32 = 0x1; - -#[repr(C)] -#[derive(Deserialize, Serialize, Debug, Clone, Copy)] -pub struct HclAttestationHeader { - pub signature: u32, - pub version: u32, - pub report_size: u32, - pub request_type: IgvmReportType, - pub reserved: [u32; 4], //<- this looks wrong -} - -#[allow(dead_code)] -const IGVM_ATTEST_VERSION_CURRENT: u32 = 0x1; - -#[repr(C)] -#[derive(Deserialize, Serialize, Debug, Clone, Copy)] -pub struct IgvmRequestData { - pub data_size: u32, - pub version: u32, - pub report_type: IgvmReportType, - pub report_data_hash_type: IgvmHashType, - pub variable_data_size: u32, - pub variable_data: [u8; 0], -} - -#[repr(C)] -#[derive(Deserialize, Serialize, Debug, Clone, Copy)] -pub struct HclAttestationReport { - pub header: HclAttestationHeader, - pub hw_report: AttestationReport, - pub hcl_data: IgvmRequestData, -} - -const_assert!(std::mem::size_of::() == 32); - -impl HclAttestationReport { - pub fn snp_report(&self) -> &AttestationReport { - &self.hw_report - } - - pub fn verify_report_data(&self, var_data: &VarData) -> Result<(), ValidationError> { - if self.hcl_data.report_data_hash_type != IgvmHashType::Sha256 { - unimplemented!(); - } - - let report_data = &self.hw_report.report_data[..32]; - let hash = var_data.sha256(); - - if hash.as_slice() != report_data { - return Err(ValidationError::ReportDataMismatchError); - } - Ok(()) - } -} - -#[derive(Debug)] -pub struct VarData(Vec); - -impl VarData { - pub fn sha256(&self) -> Vec { - let mut hasher = Sha256::new(); - hasher.update(&self.0); - hasher.finalize().to_vec() - } -} - -#[derive(Debug)] -pub struct HclData(HclAttestationReport, VarData); - -impl HclData { - pub fn report(&self) -> &HclAttestationReport { - &self.0 - } - - pub fn var_data(&self) -> &VarData { - &self.1 - } -} - -impl TryFrom<&[u8]> for HclData { - type Error = Box; - - fn try_from(bytes: &[u8]) -> Result { - let hcl_report: HclAttestationReport = bincode::deserialize(bytes)?; - let var_data_offset = - offset_of!(HclAttestationReport, hcl_data) + offset_of!(IgvmRequestData, variable_data); - let var_data_end = var_data_offset + hcl_report.hcl_data.variable_data_size as usize; - let var_data = VarData(bytes[var_data_offset..var_data_end].to_vec()); - Ok(HclData(hcl_report, var_data)) - } -} - -#[derive(Error, Debug)] -pub enum ParseError { - #[error("missing attestation key in runtime data")] - MissingAkPub, - #[error("json parse error")] - Report(#[from] serde_json::Error), - #[cfg(feature = "verifier")] - #[error("openssl error")] - OpenSSL(#[from] openssl::error::ErrorStack), -} - -#[cfg(feature = "verifier")] -impl VarData { - pub fn ak_pub(&self) -> Result, ParseError> { - let VarDataKeys { keys } = serde_json::from_slice(&self.0)?; - - let ak_pub = keys - .into_iter() - .find(|key| key.key_id.as_ref().is_some_and(|id| id == HCL_AKPUB_KEY_ID)) - .ok_or(ParseError::MissingAkPub)?; - - let pubkey = ak_pub.key.to_pem(); - let pubkey = PKey::public_key_from_pem(pubkey.as_bytes())?; - Ok(pubkey) - } -} - -#[derive(Error, Debug)] -pub enum ValidationError { - #[error("bincode error")] - Bincode(#[from] Box), - #[error("ReportData field does not match HCL RuntimeData hash")] - ReportDataMismatchError, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_report_data_hash() { - let bytes = include_bytes!("../test/hcl-report.bin"); - let HclData(hcl_report, var_data) = bytes.as_slice().try_into().unwrap(); - let res = hcl_report.verify_report_data(&var_data); - assert!(res.is_ok()); - } - - #[cfg(feature = "verifier")] - #[test] - fn test_hcl_report() { - let bytes: &[u8] = include_bytes!("../test/hcl-report.bin"); - let HclData(_, ref var_data) = bytes.try_into().unwrap(); - let pubkey = var_data.ak_pub().unwrap(); - assert!(pubkey.size() == 256); - } - - #[test] - fn test_var_data() { - // this is a var data sample containing ak_pub and ek_pub - let bytes = include_bytes!("../test/var-data.bin"); - let var_data = VarData(bytes.to_vec()); - let _key = var_data.ak_pub().unwrap(); - } -} diff --git a/az-cvm-vtpm/az-snp-vtpm/src/lib.rs b/az-cvm-vtpm/az-snp-vtpm/src/lib.rs index e552121..e6a8fd5 100644 --- a/az-cvm-vtpm/az-snp-vtpm/src/lib.rs +++ b/az-cvm-vtpm/az-snp-vtpm/src/lib.rs @@ -5,20 +5,19 @@ //! //! # SNP Report Validation //! -//! The following code will retrieve an SNP report from the vTPM device, parse it, and validate it against the AMD certificate chain. Finally it will verify that a hash of a raw HCL report's [RuntimeData](hcl::RuntimeData) is equal to the `report_data` field in an embedded [Attestation Report](sev::firmware::guest::types::AttestationReport) structure. +//! The following code will retrieve an SNP report from the vTPM device, parse it, and validate it against the AMD certificate chain. Finally it will verify that a hash of a raw HCL report's Variable Data is equal to the `report_data` field in an embedded [Attestation Report](sev::firmware::guest::AttestationReport) structure. //! //! # //! ```no_run -//! use az_snp_vtpm::vtpm::get_report; -//! use az_snp_vtpm::amd_kds; -//! use az_snp_vtpm::report::Validateable; -//! use az_snp_vtpm::hcl::{self, HclData}; +//! use az_snp_vtpm::{amd_kds, hcl, vtpm}; +//! use az_snp_vtpm::report::{AttestationReport, Validateable}; //! use std::error::Error; //! //! fn main() -> Result<(), Box> { -//! let bytes = get_report()?; -//! let hcl_data: HclData = bytes[..].try_into()?; -//! let snp_report = hcl_data.report().snp_report(); +//! let bytes = vtpm::get_report()?; +//! let hcl_report = hcl::HclReport::new(bytes)?; +//! let var_data_hash = hcl_report.var_data_sha256(); +//! let snp_report: AttestationReport = hcl_report.try_into()?; //! //! let vcek = amd_kds::get_vcek(&snp_report)?; //! let cert_chain = amd_kds::get_cert_chain()?; @@ -27,13 +26,15 @@ //! vcek.validate(&cert_chain)?; //! snp_report.validate(&vcek)?; //! -//! let var_data = hcl_data.var_data(); -//! hcl_data.report().verify_report_data(&var_data)?; +//! if var_data_hash != snp_report.report_data[..32] { +//! return Err("var_data_hash mismatch".into()); +//! } //! //! Ok(()) //! } //! ``` +pub use az_cvm_vtpm::{hcl, vtpm}; use thiserror::Error; #[derive(Error, Debug)] @@ -44,12 +45,22 @@ pub enum HttpError { Io(#[from] std::io::Error), } +/// Determines if the current VM is an SEV-SNP CVM. +/// Returns `Ok(true)` if the VM is an SEV-SNP CVM, `Ok(false)` if it is not, +/// and `Err` if an error occurs. +pub fn is_snp_cvm() -> Result { + let bytes = vtpm::get_report()?; + let Ok(hcl_report) = hcl::HclReport::new(bytes) else { + return Ok(false); + }; + let is_snp = hcl_report.report_type() == hcl::ReportType::Snp; + Ok(is_snp) +} + #[cfg(feature = "verifier")] pub mod amd_kds; #[cfg(feature = "verifier")] pub mod certs; -pub mod hcl; #[cfg(feature = "attester")] pub mod imds; pub mod report; -pub mod vtpm; diff --git a/az-cvm-vtpm/az-snp-vtpm/src/main.rs b/az-cvm-vtpm/az-snp-vtpm/src/main.rs index 1584c36..564f840 100644 --- a/az-cvm-vtpm/az-snp-vtpm/src/main.rs +++ b/az-cvm-vtpm/az-snp-vtpm/src/main.rs @@ -1,10 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use az_snp_vtpm::hcl::HclData; -use az_snp_vtpm::{amd_kds, certs, imds, report, vtpm}; +use az_cvm_vtpm::hcl::HclReport; +use az_cvm_vtpm::vtpm; +use az_snp_vtpm::{amd_kds, certs, imds, report}; use clap::Parser; -use report::Validateable; +use report::{AttestationReport, Validateable}; use std::error::Error; use std::fs::File; use std::io::Read; @@ -48,8 +49,8 @@ fn main() -> Result<(), Box> { Some(file_name) => read_file(&file_name)?, None => vtpm::get_report()?, }; - let hcl_data: HclData = bytes.as_slice().try_into()?; - let snp_report = hcl_data.report().snp_report(); + let hcl_report = HclReport::new(bytes)?; + let snp_report: AttestationReport = hcl_report.try_into()?; let (vcek, cert_chain) = if imds { let pem_certs = imds::get_certs()?; @@ -57,7 +58,7 @@ fn main() -> Result<(), Box> { let cert_chain = certs::build_cert_chain(&pem_certs.amd_chain)?; (vcek, cert_chain) } else { - let vcek = amd_kds::get_vcek(snp_report)?; + let vcek = amd_kds::get_vcek(&snp_report)?; let cert_chain = amd_kds::get_cert_chain()?; (vcek, cert_chain) }; diff --git a/az-cvm-vtpm/az-snp-vtpm/src/report.rs b/az-cvm-vtpm/az-snp-vtpm/src/report.rs index 129c329..4630aaf 100644 --- a/az-cvm-vtpm/az-snp-vtpm/src/report.rs +++ b/az-cvm-vtpm/az-snp-vtpm/src/report.rs @@ -3,12 +3,13 @@ #[cfg(feature = "verifier")] use super::certs::Vcek; +use az_cvm_vtpm::hcl::{self, HclReport}; +use az_cvm_vtpm::vtpm; #[cfg(feature = "verifier")] use openssl::{ecdsa::EcdsaSig, sha::Sha384}; #[cfg(feature = "verifier")] use sev::certs::snp::ecdsa::Signature; -use sev::firmware::guest::AttestationReport; -use std::error::Error; +pub use sev::firmware::guest::AttestationReport; use thiserror::Error; #[derive(Error, Debug)] @@ -53,9 +54,19 @@ impl Validateable for AttestationReport { } } -pub fn parse(bytes: &[u8]) -> Result> { - let decoded: AttestationReport = bincode::deserialize(bytes)?; - Ok(decoded) +#[derive(Error, Debug)] +pub enum ReportError { + #[error("deserialization error")] + Parse(#[from] Box), + #[error("vTPM error")] + Vtpm(#[from] vtpm::ReportError), + #[error("HCL error")] + Hcl(#[from] hcl::HclError), +} + +pub fn parse(bytes: &[u8]) -> Result { + let snp_report = bincode::deserialize::(bytes)?; + Ok(snp_report) } #[cfg(feature = "verifier")] @@ -71,3 +82,26 @@ fn get_report_base(report: &AttestationReport) -> Result, Box Result { + let bytes = vtpm::get_report()?; + let hcl_report = HclReport::new(bytes)?; + let snp_report = hcl_report.try_into()?; + Ok(snp_report) +} + +#[cfg(test)] +mod tests { + use super::*; + use hcl::HclReport; + + #[test] + fn test_report_data_hash() { + let bytes: &[u8] = include_bytes!("../../test/hcl-report-snp.bin"); + let hcl_report = HclReport::new(bytes.to_vec()).unwrap(); + let var_data_hash = hcl_report.var_data_sha256(); + let snp_report: AttestationReport = hcl_report.try_into().unwrap(); + assert!(var_data_hash == snp_report.report_data[..32]); + } +} diff --git a/az-cvm-vtpm/az-tdx-vtpm/Cargo.toml b/az-cvm-vtpm/az-tdx-vtpm/Cargo.toml index 9d69e58..06baebb 100644 --- a/az-cvm-vtpm/az-tdx-vtpm/Cargo.toml +++ b/az-cvm-vtpm/az-tdx-vtpm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "az-tdx-vtpm" -version = "0.1.0" +version = "0.4.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -13,23 +13,19 @@ name = "tdx-vtpm" path = "src/main.rs" [dependencies] -anyhow = "1.0.75" +az-cvm-vtpm = { path = "..", version = "0.4.0" } base64-url = "2.0.0" -bincode = "1.3.3" -jsonwebkey = { version = "0.3.5", features = ["pkcs-convert"] } -memoffset = "0.9.0" -serde = { version = "1.0.189", features = ["derive"] } -serde-big-array = "0.5.1" -serde_json = "1.0.107" -sha2 = "0.10.8" -tss-esapi = "7.4" -ureq = { version = "2.6.2", default-features = false, features = ["json"] } -az-snp-vtpm = { path = "../az-snp-vtpm" } -sev = "1.2.0" -thiserror = "1.0.49" -openssl = { version = "0.10", optional = true } +bincode.workspace = true +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +ureq.workspace = true +zerocopy.workspace = true + +[dev-dependencies] +openssl.workspace = true [features] default = ["attester", "verifier"] attester = [] -verifier = ["az-snp-vtpm/verifier", "openssl"] +verifier = ["az-cvm-vtpm/verifier"] diff --git a/az-cvm-vtpm/az-tdx-vtpm/README.md b/az-cvm-vtpm/az-tdx-vtpm/README.md index cc5421c..67ebe3a 100644 --- a/az-cvm-vtpm/az-tdx-vtpm/README.md +++ b/az-cvm-vtpm/az-tdx-vtpm/README.md @@ -1,9 +1,10 @@ # az-tdx-vtpm [![Rust](https://github.com/kinvolk/azure-cvm-tooling/actions/workflows/rust.yml/badge.svg)](https://github.com/kinvolk/azure-cvm-tooling/actions/workflows/rust.yml) +[![Crate](https://img.shields.io/crates/v/az-tdx-vtpm.svg)](https://crates.io/crates/az-tdx-vtpm) +[![Docs](https://docs.rs/rand/badge.svg)](https://docs.rs/az-tdx-vtpm) -> [!WARNING] -> This library enables guest attestation and verification for [TDX CVMs on Azure](https://learn.microsoft.com/en-us/azure/confidential-computing/tdx-confidential-vm-overview). TDX CVMs are currently in limited preview and hence the library is considered experimental and subject to change. +This library enables guest attestation and verification for [TDX CVMs on Azure](https://learn.microsoft.com/en-us/azure/confidential-computing/tdx-confidential-vm-overview). ## Build & Install diff --git a/az-cvm-vtpm/az-tdx-vtpm/src/imds.rs b/az-cvm-vtpm/az-tdx-vtpm/src/imds.rs index 9b70f56..f9501d4 100644 --- a/az-cvm-vtpm/az-tdx-vtpm/src/imds.rs +++ b/az-cvm-vtpm/az-tdx-vtpm/src/imds.rs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use az_cvm_vtpm::tdx::TdReport; use serde::Deserialize; use thiserror::Error; +use zerocopy::AsBytes; const IMDS_QUOTE_URL: &str = "http://169.254.169.254/acc/tdquote"; @@ -16,12 +18,12 @@ pub enum ImdsError { IoError(#[from] std::io::Error), } -pub struct ReportBody { +struct ReportBody { report: String, } impl ReportBody { - pub fn new(report_bytes: &[u8]) -> Self { + fn new(report_bytes: &[u8]) -> Self { let report = base64_url::encode(report_bytes); Self { report } } @@ -32,7 +34,11 @@ struct QuoteResponse { quote: String, } -pub fn get_td_quote(report_body: ReportBody) -> Result, ImdsError> { +/// Retrieves a TDX quote from the Azure Instance Metadata Service (IMDS) using a provided TD +/// report. +pub fn get_td_quote(td_report: &TdReport) -> Result, ImdsError> { + let bytes = td_report.as_bytes(); + let report_body = ReportBody::new(bytes); let response: QuoteResponse = ureq::post(IMDS_QUOTE_URL) .send_json(ureq::json!({ "report": report_body.report, diff --git a/az-cvm-vtpm/az-tdx-vtpm/src/lib.rs b/az-cvm-vtpm/az-tdx-vtpm/src/lib.rs index ee22b54..138a41f 100644 --- a/az-cvm-vtpm/az-tdx-vtpm/src/lib.rs +++ b/az-cvm-vtpm/az-tdx-vtpm/src/lib.rs @@ -1,34 +1,55 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//! This library enables guest attestation flows for [TDX CVMs on Azure](https://learn.microsoft.com/en-us/azure/confidential-computing/tdx-confidential-vm-overview). TDX CVMs are currently in limited preview and hence the library is considered experimental and subject to change. +//! This library enables guest attestation flows for [TDX CVMs on Azure](https://learn.microsoft.com/en-us/azure/confidential-computing/tdx-confidential-vm-overview). +//! +//! A TD report can be retrieved in parsed form using `report::get_report()` function, or as +//! raw bytes including the hcl envelope using `vtpm::get_report()`. The library provides a +//! function to retrieve the TD quote from the Azure Instance Metadata Service (IMDS) using +//! `imds::get_td_quote()`, produce returning a quote signed by a TDX Quoting Enclave. +//! +//! Variable Data is part of the HCL envelope and holds the public part of the vTPM Attestation +//! Key (AK). A hash of the Variable Data block is included in the TD report as `reportdata`. +//! TPM quotes retrieved with `vtpm::get_quote()` should be signed by this AK. A verification +//! function would need to check this to ensure the TD report is linked to this unique TDX CVM. +//! //! # //! ```no_run -//! use az_tdx_vtpm::{imds, hcl, vtpm}; +//! use az_tdx_vtpm::{hcl, imds, report, tdx, vtpm}; +//! use openssl::pkey::{PKey, Public}; //! use std::error::Error; //! //! fn main() -> Result<(), Box> { +//! let td_report = report::get_report()?; +//! let td_quote_bytes = imds::get_td_quote(&td_report)?; +//! std::fs::write("td_quote.bin", td_quote_bytes)?; +//! //! let bytes = vtpm::get_report()?; //! let hcl_report = hcl::HclReport::new(bytes)?; -//! let tdx_report_slice = hcl_report.tdx_report_slice(); -//! let report_body = imds::ReportBody::new(tdx_report_slice); -//! let td_quote_bytes = imds::get_td_quote(report_body)?; -//! let hash = hcl_report.var_data_sha256(); -//! println!("var_data hash: {:x?}", hash); -//! std::fs::write("td_quote.bin", td_quote_bytes)?; +//! let var_data_hash = hcl_report.var_data_sha256(); +//! let ak_pub = hcl_report.ak_pub()?; +//! +//! let td_report: tdx::TdReport = hcl_report.try_into()?; +//! assert!(var_data_hash == td_report.report_mac.reportdata[..32]); +//! let nonce = "a nonce".as_bytes(); +//! +//! let tpm_quote = vtpm::get_quote(nonce)?; +//! let der = ak_pub.key.try_to_der()?; +//! let pub_key = PKey::public_key_from_der(&der)?; +//! tpm_quote.verify(&pub_key, nonce)?; +//! //! Ok(()) //! } //! ``` -pub use az_snp_vtpm::vtpm; - -pub mod hcl; pub mod imds; -pub mod tdx; -#[cfg(feature = "verifier")] -pub mod verify; +pub mod report; +pub use az_cvm_vtpm::{hcl, tdx, vtpm}; -pub fn is_tdx_cvm() -> Result { +/// Determines if the current VM is a TDX CVM. +/// Returns `Ok(true)` if the VM is a TDX CVM, `Ok(false)` if it is not, +/// and `Err` if an error occurs. +pub fn is_tdx_cvm() -> Result { let bytes = vtpm::get_report()?; let Ok(hcl_report) = hcl::HclReport::new(bytes) else { return Ok(false); @@ -36,3 +57,19 @@ pub fn is_tdx_cvm() -> Result { let is_tdx = hcl_report.report_type() == hcl::ReportType::Tdx; Ok(is_tdx) } + +#[cfg(test)] +mod tests { + use super::*; + use hcl::HclReport; + use tdx::TdReport; + + #[test] + fn test_report_data_hash() { + let bytes: &[u8] = include_bytes!("../../test/hcl-report-tdx.bin"); + let hcl_report = HclReport::new(bytes.to_vec()).unwrap(); + let var_data_hash = hcl_report.var_data_sha256(); + let td_report: TdReport = hcl_report.try_into().unwrap(); + assert!(var_data_hash == td_report.report_mac.reportdata[..32]); + } +} diff --git a/az-cvm-vtpm/az-tdx-vtpm/src/main.rs b/az-cvm-vtpm/az-tdx-vtpm/src/main.rs index 37f9268..e6f32e1 100644 --- a/az-cvm-vtpm/az-tdx-vtpm/src/main.rs +++ b/az-cvm-vtpm/az-tdx-vtpm/src/main.rs @@ -1,19 +1,19 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use anyhow::Result; -use az_tdx_vtpm::{hcl, imds, vtpm}; +use az_tdx_vtpm::{hcl, imds, tdx, vtpm}; +use std::error::Error; -fn main() -> Result<()> { +fn main() -> Result<(), Box> { let bytes = vtpm::get_report()?; let hcl_report = hcl::HclReport::new(bytes)?; + let var_data_hash = hcl_report.var_data_sha256(); + let ak_pub = hcl_report.ak_pub()?; - let hash = hcl_report.var_data_sha256(); - println!("var_data hash: {:x?}", hash); - - let tdx_report_slice = hcl_report.tdx_report_slice(); - let report_body = imds::ReportBody::new(tdx_report_slice); - let td_quote_bytes = imds::get_td_quote(report_body)?; + let td_report: tdx::TdReport = hcl_report.try_into()?; + assert!(var_data_hash == td_report.report_mac.reportdata[..32]); + println!("vTPM AK_pub: {:?}", ak_pub); + let td_quote_bytes = imds::get_td_quote(&td_report)?; std::fs::write("td_quote.bin", td_quote_bytes)?; Ok(()) diff --git a/az-cvm-vtpm/az-tdx-vtpm/src/report.rs b/az-cvm-vtpm/az-tdx-vtpm/src/report.rs new file mode 100644 index 0000000..2fe5791 --- /dev/null +++ b/az-cvm-vtpm/az-tdx-vtpm/src/report.rs @@ -0,0 +1,28 @@ +use crate::hcl::{self, HclReport}; +use crate::tdx::TdReport; +use crate::vtpm; +use bincode::deserialize; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ReportError { + #[error("deserialization error")] + Parse(#[from] Box), + #[error("vTPM error")] + Vtpm(#[from] vtpm::ReportError), + #[error("HCL error")] + Hcl(#[from] hcl::HclError), +} + +/// Parse raw bytes into TdReport +pub fn parse(bytes: &[u8]) -> Result { + deserialize::(bytes).map_err(|e| e.into()) +} + +/// Fetch TdReport from vTPM and parse it +pub fn get_report() -> Result { + let bytes = vtpm::get_report()?; + let hcl_report = HclReport::new(bytes)?; + let td_report = hcl_report.try_into()?; + Ok(td_report) +} diff --git a/az-cvm-vtpm/az-tdx-vtpm/src/tdx.rs b/az-cvm-vtpm/az-tdx-vtpm/src/tdx.rs deleted file mode 100644 index 47f240f..0000000 --- a/az-cvm-vtpm/az-tdx-vtpm/src/tdx.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use serde::{Deserialize, Serialize}; -use serde_big_array::BigArray; - -const TDX_REPORT_DATA_LENGTH: usize = 64; - -#[repr(C)] -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct TdxReportMac { - pub report_type: u8, - pub report_sub_type: u8, - pub report_version: u8, - pub reserved_type_mbz: u8, - pub reserved_mbz1: [u8; 12], - pub cpu_svn: [u8; 16], - #[serde(with = "BigArray")] - pub tee_tcb_info_hash: [u8; 48], - #[serde(with = "BigArray")] - pub tee_info_hash: [u8; 48], - #[serde(with = "BigArray")] - pub report_data: [u8; TDX_REPORT_DATA_LENGTH], - pub reserved_mbz2: [u8; 32], - pub mac: [u8; 32], -} - -#[repr(C)] -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct TdxTeeTcbInfo { - pub tee_valid: [u8; 8], - pub tee_tcb_svn: [u8; 16], - #[serde(with = "BigArray")] - pub tee_mr_seam: [u8; 48], - #[serde(with = "BigArray")] - pub tee_mr_seam_signer: [u8; 48], - pub tee_attributes: [u8; 8], - pub tee_tcb_svn2: [u8; 16], - #[serde(with = "BigArray")] - pub tee_reserved: [u8; 95], -} - -#[repr(C)] -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct TdxRtmr { - #[serde(with = "BigArray")] - pub register_data: [u8; 48], -} - -#[repr(C)] -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct TdxTdInfo { - pub attributes: [u8; 8], - pub xfam: [u8; 8], - #[serde(with = "BigArray")] - pub mrtd: [u8; 48], - #[serde(with = "BigArray")] - pub mr_config_id: [u8; 48], - #[serde(with = "BigArray")] - pub mr_owner: [u8; 48], - #[serde(with = "BigArray")] - pub mr_owner_config: [u8; 48], - pub rtrm: [TdxRtmr; 4], - #[serde(with = "BigArray")] - pub serv_td: [u8; 48], - #[serde(with = "BigArray")] - pub reserved_mbz: [u8; 64], -} - -#[repr(C)] -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct TdxVmReport { - pub tdx_report_mac: TdxReportMac, - pub tdx_tee_tcb_info: TdxTeeTcbInfo, - pub tdx_reserved: [u8; 17], - pub tdx_td_info: TdxTdInfo, -} diff --git a/az-cvm-vtpm/az-tdx-vtpm/src/verify.rs b/az-cvm-vtpm/az-tdx-vtpm/src/verify.rs deleted file mode 100644 index 4c106f9..0000000 --- a/az-cvm-vtpm/az-tdx-vtpm/src/verify.rs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use az_snp_vtpm::vtpm; -use openssl::hash::MessageDigest; -use openssl::pkey::{PKey, Public}; -use openssl::sign::Verifier; -use thiserror::Error; -use tss_esapi::structures::Attest; -use tss_esapi::traits::UnMarshall; - -#[derive(Error, Debug)] -pub enum VerifyError { - #[error("tss error")] - Tss(#[from] tss_esapi::Error), - #[error("openssl error")] - OpenSsl(#[from] openssl::error::ErrorStack), - #[error("quote is not signed by key")] - SignatureMismatch, - #[error("nonce mismatch")] - NonceMismatch, -} - -pub trait Verify { - fn verify(&self, pub_key: &PKey, nonce: &[u8]) -> Result<(), VerifyError>; -} - -impl Verify for vtpm::Quote { - fn verify(&self, pub_key: &PKey, nonce: &[u8]) -> Result<(), VerifyError> { - let mut verifier = Verifier::new(MessageDigest::sha256(), pub_key)?; - verifier.update(&self.message)?; - let is_verified = verifier.verify(&self.signature)?; - if !is_verified { - return Err(VerifyError::SignatureMismatch); - } - let attest = Attest::unmarshall(&self.message)?; - if nonce != attest.extra_data().as_slice() { - return Err(VerifyError::NonceMismatch); - } - Ok(()) - } -} diff --git a/az-cvm-vtpm/az-tdx-vtpm/src/hcl.rs b/az-cvm-vtpm/src/hcl/mod.rs similarity index 58% rename from az-cvm-vtpm/az-tdx-vtpm/src/hcl.rs rename to az-cvm-vtpm/src/hcl/mod.rs index 2ca9dc3..27fb2ca 100644 --- a/az-cvm-vtpm/az-tdx-vtpm/src/hcl.rs +++ b/az-cvm-vtpm/src/hcl/mod.rs @@ -1,21 +1,36 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::tdx::TdxVmReport; +use crate::tdx::TdReport; use jsonwebkey::JsonWebKey; use memoffset::offset_of; use serde::{Deserialize, Serialize}; use serde_big_array::BigArray; -use sev::firmware::guest::AttestationReport as SnpVmReport; +use sev::firmware::guest::AttestationReport as SnpReport; use sha2::{Digest, Sha256}; -use std::mem; +use std::convert::TryFrom; +use std::mem::size_of; +use std::ops::Range; use thiserror::Error; const HCL_AKPUB_KEY_ID: &str = "HCLAkPub"; -const MAX_REPORT_SIZE: usize = mem::size_of::(); -const MIN_REPORT_SIZE: usize = mem::size_of::(); +const TD_REPORT_SIZE: usize = size_of::(); +const SNP_REPORT_SIZE: usize = size_of::(); +const fn max(a: usize, b: usize) -> usize { + if a > b { + return a; + } + b +} +const MAX_REPORT_SIZE: usize = max(SNP_REPORT_SIZE, TD_REPORT_SIZE); const SNP_REPORT_TYPE: u32 = 2; const TDX_REPORT_TYPE: u32 = 4; +const HW_REPORT_OFFSET: usize = offset_of!(AttestationReport, hw_report); +const fn report_range(report_size: usize) -> Range { + HW_REPORT_OFFSET..(HW_REPORT_OFFSET + report_size) +} +const TD_REPORT_RANGE: Range = report_range(TD_REPORT_SIZE); +const SNP_REPORT_RANGE: Range = report_range(SNP_REPORT_SIZE); #[derive(Error, Debug)] pub enum HclError { @@ -65,19 +80,12 @@ struct AttestationHeader { reserved: [u32; 3], } -#[repr(C)] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -struct HwReport { - tdx_vm_report: TdxVmReport, - #[serde(with = "BigArray")] - _padding: [u8; MAX_REPORT_SIZE - MIN_REPORT_SIZE], -} - #[repr(C)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] struct AttestationReport { header: AttestationHeader, - hw_report: HwReport, + #[serde(with = "BigArray")] + hw_report: [u8; MAX_REPORT_SIZE], hcl_data: IgvmRequestData, } @@ -93,7 +101,13 @@ pub enum ReportType { Snp, } +pub enum HwReport { + Tdx(TdReport), + Snp(SnpReport), +} + impl HclReport { + /// Parse a HCL report from a byte slice. pub fn new(bytes: Vec) -> Result { let attestation_report: AttestationReport = bincode::deserialize(&bytes)?; let report_type = match attestation_report.hcl_data.report_type { @@ -110,16 +124,19 @@ impl HclReport { Ok(report) } + /// Get the type of the nested hardware report pub fn report_type(&self) -> ReportType { self.report_type } - pub fn tdx_report_slice(&self) -> &[u8] { - let tdx_report_offset = offset_of!(AttestationReport, hw_report); - let tdx_report_end = tdx_report_offset + mem::size_of::(); - &self.bytes[tdx_report_offset..tdx_report_end] + fn report_slice(&self) -> &[u8] { + match self.report_type { + ReportType::Tdx => self.bytes[TD_REPORT_RANGE].as_ref(), + ReportType::Snp => self.bytes[SNP_REPORT_RANGE].as_ref(), + } } + /// Get the SHA256 hash of the VarData section pub fn var_data_sha256(&self) -> [u8; 32] { if self.attestation_report.hcl_data.report_data_hash_type != IgvmHashType::Sha256 { unimplemented!( @@ -133,6 +150,7 @@ impl HclReport { hash.into() } + /// Get the slice of the VarData section fn var_data_slice(&self) -> &[u8] { let var_data_offset = offset_of!(AttestationReport, hcl_data) + offset_of!(IgvmRequestData, variable_data); @@ -141,6 +159,7 @@ impl HclReport { &self.bytes[var_data_offset..var_data_end] } + /// Get the vTPM's AKpub from the VarData section pub fn ak_pub(&self) -> Result { let VarDataKeys { keys } = serde_json::from_slice(self.var_data_slice())?; let ak_pub = keys @@ -156,13 +175,59 @@ impl HclReport { } } +impl TryFrom<&HclReport> for TdReport { + type Error = HclError; + + fn try_from(hcl_report: &HclReport) -> Result { + if hcl_report.report_type != ReportType::Tdx { + return Err(HclError::InvalidReportType); + } + let bytes = hcl_report.report_slice(); + let td_report = bincode::deserialize::(bytes)?; + Ok(td_report) + } +} + +impl TryFrom for TdReport { + type Error = HclError; + + fn try_from(hcl_report: HclReport) -> Result { + (&hcl_report).try_into() + } +} + +impl TryFrom<&HclReport> for SnpReport { + type Error = HclError; + + fn try_from(hcl_report: &HclReport) -> Result { + if hcl_report.report_type != ReportType::Snp { + return Err(HclError::InvalidReportType); + } + let bytes = hcl_report.report_slice(); + let snp_report = bincode::deserialize::(bytes)?; + Ok(snp_report) + } +} + +impl TryFrom for SnpReport { + type Error = HclError; + + fn try_from(hcl_report: HclReport) -> Result { + (&hcl_report).try_into() + } +} + #[cfg(test)] mod tests { use super::*; #[test] fn parse_hcl_report() { - let bytes: &[u8] = include_bytes!("../test/hcl_report.bin"); + let bytes: &[u8] = include_bytes!("../../test/hcl-report-snp.bin"); + let hcl_report = HclReport::new(bytes.to_vec()).unwrap(); + let _ = hcl_report.ak_pub().unwrap(); + + let bytes: &[u8] = include_bytes!("../../test/hcl-report-tdx.bin"); let hcl_report = HclReport::new(bytes.to_vec()).unwrap(); let _ = hcl_report.ak_pub().unwrap(); } diff --git a/az-cvm-vtpm/src/lib.rs b/az-cvm-vtpm/src/lib.rs new file mode 100644 index 0000000..40a8e6b --- /dev/null +++ b/az-cvm-vtpm/src/lib.rs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +pub mod hcl; +pub mod tdx; +pub mod vtpm; diff --git a/az-cvm-vtpm/src/tdx/mod.rs b/az-cvm-vtpm/src/tdx/mod.rs new file mode 100644 index 0000000..097f8a3 --- /dev/null +++ b/az-cvm-vtpm/src/tdx/mod.rs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Types are based on "Architecture Specification: Intel Trust Domain Extensions +// Module 1.0", Feb 2023, Section 22.6 + +use serde::{Deserialize, Serialize}; +use serde_big_array::BigArray; +use zerocopy::AsBytes; + +#[repr(C)] +#[derive(AsBytes, Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct ReportType { + pub r#type: u8, + pub subtype: u8, + pub version: u8, + pub _reserved: u8, +} + +#[repr(C)] +#[derive(AsBytes, Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct ReportMac { + pub reporttype: ReportType, + pub _reserved_1: [u8; 12], + pub cpusvn: [u8; 16], + #[serde(with = "BigArray")] + pub tee_tcb_info_hash: [u8; 48], + #[serde(with = "BigArray")] + pub tee_info_hash: [u8; 48], + #[serde(with = "BigArray")] + pub reportdata: [u8; 64], + pub _reserved_2: [u8; 32], + pub mac: [u8; 32], +} + +#[repr(C)] +#[derive(AsBytes, Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct Rtmr { + #[serde(with = "BigArray")] + pub register_data: [u8; 48], +} + +#[repr(C)] +#[derive(AsBytes, Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct TdInfo { + pub attributes: [u8; 8], + pub xfam: [u8; 8], + #[serde(with = "BigArray")] + pub mrtd: [u8; 48], + #[serde(with = "BigArray")] + pub mrconfigid: [u8; 48], + #[serde(with = "BigArray")] + pub mrowner: [u8; 48], + #[serde(with = "BigArray")] + pub mrownerconfig: [u8; 48], + pub rtrm: [Rtmr; 4], + #[serde(with = "BigArray")] + pub _reserved: [u8; 112], +} + +#[repr(C)] +#[derive(AsBytes, Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct TdReport { + pub report_mac: ReportMac, + #[serde(with = "BigArray")] + pub tee_tcb_info: [u8; 239], + pub _reserved: [u8; 17], + pub tdinfo: TdInfo, +} diff --git a/az-cvm-vtpm/az-snp-vtpm/src/vtpm.rs b/az-cvm-vtpm/src/vtpm/mod.rs similarity index 62% rename from az-cvm-vtpm/az-snp-vtpm/src/vtpm.rs rename to az-cvm-vtpm/src/vtpm/mod.rs index 38ae206..a9f84b8 100644 --- a/az-cvm-vtpm/az-snp-vtpm/src/vtpm.rs +++ b/az-cvm-vtpm/src/vtpm/mod.rs @@ -1,12 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#[cfg(feature = "verifier")] -use openssl::hash::MessageDigest; -#[cfg(feature = "verifier")] -use openssl::pkey::{PKey, Public}; -#[cfg(feature = "verifier")] -use openssl::sign::Verifier; use rsa::{BigUint, RsaPublicKey}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -18,16 +12,14 @@ use tss_esapi::interface_types::resource_handles::NvAuth; use tss_esapi::interface_types::session_handles::AuthSession; use tss_esapi::structures::pcr_selection_list::PcrSelectionListBuilder; use tss_esapi::structures::pcr_slot::PcrSlot; -#[cfg(feature = "verifier")] -use tss_esapi::structures::Attest; -use tss_esapi::structures::SignatureScheme; -use tss_esapi::structures::{AttestInfo, Data, Signature}; +use tss_esapi::structures::{AttestInfo, Data, Signature, SignatureScheme}; use tss_esapi::tcti_ldr::{DeviceConfig, TctiNameConf}; use tss_esapi::traits::Marshall; -#[cfg(feature = "verifier")] -use tss_esapi::traits::UnMarshall; use tss_esapi::Context; +#[cfg(feature = "verifier")] +mod verify; + const VTPM_HCL_REPORT_NV_INDEX: u32 = 0x01400001; const VTPM_AK_HANDLE: u32 = 0x81000003; const VTPM_QUOTE_PCR_SLOTS: [PcrSlot; 24] = [ @@ -57,7 +49,14 @@ const VTPM_QUOTE_PCR_SLOTS: [PcrSlot; 24] = [ PcrSlot::Slot23, ]; -pub fn get_report() -> Result, tss_esapi::Error> { +#[derive(Error, Debug)] +pub enum ReportError { + #[error("tpm error")] + Tpm(#[from] tss_esapi::Error), +} + +/// Get a HCL report from an nvindex +pub fn get_report() -> Result, ReportError> { use tss_esapi::handles::NvIndexTpmHandle; let nv_index = NvIndexTpmHandle::new(VTPM_HCL_REPORT_NV_INDEX)?; @@ -66,7 +65,8 @@ pub fn get_report() -> Result, tss_esapi::Error> { let auth_session = AuthSession::Password; context.set_sessions((Some(auth_session), None, None)); - nv::read_full(&mut context, NvAuth::Owner, nv_index) + let report = nv::read_full(&mut context, NvAuth::Owner, nv_index)?; + Ok(report) } #[derive(Error, Debug)] @@ -79,6 +79,7 @@ pub enum AKPubError { OpenSsl(#[from] rsa::errors::Error), } +/// Get the AK pub of the vTPM pub fn get_ak_pub() -> Result { let conf: TctiNameConf = TctiNameConf::Device(DeviceConfig::default()); let mut context = Context::new(conf)?; @@ -118,6 +119,7 @@ pub struct Quote { pub message: Vec, } +/// Get a signed vTPM quote pub fn get_quote(data: &[u8]) -> Result { if data.len() > Data::MAX_SIZE { return Err(QuoteError::DataTooLarge); @@ -165,72 +167,3 @@ pub enum VerifyError { #[error("quote is not signed by the public key")] SignatureMismatch, } - -#[cfg(feature = "verifier")] -pub trait VerifyVTpmQuote { - fn verify_quote(&self, quote: &Quote, nonce: &[u8]) -> Result<(), VerifyError>; -} - -#[cfg(feature = "verifier")] -impl VerifyVTpmQuote for PKey { - fn verify_quote(&self, quote: &Quote, nonce: &[u8]) -> Result<(), VerifyError> { - let mut verifier = Verifier::new(MessageDigest::sha256(), self)?; - verifier.update("e.message)?; - let is_verified = verifier.verify("e.signature)?; - if !is_verified { - return Err(VerifyError::SignatureMismatch); - } - - let attest = Attest::unmarshall("e.message)?; - if nonce != attest.extra_data().as_slice() { - return Err(VerifyError::NonceMismatch); - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[cfg(feature = "verifier")] - #[test] - fn test_quote_validation() { - // Can be retrieved by `get_ak_pub()` or via tpm2-tools: - // `tpm2_readpublic -c 0x81000003 -f pem -o akpub.pem` - - let pem = include_bytes!("../test/akpub.pem"); - let pkey = PKey::public_key_from_pem(pem).unwrap(); - - // Can be retrieved by `get_quote()` or via tpm2-tools: - // `tpm2_quote -c 0x81000003 -l sha256:5,8 -q cafe -m quote_msg -s quote_sig` - let message = include_bytes!("../test/quote_msg").to_vec(); - let signature = include_bytes!("../test/quote_sig").to_vec(); - let quote = Quote { signature, message }; - - // proper nonce in message - let nonce = vec![1, 2, 3]; - let result = pkey.verify_quote("e, &nonce); - assert!(result.is_ok(), "Quote verification should not fail"); - - // wrong signature - let mut wrong_quote = quote.clone(); - wrong_quote.signature.reverse(); - let result = pkey.verify_quote(&wrong_quote, &nonce); - let error = result.unwrap_err(); - assert!( - matches!(error, VerifyError::SignatureMismatch), - "Expected signature mismatch" - ); - - // improper nonce - let nonce = vec![1, 2, 3, 4]; - let result = pkey.verify_quote("e, &nonce); - let error = result.unwrap_err(); - assert!( - matches!(error, VerifyError::NonceMismatch), - "Expected nonce verification error" - ); - } -} diff --git a/az-cvm-vtpm/src/vtpm/verify.rs b/az-cvm-vtpm/src/vtpm/verify.rs new file mode 100644 index 0000000..1a17860 --- /dev/null +++ b/az-cvm-vtpm/src/vtpm/verify.rs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::Quote; +use openssl::hash::MessageDigest; +use openssl::pkey::{PKey, Public}; +use openssl::sign::Verifier; +use thiserror::Error; +use tss_esapi::structures::Attest; +use tss_esapi::traits::UnMarshall; + +#[derive(Error, Debug)] +pub enum VerifyError { + #[error("tss error")] + Tss(#[from] tss_esapi::Error), + #[error("openssl error")] + OpenSsl(#[from] openssl::error::ErrorStack), + #[error("quote is not signed by key")] + SignatureMismatch, + #[error("nonce mismatch")] + NonceMismatch, +} + +impl Quote { + /// Verifies the quote's signature and nonce. + pub fn verify(&self, pub_key: &PKey, nonce: &[u8]) -> Result<(), VerifyError> { + let mut verifier = Verifier::new(MessageDigest::sha256(), pub_key)?; + verifier.update(&self.message)?; + let is_verified = verifier.verify(&self.signature)?; + if !is_verified { + return Err(VerifyError::SignatureMismatch); + } + let attest = Attest::unmarshall(&self.message)?; + if nonce != attest.extra_data().as_slice() { + return Err(VerifyError::NonceMismatch); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(feature = "verifier")] + #[test] + fn test_quote_validation() { + // Can be retrieved by `get_ak_pub()` or via tpm2-tools: + // `tpm2_readpublic -c 0x81000003 -f pem -o akpub.pem` + + let pem = include_bytes!("../../test/akpub.pem"); + let pkey = PKey::public_key_from_pem(pem).unwrap(); + + // Can be retrieved by `get_quote()` or via tpm2-tools: + // `tpm2_quote -c 0x81000003 -l sha256:5,8 -q cafe -m quote_msg -s quote_sig` + let message = include_bytes!("../../test/quote_msg").to_vec(); + let signature = include_bytes!("../../test/quote_sig").to_vec(); + let quote = Quote { signature, message }; + + // proper nonce in message + let nonce = vec![1, 2, 3]; + let result = quote.verify(&pkey, &nonce); + assert!(result.is_ok(), "Quote verification should not fail"); + + // wrong signature + let mut wrong_quote = quote.clone(); + wrong_quote.signature.reverse(); + let result = wrong_quote.verify(&pkey, &nonce); + let error = result.unwrap_err(); + assert!( + matches!(error, VerifyError::SignatureMismatch), + "Expected signature mismatch" + ); + + // improper nonce + let nonce = vec![1, 2, 3, 4]; + let result = quote.verify(&pkey, &nonce); + let error = result.unwrap_err(); + assert!( + matches!(error, VerifyError::NonceMismatch), + "Expected nonce verification error" + ); + } +} diff --git a/az-cvm-vtpm/az-snp-vtpm/test/akpub.pem b/az-cvm-vtpm/test/akpub.pem similarity index 100% rename from az-cvm-vtpm/az-snp-vtpm/test/akpub.pem rename to az-cvm-vtpm/test/akpub.pem diff --git a/az-cvm-vtpm/az-snp-vtpm/test/certs.pem b/az-cvm-vtpm/test/certs.pem similarity index 100% rename from az-cvm-vtpm/az-snp-vtpm/test/certs.pem rename to az-cvm-vtpm/test/certs.pem diff --git a/az-cvm-vtpm/az-snp-vtpm/test/hcl-report.bin b/az-cvm-vtpm/test/hcl-report-snp.bin similarity index 100% rename from az-cvm-vtpm/az-snp-vtpm/test/hcl-report.bin rename to az-cvm-vtpm/test/hcl-report-snp.bin diff --git a/az-cvm-vtpm/az-tdx-vtpm/test/hcl_report.bin b/az-cvm-vtpm/test/hcl-report-tdx.bin similarity index 100% rename from az-cvm-vtpm/az-tdx-vtpm/test/hcl_report.bin rename to az-cvm-vtpm/test/hcl-report-tdx.bin diff --git a/az-cvm-vtpm/az-snp-vtpm/test/quote_msg b/az-cvm-vtpm/test/quote_msg similarity index 100% rename from az-cvm-vtpm/az-snp-vtpm/test/quote_msg rename to az-cvm-vtpm/test/quote_msg diff --git a/az-cvm-vtpm/az-snp-vtpm/test/quote_sig b/az-cvm-vtpm/test/quote_sig similarity index 100% rename from az-cvm-vtpm/az-snp-vtpm/test/quote_sig rename to az-cvm-vtpm/test/quote_sig diff --git a/az-cvm-vtpm/az-snp-vtpm/test/var-data.bin b/az-cvm-vtpm/test/var-data.bin similarity index 100% rename from az-cvm-vtpm/az-snp-vtpm/test/var-data.bin rename to az-cvm-vtpm/test/var-data.bin