Skip to content

Attestedsettings #1391

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7278,7 +7278,9 @@ dependencies = [
"pal_async",
"serde",
"serde_json",
"sev_guest_device",
"static_assertions",
"tdx_guest_device",
"tee_call",
"thiserror 2.0.12",
"time",
Expand All @@ -7299,6 +7301,7 @@ dependencies = [
"guid",
"inspect",
"mesh",
"openssl",
"prost",
"serde",
"serde_json",
Expand Down
4 changes: 4 additions & 0 deletions openhcl/underhill_attestation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ vmgs_format.workspace = true
[lints]
workspace = true

[dependencies]
sev_guest_device.workspace = true
tdx_guest_device.workspace = true

[package.metadata.xtask.unused-deps]
# Needed for the base64-serde macros.
ignored = ["serde"]
153 changes: 153 additions & 0 deletions openhcl/underhill_attestation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ use secure_key_release::VmgsEncryptionKeys;
use static_assertions::const_assert_eq;
use std::fmt::Debug;
use tee_call::TeeCall;
use tee_call::TeeType;
use thiserror::Error;
use zerocopy::FromBytes;
use zerocopy::FromZeros;
use zerocopy::IntoBytes;

Expand Down Expand Up @@ -76,6 +78,8 @@ enum AttestationErrorInner {
UnlockVmgsDataStore(#[source] UnlockVmgsDataStoreError),
#[error("failed to read guest secret key from vmgs")]
ReadGuestSecretKey(#[source] vmgs::ReadFromVmgsError),
#[error("failed assertion of init data")]
InitDataAssertionFail(#[source] InitDataError),
}

#[derive(Debug, Error)]
Expand Down Expand Up @@ -158,6 +162,18 @@ enum PersistAllKeyProtectorsError {
WriteKeyProtectorById(#[source] vmgs::WriteToVmgsError),
}

#[derive(Debug, Error)]
enum InitDataError {
#[error("Unsupported Attestation Type")]
UnsupportedAttestationType(AttestationType),
#[error("Failed to get Attestation Report")]
FailedGetAttestationReport,
#[error("Invalid Attestation Report")]
InvalidAttestationReport,
#[error("Data does not match Asserted")]
InitDataAssertionFail,
}

/// Label used by `derive_key`
const VMGS_KEY_DERIVE_LABEL: &[u8; 7] = b"VMGSKEY";

Expand Down Expand Up @@ -1001,6 +1017,97 @@ fn get_derived_keys_by_id(
})
}

// / Verify the InitData hash in the attestation report
/// against the asserted InitData hash.
/// For SNP, this is the host data.
/// For TDX, this is the mr_config.
/// For other attestation types, this is not supported.
/// Takes the asserted InitData hash and the appropriate
/// TEE call interface as arguments.
/// Returns Ok(()) if the hashes match, or an error if they do not.
fn verify_init_data_inner(
asserted_init_data: [u8; 32],
tee_call: &dyn TeeCall,
) -> Result<(), InitDataError> {
let report_data = [0u8; 64];
let mut report = tee_call
.get_attestation_report(&report_data)
.map_err(|e| InitDataError::FailedGetAttestationReport)?;

let attestation_type = match tee_call.tee_type() {
TeeType::Snp => AttestationType::Snp,
TeeType::Tdx => AttestationType::Tdx,
};
match attestation_type {
AttestationType::Snp => {
// compare hash of the encoded settings with the host data
let snpreport =
sev_guest_device::protocol::SnpReport::mut_from_bytes(&mut report.report[..])
.map_err(|_e| InitDataError::InvalidAttestationReport)?;
let host_data = snpreport.host_data;
tracing::info!(CVM_ALLOWED, "Host data: {:?}", host_data);
tracing::info!(CVM_ALLOWED, "Asserted init data: {:?}", asserted_init_data);
if host_data == asserted_init_data {
return Ok(());
} else {
return Err(InitDataError::InitDataAssertionFail);
}
}
AttestationType::Tdx => {
// compare hash of the encoded settings with the mr_config
let tdreport =
tdx_guest_device::protocol::TdReport::mut_from_bytes(&mut report.report[..])
.map_err(|_e| InitDataError::InvalidAttestationReport)?;
let mr_config = tdreport.td_info.td_info_base.mr_owner_config;
if mr_config[0..32] == asserted_init_data {
return Ok(());
} else {
return Err(InitDataError::InitDataAssertionFail);
}
}
_ => {
// Unsupported attestation type
// Do not fail as this is not fatal
tracing::info!(
CVM_ALLOWED,
"Unsupported attestation type: {:?}",
attestation_type
);
return Ok(());
}
}
}

// Public function to verify the InitData hash
/// against the asserted InitData hash.
/// Takes the asserted InitData hash and the attestation type as arguments.
/// Passes the asserted InitData hash to the appropriate
/// TEE call interface for verify_init_data_inner.
/// Returns Ok(()) if the hashes match, or an error if they do not.
pub fn verify_init_data(
asserted_init_data: [u8; 32],
tee_type: AttestationType,
) -> Result<(), Error> {
match tee_type {
AttestationType::Snp => {
let tee_call: Box<dyn TeeCall> = Box::new(tee_call::SnpCall);
verify_init_data_inner(asserted_init_data, tee_call.as_ref())
.map_err(|e| Error::from(AttestationErrorInner::InitDataAssertionFail(e)))
}
AttestationType::Tdx => {
let tee_call: Box<dyn TeeCall> = Box::new(tee_call::TdxCall);
verify_init_data_inner(asserted_init_data, tee_call.as_ref())
.map_err(|e| Error::from(AttestationErrorInner::InitDataAssertionFail(e)))
}
_ => {
// Unsupported attestation type
// Do not fail as this is not fatal
tracing::info!(CVM_ALLOWED, "Unsupported attestation type: {:?}", tee_type);
Ok(())
}
}
}

/// Prepare the request payload and request GSP from the host via GET.
async fn get_gsp_data(
get: &GuestEmulationTransportClient,
Expand Down Expand Up @@ -1787,4 +1894,50 @@ mod tests {
key_protector_by_id.inner.id_guid
);
}

struct MockSnpTeeCall;

impl TeeCall for MockSnpTeeCall {
fn get_attestation_report(
&self,
_report_data: &[u8; 64],
) -> Result<tee_call::GetAttestationReportResult, tee_call::Error> {
Ok(tee_call::GetAttestationReportResult {
report: [0u8; sev_guest_device::protocol::SNP_REPORT_SIZE].to_vec(),
tcb_version: None,
})
}

fn supports_get_derived_key(&self) -> Option<&dyn tee_call::TeeCallGetDerivedKey> {
Some(self)
}

fn tee_type(&self) -> TeeType {
TeeType::Snp
}
}

impl tee_call::TeeCallGetDerivedKey for MockSnpTeeCall {
fn get_derived_key(&self, _tcb_version: u64) -> Result<[u8; 32], tee_call::Error> {
Ok([0u8; tee_call::HW_DERIVED_KEY_LENGTH])
}
}

#[test]
fn test_verify_init_data_inner() {
let asserted_init_data = [0u8; 32];

let mock_call: Box<dyn TeeCall> = Box::new(MockSnpTeeCall {});
let result = verify_init_data_inner(asserted_init_data, mock_call.as_ref());
assert!(result.is_ok());
}

#[test]
fn test_verify_init_data_inner_fail() {
let asserted_init_data = [1u8; 32];

let mock_call: Box<dyn TeeCall> = Box::new(MockSnpTeeCall {});
let result = verify_init_data_inner(asserted_init_data, mock_call.as_ref());
assert!(result.is_err_and(|e| { matches!(e, InitDataError::InitDataAssertionFail) }));
}
}
28 changes: 28 additions & 0 deletions openhcl/underhill_core/src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ use anyhow::Context;
use async_trait::async_trait;
use chipset_device::ChipsetDevice;
use closeable_mutex::CloseableMutex;
use cvm_tracing::CVM_ALLOWED;
use debug_ptr::DebugPtr;
use disk_backend::Disk;
use disk_blockdevice::BlockDeviceResolver;
Expand Down Expand Up @@ -125,6 +126,7 @@ use tracing::Instrument;
use tracing::instrument;
use uevent::UeventListener;
use underhill_attestation::AttestationType;
use underhill_attestation::verify_init_data;
use underhill_threadpool::AffinitizedThreadpool;
use underhill_threadpool::ThreadpoolBuilder;
use virt::Partition;
Expand Down Expand Up @@ -445,6 +447,7 @@ impl UnderhillVmWorker {
let dps = read_device_platform_settings(&get_client)
.instrument(tracing::info_span!("init/dps"))
.await?;
// Parsing the dps will parse the vtl2_settings including the attested settings

// Build the thread pool now that we know the IO ring size to use.
let threadpool = {
Expand Down Expand Up @@ -1177,6 +1180,31 @@ async fn new_underhill_vm(
bootloader_fdt_parser::IsolationType::Snp => virt::IsolationType::Snp,
bootloader_fdt_parser::IsolationType::Tdx => virt::IsolationType::Tdx,
};
// Now that we have the isolation type, we can determine if the VM is SNP/TDX
// and use the dps.vtl2_settings.attested_settings.verify() to validate
// the vtl2_settings against the init-time data.
if let Some(init_data_hash) = dps
.general
.vtl2_settings
.as_ref()
.and_then(|s| s.fixed.attested_settings.as_ref())
.and_then(|s| s.init_data_hash)
{
let attestation_type = match isolation {
virt::IsolationType::Snp => AttestationType::Snp,
virt::IsolationType::Tdx => AttestationType::Tdx,
_ => AttestationType::Host,
};
verify_init_data(init_data_hash, attestation_type).context("failed to verify init data")?;
tracing::info!(
?init_data_hash,
"using init data hash from attested settings for verification"
);
} else {
tracing::info!(
"attested_settings is missing. This fallback is acceptable because the init data hash is optional for non-isolated VMs or when attestation is not required."
);
}

let hardware_isolated = isolation.is_hardware_isolated();

Expand Down
1 change: 1 addition & 0 deletions vm/devices/get/underhill_config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ prost.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
thiserror.workspace = true
openssl.workspace = true

[lints]
workspace = true
9 changes: 9 additions & 0 deletions vm/devices/get/underhill_config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,13 @@ pub struct NicDevice {
pub max_sub_channels: Option<u16>,
}

#[derive(Default, Debug, Clone, Eq, PartialEq, MeshPayload, Inspect)]
pub struct Vtl2AttestedSettings {
pub version: i32,
// Reserved for future use
pub init_data_hash: Option<[u8; 32]>,
}

#[derive(Debug, Clone, MeshPayload, Inspect)]
pub struct Vtl2SettingsFixed {
/// number of sub-channels for the SCSI controller
Expand All @@ -181,6 +188,8 @@ pub struct Vtl2SettingsFixed {
pub io_ring_size: u32,
/// Max bounce buffer pages active per cpu
pub max_bounce_buffer_pages: Option<u32>,
/// Attested settings
pub attested_settings: Option<Vtl2AttestedSettings>,
}

#[derive(Debug, Clone, MeshPayload, Inspect)]
Expand Down
54 changes: 54 additions & 0 deletions vm/devices/get/underhill_config/src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//!
//! Generic schema structs and functions

use self::v1::NAMESPACE_ATTESTED_SETTINGS;
use self::v1::NAMESPACE_BASE;
use self::v1::NAMESPACE_NETWORK_ACCELERATION;
use self::v1::NAMESPACE_NETWORK_DEVICE;
Expand Down Expand Up @@ -45,6 +46,16 @@ impl From<ParsingStopped> for ParseErrorInner {
}
}

impl From<Vtl2AttestedSettings> for crate::Vtl2AttestedSettings {
fn from(settings: Vtl2AttestedSettings) -> crate::Vtl2AttestedSettings {
// Reserved for future use
crate::Vtl2AttestedSettings {
version: settings.version,
init_data_hash: None,
}
}
}

impl crate::Vtl2Settings {
/// Reads the settings from either a JSON- or protobuf-encoded schema.
pub fn read_from(
Expand Down Expand Up @@ -87,6 +98,7 @@ impl crate::Vtl2Settings {

let mut nic_devices: Option<Vec<crate::NicDevice>> = None;
let mut nic_acceleration: Option<Vec<crate::NicDevice>> = None;
let mut external_attested_settings: Option<crate::Vtl2AttestedSettings> = None;

for chunk in &decoded.namespace_settings {
if chunk.settings.is_empty() {
Expand Down Expand Up @@ -120,6 +132,19 @@ impl crate::Vtl2Settings {
.collect(),
);
}
NAMESPACE_ATTESTED_SETTINGS => {
// Ignore this namespace, it is not used in the current version
// of the schema.
let raw_bytes = chunk.settings.as_slice();
if !raw_bytes.is_empty() {
let mut hasher = openssl::sha::Sha256::new();
hasher.update(raw_bytes);
let attested_settings: Vtl2AttestedSettings = Self::read(raw_bytes)?;
let mut settings: crate::Vtl2AttestedSettings = attested_settings.into();
settings.init_data_hash = Some(hasher.finish());
external_attested_settings = Some(settings);
}
}
_ => {
errors.push(v1::Error::UnsupportedSchemaNamespace(
chunk.namespace.as_ref(),
Expand Down Expand Up @@ -160,6 +185,13 @@ impl crate::Vtl2Settings {
}
}

// NAMESPACE_ATTESTED_SETTINGS
// In the future, attested settings need to be added here.
// See patterns above for how to add them.
if let Some(external_attested_settings) = external_attested_settings {
old_settings.fixed.attested_settings = Some(external_attested_settings);
}

Ok(old_settings)
}

Expand Down Expand Up @@ -280,6 +312,28 @@ mod test {
crate::Vtl2Settings::read_from(&buf, Default::default()).unwrap();
}

#[test]
fn smoke_test_attested_settings_namespace() {
let settings = crate::Vtl2Settings::read_from(
include_bytes!("vtl2s_test_attested_settings_namespaces.json"),
Default::default(),
)
.unwrap();
assert_eq!(1, settings.fixed.attested_settings.unwrap().version)
}

#[test]
fn smoke_test_attested_settings_namespace_pb() {
let json = include_bytes!("vtl2s_test_attested_settings_namespaces.json");
let settings: Vtl2Settings = crate::Vtl2Settings::read(json).unwrap();
assert_eq!("AttestedSettings", settings.namespace_settings[0].namespace);
let mut buf = Vec::new();
settings.encode(&mut buf).unwrap();
print!("{:?}", buf);
let settings = crate::Vtl2Settings::read_from(&buf, Default::default()).unwrap();
assert_eq!(1, settings.fixed.attested_settings.unwrap().version)
}

#[test]
fn smoke_test_compat() {
crate::Vtl2Settings::read_from(
Expand Down
Loading