diff --git a/README.md b/README.md index adca9c2..50197bd 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ existing UEFI bootloader or booted by the hardware directly. Sprout is licensed under Apache 2.0 and is open to modifications and contributions. -**IMPORTANT WARNING**: Sprout does not support UEFI Secure Boot yet. +**IMPORTANT WARNING**: Sprout does not support all of UEFI Secure Boot yet. See [this issue](https://github.com/edera-dev/sprout/issues/20) for updates. ## Background @@ -65,13 +65,13 @@ The boot menu mechanism is very rudimentary. - [x] Load Linux initrd from disk - [x] Basic boot menu - [x] BLS autoconfiguration support +- [x] [Secure Boot support](https://github.com/edera-dev/sprout/issues/20): partial ### Roadmap - [ ] [Bootloader interface support](https://github.com/edera-dev/sprout/issues/21) - [ ] [BLS specification conformance](https://github.com/edera-dev/sprout/issues/2) - [ ] [Full-featured boot menu](https://github.com/edera-dev/sprout/issues/1) -- [ ] [Secure Boot support](https://github.com/edera-dev/sprout/issues/20): work in progress - [ ] [UKI support](https://github.com/edera-dev/sprout/issues/6): partial - [ ] [multiboot2 support](https://github.com/edera-dev/sprout/issues/7) - [ ] [Linux boot protocol (boot without EFI stub)](https://github.com/edera-dev/sprout/issues/7) diff --git a/docs/windows-setup.md b/docs/windows-setup.md index f45ef58..6bd78df 100644 --- a/docs/windows-setup.md +++ b/docs/windows-setup.md @@ -2,7 +2,7 @@ ## Prerequisites -- Secure Boot disabled +- Secure Boot is disabled or configured to allow Sprout - UEFI Windows installation ## Step 1: Base Installation diff --git a/src/actions/chainload.rs b/src/actions/chainload.rs index 62dd664..c23985d 100644 --- a/src/actions/chainload.rs +++ b/src/actions/chainload.rs @@ -36,7 +36,7 @@ pub fn chainload(context: Rc, configuration: &ChainloadConfigurat // Resolve the path to the image to chainload. let resolved = utils::resolve_path( - context.root().loaded_image_path()?, + Some(context.root().loaded_image_path()?), &context.stamp(&configuration.path), ) .context("unable to resolve chainload path")?; @@ -90,8 +90,9 @@ pub fn chainload(context: Rc, configuration: &ChainloadConfigurat // If an initrd is provided, register it with the EFI stack. let mut initrd_handle = None; if let Some(linux_initrd) = initrd { - let content = utils::read_file_contents(context.root().loaded_image_path()?, &linux_initrd) - .context("unable to read linux initrd")?; + let content = + utils::read_file_contents(Some(context.root().loaded_image_path()?), &linux_initrd) + .context("unable to read linux initrd")?; let handle = MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice()) .context("unable to register linux initrd")?; diff --git a/src/actions/edera.rs b/src/actions/edera.rs index b5a8e1f..b0794de 100644 --- a/src/actions/edera.rs +++ b/src/actions/edera.rs @@ -98,7 +98,7 @@ fn register_media_loader_file( // Stamp the path to the file. let path = context.stamp(path); // Read the file contents. - let content = utils::read_file_contents(context.root().loaded_image_path()?, &path) + let content = utils::read_file_contents(Some(context.root().loaded_image_path()?), &path) .context(format!("unable to read {} file", what))?; // Register the media loader. let handle = MediaLoaderHandle::register(guid, content.into_boxed_slice()) diff --git a/src/actions/splash.rs b/src/actions/splash.rs index 173ed43..1cf0691 100644 --- a/src/actions/splash.rs +++ b/src/actions/splash.rs @@ -143,7 +143,7 @@ pub fn splash(context: Rc, configuration: &SplashConfiguration) - // Stamp the image path value. let image = context.stamp(&configuration.image); // Read the image contents. - let image = read_file_contents(context.root().loaded_image_path()?, &image)?; + let image = read_file_contents(Some(context.root().loaded_image_path()?), &image)?; // Decode the image as a PNG. let image = ImageReader::with_format(Cursor::new(image), ImageFormat::Png) .decode() diff --git a/src/config/loader.rs b/src/config/loader.rs index d02ed18..6c5342e 100644 --- a/src/config/loader.rs +++ b/src/config/loader.rs @@ -19,7 +19,7 @@ fn load_raw_config(options: &SproutOptions) -> Result> { info!("configuration file: {}", options.config); // Read the contents of the sprout config file. - let content = utils::read_file_contents(&path, &options.config) + let content = utils::read_file_contents(Some(&path), &options.config) .context("unable to read sprout config file")?; // Return the contents of the sprout config file. Ok(content) diff --git a/src/drivers.rs b/src/drivers.rs index 0d387b1..8181be3 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -26,7 +26,7 @@ fn load_driver(context: Rc, driver: &DriverDeclaration) -> Result // Resolve the path to the driver image. let resolved = utils::resolve_path( - context.root().loaded_image_path()?, + Some(context.root().loaded_image_path()?), &context.stamp(&driver.path), ) .context("unable to resolve path to driver")?; diff --git a/src/generators/bls.rs b/src/generators/bls.rs index 4133427..8fcfab2 100644 --- a/src/generators/bls.rs +++ b/src/generators/bls.rs @@ -49,7 +49,7 @@ pub fn generate(context: Rc, bls: &BlsConfiguration) -> Result { OwnedDataBuffer(Option<&'a ResolvedPath>, Vec), /// Data loaded into a buffer and ready to be verified. DataBuffer(Option<&'a ResolvedPath>, &'a [u8]), + /// Low-level data buffer provided by the security hook. + SecurityHookBuffer(Option<*const FfiDevicePath>, &'a [u8]), + /// Low-level owned data buffer provided by the security hook. + SecurityHookOwnedBuffer(Option<*const FfiDevicePath>, Vec), + /// Low-level path provided by the security hook. + SecurityHookPath(*const FfiDevicePath), /// Data is provided as a resolved path. We will need to load the data to verify it. /// The output will them return the loaded data. ResolvedPath(&'a ResolvedPath), @@ -27,6 +40,9 @@ impl<'a> ShimInput<'a> { pub fn buffer(&self) -> Option<&[u8]> { match self { ShimInput::OwnedDataBuffer(_, data) => Some(data), + ShimInput::SecurityHookOwnedBuffer(_, data) => Some(data), + ShimInput::SecurityHookBuffer(_, data) => Some(data), + ShimInput::SecurityHookPath(_) => None, ShimInput::DataBuffer(_, data) => Some(data), ShimInput::ResolvedPath(_) => None, } @@ -37,7 +53,14 @@ impl<'a> ShimInput<'a> { match self { ShimInput::OwnedDataBuffer(path, _) => path.as_ref().map(|it| it.full_path.as_ref()), ShimInput::DataBuffer(path, _) => path.as_ref().map(|it| it.full_path.as_ref()), + ShimInput::SecurityHookBuffer(path, _) => { + path.map(|it| unsafe { DevicePath::from_ffi_ptr(it) }) + } + ShimInput::SecurityHookPath(path) => unsafe { Some(DevicePath::from_ffi_ptr(*path)) }, ShimInput::ResolvedPath(path) => Some(path.full_path.as_ref()), + ShimInput::SecurityHookOwnedBuffer(path, _) => { + path.map(|it| unsafe { DevicePath::from_ffi_ptr(it) }) + } } } @@ -51,9 +74,33 @@ impl<'a> ShimInput<'a> { Ok(ShimInput::OwnedDataBuffer(root, data.to_vec())) } + ShimInput::SecurityHookPath(ffi_path) => { + // Acquire the file path. + let Some(path) = self.file_path() else { + bail!("unable to convert security hook path to device path"); + }; + // Convert the underlying path to a string. + let path = path + .to_string(DisplayOnly(false), AllowShortcuts(false)) + .context("unable to convert device path to string")?; + let path = utils::resolve_path(None, &path.to_string()) + .context("unable to resolve path")?; + // Read the file path. + let data = path.read_file()?; + Ok(ShimInput::SecurityHookOwnedBuffer(Some(ffi_path), data)) + } + + ShimInput::SecurityHookBuffer(_, _) => { + bail!("unable to convert security hook buffer to owned data buffer") + } + ShimInput::ResolvedPath(path) => { Ok(ShimInput::OwnedDataBuffer(Some(path), path.read_file()?)) } + + ShimInput::SecurityHookOwnedBuffer(path, data) => { + Ok(ShimInput::SecurityHookOwnedBuffer(path, data)) + } } } } @@ -83,6 +130,10 @@ struct ShimLockProtocol { } impl ShimSupport { + /// Variable controller for the shim lock. + const SHIM_LOCK_VARIABLES: VariableController = + VariableController::new(VariableVendor(Self::SHIM_LOCK_GUID)); + /// GUID for the shim lock protocol. const SHIM_LOCK_GUID: Guid = guid!("605dab50-e046-4300-abb6-3dd810dd8b23"); /// GUID for the shim image loader protocol. @@ -103,7 +154,7 @@ impl ShimSupport { } /// Use the shim to validate the `input`, returning [ShimVerificationOutput] when complete. - pub fn validate(input: ShimInput) -> Result { + pub fn verify(input: ShimInput) -> Result { // Acquire the handle to the shim lock protocol. let handle = utils::find_handle(&Self::SHIM_LOCK_GUID) .context("unable to find shim lock protocol")? @@ -117,21 +168,27 @@ impl ShimSupport { ShimInput::OwnedDataBuffer(_, _data) => { bail!("owned data buffer is not supported in the verification function"); } + ShimInput::SecurityHookBuffer(_, _) => None, + ShimInput::SecurityHookOwnedBuffer(_, _) => None, ShimInput::DataBuffer(_, _) => None, ShimInput::ResolvedPath(path) => Some(path.read_file()?), + ShimInput::SecurityHookPath(_) => None, }; // Convert the input to a buffer. // If the input provides the data buffer, we will use that. // Otherwise, we will use the data loaded by this function. - let buffer = match input { - ShimInput::OwnedDataBuffer(_root, _data) => { - bail!("owned data buffer is not supported in the verification function"); - } - ShimInput::DataBuffer(_root, data) => data, + let buffer = match &input { + ShimInput::OwnedDataBuffer(_root, data) => data, + ShimInput::DataBuffer(_root, data) => *data, ShimInput::ResolvedPath(_path) => maybe_loaded_data .as_deref() .context("expected data buffer to be loaded already")?, + ShimInput::SecurityHookBuffer(_, data) => data, + ShimInput::SecurityHookOwnedBuffer(_, data) => data, + ShimInput::SecurityHookPath(_) => { + bail!("security hook path input not supported in the verification function") + } }; // Check if the buffer is too large to verify. @@ -168,12 +225,19 @@ impl ShimSupport { // Determines whether LoadImage in Boot Services must be patched. // Version 16 of the shim doesn't require extra effort to load Secure Boot binaries. - // If the image loader is installed, we can skip over the security override. - let requires_security_override = shim_loaded && !shim_loader_available; + // If the image loader is installed, we can skip over the security hook. + let requires_security_hook = shim_loaded && !shim_loader_available; - // If the security override is required, we will bail for now. - if requires_security_override { - bail!("shim image loader protocol is not available, please upgrade to shim version 16"); + // If the security hook is required, we will bail for now. + if requires_security_hook { + // Install the security hook, if possible. If it's not, this is necessary to continue + // so we should bail. + let installed = SecurityHook::install().context("unable to install security hook")?; + if !installed { + bail!("unable to install security hook require for this platform"); + } + // Retain the shim protocol after load. + Self::retain()? } // Converts the shim input to an owned data buffer. @@ -188,6 +252,21 @@ impl ShimSupport { }; // Loads the image using Boot Services LoadImage function. - uefi::boot::load_image(current_image, source).context("unable to load image") + let result = uefi::boot::load_image(current_image, source).context("unable to load image"); + + // If the security override is required, we will uninstall the security hook. + if requires_security_hook { + SecurityHook::uninstall().context("unable to uninstall security hook")?; + } + result + } + + /// Set the ShimRetainProtocol variable to indicate that shim should retain the protocols + /// for the full lifetime of boot services. + pub fn retain() -> Result<()> { + Self::SHIM_LOCK_VARIABLES + .set_bool("ShimRetainProtocol", true) + .context("unable to retain shim protocol")?; + Ok(()) } } diff --git a/src/integrations/shim/hook.rs b/src/integrations/shim/hook.rs new file mode 100644 index 0000000..568835c --- /dev/null +++ b/src/integrations/shim/hook.rs @@ -0,0 +1,214 @@ +use crate::integrations::shim::{ShimInput, ShimSupport, ShimVerificationOutput}; +use crate::utils; +use anyhow::{Context, Result, bail}; +use log::warn; +use std::sync::{LazyLock, Mutex}; +use uefi::proto::device_path::FfiDevicePath; +use uefi::proto::unsafe_protocol; +use uefi::{Guid, guid}; +use uefi_raw::Status; + +/// GUID for the EFI_SECURITY_ARCH protocol. +const SECURITY_ARCH_GUID: Guid = guid!("a46423e3-4617-49f1-b9ff-d1bfa9115839"); +/// GUID for the EFI_SECURITY_ARCH2 protocol. +const SECURITY_ARCH2_GUID: Guid = guid!("94ab2f58-1438-4ef1-9152-18941a3a0e68"); + +/// EFI_SECURITY_ARCH protocol definition. +#[unsafe_protocol(SECURITY_ARCH_GUID)] +pub struct SecurityArchProtocol { + /// Determines the file authentication state. + pub file_authentication_state: unsafe extern "efiapi" fn( + this: *const SecurityArchProtocol, + status: u32, + path: *mut FfiDevicePath, + ) -> Status, +} + +/// EFI_SECURITY_ARCH2 protocol definition. +#[unsafe_protocol(SECURITY_ARCH2_GUID)] +pub struct SecurityArch2Protocol { + /// Determines the file authentication. + pub file_authentication: unsafe extern "efiapi" fn( + this: *const SecurityArch2Protocol, + path: *mut FfiDevicePath, + file_buffer: *mut u8, + file_size: usize, + boot_policy: bool, + ) -> Status, +} + +/// Global state for the security hook. +struct SecurityHookState { + original_hook: SecurityArchProtocol, + original_hook2: SecurityArch2Protocol, +} + +/// Global state for the security hook. +/// This is messy, but it is safe given the mutex. +static GLOBAL_HOOK_STATE: LazyLock>> = + LazyLock::new(|| Mutex::new(None)); + +/// Security hook helper. +pub struct SecurityHook; + +impl SecurityHook { + /// Shared verifier logic for both hook types. + fn verify(input: ShimInput) -> Status { + // Verify the input. + match ShimSupport::verify(input) { + Ok(output) => match output { + // If the verification failed, return the access-denied status. + ShimVerificationOutput::VerificationFailed => Status::ACCESS_DENIED, + // If the verification succeeded, return the success status. + ShimVerificationOutput::VerifiedDataNotLoaded => Status::SUCCESS, + ShimVerificationOutput::VerifiedDataBuffer(_) => Status::SUCCESS, + }, + + // If an error occurs, log the error since we can't return a better error. + // Then return the access-denied status. + Err(error) => { + warn!("unable to verify image: {}", error); + Status::ACCESS_DENIED + } + } + } + + /// File authentication state verifier for the EFI_SECURITY_ARCH protocol. + /// Takes the `path` and determines the verification. + unsafe extern "efiapi" fn arch_file_authentication_state( + _this: *const SecurityArchProtocol, + _status: u32, + path: *mut FfiDevicePath, + ) -> Status { + // Verify the path is not null. + if path.is_null() { + return Status::INVALID_PARAMETER; + } + + // Construct a shim input from the path. + let input = ShimInput::SecurityHookPath(path); + + // Verify the input. + Self::verify(input) + } + + /// File authentication verifier for the EFI_SECURITY_ARCH2 protocol. + /// Takes the `path` and a file buffer to determine the verification. + unsafe extern "efiapi" fn arch2_file_authentication( + _this: *const SecurityArch2Protocol, + path: *mut FfiDevicePath, + file_buffer: *mut u8, + file_size: usize, + boot_policy: bool, + ) -> Status { + // Verify the path and file buffer are not null. + if path.is_null() || file_buffer.is_null() { + return Status::INVALID_PARAMETER; + } + + // If the boot policy is true, we can't continue as we don't support that. + if boot_policy { + return Status::INVALID_PARAMETER; + } + + // Construct a slice out of the file buffer and size. + let buffer = unsafe { std::slice::from_raw_parts_mut(file_buffer, file_size) }; + + // Construct a shim input from the path. + let input = ShimInput::SecurityHookBuffer(Some(path), buffer); + + // Verify the input. + Self::verify(input) + } + + /// Install the security hook if needed. + pub fn install() -> Result { + // Find the security arch protocol. If we can't find it, we will return false. + let Some(hook_arch) = utils::find_handle(&SECURITY_ARCH_GUID) + .context("unable to check security arch existence")? + else { + return Ok(false); + }; + + // Find the security arch2 protocol. If we can't find it, we will return false. + let Some(hook_arch2) = utils::find_handle(&SECURITY_ARCH2_GUID) + .context("unable to check security arch2 existence")? + else { + return Ok(false); + }; + + // Open the security arch protocol. + let mut arch_protocol = + uefi::boot::open_protocol_exclusive::(hook_arch) + .context("unable to open security arch protocol")?; + + // Open the security arch2 protocol. + let mut arch_protocol2 = + uefi::boot::open_protocol_exclusive::(hook_arch2) + .context("unable to open security arch2 protocol")?; + + // Construct the global state to store. + let state = SecurityHookState { + original_hook: SecurityArchProtocol { + file_authentication_state: arch_protocol.file_authentication_state, + }, + original_hook2: SecurityArch2Protocol { + file_authentication: arch_protocol2.file_authentication, + }, + }; + + // Acquire the lock to the global state and replace it. + let Ok(mut global_state) = GLOBAL_HOOK_STATE.lock() else { + bail!("unable to acquire global hook state lock"); + }; + global_state.replace(state); + + // Install the hooks into the UEFI stack. + arch_protocol.file_authentication_state = Self::arch_file_authentication_state; + arch_protocol2.file_authentication = Self::arch2_file_authentication; + + Ok(true) + } + + /// Uninstalls the global security hook, if installed. + pub fn uninstall() -> Result<()> { + // Find the security arch protocol. If we can't find it, we will do nothing. + let Some(hook_arch) = utils::find_handle(&SECURITY_ARCH_GUID) + .context("unable to check security arch existence")? + else { + return Ok(()); + }; + + // Find the security arch2 protocol. If we can't find it, we will do nothing. + let Some(hook_arch2) = utils::find_handle(&SECURITY_ARCH2_GUID) + .context("unable to check security arch2 existence")? + else { + return Ok(()); + }; + + // Open the security arch protocol. + let mut arch_protocol = + uefi::boot::open_protocol_exclusive::(hook_arch) + .context("unable to open security arch protocol")?; + + // Open the security arch2 protocol. + let mut arch_protocol2 = + uefi::boot::open_protocol_exclusive::(hook_arch2) + .context("unable to open security arch2 protocol")?; + + // Acquire the lock to the global state. + let Ok(mut global_state) = GLOBAL_HOOK_STATE.lock() else { + bail!("unable to acquire global hook state lock"); + }; + + // Take the state and replace the original functions. + let Some(state) = global_state.take() else { + return Ok(()); + }; + + // Reinstall the original functions. + arch_protocol.file_authentication_state = state.original_hook.file_authentication_state; + arch_protocol2.file_authentication = state.original_hook2.file_authentication; + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 5028efa..fd36399 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,9 +72,9 @@ pub mod utils; /// Run Sprout, returning an error if one occurs. fn run() -> Result<()> { - // For safety reasons, we will bail early if Secure Boot is enabled. + // For safety reasons, we will note that Secure Boot is in beta on Sprout. if SecureBoot::enabled().context("unable to determine Secure Boot status")? { - warn!("Secure Boot is enabled. Sprout does not currently support Secure Boot."); + warn!("Secure Boot is enabled. Sprout Secure Boot is in beta."); } // Start the platform timer. diff --git a/src/utils.rs b/src/utils.rs index 6b999ff..b65a240 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -121,7 +121,7 @@ impl ResolvedPath { /// Resolve a path specified by `input` to its various components. /// Uses `default_root_path` as the base root if one is not specified in the path. /// Returns [ResolvedPath] which contains the resolved components. -pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result { +pub fn resolve_path(default_root_path: Option<&DevicePath>, input: &str) -> Result { let mut path = text_to_device_path(input).context("unable to convert text to path")?; let path_has_device = path .node_iter() @@ -137,6 +137,9 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result Result Result> { +pub fn read_file_contents(default_root_path: Option<&DevicePath>, input: &str) -> Result> { let resolved = resolve_path(default_root_path, input)?; resolved.read_file() } diff --git a/src/utils/variables.rs b/src/utils/variables.rs index 06f171c..8448be7 100644 --- a/src/utils/variables.rs +++ b/src/utils/variables.rs @@ -90,4 +90,10 @@ impl VariableController { .collect::>(); self.set(key, &encoded, class) } + + /// Set a boolean variable specified by `key` to `value`, converting the value. + /// The variable `class` controls the attributes for the variable. + pub fn set_bool(&self, key: &str, value: bool) -> Result<()> { + self.set(key, &[value as u8], VariableClass::BootAndRuntimeTemporary) + } }