diff --git a/Cargo.lock b/Cargo.lock index dbd70f195f..6fd8ffb12a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8359,6 +8359,7 @@ dependencies = [ "chipset_device_resources", "crypto", "cvm_tracing", + "futures", "getrandom 0.4.2", "guestmem", "guid", diff --git a/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs b/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs index 1d8cefa6f6..06ccd9fa66 100644 --- a/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs +++ b/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs @@ -33,8 +33,24 @@ pub const WRAPPED_KEY_RESPONSE_BUFFER_SIZE: usize = 16 * PAGE_SIZE; /// Currently the number matches the maximum value defined by `get_protocol` pub const KEY_RELEASE_RESPONSE_BUFFER_SIZE: usize = 16 * PAGE_SIZE; /// Number of pages required by the response buffer of AK_CERT request -/// Currently the AK cert request only requires 1 page. -pub const AK_CERT_RESPONSE_BUFFER_SIZE: usize = PAGE_SIZE; +pub const AK_CERT_RESPONSE_BUFFER_SIZE: usize = 2 * PAGE_SIZE; + +pub const TVM_HOST_CERTIFICATION_BINDING_VERSION: u32 = 1; +pub const TVM_HOST_CERTIFICATION_BINDING_HASH_ALG_SHA256: u32 = 1; +pub const TVM_HOST_CERTIFICATION_EVIDENCE_MAGIC: u32 = 0x4543_4854; +pub const TVM_HOST_CERTIFICATION_EVIDENCE_VERSION: u32 = 1; +pub const TVM_HOST_CERTIFICATION_EVIDENCE_FLAG_HOST_CERTIFIED: u32 = 0x0000_0001; +pub const TVM_HOST_CERTIFICATION_EVIDENCE_HEADER_SIZE: usize = 48; +pub const TVM_HOST_CERTIFICATION_IDKS_SIGNATURE_SIZE: usize = 256; +pub const TVM_HOST_CERTIFICATION_EVIDENCE_MAX_SIZE: usize = 2900; +pub const TVM_HOST_CERTIFICATION_RESPONSE_MAGIC: u32 = 0x5243_4854; +pub const TVM_HOST_CERTIFICATION_RESPONSE_VERSION: u32 = 1; +pub const TVM_HOST_CERTIFICATION_RESPONSE_HEADER_SIZE: usize = 32; +pub const TVM_HOST_CERTIFICATION_AK_CERT_MAX_SIZE: usize = 4096; +pub const TVM_HOST_CERTIFICATION_RESPONSE_MAX_SIZE: usize = + TVM_HOST_CERTIFICATION_RESPONSE_HEADER_SIZE + + TVM_HOST_CERTIFICATION_AK_CERT_MAX_SIZE + + TVM_HOST_CERTIFICATION_EVIDENCE_MAX_SIZE; /// Current IGVM Attest response header version. pub const IGVM_ATTEST_RESPONSE_CURRENT_VERSION: IgvmAttestResponseVersion = @@ -170,13 +186,15 @@ impl IgvmAttestRequestHeader { /// 0 - error_code: Requesting IGVM Agent Error code /// 1 - retry: Retry preference /// 2 - skip_hw_unsealing: Skip hardware unsealing in case key release request fails +/// 3 - tvm_host_certification: Request host-certification evidence for a TVM AK certificate #[bitfield(u32)] #[derive(IntoBytes, FromBytes, Immutable, KnownLayout)] pub struct IgvmCapabilityBitMap { pub error_code: bool, pub retry: bool, pub skip_hw_unsealing: bool, - #[bits(29)] + pub tvm_host_certification: bool, + #[bits(28)] _reserved: u32, } @@ -245,7 +263,7 @@ pub struct IgvmSignal { /// The common response header that comply with both V1 and V2 Igvm attest response #[repr(C)] -#[derive(Default, Debug, IntoBytes, FromBytes)] +#[derive(Default, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)] pub struct IgvmAttestCommonResponseHeader { /// Data size pub data_size: u32, @@ -304,6 +322,36 @@ pub struct IgvmAttestAkCertResponseHeader { pub error_info: IgvmErrorInfo, } +/// Stable host-certification evidence persisted in the TVM attestation-report NV index. +#[repr(C)] +#[derive(Default, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)] +pub struct TvmHostCertificationEvidenceHeader { + pub magic: u32, + pub version: u32, + pub header_size: u32, + pub total_size: u32, + pub flags: u32, + pub binding_version: u32, + pub binding_hash_algorithm: u32, + pub report_size: u32, + pub report_signature_size: u32, + pub runtime_data_size: u32, + pub reserved: [u32; 2], +} + +/// Capability-gated AK certificate response payload for host-certified TVMs. +#[repr(C)] +#[derive(Default, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)] +pub struct TvmHostCertificationResponseHeader { + pub magic: u32, + pub version: u32, + pub header_size: u32, + pub total_size: u32, + pub ak_cert_size: u32, + pub evidence_size: u32, + pub reserved: [u32; 2], +} + /// Definition of the runt-time claims, which will be appended to the /// `IgvmAttestRequestBase` in raw bytes. pub mod runtime_claims { @@ -506,3 +554,32 @@ pub mod runtime_claims { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tvm_host_certification_contract() { + assert_eq!( + size_of::(), + TVM_HOST_CERTIFICATION_EVIDENCE_HEADER_SIZE + ); + assert_eq!( + size_of::(), + TVM_HOST_CERTIFICATION_RESPONSE_HEADER_SIZE + ); + assert_eq!( + IgvmCapabilityBitMap::new() + .with_tvm_host_certification(true) + .into_bits(), + 0x0000_0008 + ); + assert_eq!( + TVM_HOST_CERTIFICATION_RESPONSE_MAX_SIZE, + TVM_HOST_CERTIFICATION_RESPONSE_HEADER_SIZE + + TVM_HOST_CERTIFICATION_AK_CERT_MAX_SIZE + + TVM_HOST_CERTIFICATION_EVIDENCE_MAX_SIZE + ); + } +} diff --git a/openhcl/underhill_attestation/src/igvm_attest/ak_cert.rs b/openhcl/underhill_attestation/src/igvm_attest/ak_cert.rs index 284c423f3a..332523db17 100644 --- a/openhcl/underhill_attestation/src/igvm_attest/ak_cert.rs +++ b/openhcl/underhill_attestation/src/igvm_attest/ak_cert.rs @@ -7,6 +7,7 @@ use crate::igvm_attest::Error as CommonError; use crate::igvm_attest::parse_response_header; use thiserror::Error; +use zerocopy::FromBytes; /// AkCertError is returned by parse_ak_cert_response() in emuplat/tpm.rs #[derive(Debug, Error)] @@ -27,15 +28,43 @@ pub enum AkCertError { ParseHeader(#[source] CommonError), #[error("invalid response header version: {0}")] InvalidResponseVersion(u32), + #[error("invalid TVM host-certification response")] + InvalidHostCertificationResponse, + #[error("invalid TVM host-certification evidence")] + InvalidHostCertificationEvidence, } -/// Parse a `AK_CERT_REQUEST` response and return the payload (i.e., the AK cert). +/// Parsed AK certificate response. +#[derive(Debug, PartialEq, Eq)] +pub struct AkCertResponse { + pub ak_cert: Vec, + pub host_certification_evidence: Option>, +} + +/// Parse an `AK_CERT_REQUEST` response. /// -/// Returns `Ok(Vec)` on successfully validating the response, otherwise returns an error. -pub fn parse_response(response: &[u8]) -> Result, AkCertError> { +/// Legacy responses contain only the AK certificate. Capability-gated TVM +/// responses contain a versioned wrapper with the AK certificate and one +/// complete host-certification evidence payload. +pub fn parse_response(response: &[u8]) -> Result { use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestAkCertResponseHeader; use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestCommonResponseHeader; use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestResponseVersion; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_AK_CERT_MAX_SIZE; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_BINDING_HASH_ALG_SHA256; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_BINDING_VERSION; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_EVIDENCE_FLAG_HOST_CERTIFIED; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_EVIDENCE_HEADER_SIZE; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_EVIDENCE_MAGIC; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_EVIDENCE_MAX_SIZE; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_EVIDENCE_VERSION; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_IDKS_SIGNATURE_SIZE; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_RESPONSE_HEADER_SIZE; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_RESPONSE_MAGIC; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_RESPONSE_MAX_SIZE; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_RESPONSE_VERSION; + use openhcl_attestation_protocol::igvm_attest::get::TvmHostCertificationEvidenceHeader; + use openhcl_attestation_protocol::igvm_attest::get::TvmHostCertificationResponseHeader; let header = parse_response_header(response).map_err(AkCertError::ParseHeader)?; @@ -54,7 +83,90 @@ pub fn parse_response(response: &[u8]) -> Result, AkCertError> { }); } - Ok(response[header_size..data_size].to_vec()) + let payload = &response[header_size..data_size]; + if payload.len() < size_of::() + || u32::from_le_bytes( + payload[..size_of::()] + .try_into() + .expect("fixed-size slice"), + ) != TVM_HOST_CERTIFICATION_RESPONSE_MAGIC + { + return Ok(AkCertResponse { + ak_cert: payload.to_vec(), + host_certification_evidence: None, + }); + } + + if payload.len() < TVM_HOST_CERTIFICATION_RESPONSE_HEADER_SIZE + || payload.len() > TVM_HOST_CERTIFICATION_RESPONSE_MAX_SIZE + { + return Err(AkCertError::InvalidHostCertificationResponse); + } + + let (response_header, _) = TvmHostCertificationResponseHeader::read_from_prefix(payload) + .map_err(|_| AkCertError::InvalidHostCertificationResponse)?; + let response_total_size = usize::try_from(response_header.total_size) + .map_err(|_| AkCertError::InvalidHostCertificationResponse)?; + let ak_cert_size = usize::try_from(response_header.ak_cert_size) + .map_err(|_| AkCertError::InvalidHostCertificationResponse)?; + let evidence_size = usize::try_from(response_header.evidence_size) + .map_err(|_| AkCertError::InvalidHostCertificationResponse)?; + let expected_response_size = TVM_HOST_CERTIFICATION_RESPONSE_HEADER_SIZE + .checked_add(ak_cert_size) + .and_then(|size| size.checked_add(evidence_size)) + .ok_or(AkCertError::InvalidHostCertificationResponse)?; + if response_header.magic != TVM_HOST_CERTIFICATION_RESPONSE_MAGIC + || response_header.version != TVM_HOST_CERTIFICATION_RESPONSE_VERSION + || response_header.header_size as usize != TVM_HOST_CERTIFICATION_RESPONSE_HEADER_SIZE + || response_total_size != payload.len() + || expected_response_size != payload.len() + || ak_cert_size == 0 + || ak_cert_size > TVM_HOST_CERTIFICATION_AK_CERT_MAX_SIZE + || evidence_size < TVM_HOST_CERTIFICATION_EVIDENCE_HEADER_SIZE + || evidence_size > TVM_HOST_CERTIFICATION_EVIDENCE_MAX_SIZE + || response_header.reserved != [0; 2] + { + return Err(AkCertError::InvalidHostCertificationResponse); + } + + let ak_cert_offset = TVM_HOST_CERTIFICATION_RESPONSE_HEADER_SIZE; + let evidence_offset = ak_cert_offset + ak_cert_size; + let evidence = &payload[evidence_offset..]; + let (evidence_header, _) = TvmHostCertificationEvidenceHeader::read_from_prefix(evidence) + .map_err(|_| AkCertError::InvalidHostCertificationEvidence)?; + let evidence_total_size = usize::try_from(evidence_header.total_size) + .map_err(|_| AkCertError::InvalidHostCertificationEvidence)?; + let report_size = usize::try_from(evidence_header.report_size) + .map_err(|_| AkCertError::InvalidHostCertificationEvidence)?; + let report_signature_size = usize::try_from(evidence_header.report_signature_size) + .map_err(|_| AkCertError::InvalidHostCertificationEvidence)?; + let runtime_data_size = usize::try_from(evidence_header.runtime_data_size) + .map_err(|_| AkCertError::InvalidHostCertificationEvidence)?; + let expected_evidence_size = TVM_HOST_CERTIFICATION_EVIDENCE_HEADER_SIZE + .checked_add(report_size) + .and_then(|size| size.checked_add(report_signature_size)) + .and_then(|size| size.checked_add(runtime_data_size)) + .ok_or(AkCertError::InvalidHostCertificationEvidence)?; + if evidence_header.magic != TVM_HOST_CERTIFICATION_EVIDENCE_MAGIC + || evidence_header.version != TVM_HOST_CERTIFICATION_EVIDENCE_VERSION + || evidence_header.header_size as usize != TVM_HOST_CERTIFICATION_EVIDENCE_HEADER_SIZE + || evidence_total_size != evidence.len() + || expected_evidence_size != evidence.len() + || evidence_header.flags != TVM_HOST_CERTIFICATION_EVIDENCE_FLAG_HOST_CERTIFIED + || evidence_header.binding_version != TVM_HOST_CERTIFICATION_BINDING_VERSION + || evidence_header.binding_hash_algorithm != TVM_HOST_CERTIFICATION_BINDING_HASH_ALG_SHA256 + || report_size == 0 + || report_signature_size != TVM_HOST_CERTIFICATION_IDKS_SIGNATURE_SIZE + || runtime_data_size == 0 + || evidence_header.reserved != [0; 2] + { + return Err(AkCertError::InvalidHostCertificationEvidence); + } + + Ok(AkCertResponse { + ak_cert: payload[ak_cert_offset..evidence_offset].to_vec(), + host_certification_evidence: Some(evidence.to_vec()), + }) } #[cfg(test)] @@ -62,7 +174,111 @@ mod tests { use super::*; use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestAkCertResponseHeader; use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestCommonResponseHeader; + use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestResponseVersion; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_AK_CERT_MAX_SIZE; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_BINDING_HASH_ALG_SHA256; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_BINDING_VERSION; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_EVIDENCE_FLAG_HOST_CERTIFIED; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_EVIDENCE_HEADER_SIZE; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_EVIDENCE_MAGIC; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_EVIDENCE_MAX_SIZE; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_EVIDENCE_VERSION; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_IDKS_SIGNATURE_SIZE; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_RESPONSE_HEADER_SIZE; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_RESPONSE_MAGIC; + use openhcl_attestation_protocol::igvm_attest::get::TVM_HOST_CERTIFICATION_RESPONSE_VERSION; + use openhcl_attestation_protocol::igvm_attest::get::TvmHostCertificationEvidenceHeader; + use openhcl_attestation_protocol::igvm_attest::get::TvmHostCertificationResponseHeader; use zerocopy::FromBytes; + use zerocopy::IntoBytes; + + fn host_certification_response( + response_version: IgvmAttestResponseVersion, + ak_cert: &[u8], + report: &[u8], + runtime_data: &[u8], + ) -> Vec { + let signature = vec![0xa5; TVM_HOST_CERTIFICATION_IDKS_SIGNATURE_SIZE]; + let evidence_size = TVM_HOST_CERTIFICATION_EVIDENCE_HEADER_SIZE + + report.len() + + signature.len() + + runtime_data.len(); + let evidence_header = TvmHostCertificationEvidenceHeader { + magic: TVM_HOST_CERTIFICATION_EVIDENCE_MAGIC, + version: TVM_HOST_CERTIFICATION_EVIDENCE_VERSION, + header_size: TVM_HOST_CERTIFICATION_EVIDENCE_HEADER_SIZE as u32, + total_size: evidence_size as u32, + flags: TVM_HOST_CERTIFICATION_EVIDENCE_FLAG_HOST_CERTIFIED, + binding_version: TVM_HOST_CERTIFICATION_BINDING_VERSION, + binding_hash_algorithm: TVM_HOST_CERTIFICATION_BINDING_HASH_ALG_SHA256, + report_size: report.len() as u32, + report_signature_size: signature.len() as u32, + runtime_data_size: runtime_data.len() as u32, + reserved: [0; 2], + }; + let mut evidence = Vec::with_capacity(evidence_size); + evidence.extend_from_slice(evidence_header.as_bytes()); + evidence.extend_from_slice(report); + evidence.extend_from_slice(&signature); + evidence.extend_from_slice(runtime_data); + + let payload_size = + TVM_HOST_CERTIFICATION_RESPONSE_HEADER_SIZE + ak_cert.len() + evidence.len(); + let response_header = TvmHostCertificationResponseHeader { + magic: TVM_HOST_CERTIFICATION_RESPONSE_MAGIC, + version: TVM_HOST_CERTIFICATION_RESPONSE_VERSION, + header_size: TVM_HOST_CERTIFICATION_RESPONSE_HEADER_SIZE as u32, + total_size: payload_size as u32, + ak_cert_size: ak_cert.len() as u32, + evidence_size: evidence.len() as u32, + reserved: [0; 2], + }; + let outer_header_size = match response_version { + IgvmAttestResponseVersion::VERSION_1 => size_of::(), + IgvmAttestResponseVersion::VERSION_2 => size_of::(), + _ => unreachable!("unsupported test response version"), + }; + let data_size = (outer_header_size + payload_size) as u32; + + let mut response = Vec::with_capacity(data_size as usize); + match response_version { + IgvmAttestResponseVersion::VERSION_1 => { + response.extend_from_slice( + IgvmAttestCommonResponseHeader { + data_size, + version: response_version, + } + .as_bytes(), + ); + } + IgvmAttestResponseVersion::VERSION_2 => { + response.extend_from_slice( + IgvmAttestAkCertResponseHeader { + data_size, + version: response_version, + error_info: Default::default(), + } + .as_bytes(), + ); + } + _ => unreachable!("unsupported test response version"), + } + response.extend_from_slice(response_header.as_bytes()); + response.extend_from_slice(ak_cert); + response.extend_from_slice(&evidence); + response + } + + fn standard_host_certification_response( + response_version: IgvmAttestResponseVersion, + ) -> Vec { + host_certification_response( + response_version, + &[0x30, 0x82, 0x01, 0x00], + &[1, 2, 3, 4], + br#"{"keys":[]}"#, + ) + } #[test] fn test_undersized_response() { @@ -119,8 +335,9 @@ mod tests { let payload = result.unwrap(); let data_size = parse_response_header(&VALID_RESPONSE).unwrap().data_size as usize; - assert_eq!(payload.len(), data_size - HEADER_SIZE); - assert_eq!(payload, &VALID_RESPONSE[HEADER_SIZE..data_size]); + assert_eq!(payload.ak_cert.len(), data_size - HEADER_SIZE); + assert_eq!(payload.ak_cert, &VALID_RESPONSE[HEADER_SIZE..data_size]); + assert!(payload.host_certification_evidence.is_none()); } #[test] @@ -153,7 +370,129 @@ mod tests { let payload = result.unwrap(); let data_size = parse_response_header(&VALID_RESPONSE).unwrap().data_size as usize; - assert_eq!(payload.len(), data_size - HEADER_SIZE); - assert_eq!(payload, &VALID_RESPONSE[HEADER_SIZE..data_size]); + assert_eq!(payload.ak_cert.len(), data_size - HEADER_SIZE); + assert_eq!(payload.ak_cert, &VALID_RESPONSE[HEADER_SIZE..data_size]); + assert!(payload.host_certification_evidence.is_none()); + } + + #[test] + fn test_valid_host_certification_response() { + for response_version in [ + IgvmAttestResponseVersion::VERSION_1, + IgvmAttestResponseVersion::VERSION_2, + ] { + let response = standard_host_certification_response(response_version); + let parsed = parse_response(&response).expect("valid response"); + assert_eq!(parsed.ak_cert, [0x30, 0x82, 0x01, 0x00]); + let evidence = parsed + .host_certification_evidence + .expect("host-certification evidence"); + assert!(evidence.len() > TVM_HOST_CERTIFICATION_EVIDENCE_HEADER_SIZE); + } + } + + #[test] + fn test_host_certification_response_rejects_invalid_wrapper() { + let mutations: [fn(&mut TvmHostCertificationResponseHeader); 6] = [ + |header| header.version += 1, + |header| header.header_size -= 1, + |header| header.total_size -= 1, + |header| header.ak_cert_size = 0, + |header| header.evidence_size = 0, + |header| header.reserved[0] = 1, + ]; + for mutate in mutations { + let mut response = + standard_host_certification_response(IgvmAttestResponseVersion::VERSION_2); + let offset = size_of::(); + let (header, _) = + TvmHostCertificationResponseHeader::mut_from_prefix(&mut response[offset..]) + .expect("response header"); + mutate(header); + assert!(matches!( + parse_response(&response), + Err(AkCertError::InvalidHostCertificationResponse) + )); + } + } + + #[test] + fn test_host_certification_response_rejects_invalid_evidence() { + let mutations: [fn(&mut TvmHostCertificationEvidenceHeader); 12] = [ + |header| header.magic ^= 1, + |header| header.version += 1, + |header| header.header_size -= 1, + |header| header.total_size -= 1, + |header| header.flags = 0, + |header| header.binding_version += 1, + |header| header.binding_hash_algorithm += 1, + |header| header.report_size = 0, + |header| header.report_signature_size -= 1, + |header| header.runtime_data_size = 0, + |header| header.reserved[0] = 1, + |header| header.reserved[1] = 1, + ]; + for mutate in mutations { + let mut response = + standard_host_certification_response(IgvmAttestResponseVersion::VERSION_2); + let response_offset = size_of::(); + let (response_header, _) = + TvmHostCertificationResponseHeader::read_from_prefix(&response[response_offset..]) + .expect("response header"); + let evidence_offset = response_offset + + TVM_HOST_CERTIFICATION_RESPONSE_HEADER_SIZE + + response_header.ak_cert_size as usize; + let (evidence_header, _) = TvmHostCertificationEvidenceHeader::mut_from_prefix( + &mut response[evidence_offset..], + ) + .expect("evidence header"); + mutate(evidence_header); + assert!(matches!( + parse_response(&response), + Err(AkCertError::InvalidHostCertificationEvidence) + )); + } + } + + #[test] + fn test_host_certification_response_size_boundaries() { + let ak_cert = vec![0x30; TVM_HOST_CERTIFICATION_AK_CERT_MAX_SIZE]; + let report = [1]; + let runtime_data = vec![ + 2; + TVM_HOST_CERTIFICATION_EVIDENCE_MAX_SIZE + - TVM_HOST_CERTIFICATION_EVIDENCE_HEADER_SIZE + - TVM_HOST_CERTIFICATION_IDKS_SIGNATURE_SIZE + - report.len() + ]; + let response = host_certification_response( + IgvmAttestResponseVersion::VERSION_2, + &ak_cert, + &report, + &runtime_data, + ); + let parsed = parse_response(&response).expect("maximum-sized response"); + assert_eq!( + parsed.ak_cert.len(), + TVM_HOST_CERTIFICATION_AK_CERT_MAX_SIZE + ); + assert_eq!( + parsed + .host_certification_evidence + .expect("host-certification evidence") + .len(), + TVM_HOST_CERTIFICATION_EVIDENCE_MAX_SIZE + ); + + let oversized_response = host_certification_response( + IgvmAttestResponseVersion::VERSION_2, + &ak_cert, + &report, + &[runtime_data, vec![3]].concat(), + ); + assert!(matches!( + parse_response(&oversized_response), + Err(AkCertError::InvalidHostCertificationResponse) + )); } } diff --git a/openhcl/underhill_attestation/src/igvm_attest/mod.rs b/openhcl/underhill_attestation/src/igvm_attest/mod.rs index 85543fa8a7..4060585c88 100644 --- a/openhcl/underhill_attestation/src/igvm_attest/mod.rs +++ b/openhcl/underhill_attestation/src/igvm_attest/mod.rs @@ -104,6 +104,8 @@ pub struct IgvmAttestRequestHelper { runtime_claims_hash: [u8; tee_call::REPORT_DATA_SIZE], /// THe hash type of the `runtime_claims_hash`. hash_type: IgvmAttestHashType, + /// Whether to request TVM host-certification evidence. + request_tvm_host_certification: bool, } impl IgvmAttestRequestHelper { @@ -139,6 +141,7 @@ impl IgvmAttestRequestHelper { runtime_claims, runtime_claims_hash, hash_type, + request_tvm_host_certification: false, } } @@ -183,6 +186,7 @@ impl IgvmAttestRequestHelper { runtime_claims, runtime_claims_hash, hash_type, + request_tvm_host_certification: false, } } @@ -196,6 +200,11 @@ impl IgvmAttestRequestHelper { self.request_type = request_type } + /// Request host-certification evidence for a TVM AK certificate. + pub fn set_request_tvm_host_certification(&mut self, value: bool) { + self.request_tvm_host_certification = value; + } + /// Create the request in raw bytes. pub fn create_request( &self, @@ -209,6 +218,7 @@ impl IgvmAttestRequestHelper { attestation_report, &self.report_type, self.hash_type, + self.request_tvm_host_certification, ) } } @@ -273,6 +283,7 @@ fn create_request( attestation_report: &[u8], report_type: &ReportType, hash_type: IgvmAttestHashType, + request_tvm_host_certification: bool, ) -> Result, Error> { use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestBase; use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestData; @@ -318,7 +329,8 @@ fn create_request( let capability_bitmap = IgvmCapabilityBitMap::new() .with_error_code(true) .with_retry(true) - .with_skip_hw_unsealing(true); + .with_skip_hw_unsealing(true) + .with_tvm_host_certification(request_tvm_host_certification); let ext = IgvmAttestRequestDataExt::new(capability_bitmap); buffer.extend_from_slice(ext.as_bytes()); } @@ -373,6 +385,7 @@ mod tests { &[0u8; openhcl_attestation_protocol::igvm_attest::get::SNP_VM_REPORT_SIZE], &ReportType::Snp, IgvmAttestHashType::SHA_256, + false, ); assert!(result.is_ok()); @@ -383,6 +396,7 @@ mod tests { &[0u8; openhcl_attestation_protocol::igvm_attest::get::SNP_VM_REPORT_SIZE + 1], &ReportType::Snp, IgvmAttestHashType::SHA_256, + false, ); assert!(result.is_err()); } @@ -403,6 +417,7 @@ mod tests { &attestation_report, &ReportType::Snp, IgvmAttestHashType::SHA_256, + false, ) .expect("request generation"); @@ -444,6 +459,7 @@ mod tests { &attestation_report, &ReportType::Snp, IgvmAttestHashType::SHA_256, + true, ) .expect("request generation"); @@ -469,6 +485,7 @@ mod tests { assert!(ext.capability_bitmap.error_code()); assert!(ext.capability_bitmap.retry()); assert!(ext.capability_bitmap.skip_hw_unsealing()); + assert!(ext.capability_bitmap.tvm_host_certification()); assert_eq!( buffer.len(), @@ -480,6 +497,31 @@ mod tests { ); } + #[test] + fn test_create_request_version2_host_certification_is_opt_in() { + use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestBase; + use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestDataExt; + use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestVersion; + + let attestation_report = + vec![0u8; openhcl_attestation_protocol::igvm_attest::get::SNP_VM_REPORT_SIZE]; + let buffer = create_request( + IgvmAttestRequestVersion::VERSION_2, + IgvmAttestRequestType::AK_CERT_REQUEST, + &[], + &attestation_report, + &ReportType::Snp, + IgvmAttestHashType::SHA_256, + false, + ) + .expect("request generation"); + + let ext_offset = size_of::(); + let (ext, _) = IgvmAttestRequestDataExt::read_from_prefix(&buffer[ext_offset..]) + .expect("parse IgvmAttestRequestDataExt"); + assert!(!ext.capability_bitmap.tvm_host_certification()); + } + #[test] fn test_transfer_key_jwk() { const EXPECTED_JWK: &str = r#"[{"kid":"HCLTransferKey","key_ops":["encrypt"],"kty":"RSA","e":"RVhQT05FTlQ","n":"TU9EVUxVUw"}]"#; diff --git a/openhcl/underhill_attestation/src/lib.rs b/openhcl/underhill_attestation/src/lib.rs index 25513d9be9..25fb4d9a68 100644 --- a/openhcl/underhill_attestation/src/lib.rs +++ b/openhcl/underhill_attestation/src/lib.rs @@ -21,6 +21,7 @@ mod test_helpers; pub use igvm_attest::Error as IgvmAttestError; pub use igvm_attest::IgvmAttestRequestHelper; +pub use igvm_attest::ak_cert::AkCertResponse; pub use igvm_attest::ak_cert::parse_response as parse_ak_cert_response; use crate::hardware_key_sealing::HardwareKeySealingError; diff --git a/openhcl/underhill_core/src/emuplat/tpm.rs b/openhcl/underhill_core/src/emuplat/tpm.rs index 2d4cf1404b..09142fd419 100644 --- a/openhcl/underhill_core/src/emuplat/tpm.rs +++ b/openhcl/underhill_core/src/emuplat/tpm.rs @@ -29,6 +29,7 @@ pub struct TpmRequestAkCertHelper { attestation_type: AttestationType, attestation_vm_config: AttestationVmConfig, attestation_agent_data: Option>, + tvm_host_certification: bool, } impl TpmRequestAkCertHelper { @@ -38,6 +39,7 @@ impl TpmRequestAkCertHelper { attestation_type: AttestationType, attestation_vm_config: AttestationVmConfig, attestation_agent_data: Option>, + tvm_host_certification: bool, ) -> Self { Self { get_client, @@ -45,6 +47,7 @@ impl TpmRequestAkCertHelper { attestation_type, attestation_vm_config, attestation_agent_data, + tvm_host_certification, } } } @@ -67,7 +70,7 @@ impl RequestAkCert for TpmRequestAkCertHelper { AttestationType::Vbs => Some(tee_call::TeeType::Vbs), AttestationType::Host => None, }; - let ak_cert_request_helper = + let mut ak_cert_request_helper = underhill_attestation::IgvmAttestRequestHelper::prepare_ak_cert_request( tee_type, ak_pub_exponent, @@ -77,6 +80,7 @@ impl RequestAkCert for TpmRequestAkCertHelper { &self.attestation_vm_config, guest_input, ); + ak_cert_request_helper.set_request_tvm_host_certification(self.tvm_host_certification); let attestation_report = if let Some(tee_call) = &self.tee_call { tee_call @@ -106,7 +110,10 @@ impl RequestAkCert for TpmRequestAkCertHelper { async fn request_ak_cert( &self, request: Vec, - ) -> Result, Box> { + ) -> Result< + tpm_device::ak_cert::AkCertRequestResult, + Box, + > { let agent_data = self.attestation_agent_data.clone().unwrap_or_default(); let result = self .get_client @@ -116,10 +123,16 @@ impl RequestAkCert for TpmRequestAkCertHelper { underhill_attestation::parse_ak_cert_response(&result.response)? } else { // Let the caller to handle the empty response. - vec![] + underhill_attestation::AkCertResponse { + ak_cert: vec![], + host_certification_evidence: None, + } }; - Ok(payload) + Ok(tpm_device::ak_cert::AkCertRequestResult { + ak_cert: payload.ak_cert, + host_certification_evidence: payload.host_certification_evidence, + }) } } @@ -181,6 +194,7 @@ pub mod resources { attestation_type: AttestationType, attestation_vm_config: AttestationVmConfig, attestation_agent_data: Option>, + tvm_host_certification: bool, } impl GetTpmRequestAkCertHelperHandle { @@ -188,11 +202,13 @@ pub mod resources { attestation_type: AttestationType, attestation_vm_config: AttestationVmConfig, attestation_agent_data: Option>, + tvm_host_certification: bool, ) -> Self { Self { attestation_type, attestation_vm_config, attestation_agent_data, + tvm_host_certification, } } } @@ -242,6 +258,7 @@ pub mod resources { handle.attestation_type, handle.attestation_vm_config, handle.attestation_agent_data, + handle.tvm_host_certification, ) .into()) } diff --git a/openhcl/underhill_core/src/lib.rs b/openhcl/underhill_core/src/lib.rs index a7892f9fdf..d97323e772 100644 --- a/openhcl/underhill_core/src/lib.rs +++ b/openhcl/underhill_core/src/lib.rs @@ -355,6 +355,7 @@ async fn launch_workers( efi_diagnostics_rate_limit: opt.efi_diagnostics_rate_limit, strict_encryption_policy: opt.strict_encryption_policy, attempt_ak_cert_callback: opt.attempt_ak_cert_callback, + tvm_host_certification: opt.tvm_host_certification, enable_vpci_relay: opt.enable_vpci_relay, disable_proxy_redirect: opt.disable_proxy_redirect, disable_lower_vtl_timer_virt: opt.disable_lower_vtl_timer_virt, diff --git a/openhcl/underhill_core/src/options.rs b/openhcl/underhill_core/src/options.rs index c96127db10..3c156197e0 100644 --- a/openhcl/underhill_core/src/options.rs +++ b/openhcl/underhill_core/src/options.rs @@ -333,6 +333,10 @@ pub struct Options { /// If not specified, use the configuration in DPSv2 ManagementVtlFeatures. pub attempt_ak_cert_callback: Option, + /// (HCL_TVM_HOST_CERTIFICATION=1) Request and persist TVM + /// host-certification evidence. + pub tvm_host_certification: Option, + /// (OPENHCL_ENABLE_VPCI_RELAY=1) Enable the VPCI relay. pub enable_vpci_relay: Option, @@ -539,6 +543,7 @@ impl Options { .transpose()?; let strict_encryption_policy = parse_env_bool_opt("HCL_STRICT_ENCRYPTION_POLICY"); let attempt_ak_cert_callback = parse_env_bool_opt("HCL_ATTEMPT_AK_CERT_CALLBACK"); + let tvm_host_certification = parse_env_bool_opt("HCL_TVM_HOST_CERTIFICATION"); let enable_vpci_relay = parse_env_bool_opt("OPENHCL_ENABLE_VPCI_RELAY"); let disable_proxy_redirect = parse_env_bool("OPENHCL_DISABLE_PROXY_REDIRECT"); let disable_lower_vtl_timer_virt = parse_env_bool("OPENHCL_DISABLE_LOWER_VTL_TIMER_VIRT"); @@ -610,6 +615,7 @@ impl Options { efi_diagnostics_rate_limit, strict_encryption_policy, attempt_ak_cert_callback, + tvm_host_certification, enable_vpci_relay, disable_proxy_redirect, disable_lower_vtl_timer_virt, diff --git a/openhcl/underhill_core/src/worker.rs b/openhcl/underhill_core/src/worker.rs index 192f0bbf40..ed2412d6b8 100644 --- a/openhcl/underhill_core/src/worker.rs +++ b/openhcl/underhill_core/src/worker.rs @@ -310,6 +310,8 @@ pub struct UnderhillEnvCfg { pub strict_encryption_policy: Option, /// Attempt to renew the AK cert pub attempt_ak_cert_callback: Option, + /// Request and persist TVM host-certification evidence + pub tvm_host_certification: Option, /// Enable the VPCI relay pub enable_vpci_relay: Option, /// Disable proxy interrupt redirection @@ -1609,6 +1611,13 @@ async fn new_underhill_vm( .set_attempt_ak_cert_callback(value); } + if let Some(value) = env_cfg.tvm_host_certification { + tracing::info!("using HCL_TVM_HOST_CERTIFICATION={value} from cmdline"); + dps.general + .management_vtl_features + .set_tvm_host_certification(value); + } + dps }; @@ -3035,10 +3044,13 @@ async fn new_underhill_vm( }; let ak_cert_type = { + let tvm_host_certification = attestation_type == AttestationType::Host + && dps.general.management_vtl_features.tvm_host_certification(); let request_ak_cert = GetTpmRequestAkCertHelperHandle::new( attestation_type, attestation_vm_config, platform_attestation_data.agent_data, + tvm_host_certification, ) .into_resource(); @@ -3047,17 +3059,22 @@ async fn new_underhill_vm( TpmAkCertTypeResource::HwAttested(request_ak_cert) } AttestationType::Vbs => TpmAkCertTypeResource::SwAttested(request_ak_cert), - AttestationType::Host => TpmAkCertTypeResource::Trusted( - request_ak_cert, - dps.general + AttestationType::Host => { + let handle_ak_cert = dps + .general .management_vtl_features .control_ak_cert_provisioning() .then(|| { dps.general .management_vtl_features .attempt_ak_cert_callback() - }), - ), + }); + if tvm_host_certification { + TpmAkCertTypeResource::HostCertified(request_ak_cert, handle_ak_cert) + } else { + TpmAkCertTypeResource::Trusted(request_ak_cert, handle_ak_cert) + } + } AttestationType::Cca => { anyhow::bail!( "CCA attestation is not supported for TPM AK certificate provisioning" diff --git a/petri/src/vm/hyperv/powershell.rs b/petri/src/vm/hyperv/powershell.rs index cb2f5dfff5..95a280d390 100644 --- a/petri/src/vm/hyperv/powershell.rs +++ b/petri/src/vm/hyperv/powershell.rs @@ -237,7 +237,8 @@ pub struct HyperVManagementVtlFeatureFlags { pub control_ak_cert_provisioning: bool, pub attempt_ak_cert_callback: bool, pub tx_only_serial_port: bool, - #[bits(59)] + pub tvm_host_certification: bool, + #[bits(58)] pub _reserved2: u64, } diff --git a/vm/devices/get/get_protocol/src/dps_json.rs b/vm/devices/get/get_protocol/src/dps_json.rs index 7773b142ed..d4e0289da5 100644 --- a/vm/devices/get/get_protocol/src/dps_json.rs +++ b/vm/devices/get/get_protocol/src/dps_json.rs @@ -181,7 +181,8 @@ pub struct ManagementVtlFeatures { pub control_ak_cert_provisioning: bool, pub attempt_ak_cert_callback: bool, pub tx_only_serial_port: bool, - #[bits(59)] + pub tvm_host_certification: bool, + #[bits(58)] pub _reserved2: u64, } @@ -315,4 +316,14 @@ mod test { )) .unwrap(); } + + #[test] + fn tvm_host_certification_is_default_off_at_bit_five() { + let features = ManagementVtlFeatures::new(); + assert!(!features.tvm_host_certification()); + assert_eq!( + features.with_tvm_host_certification(true).into_bits(), + 1 << 5 + ); + } } diff --git a/vm/devices/tpm/tpm_device/Cargo.toml b/vm/devices/tpm/tpm_device/Cargo.toml index 26967f9891..0f1da69e77 100644 --- a/vm/devices/tpm/tpm_device/Cargo.toml +++ b/vm/devices/tpm/tpm_device/Cargo.toml @@ -47,5 +47,8 @@ ms-tpm-20-ref = { optional = true, workspace = true, features = ["openssl"] } [target.'cfg(not(target_env = "musl"))'.dependencies] ms-tpm-20-ref = { optional = true, workspace = true, features = ["openssl", "vendored"] } +[dev-dependencies] +futures.workspace = true + [lints] workspace = true diff --git a/vm/devices/tpm/tpm_device/src/ak_cert.rs b/vm/devices/tpm/tpm_device/src/ak_cert.rs index 814563f655..9c86adcaa6 100644 --- a/vm/devices/tpm/tpm_device/src/ak_cert.rs +++ b/vm/devices/tpm/tpm_device/src/ak_cert.rs @@ -15,6 +15,8 @@ pub enum TpmAkCertType { /// whether OpenHCL handles renewal. /// Used by TVM Trusted(Arc, Option), + /// Authorized TVM AK cert with IDK_S-backed host-certification evidence. + HostCertified(Arc, Option), /// Authorized and hardware-attested AK cert (backed by /// a TEE attestation report). /// Used by CVM @@ -32,18 +34,33 @@ impl TpmAkCertType { TpmAkCertType::HwAttested(helper) => Some(helper), TpmAkCertType::SwAttested(helper) => Some(helper), TpmAkCertType::Trusted(helper, _) => Some(helper), + TpmAkCertType::HostCertified(helper, _) => Some(helper), TpmAkCertType::None => None, } } - /// Returns true if this AKCert type is attested, either with a TEE - /// attestation report or a software-based VM attestation report. - pub fn attested(&self) -> bool { + /// Returns true if this AKCert type exposes an attestation-report NV index. + pub fn has_attestation_report(&self) -> bool { match self { - TpmAkCertType::HwAttested(_) | TpmAkCertType::SwAttested(_) => true, + TpmAkCertType::HwAttested(_) + | TpmAkCertType::SwAttested(_) + | TpmAkCertType::HostCertified(_, _) => true, TpmAkCertType::Trusted(_, _) | TpmAkCertType::None => false, } } + + /// Returns true if the attestation report is generated locally on NV read. + pub fn refreshes_attestation_report(&self) -> bool { + matches!( + self, + TpmAkCertType::HwAttested(_) | TpmAkCertType::SwAttested(_) + ) + } + + /// Returns true if the attestation report must be preserved across boot. + pub fn preserves_attestation_report(&self) -> bool { + matches!(self, TpmAkCertType::HostCertified(_, _)) + } } impl CanResolveTo for RequestAkCertKind { @@ -54,6 +71,12 @@ impl CanResolveTo for RequestAkCertKind { /// A resolved request AK cert helper resource. pub struct ResolvedRequestAkCert(pub Arc); +/// AK certificate request result. +pub struct AkCertRequestResult { + pub ak_cert: Vec, + pub host_certification_evidence: Option>, +} + impl From for ResolvedRequestAkCert { fn from(value: T) -> Self { Self(Arc::new(value)) @@ -78,5 +101,5 @@ pub trait RequestAkCert: Send + Sync { async fn request_ak_cert( &self, request: Vec, - ) -> Result, Box>; + ) -> Result>; } diff --git a/vm/devices/tpm/tpm_device/src/lib.rs b/vm/devices/tpm/tpm_device/src/lib.rs index b3734a8c26..e8749bb63f 100644 --- a/vm/devices/tpm/tpm_device/src/lib.rs +++ b/vm/devices/tpm/tpm_device/src/lib.rs @@ -26,6 +26,7 @@ use tpm_lib::TpmRsa2kPublic; use self::io_port_interface::PpiOperation; use self::io_port_interface::TpmIoCommand; +use crate::ak_cert::AkCertRequestResult; use crate::ak_cert::TpmAkCertType; use base64::Engine; use chipset_device::ChipsetDevice; @@ -219,7 +220,13 @@ pub struct TpmKeys { } type AkCertRequestFuture = Box< - dyn Send + Future, Box>>, + dyn Send + + Future< + Output = Result< + AkCertRequestResult, + Box, + >, + >, >; struct AkCertRequest { @@ -350,6 +357,10 @@ pub enum TpmErrorKind { #[source] error: Box, }, + #[error("host-certified TVM AK certificate response is missing evidence")] + MissingHostCertificationEvidence, + #[error("host-certified TVM NV index {nv_index:#x} is not allocated")] + MissingHostCertificationNvIndex { nv_index: u32 }, #[error("failed to set pcr banks")] SetPcrBanks(#[source] tpm_lib::Error), } @@ -731,7 +742,9 @@ impl Tpm { auth_value, AllocateNvIndicesParams { preserve_ak_cert: !self.refresh_tpm_seeds, // Preserve AK cert if TPM seeds are not refreshed - support_attestation_report: self.ak_cert_type.attested(), + support_attestation_report: self.ak_cert_type.has_attestation_report(), + preserve_attestation_report: !self.refresh_tpm_seeds + && self.ak_cert_type.preserves_attestation_report(), mitigate_legacy_akcert: fixup_16k_ak_cert, create_if_missing: large_vtpm_blob, }, @@ -751,12 +764,16 @@ impl Tpm { // controls AKCert renewal, follow that. should_handle } + TpmAkCertType::HostCertified(_, Some(should_handle)) => should_handle, TpmAkCertType::Trusted(_, _) => { // Otherwise, if the existing AKCert index is platform- // defined and this appears to be an HCL-provisioned // vTPM, then handle AKCert renewal from OpenHCL. self.tpm_engine_helper.has_platform_akcert_index() && large_vtpm_blob } + TpmAkCertType::HostCertified(_, _) => { + self.tpm_engine_helper.has_platform_akcert_index() && large_vtpm_blob + } // If there's no AKCert, then don't handle renewal. TpmAkCertType::None => false, // If TpmAkCertType is one that should always be handled by @@ -773,7 +790,7 @@ impl Tpm { // Initialize `TPM_NV_INDEX_ATTESTATION_REPORT` if `ak_cert_type` supports attestation // report. - if self.ak_cert_type.attested() { + if self.ak_cert_type.refreshes_attestation_report() { self.renew_attestation_report()?; } } @@ -1135,6 +1152,76 @@ impl Tpm { Ok(()) } + fn validate_host_certification_nv_write( + &mut self, + nv_index: u32, + input_size: usize, + ) -> Result<(), TpmError> { + let Some(index) = self + .tpm_engine_helper + .find_nv_index(nv_index) + .map_err(TpmErrorKind::WriteToNvIndex)? + else { + return Err(TpmErrorKind::MissingHostCertificationNvIndex { nv_index }.into()); + }; + let attributes = tpm20proto::TpmaNvBits::from(index.nv_public.nv_public.attributes.0.get()); + if !attributes.nv_authwrite() || !attributes.nv_platformcreate() { + return Err( + TpmErrorKind::WriteToNvIndex(tpm_lib::Error::InvalidPermission { + nv_index, + auth_write: attributes.nv_authwrite(), + platform_created: attributes.nv_platformcreate(), + }) + .into(), + ); + } + + let allocated_size = index.nv_public.nv_public.data_size.get() as usize; + if input_size > allocated_size { + return Err( + TpmErrorKind::WriteToNvIndex(tpm_lib::Error::NvWriteInputTooLarge { + nv_index, + input_size, + allocated_size, + }) + .into(), + ); + } + + Ok(()) + } + + fn write_ak_cert_response(&mut self, response: &AkCertRequestResult) -> Result<(), TpmError> { + let evidence = if matches!(self.ak_cert_type, TpmAkCertType::HostCertified(_, _)) { + let evidence = response + .host_certification_evidence + .as_deref() + .ok_or(TpmErrorKind::MissingHostCertificationEvidence)?; + self.validate_host_certification_nv_write( + TPM_NV_INDEX_AIK_CERT, + response.ak_cert.len(), + )?; + self.validate_host_certification_nv_write( + TPM_NV_INDEX_ATTESTATION_REPORT, + evidence.len(), + )?; + Some(evidence) + } else { + None + }; + + let auth_value = self.auth_value.expect("auth value is uninitialized"); + if let Some(evidence) = evidence { + self.tpm_engine_helper + .write_to_nv_index(auth_value, TPM_NV_INDEX_ATTESTATION_REPORT, evidence) + .map_err(TpmErrorKind::WriteToNvIndex)?; + } + self.tpm_engine_helper + .write_to_nv_index(auth_value, TPM_NV_INDEX_AIK_CERT, &response.ak_cert) + .map_err(TpmErrorKind::WriteToNvIndex)?; + Ok(()) + } + /// Poll the AK cert request made by `get_ak_cert`. This function is called by [`PollDevice::poll_device`]. fn poll_ak_cert_request(&mut self, cx: &mut std::task::Context<'_>) { if let Some(async_ak_cert_request) = self.async_ak_cert_request.as_mut() { @@ -1152,15 +1239,8 @@ impl Tpm { // Parse the response. Empty response indicates that the host agent is unavailable. let response = match result { - Ok(data) if !data.is_empty() => { - // Set the renew time if successfully receiving the data. - // The next renew request will be made after `AK_CERT_RENEW_PERIOD` passes and - // `refresh_device_attestation_data_on_nv_read` is triggered. - self.ak_cert_renew_time = Some(now); - - data - } - Ok(_data) => { + Ok(data) if !data.ak_cert.is_empty() => data, + Ok(_) => { tracelimit::warn_ratelimited!( CVM_ALLOWED, op_type = ?LogOpType::AkCertProvision, @@ -1202,19 +1282,15 @@ impl Tpm { } }; - let auth_value = self.auth_value.expect("auth value is uninitialized"); - if let Err(e) = self.tpm_engine_helper.write_to_nv_index( - auth_value, - TPM_NV_INDEX_AIK_CERT, - &response, - ) { + if let Err(e) = self.write_ak_cert_response(&response) { tracelimit::error_ratelimited!( CVM_ALLOWED, error = &e as &dyn std::error::Error, - "Failed write new TPM AK cert to NV index" + "Failed to write TPM AK certificate response to NV indices" ); return; } + self.ak_cert_renew_time = Some(now); let duration = now.duration_since(std::time::UNIX_EPOCH); @@ -1225,7 +1301,7 @@ impl Tpm { ak_pub_hash = self.ak_pub_str(), is_renew, got_cert = 1, - size = response.len(), + size = response.ak_cert.len(), latency = latency.map_or(0, |d| d.as_millis()), cert_renew_time = ?duration, "ak cert renewal is complete", @@ -1278,7 +1354,7 @@ impl Tpm { // On start of read of attestation report index, refresh report when // attestation report is supported. if u32::from(nv_read.nv_index) == TPM_NV_INDEX_ATTESTATION_REPORT - && self.ak_cert_type.attested() + && self.ak_cert_type.refreshes_attestation_report() { if attestation_report_renew_elapsed > REPORT_TIMER_PERIOD || self.attestation_report_renew_time.is_none() @@ -2028,10 +2104,18 @@ mod tests { use guestmem::GuestMemory; use pal_async::async_test; use std::sync::Arc; + use std::sync::LazyLock; + use tpm_lib::NvIndexState; + use tpm_protocol::TPM_NV_INDEX_AIK_CERT; + use tpm_protocol::TPM_NV_INDEX_ATTESTATION_REPORT; use tpm_protocol::TPM_NV_INDEX_MITIGATED; use tpm_protocol::tpm20proto::TpmaNvBits; use tpm_resources::TpmRegisterLayout; use vmcore::non_volatile_store::EphemeralNonVolatileStore; + + static TPM_TEST_LOCK: LazyLock> = + LazyLock::new(|| futures::lock::Mutex::new(())); + struct TestRequestAkCertHelper; #[async_trait::async_trait] @@ -2051,13 +2135,49 @@ mod tests { async fn request_ak_cert( &self, _request: Vec, - ) -> Result, Box> { - Ok(Vec::new()) + ) -> Result> + { + Ok(AkCertRequestResult { + ak_cert: Vec::new(), + host_certification_evidence: None, + }) } } + async fn new_test_tpm(ak_cert_type: TpmAkCertType) -> Tpm { + Tpm::new( + TpmRegisterLayout::IoPort, + GuestMemory::allocate(0x10000), + EphemeralNonVolatileStore::new_boxed(), + EphemeralNonVolatileStore::new_boxed(), + None, + Box::new(|| std::time::Duration::new(0, 0)), + false, + false, + ak_cert_type, + None, + None, + false, + guid::guid!("00112233-4455-6677-8899-aabbccddeeff"), + ) + .await + .unwrap() + } + + fn assert_nv_index_contents(tpm: &mut Tpm, nv_index: u32, size: usize, expected: &[u8]) { + let mut data = vec![0; size]; + let state = tpm + .tpm_engine_helper + .read_from_nv_index(nv_index, &mut data) + .unwrap(); + assert!(matches!(state, NvIndexState::Available)); + assert_eq!(&data[..expected.len()], expected); + assert!(data[expected.len()..].iter().all(|value| *value == 0)); + } + #[async_test] async fn test_fix_corrupted_vmgs() { + let _test_guard = TPM_TEST_LOCK.lock().await; let tpm_state_blob = include_bytes!("../../test_data/vTpmState-corrupt.blob"); let tpm_state_vec = tpm_state_blob.to_vec(); let mut store = EphemeralNonVolatileStore::new_boxed(); @@ -2100,4 +2220,190 @@ mod tests { .expect("find_nv_index should succeed") .expect("mitigation marker NV index present"); } + + #[async_test] + async fn test_trusted_and_host_certified_nv_index_allocation() { + let _test_guard = TPM_TEST_LOCK.lock().await; + let helper = Arc::new(TestRequestAkCertHelper); + let mut trusted = new_test_tpm(TpmAkCertType::Trusted(helper.clone(), Some(false))).await; + let mut data = vec![0; tpm_lib::MAX_ATTESTATION_INDEX_SIZE as usize]; + let state = trusted + .tpm_engine_helper + .read_from_nv_index(TPM_NV_INDEX_ATTESTATION_REPORT, &mut data) + .unwrap(); + assert!(matches!(state, NvIndexState::Unallocated)); + drop(trusted); + + let mut host_certified = + new_test_tpm(TpmAkCertType::HostCertified(helper, Some(false))).await; + let state = host_certified + .tpm_engine_helper + .read_from_nv_index(TPM_NV_INDEX_ATTESTATION_REPORT, &mut data) + .unwrap(); + assert!(matches!(state, NvIndexState::Uninitialized)); + } + + #[async_test] + async fn test_host_certified_response_updates_certificate_and_evidence() { + let _test_guard = TPM_TEST_LOCK.lock().await; + let mut tpm = new_test_tpm(TpmAkCertType::HostCertified( + Arc::new(TestRequestAkCertHelper), + Some(false), + )) + .await; + let initial_response = AkCertRequestResult { + ak_cert: vec![1, 2, 3], + host_certification_evidence: Some(vec![4, 5, 6, 7]), + }; + tpm.write_ak_cert_response(&initial_response).unwrap(); + assert_nv_index_contents( + &mut tpm, + TPM_NV_INDEX_AIK_CERT, + tpm_protocol::TPM_DEFAULT_AKCERT_SIZE, + &[1, 2, 3], + ); + assert_nv_index_contents( + &mut tpm, + TPM_NV_INDEX_ATTESTATION_REPORT, + tpm_lib::MAX_ATTESTATION_INDEX_SIZE as usize, + &[4, 5, 6, 7], + ); + + let renewed_response = AkCertRequestResult { + ak_cert: vec![8, 9], + host_certification_evidence: Some(vec![10, 11, 12]), + }; + tpm.write_ak_cert_response(&renewed_response).unwrap(); + assert_nv_index_contents( + &mut tpm, + TPM_NV_INDEX_AIK_CERT, + tpm_protocol::TPM_DEFAULT_AKCERT_SIZE, + &[8, 9], + ); + assert_nv_index_contents( + &mut tpm, + TPM_NV_INDEX_ATTESTATION_REPORT, + tpm_lib::MAX_ATTESTATION_INDEX_SIZE as usize, + &[10, 11, 12], + ); + } + + #[async_test] + async fn test_host_certified_response_survives_cold_boot() { + let _test_guard = TPM_TEST_LOCK.lock().await; + let mut tpm = new_test_tpm(TpmAkCertType::HostCertified( + Arc::new(TestRequestAkCertHelper), + Some(false), + )) + .await; + let response = AkCertRequestResult { + ak_cert: vec![1, 2, 3], + host_certification_evidence: Some(vec![4, 5, 6, 7]), + }; + tpm.write_ak_cert_response(&response).unwrap(); + let nvram = tpm.pending_nvram.lock().clone(); + assert!(!nvram.is_empty()); + drop(tpm); + + let mut nvram_store = EphemeralNonVolatileStore::default(); + nvram_store.persist(nvram).await.unwrap(); + let mut rebooted_tpm = Tpm::new( + TpmRegisterLayout::IoPort, + GuestMemory::allocate(0x10000), + EphemeralNonVolatileStore::new_boxed(), + Box::new(nvram_store), + None, + Box::new(|| std::time::Duration::new(0, 0)), + false, + false, + TpmAkCertType::HostCertified(Arc::new(TestRequestAkCertHelper), Some(false)), + None, + None, + false, + guid::guid!("00112233-4455-6677-8899-aabbccddeeff"), + ) + .await + .unwrap(); + + assert_nv_index_contents( + &mut rebooted_tpm, + TPM_NV_INDEX_AIK_CERT, + tpm_protocol::TPM_DEFAULT_AKCERT_SIZE, + &[1, 2, 3], + ); + assert_nv_index_contents( + &mut rebooted_tpm, + TPM_NV_INDEX_ATTESTATION_REPORT, + tpm_lib::MAX_ATTESTATION_INDEX_SIZE as usize, + &[4, 5, 6, 7], + ); + } + + #[async_test] + async fn test_host_certified_response_failure_preserves_previous_values() { + let _test_guard = TPM_TEST_LOCK.lock().await; + let mut tpm = new_test_tpm(TpmAkCertType::HostCertified( + Arc::new(TestRequestAkCertHelper), + Some(false), + )) + .await; + let initial_response = AkCertRequestResult { + ak_cert: vec![1, 2, 3], + host_certification_evidence: Some(vec![4, 5, 6, 7]), + }; + tpm.write_ak_cert_response(&initial_response).unwrap(); + + let missing_evidence = AkCertRequestResult { + ak_cert: vec![8, 9], + host_certification_evidence: None, + }; + assert!(matches!( + tpm.write_ak_cert_response(&missing_evidence), + Err(TpmError(TpmErrorKind::MissingHostCertificationEvidence)) + )); + + let oversized_certificate = AkCertRequestResult { + ak_cert: vec![8; tpm_protocol::TPM_DEFAULT_AKCERT_SIZE + 1], + host_certification_evidence: Some(vec![9]), + }; + assert!(matches!( + tpm.write_ak_cert_response(&oversized_certificate), + Err(TpmError(TpmErrorKind::WriteToNvIndex( + tpm_lib::Error::NvWriteInputTooLarge { + nv_index: TPM_NV_INDEX_AIK_CERT, + .. + } + ))) + )); + + let oversized_evidence = AkCertRequestResult { + ak_cert: vec![8], + host_certification_evidence: Some(vec![ + 9; + tpm_lib::MAX_ATTESTATION_INDEX_SIZE as usize + 1 + ]), + }; + assert!(matches!( + tpm.write_ak_cert_response(&oversized_evidence), + Err(TpmError(TpmErrorKind::WriteToNvIndex( + tpm_lib::Error::NvWriteInputTooLarge { + nv_index: TPM_NV_INDEX_ATTESTATION_REPORT, + .. + } + ))) + )); + + assert_nv_index_contents( + &mut tpm, + TPM_NV_INDEX_AIK_CERT, + tpm_protocol::TPM_DEFAULT_AKCERT_SIZE, + &[1, 2, 3], + ); + assert_nv_index_contents( + &mut tpm, + TPM_NV_INDEX_ATTESTATION_REPORT, + tpm_lib::MAX_ATTESTATION_INDEX_SIZE as usize, + &[4, 5, 6, 7], + ); + } } diff --git a/vm/devices/tpm/tpm_device/src/resolver.rs b/vm/devices/tpm/tpm_device/src/resolver.rs index 8f8a957f3e..ec88cf290d 100644 --- a/vm/devices/tpm/tpm_device/src/resolver.rs +++ b/vm/devices/tpm/tpm_device/src/resolver.rs @@ -87,6 +87,16 @@ impl AsyncResolveResource for TpmDevic .0, handle, ), + TpmAkCertTypeResource::HostCertified(request_ak_cert, handle) => { + TpmAkCertType::HostCertified( + resolver + .resolve(request_ak_cert, &()) + .await + .map_err(ResolveTpmError::ResolveRequestAkCert)? + .0, + handle, + ) + } TpmAkCertTypeResource::None => TpmAkCertType::None, }; diff --git a/vm/devices/tpm/tpm_lib/src/lib.rs b/vm/devices/tpm/tpm_lib/src/lib.rs index a206b7838d..a42afbb082 100644 --- a/vm/devices/tpm/tpm_lib/src/lib.rs +++ b/vm/devices/tpm/tpm_lib/src/lib.rs @@ -62,8 +62,8 @@ use zerocopy::IntoBytes; const TPM_PAGE_SIZE: usize = 4096; const MAX_NV_BUFFER_SIZE: usize = MAX_DIGEST_BUFFER_SIZE; const MAX_NV_INDEX_SIZE: u16 = 4096; -// Scale this with maximum attestation payload -const MAX_ATTESTATION_INDEX_SIZE: u16 = 2900; +/// Maximum size of the platform attestation-report NV index. +pub const MAX_ATTESTATION_INDEX_SIZE: u16 = 2900; const RSA_2K_MODULUS_BITS: u16 = 2048; const RSA_2K_MODULUS_SIZE: usize = (RSA_2K_MODULUS_BITS / 8) as usize; @@ -278,6 +278,8 @@ pub struct AllocateNvIndicesParams { pub preserve_ak_cert: bool, /// Allocate NV index for the attestation report. pub support_attestation_report: bool, + /// Preserve the previous attestation report in the newly-created NV index. + pub preserve_attestation_report: bool, /// Attempt to mitigate a platform-defined AKCert in a legacy TPM. pub mitigate_legacy_akcert: bool, /// Create the AKCert index if it is not present. @@ -767,6 +769,16 @@ impl TpmEngineHelper { ); } + let previous_attestation_report = + if params.support_attestation_report && params.preserve_attestation_report { + let mut report = vec![0; MAX_ATTESTATION_INDEX_SIZE as usize]; + match self.read_from_nv_index(TPM_NV_INDEX_ATTESTATION_REPORT, &mut report)? { + NvIndexState::Available => Some(report), + NvIndexState::Unallocated | NvIndexState::Uninitialized => None, + } + } else { + None + }; let previous_ak_cert = self.take_existing_ak_cert()?; match previous_ak_cert { @@ -957,6 +969,11 @@ impl TpmEngineHelper { }, error, })?; + + if let Some(report) = previous_attestation_report { + tracing::info!("Preserve previous attestation report across boot"); + self.write_to_nv_index(auth_value, TPM_NV_INDEX_ATTESTATION_REPORT, &report)?; + } } Ok(()) @@ -2380,6 +2397,7 @@ mod tests { AllocateNvIndicesParams { preserve_ak_cert: true, support_attestation_report: false, + preserve_attestation_report: false, mitigate_legacy_akcert: false, create_if_missing: true, }, @@ -2423,6 +2441,7 @@ mod tests { AllocateNvIndicesParams { preserve_ak_cert: true, support_attestation_report: false, + preserve_attestation_report: false, mitigate_legacy_akcert: false, create_if_missing: true, }, @@ -2484,6 +2503,7 @@ mod tests { AllocateNvIndicesParams { preserve_ak_cert: true, support_attestation_report: false, + preserve_attestation_report: false, mitigate_legacy_akcert: false, create_if_missing: true, }, @@ -2556,6 +2576,7 @@ mod tests { AllocateNvIndicesParams { preserve_ak_cert: false, support_attestation_report: false, + preserve_attestation_report: false, mitigate_legacy_akcert: false, create_if_missing: true, }, @@ -2617,6 +2638,7 @@ mod tests { AllocateNvIndicesParams { preserve_ak_cert: false, support_attestation_report: true, + preserve_attestation_report: false, mitigate_legacy_akcert: false, create_if_missing: true, }, @@ -2676,8 +2698,7 @@ mod tests { assert_eq!(&attestation_report_output, input_with_padding.as_slice()); } - // Test allocation after a restart preserve_ak_cert = false, support_attestation_report = true - // Expect both ak cert and attestation report nv indices to be re-created + // Test allocation after a restart with AK cert replacement and attestation report preservation. { let mut ak_cert_output = [0u8; MAX_NV_INDEX_SIZE as usize]; let mut attestation_report_output = [0u8; MAX_ATTESTATION_INDEX_SIZE as usize]; @@ -2700,23 +2721,30 @@ mod tests { AllocateNvIndicesParams { preserve_ak_cert: false, support_attestation_report: true, + preserve_attestation_report: true, mitigate_legacy_akcert: false, create_if_missing: true, }, ); assert!(result.is_ok()); - // Expect read to return Ok(false) given that the nv index is re-created and data is not preserved + // The AK cert index is re-created without preserving its data. let result = tpm_engine_helper.read_from_nv_index(TPM_NV_INDEX_AIK_CERT, &mut ak_cert_output); assert!(matches!(result.unwrap(), NvIndexState::Uninitialized)); - // Expect read to return Ok(false) given that the nv index is re-created and no data has been written + // The attestation report is preserved in the re-created index. let result = tpm_engine_helper.read_from_nv_index( TPM_NV_INDEX_ATTESTATION_REPORT, &mut attestation_report_output, ); - assert!(matches!(result.unwrap(), NvIndexState::Uninitialized)); + assert!(matches!(result.unwrap(), NvIndexState::Available)); + let input_with_padding = { + let mut input = ATTESTATION_REPORT_INPUT.to_vec(); + input.resize(MAX_ATTESTATION_INDEX_SIZE.into(), 0); + input + }; + assert_eq!(&attestation_report_output, input_with_padding.as_slice()); } } @@ -2730,6 +2758,7 @@ mod tests { AllocateNvIndicesParams { preserve_ak_cert: true, support_attestation_report: true, + preserve_attestation_report: false, mitigate_legacy_akcert: false, create_if_missing: true, }, @@ -2971,6 +3000,7 @@ mod tests { AllocateNvIndicesParams { preserve_ak_cert: true, support_attestation_report: false, + preserve_attestation_report: false, mitigate_legacy_akcert: false, create_if_missing: true, }, @@ -3881,6 +3911,7 @@ mod tests { AllocateNvIndicesParams { preserve_ak_cert: false, support_attestation_report: true, + preserve_attestation_report: false, mitigate_legacy_akcert: false, create_if_missing: true, }, @@ -3916,6 +3947,7 @@ mod tests { AllocateNvIndicesParams { preserve_ak_cert: false, support_attestation_report: true, + preserve_attestation_report: false, mitigate_legacy_akcert: true, create_if_missing: true, }, diff --git a/vm/devices/tpm_resources/src/lib.rs b/vm/devices/tpm_resources/src/lib.rs index 911e179468..f5bd90a828 100644 --- a/vm/devices/tpm_resources/src/lib.rs +++ b/vm/devices/tpm_resources/src/lib.rs @@ -59,6 +59,8 @@ pub enum TpmAkCertTypeResource { /// whether OpenHCL handles renewal. /// Used by TVM Trusted(Resource, Option), + /// Authorized TVM AK cert with host-certification evidence. + HostCertified(Resource, Option), /// Authorized and hardware-attested AK cert (backed by /// a TEE attestation report). /// Used by CVM