Skip to content

Commit 62d952d

Browse files
stunes-msbenhillisCopilot
authored
[MSRC 97247] Fix Trust Fall MSRC (#1849)
A malicious admin can evict the AK from their VM's vTPM and replace it with their own key. At boot, Azure will load that key from the VMGS and then sign an AKCert with that key, allowing the admin to spoof KeyGuard and CVM attestation. CVM: This change mirrors changes in the legacy HCL: Regenerate the AK at boot from the TPM seeds, instead of loading it from VMGS. This ensures that the original AKCert is always present in the vTPM. TVM: OpenHCL currently cannot regenerate the AK for a TVM, because the original AK (provisioned by the vtpmservice) contains an auth policy; OpenHCL does not implement that policy creation. As an alternative, when OpenHCL boots, it will check the attributes on the AK that it loads from VMGS. If the attributes are wrong (indicating a possibly malicious key), it will not make any calls to renew the AKCert. CVE-2025-49707 --------- Co-authored-by: Ben Hillis <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 943b003 commit 62d952d

File tree

5 files changed

+93
-12
lines changed

5 files changed

+93
-12
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7189,6 +7189,7 @@ dependencies = [
71897189
"tpm_resources",
71907190
"tracelimit",
71917191
"tracing",
7192+
"underhill_confidentiality",
71927193
"vm_resource",
71937194
"vmcore",
71947195
"zerocopy 0.8.24",

vm/devices/tpm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ chipset_device.workspace = true
1919
chipset_device_resources.workspace = true
2020
cvm_tracing.workspace = true
2121
guestmem.workspace = true
22+
underhill_confidentiality = { workspace = true, features = ["std"] }
2223
vmcore.workspace = true
2324
vm_resource.workspace = true
2425

vm/devices/tpm/src/lib.rs

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ mod tpm_helper;
2222
use self::io_port_interface::PpiOperation;
2323
use self::io_port_interface::TpmIoCommand;
2424
use crate::ak_cert::TpmAkCertType;
25+
use crate::tpm20proto::TpmaObject;
26+
use crate::tpm20proto::TpmaObjectBits;
2527
use chipset_device::ChipsetDevice;
2628
use chipset_device::io::IoError;
2729
use chipset_device::io::IoResult;
@@ -55,6 +57,7 @@ use tpm20proto::NV_INDEX_RANGE_BASE_TCG_ASSIGNED;
5557
use tpm20proto::ReservedHandle;
5658
use tpm20proto::TPM20_HT_PERSISTENT;
5759
use tpm20proto::TPM20_RH_PLATFORM;
60+
use underhill_confidentiality::is_confidential_vm;
5861
use vmcore::device_state::ChangeDeviceState;
5962
use vmcore::non_volatile_store::NonVolatileStore;
6063
use vmcore::non_volatile_store::NonVolatileStoreError;
@@ -224,6 +227,7 @@ pub struct Tpm {
224227
io_region: Option<(&'static str, RangeInclusive<u16>)>, // Valid only on HypervX64
225228
#[inspect(skip)]
226229
mmio_region: Vec<(&'static str, RangeInclusive<u64>)>,
230+
allow_ak_cert_renewal: bool,
227231

228232
// Runtime glue
229233
rt: TpmRuntime,
@@ -396,6 +400,7 @@ impl Tpm {
396400
refresh_tpm_seeds,
397401
io_region,
398402
mmio_region,
403+
allow_ak_cert_renewal: false,
399404

400405
rt: TpmRuntime {
401406
mem,
@@ -446,6 +451,7 @@ impl Tpm {
446451

447452
async fn on_first_boot(&mut self, guest_secret_key: Option<Vec<u8>>) -> Result<(), TpmError> {
448453
use ms_tpm_20_ref::NvError;
454+
let mut force_ak_regen = false;
449455
let fixup_16k_ak_cert;
450456

451457
// Check whether or not we need to pave-over the blank TPM with our
@@ -473,6 +479,13 @@ impl Tpm {
473479
return Err(TpmErrorKind::ResetTpmWithState(e).into());
474480
}
475481

482+
// If this is a confidential VM or has a vTPM blob size that indicates that it was
483+
// HCL-provisioned, regenerate the AK from TPM seeds. This prevents an attack where
484+
// the VTL0 admin can replace the AK and get an AKCert for it.
485+
force_ak_regen = self.refresh_tpm_seeds
486+
|| blob.len() != LEGACY_VTPM_SIZE
487+
|| is_confidential_vm();
488+
476489
// If this is a small vTPM blob, potentially fixup the AK cert.
477490
fixup_16k_ak_cert = blob.len() == LEGACY_VTPM_SIZE;
478491
} else {
@@ -543,15 +556,21 @@ impl Tpm {
543556
// Initialize `TpmKeys`.
544557
// The procedure also generates randomized AK based on the TPM seed
545558
// and writes the AK into `TPM_AZURE_AIK_HANDLE` NV store.
546-
let ak_pub = self
559+
let (ak_pub, can_renew_ak) = self
547560
.tpm_engine_helper
548-
.create_ak_pub(self.refresh_tpm_seeds)
561+
.create_ak_pub(force_ak_regen)
549562
.map_err(TpmErrorKind::CreateAkPublic)?;
550563
let ek_pub = self
551564
.tpm_engine_helper
552565
.create_ek_pub()
553566
.map_err(TpmErrorKind::CreateEkPublic)?;
554567
self.keys = Some(TpmKeys { ak_pub, ek_pub });
568+
tracing::info!(
569+
CVM_ALLOWED,
570+
can_renew_ak = can_renew_ak,
571+
"loaded existing AK from VMGS vTPM state"
572+
);
573+
self.allow_ak_cert_renewal = can_renew_ak;
555574

556575
// Conditionally define nv indexes for ak cert and attestation report.
557576
// The Nvram size can only be defined with platform hierarchy. Otherwise
@@ -869,6 +888,12 @@ impl Tpm {
869888
/// This routine calls (via GET) external server to issue AK cert.
870889
/// This function can only be called when `ak_cert_type` is `Trusted` or `HwAttested`.
871890
fn renew_ak_cert(&mut self) -> Result<(), TpmError> {
891+
// Silently do nothing if renewal is not allowed.
892+
if !self.allow_ak_cert_renewal {
893+
tracing::info!(CVM_ALLOWED, "AK cert renewal is not allowed");
894+
return Ok(());
895+
}
896+
872897
// Return if the request is pending
873898
if self.async_ak_cert_request.is_some() {
874899
return Ok(());
@@ -983,6 +1008,12 @@ impl Tpm {
9831008

9841009
/// Renew device attestation data (i.e., attestation report and AK cert) on NV_Read if needed
9851010
fn refresh_device_attestation_data_on_nv_read(&mut self) {
1011+
// Silently do nothing if renewal is not allowed.
1012+
if !self.allow_ak_cert_renewal {
1013+
tracing::info!(CVM_ALLOWED, "AK cert renewal is not allowed");
1014+
return;
1015+
}
1016+
9861017
let Some(nv_read) = tpm20proto::protocol::NvReadCmd::deserialize(&self.command_buffer)
9871018
else {
9881019
return;
@@ -1334,6 +1365,19 @@ impl MmioIntercept for Tpm {
13341365
}
13351366
}
13361367

1368+
/// Expected attributes for a correctly-provisioned AK.
1369+
pub fn expected_ak_attributes() -> TpmaObject {
1370+
TpmaObjectBits::new()
1371+
.with_fixed_tpm(true)
1372+
.with_fixed_parent(true)
1373+
.with_sensitive_data_origin(true)
1374+
.with_user_with_auth(true)
1375+
.with_no_da(true)
1376+
.with_restricted(true)
1377+
.with_sign_encrypt(true)
1378+
.into()
1379+
}
1380+
13371381
/// The IO port interface bespoke to the Hyper-V implementation of the vTPM.
13381382
mod io_port_interface {
13391383
use inspect::Inspect;
@@ -1566,6 +1610,8 @@ mod save_restore {
15661610
pub auth_value: Option<u64>,
15671611
#[mesh(61)]
15681612
pub keys: Option<SavedTpmKeys>,
1613+
#[mesh(62)]
1614+
pub allow_ak_cert_renewal: Option<bool>,
15691615
}
15701616
}
15711617

@@ -1663,6 +1709,7 @@ mod save_restore {
16631709
tpm_state_blob: self.tpm_engine_helper.tpm_engine.save_state(),
16641710
auth_value: self.auth_value,
16651711
keys,
1712+
allow_ak_cert_renewal: Some(self.allow_ak_cert_renewal),
16661713
};
16671714

16681715
Ok(saved_state)
@@ -1677,6 +1724,7 @@ mod save_restore {
16771724
tpm_state_blob,
16781725
auth_value,
16791726
keys,
1727+
allow_ak_cert_renewal,
16801728
} = state;
16811729

16821730
self.control_area = {
@@ -1743,6 +1791,18 @@ mod save_restore {
17431791
},
17441792
});
17451793

1794+
if allow_ak_cert_renewal.is_none() {
1795+
// Whether AKCert renewal is allowed depends on the attributes of the AK
1796+
// saved in the vTPM. It may not be safe to read it here (which requires
1797+
// executing a readpublic command) because the vTPM may be in the middle
1798+
// of executing another command.
1799+
tracing::info!(
1800+
CVM_ALLOWED,
1801+
"vTPM servicing state does not include allow_ak_cert_renewal; denying renewal until reboot"
1802+
);
1803+
}
1804+
self.allow_ak_cert_renewal = allow_ak_cert_renewal.unwrap_or(false);
1805+
17461806
Ok(())
17471807
}
17481808
}

vm/devices/tpm/src/tpm20proto.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1375,7 +1375,7 @@ pub mod protocol {
13751375
pub struct TpmtPublic {
13761376
my_type: AlgId,
13771377
name_alg: AlgId,
1378-
object_attributes: TpmaObject,
1378+
pub object_attributes: TpmaObject,
13791379
auth_policy: Tpm2bBuffer,
13801380
// `TPMS_RSA_PARAMS`
13811381
pub parameters: TpmsRsaParams,

vm/devices/tpm/src/tpm_helper.rs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::TPM_NV_INDEX_ATTESTATION_REPORT;
1010
use crate::TPM_NV_INDEX_MITIGATED;
1111
use crate::TPM_RSA_SRK_HANDLE;
1212
use crate::TpmRsa2kPublic;
13+
use crate::expected_ak_attributes;
1314
use crate::tpm20proto;
1415
use crate::tpm20proto::AlgIdEnum;
1516
use crate::tpm20proto::CommandCodeEnum;
@@ -302,26 +303,44 @@ impl TpmEngineHelper {
302303
/// # Arguments
303304
/// * `force_create`: Whether to remove the existing AK and re-create one.
304305
///
305-
/// Returns the AK public in `TpmRsa2kPublic`.
306-
pub fn create_ak_pub(&mut self, force_create: bool) -> Result<TpmRsa2kPublic, TpmHelperError> {
306+
/// Returns the AK public in `TpmRsa2kPublic`, and a bool indicating whether AKCert
307+
/// renewal is allowed.
308+
pub fn create_ak_pub(
309+
&mut self,
310+
force_create: bool,
311+
) -> Result<(TpmRsa2kPublic, bool), TpmHelperError> {
307312
if let Some(res) = self.find_object(TPM_AZURE_AIK_HANDLE)? {
308313
if force_create {
309314
// Remove existing key before creating a new one
310315
self.evict_or_persist_handle(EvictOrPersist::Evict(TPM_AZURE_AIK_HANDLE))?;
311316
} else {
312-
// Use existing key
313-
return export_rsa_public(&res.out_public).map_err(|error| {
314-
TpmHelperError::ExportRsaPublicFromAkHandle {
317+
let expected_attributes = expected_ak_attributes();
318+
319+
// If an existing key has the wrong attributes, deny renewing the AKCert later.
320+
// This prevents an attack where the VTL0 admin can replace the AK with their own
321+
// and get a signed AKCert.
322+
let actual_attributes = res.out_public.public_area.object_attributes;
323+
if actual_attributes != expected_attributes {
324+
tracing::warn!(
325+
CVM_ALLOWED,
326+
attrs = actual_attributes.0.get(),
327+
"incorrect AK attributes; denying AKCert renewal"
328+
);
329+
}
330+
331+
return export_rsa_public(&res.out_public)
332+
.map_err(|error| TpmHelperError::ExportRsaPublicFromAkHandle {
315333
ak_handle: TPM_AZURE_AIK_HANDLE.0.get(),
316334
error,
317-
}
318-
});
335+
})
336+
.map(|ak_pub| (ak_pub, actual_attributes == expected_attributes));
319337
}
320338
}
321339

322340
let in_public = ak_pub_template().map_err(TpmHelperError::CreateAkPubTemplateFailed)?;
323341

324342
self.create_key_object(in_public, Some(TPM_AZURE_AIK_HANDLE))
343+
.map(|res| (res, true))
325344
}
326345

327346
/// Create Windows-style Endorsement key (EK) based on the template from the TPM specification. Note that
@@ -968,7 +987,7 @@ impl TpmEngineHelper {
968987
///
969988
/// Returns Ok(Some(ReadPublicReply)) if the object is present.
970989
/// Returns Ok(None) if nv index is not present.
971-
fn find_object(
990+
pub fn find_object(
972991
&mut self,
973992
object_handle: ReservedHandle,
974993
) -> Result<Option<ReadPublicReply>, TpmHelperError> {
@@ -2096,7 +2115,7 @@ mod tests {
20962115
) -> (TpmRsa2kPublic, TpmRsa2kPublic) {
20972116
let result = tpm_engine_helper.create_ak_pub(false);
20982117
assert!(result.is_ok());
2099-
let ak_pub = result.unwrap();
2118+
let (ak_pub, _) = result.unwrap();
21002119

21012120
// Ensure `create_ak_pub` persists AK
21022121
assert!(

0 commit comments

Comments
 (0)