diff --git a/.pipelines/templates/trident-platform-cicd-template.yml b/.pipelines/templates/trident-platform-cicd-template.yml index 98907fa09..403ea0fd7 100644 --- a/.pipelines/templates/trident-platform-cicd-template.yml +++ b/.pipelines/templates/trident-platform-cicd-template.yml @@ -17,6 +17,12 @@ parameters: - amd64 - arm64 + - name: micVersion + displayName: MIC Version + type: string + default: "*.*.*" + + stages: - ${{ if eq( parameters.targetArchitecture, 'amd64') }}: - template: e2e-template.yml @@ -27,6 +33,8 @@ stages: forceFunctionalTestImageRebuild: true baremetalTestsEnabled: ${{ parameters.baremetalTestsEnabled }} baseImageArtifactStage: ${{ parameters.baseImageArtifactStage }} + micVersion: ${{ parameters.micVersion }} + micBuildType: dev - ${{ if eq( parameters.targetArchitecture, 'arm64') }}: - template: e2e-arm64-template.yml diff --git a/crates/osutils/src/dependencies.rs b/crates/osutils/src/dependencies.rs index 6d8907ccb..c4696f4e3 100644 --- a/crates/osutils/src/dependencies.rs +++ b/crates/osutils/src/dependencies.rs @@ -1,7 +1,7 @@ use std::{ borrow::Cow, ffi::{OsStr, OsString}, - io, + fmt, io, os::unix::process::ExitStatusExt, path::PathBuf, process::{Command as StdCommand, Output}, @@ -149,8 +149,8 @@ pub enum Dependency { False, } -impl std::fmt::Display for Dependency { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for Dependency { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.into()) } } @@ -261,6 +261,12 @@ impl Command { self.output()?.check_output() } + pub fn output_and_stderr_and_check(&self) -> Result<(String, String), Box> { + let output = self.output()?; + let stdout = output.check_output()?; + Ok((stdout, output.error_output())) + } + pub fn raw_output_and_check(&self) -> Result> { self.output()?.check_raw_output() } diff --git a/crates/osutils/src/encryption.rs b/crates/osutils/src/encryption.rs index 5e2646a8c..d25c6fdd4 100644 --- a/crates/osutils/src/encryption.rs +++ b/crates/osutils/src/encryption.rs @@ -8,7 +8,7 @@ use anyhow::{Context, Error}; use enumflags2::BitFlags; use log::debug; -use crate::{dependencies::Dependency, pcrlock::PCRLOCK_POLICY_JSON_PATH}; +use crate::dependencies::Dependency; use sysdefs::tpm2::Pcr; use trident_api::constants::LUKS_HEADER_SIZE_IN_MIB; @@ -34,6 +34,7 @@ pub fn systemd_cryptenroll( key_file: impl AsRef, device_path: impl AsRef, pcrs: Option>, + pcrlock_policy_path: Option<&Path>, ) -> Result<(), Error> { debug!( "Enrolling TPM 2.0 device for underlying encrypted volume '{}'", @@ -50,8 +51,9 @@ pub fn systemd_cryptenroll( // against a pcrlock policy. if let Some(pcrs) = pcrs { cmd.arg(to_tpm2_pcrs_arg(pcrs)); - } else { - cmd.arg(format!("--tpm2-pcrlock={PCRLOCK_POLICY_JSON_PATH}")); + } else if let Some(pcrlock_policy_path) = pcrlock_policy_path { + // TODO: ADJUST PATH FOR CONTAINER! + cmd.arg(format!("--tpm2-pcrlock={}", pcrlock_policy_path.display())); } cmd.run_and_check().context(format!( @@ -178,6 +180,12 @@ pub fn cryptsetup_open( device_path: impl AsRef, device_name: &str, ) -> Result<(), Error> { + debug!( + "Opening underlying encrypted device '{}' as '{}'", + device_path.as_ref().display(), + device_name + ); + Dependency::Cryptsetup .cmd() .arg("luksOpen") @@ -337,6 +345,7 @@ mod functional_test { use pytest_gen::functional_test; use sysdefs::partition_types::DiscoverablePartitionType; + use trident_api::constants::TRIDENT_DATASTORE_PATH_DEFAULT; use crate::{ filesystems::MkfsFileSystemType, @@ -416,10 +425,19 @@ mod functional_test { copy_static_pcrlock_files(); // Generate a pcrlock policy that only includes PCR 0 let pcrs = BitFlags::from(Pcr::Pcr0); - pcrlock::generate_pcrlock_policy(pcrs, vec![], vec![]).unwrap(); + let pcrlock_policy_path = + pcrlock::construct_pcrlock_path(Path::new(TRIDENT_DATASTORE_PATH_DEFAULT), None) + .unwrap(); + pcrlock::generate_pcrlock_policy(pcrs, &pcrlock_policy_path, vec![], vec![]).unwrap(); // Run `systemd-cryptenroll` on the partition - systemd_cryptenroll(key_file_path, &partition1.node, None).unwrap(); + systemd_cryptenroll( + key_file_path, + &partition1.node, + None, + Some(&pcrlock_policy_path), + ) + .unwrap(); // Open the encrypted volume, to make the block device available cryptsetup_open(key_file_path, &partition1.node, ENCRYPTED_VOLUME_NAME).unwrap(); @@ -558,10 +576,19 @@ mod functional_test { copy_static_pcrlock_files(); // Generate a pcrlock policy that only includes PCR 0 let pcrs = BitFlags::from(Pcr::Pcr0); - pcrlock::generate_pcrlock_policy(pcrs, vec![], vec![]).unwrap(); + let pcrlock_policy_path = + pcrlock::construct_pcrlock_path(Path::new(TRIDENT_DATASTORE_PATH_DEFAULT), None) + .unwrap(); + pcrlock::generate_pcrlock_policy(pcrs, &pcrlock_policy_path, vec![], vec![]).unwrap(); // Run `systemd-cryptenroll` on the partition - systemd_cryptenroll(key_file_path, &partition1.node, None).unwrap(); + systemd_cryptenroll( + key_file_path, + &partition1.node, + None, + Some(&pcrlock_policy_path), + ) + .unwrap(); // Open the encrypted volume, to make the block device available cryptsetup_open(key_file_path, &partition1.node, ENCRYPTED_VOLUME_NAME).unwrap(); diff --git a/crates/osutils/src/pcrlock.rs b/crates/osutils/src/pcrlock.rs index 90285cc5b..da6d81b06 100644 --- a/crates/osutils/src/pcrlock.rs +++ b/crates/osutils/src/pcrlock.rs @@ -29,11 +29,15 @@ use crate::{ /// /// `systemd-pcrlock` will search for .pcrlock files in a number of dir-s, but Trident will place /// the files exclusively in this directory. -pub const PCRLOCK_DIR: &str = "/var/lib/pcrlock.d"; +pub(crate) const PCRLOCK_DIR: &str = "/var/lib/pcrlock.d"; -/// Path to the pcrlock policy JSON file. This represents the TPM 2.0 access policy that has been -/// generated by `systemd-pcrlock`. -pub const PCRLOCK_POLICY_JSON_PATH: &str = "/var/lib/systemd/pcrlock.json"; +/// Name of the pcrlock policy JSON file that represents the TPM 2.0 access policy that has been +/// generated by `systemd-pcrlock`. This file is placed into the directory adjacent to the +/// datastore. +const PCRLOCK_POLICY_JSON: &str = "pcrlock.json"; + +/// Default path to the pcrlock policy JSON. +pub const PCRLOCK_POLICY_JSON_DEFAULT: &str = "/var/lib/systemd/pcrlock.json"; /// `/var/lib/pcrlock.d/630-boot-loader-code-shim.pcrlock.d`, where `lock-pe` measures the shim /// bootloader binary, i.e. `/EFI/AZL{A/B}/bootx64.efi`, as recorded into PCR 4 following @@ -67,6 +71,7 @@ struct PcrPolicy { /// Generates a new pcrlock policy for the given PCRs, UKI binaries, and bootloader binaries. pub fn generate_pcrlock_policy( pcrs: BitFlags, + pcrlock_policy_path: &Path, uki_binaries: Vec, bootloader_binaries: Vec, ) -> Result<(), TridentError> { @@ -81,15 +86,47 @@ pub fn generate_pcrlock_policy( // Generate pcrlock policy; on A/B update, the existing binding will be automatically // updated with the new pcrlock policy - generate_tpm2_access_policy(pcrs).structured(ServicingError::GenerateTpm2AccessPolicy)?; + generate_tpm2_access_policy(pcrs, pcrlock_policy_path) + .structured(ServicingError::GenerateTpm2AccessPolicy)?; Ok(()) } +/// Constructs the full path to the pcrlock policy JSON file, located in the directory adjacent +/// to the datastore. +pub fn construct_pcrlock_path( + datastore_path: &Path, + newroot_path: Option<&Path>, +) -> Result { + // Fetch the directory path from the full datastore path + let Some(datastore_dir) = datastore_path.parent() else { + bail!( + "Failed to get parent directory for datastore path '{}'", + datastore_path.display() + ); + }; + + // Construct full path to pcrlock policy JSON file + let pcrlock_policy_path = if let Some(new_root) = newroot_path { + path::join_relative(new_root, datastore_dir).join(PCRLOCK_POLICY_JSON) + } else { + datastore_dir.join(PCRLOCK_POLICY_JSON) + }; + trace!( + "Constructed full pcrlock policy JSON path at '{}'", + pcrlock_policy_path.display() + ); + + Ok(pcrlock_policy_path) +} + /// Calls a helper function `systemd-pcrlock make-policy` to generate a TPM 2.0 access policy. /// Parses the contents of the JSON to validate that the pcrlock policy has been updated as /// expected. -fn generate_tpm2_access_policy(pcrs: BitFlags) -> Result<(), Error> { +fn generate_tpm2_access_policy( + pcrs: BitFlags, + pcrlock_policy_path: &Path, +) -> Result<(), Error> { debug!( "Generating a new TPM 2.0 access policy with the following PCRs: {:?}", pcrs.iter().map(|pcr| pcr.to_num()).collect::>() @@ -102,13 +139,13 @@ fn generate_tpm2_access_policy(pcrs: BitFlags) -> Result<(), Error> { { let host_root = container::get_host_root_path().unstructured("Failed to get host root path")?; - let host_pcrlock_json_path = path::join_relative(host_root, PCRLOCK_POLICY_JSON_PATH); + let host_pcrlock_json_path = path::join_relative(host_root, pcrlock_policy_path); if host_pcrlock_json_path.exists() { debug!("Running inside of a container, so copying pcrlock policy JSON from the host at '{}' into the container at '{}'", host_pcrlock_json_path.display(), - PCRLOCK_POLICY_JSON_PATH + pcrlock_policy_path.display() ); - fs::copy(host_pcrlock_json_path, PCRLOCK_POLICY_JSON_PATH) + fs::copy(host_pcrlock_json_path, pcrlock_policy_path) .context("Failed to copy pcrlock policy JSON from host to container")?; } } @@ -116,15 +153,17 @@ fn generate_tpm2_access_policy(pcrs: BitFlags) -> Result<(), Error> { // Run predict command to view predictions, to then compare to the generated pcrlock policy predict().context("Failed to run 'systemd-pcrlock predict' command")?; - make_policy(pcrs).context("Failed to run 'systemd-pcrlock make-policy' command")?; + make_policy(pcrs, pcrlock_policy_path) + .context("Failed to run 'systemd-pcrlock make-policy' command")?; // Log pcrlock policy JSON contents - let pcrlock_policy = fs::read_to_string(PCRLOCK_POLICY_JSON_PATH).context(format!( - "Failed to read pcrlock policy JSON at path '{PCRLOCK_POLICY_JSON_PATH}'" + let pcrlock_policy = fs::read_to_string(pcrlock_policy_path).context(format!( + "Failed to read pcrlock policy JSON at path '{}'", + pcrlock_policy_path.display() ))?; trace!( "Contents of pcrlock policy JSON at '{}':\n{}", - PCRLOCK_POLICY_JSON_PATH, + pcrlock_policy_path.display(), pcrlock_policy ); @@ -134,12 +173,12 @@ fn generate_tpm2_access_policy(pcrs: BitFlags) -> Result<(), Error> { { let host_root = container::get_host_root_path().unstructured("Failed to get host root path")?; - let host_pcrlock_json_path = path::join_relative(host_root, PCRLOCK_POLICY_JSON_PATH); + let host_pcrlock_json_path = path::join_relative(host_root, pcrlock_policy_path); debug!("Running inside of a container, so copying pcrlock policy JSON from the container at '{}' onto the host at '{}'", - PCRLOCK_POLICY_JSON_PATH, + pcrlock_policy_path.display(), host_pcrlock_json_path.display() ); - fs::copy(PCRLOCK_POLICY_JSON_PATH, host_pcrlock_json_path) + fs::copy(pcrlock_policy_path, host_pcrlock_json_path) .context("Failed to copy pcrlock policy JSON from container to host")?; } @@ -304,43 +343,33 @@ fn unrecognized_log_entries( /// Runs `systemd-pcrlock make-policy` command to predict the PCR state for future boots and then /// generate a TPM 2.0 access policy, stored in a TPM 2.0 NV index. The prediction and info about -/// the used TPM 2.0 and its NV index are written to PCRLOCK_POLICY_JSON_PATH. -fn make_policy(pcrs: BitFlags) -> Result<(), Error> { +/// the used TPM 2.0 and its NV index are written to pcrlock_policy_path. +fn make_policy(pcrs: BitFlags, pcrlock_policy_path: &Path) -> Result<(), Error> { debug!( "Running 'systemd-pcrlock make-policy' command to make a new pcrlock policy \ with the following PCRs: {:?}", pcrs.iter().map(|pcr| pcr.to_num()).collect::>() ); - // Run command directly since pcrlock may write to stderr even when a pcrlock policy is - // successfully generated - let mut cmd = Command::new("/usr/lib/systemd/systemd-pcrlock"); - cmd.arg("make-policy").arg(to_pcr_arg(pcrs)); - - // Execute command and capture full output - let output = cmd - .output() + // Use the dependency system to execute the command and capture both stdout and stderr + let (stdout_str, stderr_str) = Dependency::SystemdPcrlock + .cmd() + .arg("make-policy") + .arg(to_pcr_arg(pcrs)) + // TODO: In v255, expected arg `--policy` is ignored and instead, `--pcrlock=` is + // respected. This issue has been fixed in v258: + // https://github.com/systemd/systemd/issues/38506. + .arg(format!("--pcrlock={}", pcrlock_policy_path.display())) + .output_and_stderr_and_check() .context("Failed to execute 'systemd-pcrlock make-policy' command")?; - // Check exit status using standard fields - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - warn!( - "Command 'systemd-pcrlock make-policy' failed with status {}: {}", - output.status, stderr - ); - } - - // Convert stdout to UTF-8 - let stdout_str = String::from_utf8(output.stdout) - .context("Failed to convert stdout of 'systemd-pcrlock make-policy' to a string as it contains invalid UTF-8")?; - let stderr_str = String::from_utf8(output.stderr) - .context("Failed to convert stderr of 'systemd-pcrlock make-policy' to a string as it contains invalid UTF-8")?; - // Log both outputs debug!( - "Output of 'systemd-pcrlock make-policy':\nSTDOUT:\n{}\nSTDERR:\n{}", - stdout_str, stderr_str + "Output of 'systemd-pcrlock make-policy --pcrlock={} {}':\nSTDOUT:\n{}\nSTDERR:\n{}", + pcrlock_policy_path.display(), + to_pcr_arg(pcrs), + stdout_str, + stderr_str ); // Join stdout and stderr for parsing, since systemd-pcrlock will output to stderr even when we // don't get an error, e.g. when components for PCRs we don't care about aren't recognized @@ -351,12 +380,23 @@ fn make_policy(pcrs: BitFlags) -> Result<(), Error> { if !output_str.contains("Calculated new PCR policy") || !output_str.contains("Updated NV index") { warn!( - "The 'systemd-pcrlock make-policy' command did not update the PCR policy as expected. \ + "The 'systemd-pcrlock make-policy --pcrlock={} {}' command did not update the PCR policy as expected. \ Output:\n{}", + pcrlock_policy_path.display(), + to_pcr_arg(pcrs), output_str ); } + // Copy pcrlock policy JSON to PCRLOCK_POLICY_JSON_DEFAULT_PATH because that is what + // cryptsetup expects + debug!( + "Copying generated pcrlock policy JSON from '{}' to default location at '{}'", + pcrlock_policy_path.display(), + PCRLOCK_POLICY_JSON_DEFAULT + ); + fs::copy(pcrlock_policy_path, PCRLOCK_POLICY_JSON_DEFAULT)?; + Ok(()) } @@ -372,9 +412,9 @@ fn predict() -> Result<(), Error> { } /// Removes the previously generated pcrlock policy and deallocates the NV index. -pub fn remove_policy() -> Result<(), Error> { +pub fn remove_policy(pcrlock_policy_path: &Path) -> Result<(), Error> { // Remove the pcrlock policy - let mut pcrlock_policy = vec![PathBuf::from(PCRLOCK_POLICY_JSON_PATH)]; + let mut pcrlock_policy = vec![PathBuf::from(pcrlock_policy_path)]; // If running from inside a container, also remove the pcrlock policy on the host if container::is_running_in_container() @@ -382,7 +422,7 @@ pub fn remove_policy() -> Result<(), Error> { { let host_root = container::get_host_root_path().unstructured("Failed to get host root path")?; - let host_pcrlock_json_path = path::join_relative(host_root, PCRLOCK_POLICY_JSON_PATH); + let host_pcrlock_json_path = path::join_relative(host_root, pcrlock_policy_path); // Append this host path to vector pcrlock_policy.push(host_pcrlock_json_path); } @@ -770,19 +810,22 @@ mod functional_test { use super::*; use pytest_gen::functional_test; + use trident_api::constants::TRIDENT_DATASTORE_PATH_DEFAULT; #[functional_test(feature = "helpers")] fn test_generate_tpm2_access_policy() { // Test case #0. Since no .pcrlock files have been generated yet, only 0-valued PCRs can be // used to generate a TPM 2.0 access policy. let zero_pcrs = Pcr::Pcr11 | Pcr::Pcr12 | Pcr::Pcr13; - generate_tpm2_access_policy(zero_pcrs).unwrap(); + let pcrlock_policy_path = + construct_pcrlock_path(Path::new(TRIDENT_DATASTORE_PATH_DEFAULT), None).unwrap(); + generate_tpm2_access_policy(zero_pcrs, &pcrlock_policy_path).unwrap(); // Test case #1. Try to generate a TPM 2.0 access policy with all PCRs; should return an // error since no .pcrlock files have been generated yet. let pcrs = BitFlags::::all(); assert_eq!( - generate_tpm2_access_policy(pcrs) + generate_tpm2_access_policy(pcrs, &pcrlock_policy_path) .unwrap_err() .root_cause() .to_string(), @@ -790,6 +833,6 @@ mod functional_test { ); // Clean up the generated pcrlock policy - fs::remove_file(PCRLOCK_POLICY_JSON_PATH).unwrap(); + remove_policy(&pcrlock_policy_path).unwrap(); } } diff --git a/crates/trident/src/engine/rollback.rs b/crates/trident/src/engine/rollback.rs index f46593de8..3264f88dc 100644 --- a/crates/trident/src/engine/rollback.rs +++ b/crates/trident/src/engine/rollback.rs @@ -253,8 +253,20 @@ fn commit_finalized_on_expected_root( encryption::get_binary_paths_pcrlock(ctx, pcrs, None) .structured(ServicingError::GetBinaryPathsForPcrlockEncryption)?; + // Construct full path to pcrlock policy JSON file + let pcrlock_policy_path = pcrlock::construct_pcrlock_path( + &datastore.host_status().spec.trident.datastore_path, + None, + ) + .structured(ServicingError::ConstructPcrlockPolicyPath)?; + // Generate a pcrlock policy - pcrlock::generate_pcrlock_policy(pcrs, uki_binaries, bootloader_binaries)?; + pcrlock::generate_pcrlock_policy( + pcrs, + &pcrlock_policy_path, + uki_binaries, + bootloader_binaries, + )?; } else { debug!( "Target OS image is a grub image, \ diff --git a/crates/trident/src/engine/storage/encryption.rs b/crates/trident/src/engine/storage/encryption.rs index f38611098..e45c22ebc 100644 --- a/crates/trident/src/engine/storage/encryption.rs +++ b/crates/trident/src/engine/storage/encryption.rs @@ -17,7 +17,7 @@ use osutils::{ encryption::{self, KeySlotType}, lsblk::{self, BlockDeviceType}, path::join_relative, - pcrlock, + pcrlock::{self, PCRLOCK_POLICY_JSON_DEFAULT}, }; use sysdefs::tpm2::Pcr; use trident_api::{ @@ -119,22 +119,36 @@ pub(super) fn create_encrypted_devices(ctx: &EngineContext) -> Result<(), Triden // If this is for a grub ROS, seal against the value of PCR 7; if this is for a UKI ROS, // seal against a "bootstrapping" pcrlock policy that exclusively contains PCR 0. - let pcr = if ctx.is_uki()? { + let (pcr, pcrlock_policy_path) = if ctx.is_uki()? { debug!("Target OS image is a UKI image, so sealing against a pcrlock policy of PCR 0"); + // Construct full path to pcrlock policy JSON file + let pcrlock_policy_path = + pcrlock::construct_pcrlock_path(&ctx.spec.trident.datastore_path, None) + .structured(ServicingError::ConstructPcrlockPolicyPath)?; + // Remove any pre-existing policy - pcrlock::remove_policy().structured(ServicingError::RemovePcrlockPolicy)?; + pcrlock::remove_policy(&pcrlock_policy_path) + .structured(ServicingError::RemovePcrlockPolicy)?; // Generate a pcrlock policy for the first time - pcrlock::generate_pcrlock_policy(BitFlags::from(Pcr::Pcr0), vec![], vec![])?; - None + pcrlock::generate_pcrlock_policy( + BitFlags::from(Pcr::Pcr0), + &pcrlock_policy_path, + vec![], + vec![], + )?; + (None, Some(pcrlock_policy_path)) } else { debug!("Target OS image is a grub image, so sealing against PCR 7"); - Some( - encryption - .pcrs - .iter() - .fold(BitFlags::empty(), |acc, &pcr| acc | BitFlags::from(pcr)), + ( + Some( + encryption + .pcrs + .iter() + .fold(BitFlags::empty(), |acc, &pcr| acc | BitFlags::from(pcr)), + ), + None, ) }; @@ -185,6 +199,7 @@ pub(super) fn create_encrypted_devices(ctx: &EngineContext) -> Result<(), Triden &key_file_path, encryption_type, pcr, + pcrlock_policy_path.as_deref(), ) .structured(ServicingError::EncryptBlockDevice { device_path: device_path.to_string_lossy().to_string(), @@ -210,6 +225,25 @@ pub(super) fn create_encrypted_devices(ctx: &EngineContext) -> Result<(), Triden })?; } } + + // TODO: test + // If exists, remove file at PCRLOCK_POLICY_JSON_DEFAULT + if Path::new(PCRLOCK_POLICY_JSON_DEFAULT).exists() { + fs::remove_file(PCRLOCK_POLICY_JSON_DEFAULT) + .structured(ServicingError::RemoveDefaultPcrlockPolicyJson)?; + + // Validate that the file has been removed + if Path::new(PCRLOCK_POLICY_JSON_DEFAULT).exists() { + return Err(TridentError::new( + ServicingError::RemoveDefaultPcrlockPolicyJson, + )); + } + debug!( + "Removed default pcrlock policy JSON file at '{}'", + PCRLOCK_POLICY_JSON_DEFAULT + ); + } + tracing::Span::current().record("total_partition_size_bytes", total_partition_size_bytes); } @@ -235,6 +269,7 @@ fn encrypt_and_open_device( key_file: &Path, encryption_type: EncryptionType, pcr: Option>, + pcrlock_policy_path: Option<&Path>, ) -> Result<(), Error> { match encryption_type { EncryptionType::Reencrypt => { @@ -265,7 +300,7 @@ fn encrypt_and_open_device( ); // Enroll the TPM 2.0 device for the underlying device - encryption::systemd_cryptenroll(key_file, device_path, pcr)?; + encryption::systemd_cryptenroll(key_file, device_path, pcr, pcrlock_policy_path)?; debug!( "Opening underlying encrypted device '{}' as '{}'", diff --git a/crates/trident/src/subsystems/storage/encryption.rs b/crates/trident/src/subsystems/storage/encryption.rs index a3333fb32..a288f1ec7 100644 --- a/crates/trident/src/subsystems/storage/encryption.rs +++ b/crates/trident/src/subsystems/storage/encryption.rs @@ -1,5 +1,6 @@ use std::{ fs, + io::Write, os::unix::fs::PermissionsExt, path::{Path, PathBuf}, }; @@ -7,10 +8,7 @@ use std::{ use enumflags2::BitFlags; use log::{debug, info, trace}; -use osutils::{ - container, efivar, encryption as osutils_encryption, files, path, - pcrlock::{self, PCRLOCK_POLICY_JSON_PATH}, -}; +use osutils::{container, efivar, encryption as osutils_encryption, files, path, pcrlock}; use sysdefs::tpm2::Pcr; use trident_api::{ config::{ @@ -177,6 +175,11 @@ pub fn provision(ctx: &EngineContext, mount_path: &Path) -> Result<(), TridentEr } }; + // Construct full path to pcrlock policy JSON file + let pcrlock_policy_path = + pcrlock::construct_pcrlock_path(&ctx.spec.trident.datastore_path, None) + .structured(ServicingError::ConstructPcrlockPolicyPath)?; + // If updated PCRs are specified, re-generate pcrlock policy if let Some(pcrs) = updated_pcrs { debug!( @@ -189,24 +192,85 @@ pub fn provision(ctx: &EngineContext, mount_path: &Path) -> Result<(), TridentEr .structured(ServicingError::GetBinaryPathsForPcrlockEncryption)?; // Re-generate pcrlock policy - pcrlock::generate_pcrlock_policy(pcrs, uki_binaries, bootloader_binaries)?; + pcrlock::generate_pcrlock_policy( + pcrs, + &pcrlock_policy_path, + uki_binaries, + bootloader_binaries, + )?; } // If a pcrlock policy JSON file exists, copy it to the update volume - if Path::new(PCRLOCK_POLICY_JSON_PATH).exists() { - let pcrlock_json_copy = path::join_relative(mount_path, PCRLOCK_POLICY_JSON_PATH); + if pcrlock_policy_path.exists() { + let pcrlock_json_copy = path::join_relative(mount_path, &pcrlock_policy_path); debug!( "Copying pcrlock policy JSON from path '{}' to update volume at path '{}'", - PCRLOCK_POLICY_JSON_PATH, + pcrlock_policy_path.display(), pcrlock_json_copy.display() ); - fs::copy(PCRLOCK_POLICY_JSON_PATH, pcrlock_json_copy.clone()).structured( + fs::copy(&pcrlock_policy_path, pcrlock_json_copy.clone()).structured( ServicingError::CopyPcrlockPolicyJson { - path: PCRLOCK_POLICY_JSON_PATH.to_string(), + path: pcrlock_policy_path.display().to_string(), destination: pcrlock_json_copy.display().to_string(), }, )?; } + + // TODO: REMOVE BEFORE MERGING + // Create drop in systemd files under /etc/systemd to check if that will ensure that /var/lib/trident + // is mounted before systemd-cryptsetup@.services are run + if matches!(ctx.servicing_type, ServicingType::CleanInstall) { + const DROPIN_CONTENTS: &str = "[Unit]\nRequiresMountsFor=/var/lib/trident\n"; + + // Helper: create one drop-in file for a given unit instance name + let create_dropin = |unit: &str| -> Result<(), TridentError> { + // unit is e.g. "systemd-cryptsetup@web\\x2da.service" + let dropin_dir = mount_path + .join("etc/systemd/system") + .join(format!("{unit}.d")); + + fs::create_dir_all(&dropin_dir).structured( + ServicingError::WriteDropInForSystemdCryptsetup { + path: dropin_dir.display().to_string(), + }, + )?; + + let dropin_path = dropin_dir.join("10-trident-requires-trident.mount.conf"); + let mut f = fs::File::create(&dropin_path).structured( + ServicingError::WriteDropInForSystemdCryptsetup { + path: dropin_path.display().to_string(), + }, + )?; + + f.write_all(DROPIN_CONTENTS.as_bytes()).structured( + ServicingError::WriteDropInForSystemdCryptsetup { + path: dropin_path.display().to_string(), + }, + )?; + + debug!( + "Wrote systemd drop-in '{}' with RequiresMountsFor=/var/lib/trident", + dropin_path.display() + ); + + // log contents for debugging + let dropin_file_contents = fs::read_to_string(&dropin_path).structured( + ServicingError::WriteDropInForSystemdCryptsetup { + path: dropin_path.display().to_string(), + }, + )?; + debug!("Drop-in file contents:\n{}", dropin_file_contents); + + Ok(()) + }; + + // web-a instance: systemd-cryptsetup@web\x2da.service + debug!("Creating systemd drop-in for web-a encrypted volume"); + create_dropin("systemd-cryptsetup@web\\x2da.service")?; + // web-b instance: systemd-cryptsetup@web\x2db.service + debug!("Creating systemd drop-in for web-b encrypted volume"); + create_dropin("systemd-cryptsetup@web\\x2db.service")?; + } } Ok(()) @@ -217,54 +281,67 @@ pub fn configure(ctx: &EngineContext) -> Result<(), TridentError> { let path = PathBuf::from(CRYPTTAB_PATH); let mut contents = String::new(); - let Some(ref encryption) = ctx.spec.storage.encryption else { - return Ok(()); - }; - - for ev in encryption.volumes.iter() { - let backing_partition = - ctx.get_first_backing_partition(&ev.device_id) - .structured(InvalidInputError::from( + if let Some(ref encryption) = ctx.spec.storage.encryption { + for ev in encryption.volumes.iter() { + let backing_partition = ctx.get_first_backing_partition(&ev.device_id).structured( + InvalidInputError::from( HostConfigurationStaticValidationError::EncryptedVolumeNotPartitionOrRaid { encrypted_volume: ev.id.clone(), }, - ))?; - let device_path = &ctx.get_block_device_path(&ev.device_id).structured( - ServicingError::FindEncryptedVolumeBlockDevice { - device_id: ev.device_id.clone(), - encrypted_volume: ev.id.clone(), - }, - )?; + ), + )?; + let device_path = &ctx.get_block_device_path(&ev.device_id).structured( + ServicingError::FindEncryptedVolumeBlockDevice { + device_id: ev.device_id.clone(), + encrypted_volume: ev.id.clone(), + }, + )?; - // An encrypted swap device is special-cased in the crypttab due to the unique nature and - // requirements of swap spaces in a Linux system. Since it often contains sensitive data - // temporarily stored in RAM, encrypting it is crucial for security. However, unlike the - // regular partitions, which use TPM 2.0 devices for passwordless startup, systemd - // completely wipes the swap device and formats it on each system startup. - // - // For systemd to do this, it needs a key, and here in the crypttab, the swap device is - // configured with a randomly generated key from `/dev/random`. This is the most reliable - // way to generate a truly random key on Linux systems. - // - // The default cipher (aes-cbc-essiv:sha256) and key size (256) are not used here, to - // enhance the security posture of the swap space and align it with the rest of the - // encrypted devices. - if backing_partition.partition_type == PartitionType::Swap { - contents.push_str(&format!( - "{}\t{}\t{}\tluks,swap,cipher={},size={}\n", - ev.device_name, - device_path.display(), - osutils_encryption::DEV_RANDOM_PATH, - osutils_encryption::CIPHER, - osutils_encryption::KEY_SIZE - )); - } else { - contents.push_str(&format!( - "{}\t{}\t{}\tluks,tpm2-device=auto\n", - ev.device_name, - device_path.display(), - "none" - )); + // Build options + let options = if ctx.is_uki()? { + // Construct full path to pcrlock policy JSON file + let pcrlock_policy_path = + pcrlock::construct_pcrlock_path(&ctx.spec.trident.datastore_path, None) + .structured(ServicingError::ConstructPcrlockPolicyPath)?; + format!( + "luks,tpm2-device=auto,tpm2-pcrlock={}", + pcrlock_policy_path.display() + ) + } else { + "luks,tpm2-device=auto".to_string() + }; + + // An encrypted swap device is special-cased in the crypttab due to the unique nature and + // requirements of swap spaces in a Linux system. Since it often contains sensitive data + // temporarily stored in RAM, encrypting it is crucial for security. However, unlike the + // regular partitions, which use TPM 2.0 devices for passwordless startup, systemd + // completely wipes the swap device and formats it on each system startup. + // + // For systemd to do this, it needs a key, and here in the crypttab, the swap device is + // configured with a randomly generated key from `/dev/random`. This is the most reliable + // way to generate a truly random key on Linux systems. + // + // The default cipher (aes-cbc-essiv:sha256) and key size (256) are not used here, to + // enhance the security posture of the swap space and align it with the rest of the + // encrypted devices. + if backing_partition.partition_type == PartitionType::Swap { + contents.push_str(&format!( + "{}\t{}\t{}\tluks,swap,cipher={},size={}\n", + ev.device_name, + device_path.display(), + osutils_encryption::DEV_RANDOM_PATH, + osutils_encryption::CIPHER, + osutils_encryption::KEY_SIZE + )); + } else { + contents.push_str(&format!( + "{}\t{}\t{}\t{}\n", + ev.device_name, + device_path.display(), + "none", + options + )); + } } } diff --git a/crates/trident_api/src/error.rs b/crates/trident_api/src/error.rs index 87b8f808b..c59f13fc9 100644 --- a/crates/trident_api/src/error.rs +++ b/crates/trident_api/src/error.rs @@ -370,6 +370,9 @@ pub enum ServicingError { explanation: String, }, + #[error("Failed to construct pcrlock policy path in directory adjacent to the datastore")] + ConstructPcrlockPolicyPath, + #[error("Failed to create extension image directories on target OS")] CreateExtensionImageDirectories, @@ -545,6 +548,9 @@ pub enum ServicingError { #[error("Failed to parse non-Unicode path '{path}'")] PathIsNotUnicode { path: String }, + #[error("Failed to persist pcrlock policy from '{path}' to '{destination}'")] + PersistPcrlockPolicy { path: String, destination: String }, + #[error("Failed to do a read operation with efibootmgr")] ReadEfibootmgr, @@ -572,6 +578,11 @@ pub enum ServicingError { #[error("Failed to remove crypttab at path '{crypttab_path}'")] RemoveCrypttab { crypttab_path: String }, + #[error( + "Failed to remove default pcrlock policy JSON file at '/var/lib/systemd/pcrlock.json'" + )] + RemoveDefaultPcrlockPolicyJson, + #[error("Failed to remove Netplan config")] RemoveNetplanConfig, @@ -652,6 +663,9 @@ pub enum ServicingError { #[error("Failed to write an additional file '{file_name}'")] WriteAdditionalFile { file_name: String }, + #[error("Failed to write drop-in for systemd-cryptsetup at path '{path}'")] + WriteDropInForSystemdCryptsetup { path: String }, + #[error("Failed to write Netplan config")] WriteNetplanConfig, } diff --git a/tests/e2e_tests/trident_configurations/combined/trident-config.yaml b/tests/e2e_tests/trident_configurations/combined/trident-config.yaml index 722b7e508..d25a44a61 100644 --- a/tests/e2e_tests/trident_configurations/combined/trident-config.yaml +++ b/tests/e2e_tests/trident_configurations/combined/trident-config.yaml @@ -156,7 +156,9 @@ storage: options: defaults,ro - deviceId: web source: new - mountPoint: /web + mountPoint: + path: /web + options: defaults,x-systemd.device-timeout=4,x-systemd.requires=var-lib-trident.mount,x-systemd.after=var-lib-trident.mount - deviceId: trident source: new mountPoint: /var/lib/trident