diff --git a/Cargo.toml b/Cargo.toml index 2222dfc..c2cdf31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ hex = "0.4" x509-parser = { version="=0.16.0", features=["verify"] } asn1-rs = "=0.6.2" rand = "0.8.5" -tss-esapi = { version = "7.2", optional=true } +tss-esapi = { version = "7.6", optional=true } msru = "0.2.0" colorful = "0.2.2" bitfield = "0.15.0" diff --git a/src/hyperv/check.rs b/src/hyperv/check.rs new file mode 100644 index 0000000..a74b6b2 --- /dev/null +++ b/src/hyperv/check.rs @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +// This file contains code for checking Hyper-V feature is present. + +use std::arch::x86_64::__cpuid; + +const CPUID_GET_HIGHEST_FUNCTION: u32 = 0x80000000; +const CPUID_PROCESSOR_INFO_AND_FEATURE_BITS: u32 = 0x1; + +const CPUID_FEATURE_HYPERVISOR: u32 = 1 << 31; + +const CPUID_HYPERV_SIG: &str = "Microsoft Hv"; +const CPUID_HYPERV_VENDOR_AND_MAX_FUNCTIONS: u32 = 0x40000000; +const CPUID_HYPERV_FEATURES: u32 = 0x40000003; +const CPUID_HYPERV_MIN: u32 = 0x40000005; +const CPUID_HYPERV_MAX: u32 = 0x4000ffff; +const CPUID_HYPERV_ISOLATION: u32 = 1 << 22; +const CPUID_HYPERV_CPU_MANAGEMENT: u32 = 1 << 12; +const CPUID_HYPERV_ISOLATION_CONFIG: u32 = 0x4000000C; +const CPUID_HYPERV_ISOLATION_TYPE_MASK: u32 = 0xf; +const CPUID_HYPERV_ISOLATION_TYPE_SNP: u32 = 2; + +pub fn present() -> bool { + let mut cpuid = unsafe { __cpuid(CPUID_PROCESSOR_INFO_AND_FEATURE_BITS) }; + if (cpuid.ecx & CPUID_FEATURE_HYPERVISOR) == 0 { + return false; + } + + cpuid = unsafe { __cpuid(CPUID_GET_HIGHEST_FUNCTION) }; + if cpuid.eax < CPUID_HYPERV_VENDOR_AND_MAX_FUNCTIONS { + return false; + } + + cpuid = unsafe { __cpuid(CPUID_HYPERV_VENDOR_AND_MAX_FUNCTIONS) }; + if cpuid.eax < CPUID_HYPERV_MIN || cpuid.eax > CPUID_HYPERV_MAX { + return false; + } + + let mut sig: Vec = vec![]; + sig.append(&mut cpuid.ebx.to_le_bytes().to_vec()); + sig.append(&mut cpuid.ecx.to_le_bytes().to_vec()); + sig.append(&mut cpuid.edx.to_le_bytes().to_vec()); + + if sig != CPUID_HYPERV_SIG.as_bytes() { + return false; + } + + cpuid = unsafe { __cpuid(CPUID_HYPERV_FEATURES) }; + + let isolated: bool = (cpuid.ebx & CPUID_HYPERV_ISOLATION) != 0; + let managed: bool = (cpuid.ebx & CPUID_HYPERV_CPU_MANAGEMENT) != 0; + + if !isolated || managed { + return false; + } + + cpuid = unsafe { __cpuid(CPUID_HYPERV_ISOLATION_CONFIG) }; + let mask = cpuid.ebx & CPUID_HYPERV_ISOLATION_TYPE_MASK; + let snp = CPUID_HYPERV_ISOLATION_TYPE_SNP; + + if mask != snp { + return false; + } + + true +} diff --git a/src/hyperv/mod.rs b/src/hyperv/mod.rs index 83d53c4..c0fa704 100644 --- a/src/hyperv/mod.rs +++ b/src/hyperv/mod.rs @@ -1,132 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// This file contains code related to Hyper-V integration (Hypervisor). It provides a flag (`hyperv::present`) indicating whether the SNP Guest is running within a Hyper-V guest environment. -use super::*; - -use std::arch::x86_64::__cpuid; -use std::mem::size_of; - -const CPUID_GET_HIGHEST_FUNCTION: u32 = 0x80000000; -const CPUID_PROCESSOR_INFO_AND_FEATURE_BITS: u32 = 0x1; - -const CPUID_FEATURE_HYPERVISOR: u32 = 1 << 31; - -const CPUID_HYPERV_SIG: &str = "Microsoft Hv"; -const CPUID_HYPERV_VENDOR_AND_MAX_FUNCTIONS: u32 = 0x40000000; -const CPUID_HYPERV_FEATURES: u32 = 0x40000003; -const CPUID_HYPERV_MIN: u32 = 0x40000005; -const CPUID_HYPERV_MAX: u32 = 0x4000ffff; -const CPUID_HYPERV_ISOLATION: u32 = 1 << 22; -const CPUID_HYPERV_CPU_MANAGEMENT: u32 = 1 << 12; -const CPUID_HYPERV_ISOLATION_CONFIG: u32 = 0x4000000C; -const CPUID_HYPERV_ISOLATION_TYPE_MASK: u32 = 0xf; -const CPUID_HYPERV_ISOLATION_TYPE_SNP: u32 = 2; - -const RSV1_SIZE: usize = size_of::() * 8; -const REPORT_SIZE: usize = 1184; -const RSV2_SIZE: usize = size_of::() * 5; -const TOTAL_SIZE: usize = RSV1_SIZE + REPORT_SIZE + RSV2_SIZE; -const REPORT_RANGE: std::ops::Range = RSV1_SIZE..(RSV1_SIZE + REPORT_SIZE); - -pub fn present() -> bool { - let mut cpuid = unsafe { __cpuid(CPUID_PROCESSOR_INFO_AND_FEATURE_BITS) }; - if (cpuid.ecx & CPUID_FEATURE_HYPERVISOR) == 0 { - return false; - } - - cpuid = unsafe { __cpuid(CPUID_GET_HIGHEST_FUNCTION) }; - if cpuid.eax < CPUID_HYPERV_VENDOR_AND_MAX_FUNCTIONS { - return false; - } - - cpuid = unsafe { __cpuid(CPUID_HYPERV_VENDOR_AND_MAX_FUNCTIONS) }; - if cpuid.eax < CPUID_HYPERV_MIN || cpuid.eax > CPUID_HYPERV_MAX { - return false; - } - - let mut sig: Vec = vec![]; - sig.append(&mut cpuid.ebx.to_le_bytes().to_vec()); - sig.append(&mut cpuid.ecx.to_le_bytes().to_vec()); - sig.append(&mut cpuid.edx.to_le_bytes().to_vec()); - - if sig != CPUID_HYPERV_SIG.as_bytes() { - return false; - } - - cpuid = unsafe { __cpuid(CPUID_HYPERV_FEATURES) }; - - let isolated: bool = (cpuid.ebx & CPUID_HYPERV_ISOLATION) != 0; - let managed: bool = (cpuid.ebx & CPUID_HYPERV_CPU_MANAGEMENT) != 0; - - if !isolated || managed { - return false; - } - - cpuid = unsafe { __cpuid(CPUID_HYPERV_ISOLATION_CONFIG) }; - let mask = cpuid.ebx & CPUID_HYPERV_ISOLATION_TYPE_MASK; - let snp = CPUID_HYPERV_ISOLATION_TYPE_SNP; - - if mask != snp { - return false; - } - - true -} - -pub mod report { - use super::*; - - use anyhow::{anyhow, Context}; - use serde::{Deserialize, Serialize}; - use sev::firmware::guest::AttestationReport; - use tss_esapi::{ - abstraction::nv, - handles::NvIndexTpmHandle, - interface_types::{resource_handles::NvAuth, session_handles::AuthSession}, - tcti_ldr::{DeviceConfig, TctiNameConf}, - }; - - const VTPM_HCL_REPORT_NV_INDEX: u32 = 0x01400001; - - #[repr(C)] - #[derive(Deserialize, Serialize, Debug, Clone, Copy)] - struct Hcl { - rsv1: [u32; 8], - report: AttestationReport, - rsv2: [u32; 5], - } - - pub fn get(vmpl: u32) -> Result { - if vmpl > 0 { - eprintln!("Warning: --vmpl argument was ignored because attestation report is pre-fetched at VMPL 0 and stored in vTPM."); - } - let bytes = tpm2_read().context("unable to read attestation report bytes from vTPM")?; - - hcl_report(&bytes) - } - - fn tpm2_read() -> Result> { - let handle = NvIndexTpmHandle::new(VTPM_HCL_REPORT_NV_INDEX) - .context("unable to initialize TPM handle")?; - let mut ctx = tss_esapi::Context::new(TctiNameConf::Device(DeviceConfig::default()))?; - ctx.set_sessions((Some(AuthSession::Password), None, None)); - - nv::read_full(&mut ctx, NvAuth::Owner, handle) - .context("unable to read non-volatile vTPM data") - } - - fn hcl_report(bytes: &[u8]) -> Result { - if bytes.len() < TOTAL_SIZE { - return Err(anyhow!( - "HCL report size mismatch: expected at least {}, got {}", - TOTAL_SIZE, - bytes.len() - )); - } - - let report_bytes = &bytes[REPORT_RANGE]; - - AttestationReport::from_bytes(report_bytes) - .context("Unable to convert HCL report bytes to AttestationReport") - } -} +pub mod check; +pub mod report; +pub(crate) mod tpm; diff --git a/src/hyperv/report.rs b/src/hyperv/report.rs new file mode 100644 index 0000000..d30e329 --- /dev/null +++ b/src/hyperv/report.rs @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +// This file contains code for requesting attestation reports from vTPMs on Azure Confidential VMs. + +use super::tpm; +use anyhow::{anyhow, Context, Result}; +use sev::{firmware::guest::AttestationReport, parser::ByteParser}; +use tss_esapi::{ + abstraction::nv, + handles::NvIndexTpmHandle, + interface_types::{resource_handles::NvAuth, session_handles::AuthSession}, + tcti_ldr::{DeviceConfig, TctiNameConf}, +}; + +const VTPM_HCL_REPORT_NV_INDEX: u32 = 0x01400001; +const VTPM_USER_DATA_NV_INDEX: u32 = 0x01400002; +const VTPM_USER_DATA_SIZE: usize = 64; + +const HCL_REPORT_HEADER_SIZE: usize = 32; +const HW_REPORT_SIZE: usize = 1184; +const REPORT_RANGE: std::ops::Range = + HCL_REPORT_HEADER_SIZE..(HCL_REPORT_HEADER_SIZE + HW_REPORT_SIZE); + +pub fn get(data: [u8; VTPM_USER_DATA_SIZE]) -> Result { + write_user_data_to_vtpm(data).context("unable to write user data to vTPM")?; + let hcl_report_bytes = + read_hcl_report_from_vtpm().context("unable to read attestation report bytes from vTPM")?; + if hcl_report_bytes.len() < HCL_REPORT_HEADER_SIZE + HW_REPORT_SIZE { + return Err(anyhow!( + "HCL report size mismatch: expected at least {}, got {}", + HCL_REPORT_HEADER_SIZE + HW_REPORT_SIZE, + hcl_report_bytes.len() + )); + } + let hw_report_bytes = &hcl_report_bytes[REPORT_RANGE]; + AttestationReport::from_bytes(hw_report_bytes) + .context("unable to convert HCL report bytes to AttestationReport") +} + +fn read_hcl_report_from_vtpm() -> Result> { + let handle = NvIndexTpmHandle::new(VTPM_HCL_REPORT_NV_INDEX) + .context("unable to initialize TPM handle")?; + let mut ctx = tss_esapi::Context::new(TctiNameConf::Device(DeviceConfig::default()))?; + ctx.set_sessions((Some(AuthSession::Password), None, None)); + + nv::read_full(&mut ctx, NvAuth::Owner, handle).context("unable to read non-volatile vTPM data") +} + +fn write_user_data_to_vtpm(data: [u8; VTPM_USER_DATA_SIZE]) -> Result<()> { + let mut ctx = tss_esapi::Context::new(TctiNameConf::Device(DeviceConfig::default()))?; + ctx.set_sessions((Some(AuthSession::Password), None, None)); + + let handle = NvIndexTpmHandle::new(VTPM_USER_DATA_NV_INDEX) + .context("unable to initialize TPM handle")?; + + let result = tpm::find_nv_index(&mut ctx, handle)?; + + if let Some((public, _)) = result { + if public.data_size() != VTPM_USER_DATA_SIZE { + tpm::nv_undefine(&mut ctx, handle)?; + tpm::nv_define(&mut ctx, handle, VTPM_USER_DATA_SIZE)?; + } + } else { + tpm::nv_define(&mut ctx, handle, VTPM_USER_DATA_SIZE)?; + } + + tpm::nv_write(&mut ctx, handle, &data).context("unable to write data to NV index")?; + + Ok(()) +} diff --git a/src/hyperv/tpm.rs b/src/hyperv/tpm.rs new file mode 100644 index 0000000..c3f961e --- /dev/null +++ b/src/hyperv/tpm.rs @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +// This file contains code for handling TPM 2.0. + +use anyhow::{Context, Result}; +use tss_esapi::{ + abstraction::nv, + attributes::NvIndexAttributesBuilder, + handles::{NvIndexHandle, NvIndexTpmHandle}, + interface_types::{ + algorithm::HashingAlgorithm, + resource_handles::{NvAuth, Provision}, + }, + structures::{MaxNvBuffer, Name, NvPublic, NvPublicBuilder}, +}; + +/// Find an NV index +pub fn find_nv_index( + ctx: &mut tss_esapi::Context, + nv_index: NvIndexTpmHandle, +) -> Result> { + let list = nv::list(ctx).context("unable to list NV indices")?; + + let entry = list + .into_iter() + .find(|(public, _)| public.nv_index() == nv_index); + + Ok(entry) +} + +/// Define a new NV index with the specified size +pub fn nv_define( + ctx: &mut tss_esapi::Context, + handle: NvIndexTpmHandle, + len: usize, +) -> Result { + let attributes = NvIndexAttributesBuilder::new() + .with_owner_read(true) + .with_owner_write(true) + .build() + .context("unable to build NV index attributes")?; + + let nv_public = NvPublicBuilder::new() + .with_nv_index(handle) + .with_index_attributes(attributes) + .with_index_name_algorithm(HashingAlgorithm::Sha256) + .with_data_area_size(len) + .build() + .context("unable to build NV public structure")?; + + let index = ctx + .nv_define_space(Provision::Owner, None, nv_public) + .context("unable to define NV index")?; + + Ok(index) +} + +/// Undefine an existing NV index +pub fn nv_undefine(ctx: &mut tss_esapi::Context, handle: NvIndexTpmHandle) -> Result<()> { + let key_handle = ctx + .execute_without_session(|c| c.tr_from_tpm_public(handle.into())) + .context("unable to resolve NV index handle")?; + let index = key_handle.into(); + ctx.nv_undefine_space(Provision::Owner, index) + .context("unable to undefine NV index") +} + +/// Write data to an NV index +pub fn nv_write(ctx: &mut tss_esapi::Context, handle: NvIndexTpmHandle, data: &[u8]) -> Result<()> { + let buffer = MaxNvBuffer::try_from(data).context("unable to create MaxNvBuffer from data")?; + let key_handle = ctx + .execute_without_session(|c| c.tr_from_tpm_public(handle.into())) + .context("unable to resolve NV index handle")?; + let index = key_handle.into(); + ctx.nv_write(NvAuth::Owner, index, buffer, 0) + .context("unable to write data to NV index") +} diff --git a/src/main.rs b/src/main.rs index 639e8c7..6752203 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,13 +76,13 @@ fn main() -> Result<()> { let snpguest = SnpGuest::parse(); #[cfg(feature = "hyperv")] - let hv = hyperv::present(); + let azcvm_present = hyperv::check::present(); #[cfg(not(feature = "hyperv"))] - let hv = false; + let azcvm_present = false; let status = match snpguest.cmd { - SnpGuestCmd::Report(args) => report::get_report(args, hv), + SnpGuestCmd::Report(args) => report::get_report(args, azcvm_present), SnpGuestCmd::Certificates(args) => certs::get_ext_certs(args), SnpGuestCmd::Fetch(subcmd) => fetch::cmd(subcmd), SnpGuestCmd::Verify(subcmd) => verify::cmd(subcmd, snpguest.quiet), diff --git a/src/report.rs b/src/report.rs index 1d84bce..fe6e5d3 100644 --- a/src/report.rs +++ b/src/report.rs @@ -46,75 +46,60 @@ pub struct ReportArgs { #[arg(value_name = "att-report-path", required = true)] pub att_report_path: PathBuf, - /// Use random data for attestation report request. Writes data - /// to ./random-request-file.txt by default, use --request to specify - /// where to write data. - #[arg(short, long, default_value_t = false, conflicts_with = "platform")] - pub random: bool, - - /// Specify an integer VMPL level between 0 and 3 that the Guest is running on. - #[arg(short, long, default_value = "1", value_name = "vmpl")] - pub vmpl: Option, - /// Provide file with data for attestation-report request. If provided /// with random flag, then the random data will be written in the /// provided path. #[arg(value_name = "request-file", required = true)] pub request_file: PathBuf, - /// Expect that the 64-byte report data will already be provided by the platform provider. - #[arg(short, long, conflicts_with = "random")] - pub platform: bool, -} - -impl ReportArgs { - pub fn verify(&self, hyperv: bool) -> Result<()> { - if self.random && self.platform { - return Err(anyhow!( - "--random and --platform both enabled (not allowed). Consult man page." - )); - } - - if self.platform && !hyperv { - #[cfg(feature = "hyperv")] - let msg = "--platform enabled yet Hyper-V guest with SEV-SNP isolation not detected (not allowed). Consult man page."; - #[cfg(not(feature = "hyperv"))] - let msg = - "--platform requires a binary built with --features hyperv. Consult man page."; + /// Use random data for attestation report request and write it to the request file. + #[arg(short, long, default_value_t = false)] + pub random: bool, - return Err(anyhow!(msg)); - } + /// Specify an integer VMPL level between 0 and 3 that the Guest is running on. + #[arg(short, long, default_value = "1", value_name = "vmpl")] + pub vmpl: Option, - Ok(()) - } + /// Request attestation report on vTPM-based Azure Confidential VM. + #[arg(short, long)] + pub azure_cvm: bool, } fn request_hardware_report( - data: Option<[u8; 64]>, + data: [u8; 64], vmpl: Option, - _platform: bool, + _azure_cvm: bool, ) -> Result { #[cfg(feature = "hyperv")] - if _platform { - return hyperv::report::get(vmpl.unwrap_or(0)); + if _azure_cvm { + if vmpl.unwrap_or(0) > 0 { + eprintln!("Warning: --vmpl argument was ignored because attestation report is requested by the paravisor at VMPL 0."); + } + return hyperv::report::get(data); } let mut fw = Firmware::open().context("unable to open /dev/sev-guest")?; Ok(AttestationReport::from_bytes( - fw.get_report(None, data, vmpl) - .context("unable to fetch attestation report")? + fw.get_report(None, Some(data), vmpl) + .context("unable to get attestation report")? .as_slice(), )?) } // Request attestation report and write it into a file -pub fn get_report(args: ReportArgs, hv: bool) -> Result<()> { - args.verify(hv)?; +pub fn get_report(args: ReportArgs, azcvm_present: bool) -> Result<()> { + if args.azure_cvm && !azcvm_present { + #[cfg(feature = "hyperv")] + let msg = + "--azure-cvm enabled yet Hyper-V guest with SEV-SNP isolation not detected (not allowed)."; + #[cfg(not(feature = "hyperv"))] + let msg = + "--azure-cvm requires a binary built with --features hyperv. Please rebuild with --features hyperv."; + return Err(anyhow!(msg)); + } - let data: Option<[u8; 64]> = if args.random { - Some(create_random_request()) - } else if args.platform { - None + let data: [u8; 64] = if args.random { + create_random_request() } else { /* * Read from the request file. @@ -124,10 +109,10 @@ pub fn get_report(args: ReportArgs, hv: bool) -> Result<()> { file.read_exact(&mut bytes) .context("unable to read 64 bytes from REQUEST_FILE")?; - Some(bytes) + bytes }; - let report = request_hardware_report(data, args.vmpl, args.platform)?; + let report = request_hardware_report(data, args.vmpl, args.azure_cvm)?; /* * Serialize and write attestation report. @@ -144,17 +129,8 @@ pub fn get_report(args: ReportArgs, hv: bool) -> Result<()> { * Write reports report data (only for --random or --platform). */ if args.random { - if let Some(data) = data { - reqdata_write(args.request_file, &data) - .context("unable to write random request data to specified file")?; - } else { - return Err(anyhow!("unable to write empty buffer to specified file.")); - } - } else if args.platform { - // Because random data cannot be provided for platform, we will pull the - // data provided by the vTPM from the report. - reqdata_write(args.request_file, &report.report_data) - .context("unable to write platform request data")?; + reqdata_write(args.request_file, &data) + .context("unable to write random request data to specified file")?; } Ok(())