Skip to content

Commit

Permalink
Increase underhill_attestation vmgs unit test coverage (#618)
Browse files Browse the repository at this point in the history
This PR:
* Adds unit tests to the `underhill_attestation` crate's `vmgs` module
  * Covers all functions in the `vmgs` module
  • Loading branch information
mattbodd authored Jan 9, 2025
1 parent d680cdc commit 483db84
Show file tree
Hide file tree
Showing 3 changed files with 353 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6889,6 +6889,8 @@ dependencies = [
"base64 0.21.7",
"base64-serde",
"cvm_tracing",
"disk_backend",
"disklayer_ram",
"getrandom",
"guest_emulation_transport",
"guid",
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 @@ -33,5 +33,9 @@ thiserror.workspace = true
time = { workspace = true, features = ["macros"] }
zerocopy.workspace = true

[target.'cfg(target_os = "linux")'.dev-dependencies]
disklayer_ram.workspace = true
disk_backend.workspace = true

[lints]
workspace = true
347 changes: 347 additions & 0 deletions openhcl/underhill_attestation/src/vmgs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,350 @@ pub async fn read_guest_secret_key(vmgs: &mut Vmgs) -> Result<GuestSecretKey, Re
Err(vmgs_err) => Err(ReadFromVmgsError::ReadFromVmgs { file_id, vmgs_err }),
}
}

#[cfg(test)]
mod tests {
use super::*;
use disk_backend::Disk;
use disklayer_ram::ram_disk;
use openhcl_attestation_protocol::vmgs::DekKp;
use openhcl_attestation_protocol::vmgs::GspKp;
use openhcl_attestation_protocol::vmgs::HardwareKeyProtectorHeader;
use openhcl_attestation_protocol::vmgs::KeyProtector;
use openhcl_attestation_protocol::vmgs::KeyProtectorById;
use openhcl_attestation_protocol::vmgs::AES_CBC_IV_LENGTH;
use openhcl_attestation_protocol::vmgs::AES_GCM_KEY_LENGTH;
use openhcl_attestation_protocol::vmgs::DEK_BUFFER_SIZE;
use openhcl_attestation_protocol::vmgs::GSP_BUFFER_SIZE;
use openhcl_attestation_protocol::vmgs::HMAC_SHA_256_KEY_LENGTH;
use openhcl_attestation_protocol::vmgs::HW_KEY_PROTECTOR_SIZE;
use openhcl_attestation_protocol::vmgs::KEY_PROTECTOR_SIZE;
use openhcl_attestation_protocol::vmgs::NUMBER_KP;
use pal_async::async_test;

const ONE_MEGA_BYTE: u64 = 1024 * 1024;

fn new_test_file() -> Disk {
ram_disk(4 * ONE_MEGA_BYTE, false).unwrap()
}

async fn new_formatted_vmgs() -> Vmgs {
let disk = new_test_file();

Vmgs::format_new(disk).await.unwrap()
}

fn new_hardware_key_protector() -> HardwareKeyProtector {
let header = HardwareKeyProtectorHeader::new(1, HW_KEY_PROTECTOR_SIZE as u32, 2);
let iv = [3; AES_CBC_IV_LENGTH];
let ciphertext = [4; AES_GCM_KEY_LENGTH];
let hmac = [5; HMAC_SHA_256_KEY_LENGTH];

HardwareKeyProtector {
header,
iv,
ciphertext,
hmac,
}
}

fn new_key_protector() -> KeyProtector {
// Ingress and egress KPs are assumed to be the only two KPs, therefore `NUMBER_KP` should be 2
assert_eq!(NUMBER_KP, 2);

let ingress_dek = DekKp {
dek_buffer: [1; DEK_BUFFER_SIZE],
};
let egress_dek = DekKp {
dek_buffer: [2; DEK_BUFFER_SIZE],
};
let ingress_gsp = GspKp {
gsp_length: GSP_BUFFER_SIZE as u32,
gsp_buffer: [3; GSP_BUFFER_SIZE],
};
let egress_gsp = GspKp {
gsp_length: GSP_BUFFER_SIZE as u32,
gsp_buffer: [4; GSP_BUFFER_SIZE],
};
KeyProtector {
dek: [ingress_dek, egress_dek],
gsp: [ingress_gsp, egress_gsp],
active_kp: u32::MAX,
}
}

#[async_test]
async fn write_read_vmgs_key_protector() {
let mut vmgs = new_formatted_vmgs().await;
let key_protector = new_key_protector();
write_key_protector(&key_protector, &mut vmgs)
.await
.unwrap();

let key_protector = read_key_protector(&mut vmgs, KEY_PROTECTOR_SIZE)
.await
.unwrap();

assert!(key_protector.dek[0].dek_buffer.iter().all(|&x| x == 1));
assert!(key_protector.dek[1].dek_buffer.iter().all(|&x| x == 2));

assert_eq!(key_protector.gsp[0].gsp_length, GSP_BUFFER_SIZE as u32);
assert!(key_protector.gsp[0].gsp_buffer.iter().all(|&x| x == 3));

assert_eq!(key_protector.gsp[1].gsp_length, GSP_BUFFER_SIZE as u32);
assert!(key_protector.gsp[1].gsp_buffer.iter().all(|&x| x == 4));

assert_eq!(key_protector.active_kp, u32::MAX);

// Read an undersized key protector
let key_protector_bytes = key_protector.as_bytes();
vmgs.write_file(
FileId::KEY_PROTECTOR,
&key_protector_bytes[..key_protector_bytes.len() - 1],
)
.await
.unwrap();
let found_key_protector_result =
read_key_protector(&mut vmgs, key_protector_bytes.len()).await;
assert!(found_key_protector_result.is_err());
assert_eq!(
found_key_protector_result.unwrap_err().to_string(),
"KEY_PROTECTOR valid bytes 2059 smaller than the minimal size 2060"
);

// Read an oversized key protector
vmgs.write_file(FileId::KEY_PROTECTOR, &[1; KEY_PROTECTOR_SIZE + 1])
.await
.unwrap();
let found_key_protector_result = read_key_protector(&mut vmgs, KEY_PROTECTOR_SIZE).await;
assert!(found_key_protector_result.is_err());
assert_eq!(
found_key_protector_result.unwrap_err().to_string(),
"KEY_PROTECTOR valid bytes 2061 larger than the maximum size 2060"
);

// Read a key protector that is equal to the `dek_minimal_size` and smaller than the `KEY_PROTECTOR_SIZE`
// so that padding is added
vmgs.write_file(
FileId::KEY_PROTECTOR,
&key_protector_bytes[..(key_protector_bytes.len() - 10)],
)
.await
.unwrap();
let found_key_protector = read_key_protector(&mut vmgs, key_protector_bytes.len() - 10)
.await
.unwrap();
assert_eq!(
found_key_protector.as_bytes()[..(key_protector_bytes.len() - 10)],
key_protector_bytes[..(key_protector_bytes.len() - 10)]
);
assert_eq!(
found_key_protector.as_bytes()[key_protector_bytes.len() - 10..],
[0; 10]
);
}

#[async_test]
async fn write_vmgs_key_protector_by_id() {
let kp_guid = Guid::new_random();

let mut vmgs = new_formatted_vmgs().await;
let mut key_protector_by_id = KeyProtectorById {
id_guid: kp_guid,
ported: 1,
pad: [0; 3],
};

// Try to read the `key_protector_by_id` from the VMGS file which doesn't have a `key_protector_by_id` entry
let found_key_protector_by_id_result = read_key_protector_by_id(&mut vmgs).await;
assert!(found_key_protector_by_id_result.is_err());
assert_eq!(
found_key_protector_by_id_result.unwrap_err().to_string(),
"entry does not exist, file id: VM_UNIQUE_ID"
);

// Populate the VMGS file with `key_protector_by_id`
write_key_protector_by_id(&mut key_protector_by_id, &mut vmgs, true, kp_guid)
.await
.unwrap();

// Without using force, write the same `kp_guid` to the VMGS file and find that nothing changes
write_key_protector_by_id(&mut key_protector_by_id, &mut vmgs, false, kp_guid)
.await
.unwrap();
// `key_protector_by_id` should still hold `kp_guid`
let found_key_protector_by_id = read_key_protector_by_id(&mut vmgs).await.unwrap();
assert_eq!(found_key_protector_by_id.id_guid, kp_guid);

// Without using force, write a new `Guid` to the VMGS file and find that the `key_protector_by_id` is updated
let bios_guid = Guid::new_random();
write_key_protector_by_id(&mut key_protector_by_id, &mut vmgs, false, bios_guid)
.await
.unwrap();
// `key_protector_by_id` should now hold `new_guid`
let found_key_protector_by_id = read_key_protector_by_id(&mut vmgs).await.unwrap();
assert_eq!(found_key_protector_by_id.id_guid, bios_guid);

// Read a key protector by id from the VMGS file that is undersized
// ported and pad fields are expected to be zeroed
let undersized_key_protector_by_id = key_protector_by_id.as_bytes();
let undersized_key_protector_by_id =
&undersized_key_protector_by_id[..undersized_key_protector_by_id.len() - 1];
vmgs.write_file(FileId::VM_UNIQUE_ID, undersized_key_protector_by_id)
.await
.unwrap();

let found_key_protector_by_id = read_key_protector_by_id(&mut vmgs).await.unwrap();
assert_eq!(
found_key_protector_by_id.id_guid,
key_protector_by_id.id_guid
);
assert_eq!(found_key_protector_by_id.ported, 0);
assert_eq!(found_key_protector_by_id.pad, [0, 0, 0]);
}

#[async_test]
async fn read_security_profile_from_vmgs() {
let mut vmgs = new_formatted_vmgs().await;
let found_security_profile = read_security_profile(&mut vmgs).await.unwrap();

// When no security profile exists, a zeroed security profile will be written to the VMGS
assert_eq!(
found_security_profile.agent_data,
SecurityProfile::new_zeroed().agent_data
);

// Write a security profile to the VMGS
let security_profile = SecurityProfile {
agent_data: [5; AGENT_DATA_MAX_SIZE],
};
vmgs.write_file(FileId::ATTEST, security_profile.as_bytes())
.await
.unwrap();
let found_security_profile = read_security_profile(&mut vmgs).await.unwrap();
assert_eq!(
found_security_profile.agent_data,
security_profile.agent_data
);

// Write a security profile larger than the maximum size to the VMGS
let oversized_security_profile = [6u8; AGENT_DATA_MAX_SIZE + 1];
vmgs.write_file(FileId::ATTEST, oversized_security_profile.as_bytes())
.await
.unwrap();
let found_security_profile_result = read_security_profile(&mut vmgs).await;
assert!(found_security_profile_result.is_err());
assert_eq!(
found_security_profile_result.unwrap_err().to_string(),
"ATTEST valid bytes 2049 larger than the maximum size 2048"
);

// Write a security profile smaller than the maximum size to the VMGS and observe that it is padded with zeros
let undersized_security_profile = [7u8; AGENT_DATA_MAX_SIZE - 10];
vmgs.write_file(FileId::ATTEST, undersized_security_profile.as_bytes())
.await
.unwrap();
let found_security_profile = read_security_profile(&mut vmgs).await.unwrap();
assert_eq!(
found_security_profile.agent_data[..AGENT_DATA_MAX_SIZE - 10],
undersized_security_profile[..]
);
assert_eq!(
found_security_profile.agent_data[AGENT_DATA_MAX_SIZE - 10..],
[0; 10]
);
}

#[async_test]
async fn write_read_hardware_key_protector() {
let mut vmgs = new_formatted_vmgs().await;
let hardware_key_protector = new_hardware_key_protector();
write_hardware_key_protector(&hardware_key_protector, &mut vmgs)
.await
.unwrap();

let found_hardware_key_protector = read_hardware_key_protector(&mut vmgs).await.unwrap();

assert_eq!(
found_hardware_key_protector.header.as_bytes(),
hardware_key_protector.header.as_bytes()
);
assert_eq!(found_hardware_key_protector.iv, hardware_key_protector.iv);
assert_eq!(
found_hardware_key_protector.ciphertext,
hardware_key_protector.ciphertext
);
assert_eq!(
found_hardware_key_protector.hmac,
hardware_key_protector.hmac
);

// Write and then fail to read a hardware key protector larger than the expected size to the VMGS
let oversized_hardware_key_protector = [8u8; HW_KEY_PROTECTOR_SIZE + 1];
vmgs.write_file(FileId::HW_KEY_PROTECTOR, &oversized_hardware_key_protector)
.await
.unwrap();
let found_hardware_key_protector_result = read_hardware_key_protector(&mut vmgs).await;
assert!(found_hardware_key_protector_result.is_err());
assert_eq!(
found_hardware_key_protector_result.unwrap_err().to_string(),
"HW_KEY_PROTECTOR valid bytes 105, expected 104"
);
}

#[async_test]
async fn read_guest_secret_key_from_vmgs() {
let mut vmgs = new_formatted_vmgs().await;

// When no guest secret key exists, an error should be returned
let found_guest_secret_key_result = read_guest_secret_key(&mut vmgs).await;
assert!(found_guest_secret_key_result.is_err());
assert_eq!(
found_guest_secret_key_result.unwrap_err().to_string(),
"entry does not exist, file id: GUEST_SECRET_KEY"
);

// Write a guest secret key to the VMGS
let guest_secret_key = GuestSecretKey {
guest_secret_key: [9; GUEST_SECRET_KEY_MAX_SIZE],
};
vmgs.write_file(FileId::GUEST_SECRET_KEY, guest_secret_key.as_bytes())
.await
.unwrap();
let found_guest_secret_key = read_guest_secret_key(&mut vmgs).await.unwrap();
assert_eq!(
found_guest_secret_key.guest_secret_key,
guest_secret_key.guest_secret_key
);

// Write a guest secret key larger than the maximum size to the VMGS
let oversized_guest_secret_key = [10u8; GUEST_SECRET_KEY_MAX_SIZE + 1];
vmgs.write_file(FileId::GUEST_SECRET_KEY, &oversized_guest_secret_key)
.await
.unwrap();
let found_guest_secret_key_result = read_guest_secret_key(&mut vmgs).await;
assert!(found_guest_secret_key_result.is_err());
assert_eq!(
found_guest_secret_key_result.unwrap_err().to_string(),
"GUEST_SECRET_KEY valid bytes 2049 larger than the maximum size 2048"
);

// Write a guest secret smaller than the maximum size to the VMGS and observe that it is padded with zeros
let undersized_guest_secret_key = [7u8; GUEST_SECRET_KEY_MAX_SIZE - 10];
vmgs.write_file(
FileId::GUEST_SECRET_KEY,
undersized_guest_secret_key.as_bytes(),
)
.await
.unwrap();
let found_guest_secret_key = read_guest_secret_key(&mut vmgs).await.unwrap();
assert_eq!(
found_guest_secret_key.guest_secret_key[..AGENT_DATA_MAX_SIZE - 10],
undersized_guest_secret_key[..]
);
assert_eq!(
found_guest_secret_key.guest_secret_key[AGENT_DATA_MAX_SIZE - 10..],
[0; 10]
);
}
}

0 comments on commit 483db84

Please sign in to comment.