From 519d09bb3ed5b4422b4c697ff118618f0e03b197 Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Sun, 8 Nov 2026 04:00:20 +1100 Subject: [PATCH 1/9] soc: apple: sep: support a trusted-key-only initrd attach Keep host state, identity-index loading and biometric device publication off until the real root completes the original attach. Open xART read-only in the initrd, suppress keybag and ref-key provisioning, and cache trusted-key material without retaining initrd file handles. Refuse an unseal when persistent-state writes were denied, while preserving the real root's boot-policy window. Add the root-only complete_attach attribute and let load-driver finish an initrd-loaded module without another SEP registration. Cover completion rejection, retries and idempotence in KUnit, plus production cache, refusal and loader paths in buildbox host tests. Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Assisted-by: LLM --- .../ABI/testing/sysfs-driver-apple-sep | 23 ++ drivers/soc/apple/Makefile | 2 +- drivers/soc/apple/attach.rs | 157 ++++++++++ drivers/soc/apple/attach_shim.c | 50 +++ drivers/soc/apple/diag.rs | 20 ++ drivers/soc/apple/diag_shim.c | 24 +- drivers/soc/apple/refkey.rs | 101 ++++-- drivers/soc/apple/sbio.rs | 15 +- drivers/soc/apple/sep.rs | 145 ++++++++- drivers/soc/apple/shim.h | 9 + tools/aurora-sep/load-driver | 17 +- tools/aurora-sep/tests/keystore-only.config | 21 ++ tools/aurora-sep/tests/test_keystore_only.py | 288 ++++++++++++++++++ 13 files changed, 817 insertions(+), 55 deletions(-) create mode 100644 drivers/soc/apple/attach.rs create mode 100644 drivers/soc/apple/attach_shim.c create mode 100644 tools/aurora-sep/tests/keystore-only.config create mode 100644 tools/aurora-sep/tests/test_keystore_only.py diff --git a/Documentation/ABI/testing/sysfs-driver-apple-sep b/Documentation/ABI/testing/sysfs-driver-apple-sep index b48d26187f67d..6049df1852470 100644 --- a/Documentation/ABI/testing/sysfs-driver-apple-sep +++ b/Documentation/ABI/testing/sysfs-driver-apple-sep @@ -359,3 +359,26 @@ Description: tools/aurora-sep/XART-SAFETY.md. During a boot the value can only move from "unverified" to "blocked", once the guard refuses a write. + +What: /sys/bus/platform/devices/.../complete_attach +Date: November 2026 +Contact: Aurora Silicon maintainers +Description: + Write-only, root-only (0200). Writing 1 completes an attach + started with apple_sep.keystore_only=1: opens host state and + the identity index on the real root, reopens xART with the + configured write and guard policy, and publishes the biometric + devices. Sensor activation still occurs on first device open, + just as with a normal attach. No SEP registration is repeated. + Successful completion is idempotent. Failed store opens may + be retried. Returns EINVAL for other values, or if the driver + was loaded without keystore_only. The read-only module + parameter retains its load-time value after completion. + + In keystore-only mode, xART is read-only regardless of + xart_writes, provisioning is disabled, and no host-state or + guard files are opened for writing. The initrd must carry the + existing identity keybag and machine ref-key at their usual + /var/lib paths; both are cached on first trusted-key use. + An unseal needing an xART write fails with EBADMSG so userspace + can use its passphrase fallback. diff --git a/drivers/soc/apple/Makefile b/drivers/soc/apple/Makefile index 57d06930401c4..14fae267889d5 100644 --- a/drivers/soc/apple/Makefile +++ b/drivers/soc/apple/Makefile @@ -44,7 +44,7 @@ obj-$(CONFIG_APPLE_SEP_FV_INTERFACE) += fv_shim.o obj-$(CONFIG_APPLE_SEP) += apple-sep.o apple-sep-y := sep.o hwrng_shim.o store_shim.o bio_shim.o sha_shim.o \ crypto_shim.o sensor_shim.o p256_shim.o trusted_shim.o work_shim.o \ - pm_shim.o diag_shim.o + pm_shim.o diag_shim.o attach_shim.o obj-$(CONFIG_APPLE_PMP) += apple-pmp.o apple-pmp-y := pmp.o pmp-export.o pmp-display.o pmp-telemetry.o diff --git a/drivers/soc/apple/attach.rs b/drivers/soc/apple/attach.rs new file mode 100644 index 0000000000000..7e628c16fb0bc --- /dev/null +++ b/drivers/soc/apple/attach.rs @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +//! The initrd attach phase and its real-root completion. + +use kernel::{prelude::*, sync::Arc}; + +use crate::SepData; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Phase { + Normal, + KeyStoreOnly, + Complete, +} + +impl Phase { + pub(crate) fn new(keystore_only: bool) -> Self { + if keystore_only { + Self::KeyStoreOnly + } else { + Self::Normal + } + } + + pub(crate) fn needs_completion(self) -> Result { + match self { + Self::Normal => Err(EINVAL), + Self::KeyStoreOnly => Ok(true), + Self::Complete => Ok(false), + } + } + + /// Serialize this under attach_phase. A failed operation stays retryable; + /// success never repeats the operation, even with concurrent sysfs writers. + pub(crate) fn complete(&mut self, operation: impl FnOnce() -> Result<()>) -> Result<()> { + if self.needs_completion()? { + operation()?; + *self = Self::Complete; + } + Ok(()) + } +} + +/// An enclave success cannot override a refused persistent-state write. +pub(crate) fn unseal_allowed(before: u32, after: u32) -> bool { + before == after +} + +extern "C" { + fn sep_keystore_only() -> bool; + fn sep_complete_attach_register( + dev: *mut c_void, + ctx: *mut c_void, + complete: unsafe extern "C" fn(*mut c_void) -> c_int, + ) -> c_int; + fn sep_complete_attach_unregister(dev: *mut c_void); +} + +pub(crate) fn requested() -> bool { + // SAFETY: the parameter is immutable after module loading. + unsafe { sep_keystore_only() } +} + +/// Holds the callback context alive until sysfs has drained all writers. +pub(crate) struct Attribute { + sep: Arc, + registered: bool, +} + +impl Attribute { + pub(crate) fn register(sep: Arc) -> Result { + // SAFETY: the owned Arc pins the context until unregister has drained + // sysfs callbacks. The shim keeps no pointer after unregister returns. + kernel::error::to_result(unsafe { + sep_complete_attach_register( + sep.dev.as_raw().cast(), + Arc::as_ptr(&sep).cast_mut().cast(), + complete, + ) + })?; + Ok(Self { + sep, + registered: true, + }) + } + + pub(crate) fn unregister(&mut self) { + if self.registered { + // SAFETY: matched to the successful register and called only once. + unsafe { sep_complete_attach_unregister(self.sep.dev.as_raw().cast()) }; + self.registered = false; + } + } +} + +impl Drop for Attribute { + fn drop(&mut self) { + self.unregister(); + } +} + +/// # Safety +/// The shim calls with the context pinned by Attribute, under sysfs active +/// protection; unregister waits for this callback before releasing the Arc. +unsafe extern "C" fn complete(ctx: *mut c_void) -> c_int { + // SAFETY: guaranteed by the registration above. + let sep = unsafe { &*ctx.cast::() }; + match sep.complete_attach() { + Ok(()) => 0, + Err(e) => e.to_errno(), + } +} + +#[kernel::macros::kunit_tests(apple_sep_attach)] +mod tests { + use super::*; + + #[test] + fn normal_attach_rejects_completion() { + let mut phase = Phase::new(false); + assert_eq!( + phase.complete(|| panic!("must not touch stores")), + Err(EINVAL) + ); + } + + #[test] + fn completion_is_idempotent() { + let mut phase = Phase::new(true); + let mut calls = 0; + assert_eq!( + phase.complete(|| { + calls += 1; + Ok(()) + }), + Ok(()) + ); + assert_eq!(phase.complete(|| panic!("must not repeat attach")), Ok(())); + assert_eq!(calls, 1); + } + + #[test] + fn failed_completion_can_retry() { + let mut phase = Phase::new(true); + assert_eq!(phase.complete(|| Err(EIO)), Err(EIO)); + assert_eq!(phase.needs_completion(), Ok(true)); + assert_eq!(phase.complete(|| Ok(())), Ok(())); + assert_eq!(phase.needs_completion(), Ok(false)); + } + + #[test] + fn refused_initrd_write_fails_unseal_closed() { + assert!(unseal_allowed(0, 0)); + assert!(unseal_allowed(17, 17)); + assert!(!unseal_allowed(0, 1)); + assert!(!unseal_allowed(u32::MAX, 0)); + } +} diff --git a/drivers/soc/apple/attach_shim.c b/drivers/soc/apple/attach_shim.c new file mode 100644 index 0000000000000..1fa8f9cdea620 --- /dev/null +++ b/drivers/soc/apple/attach_shim.c @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* Initrd mode parameter and the root-only completion trigger. */ +#include +#include +#include + +#include "shim.h" + +/* Rust's module! parameters currently have no sysfs getter. */ +static unsigned char keystore_only; +module_param(keystore_only, byte, 0444); +MODULE_PARM_DESC(keystore_only, + "Attach for trusted keys only; defer persistent host state and Touch ID until complete_attach"); + +bool sep_keystore_only(void) +{ + return keystore_only != 0; +} + +/* One SEP per machine. device_remove_file drains callbacks before release. */ +static void *sep_attach_ctx; +static int (*sep_attach_complete)(void *ctx); + +static ssize_t complete_attach_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + + if (!sysfs_streq(buf, "1")) + return -EINVAL; + ret = sep_attach_complete(sep_attach_ctx); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(complete_attach); + +int sep_complete_attach_register(struct device *dev, void *ctx, + int (*complete)(void *ctx)) +{ + sep_attach_ctx = ctx; + sep_attach_complete = complete; + return device_create_file(dev, &dev_attr_complete_attach); +} + +void sep_complete_attach_unregister(struct device *dev) +{ + device_remove_file(dev, &dev_attr_complete_attach); + sep_attach_ctx = NULL; + sep_attach_complete = NULL; +} diff --git a/drivers/soc/apple/diag.rs b/drivers/soc/apple/diag.rs index 1b6fdd2288c52..6d362d1019c72 100644 --- a/drivers/soc/apple/diag.rs +++ b/drivers/soc/apple/diag.rs @@ -33,6 +33,8 @@ extern "C" { fn sep_diag_set_attach(attached: bool); fn sep_diag_set_endpoints(count: c_uint); fn sep_diag_set_keystore_open(); + fn sep_diag_keystore_ended(); + fn sep_diag_set_xart(writable: bool); fn sep_diag_set_keybag(state: c_int); fn sep_diag_sensor_bind() -> c_uint; fn sep_diag_set_sensor_result(bind: c_uint, online: bool); @@ -114,6 +116,24 @@ pub(crate) fn register( kernel::error::to_result(ret) } +pub(crate) fn xart_state(writable: bool, guard: Option<(u8, bool)>) { + XART_WRITABLE.store(writable, Relaxed); + let (level, loaded) = guard.unwrap_or((0, false)); + GUARD_LEVEL.store(u32::from(level), Relaxed); + GUARD_LIST.store(loaded, Relaxed); + REFUSALS.store(0, Relaxed); + // SAFETY: each setter only publishes diagnostic state. + unsafe { + sep_diag_set_xart(writable); + sep_diag_set_xart_guard(c_uint::from(level), loaded, matching() as c_int); + } +} + +pub(crate) fn keystore_ended() { + // SAFETY: no preconditions; updates one diagnostic word. + unsafe { sep_diag_keystore_ended() }; +} + pub(crate) fn attach(attached: bool) { // SAFETY: no preconditions; stores one word. unsafe { sep_diag_set_attach(attached) }; diff --git a/drivers/soc/apple/diag_shim.c b/drivers/soc/apple/diag_shim.c index 5c54d5d8410b3..5bb2d2366a325 100644 --- a/drivers/soc/apple/diag_shim.c +++ b/drivers/soc/apple/diag_shim.c @@ -116,7 +116,7 @@ static DEVICE_ATTR_RO(protocol); static ssize_t xart_show(struct device *dev, struct device_attribute *attr, char *buf) { - return sysfs_emit(buf, "%s\n", sep_diag.xart ? "enabled" : "disabled"); + return sysfs_emit(buf, "%s\n", READ_ONCE(sep_diag.xart) ? "enabled" : "disabled"); } static DEVICE_ATTR_RO(xart); @@ -289,7 +289,7 @@ static DEVICE_ATTR_RO(restore); static ssize_t xart_guard_show(struct device *dev, struct device_attribute *attr, char *buf) { - return sysfs_emit(buf, "%u\n", sep_diag.xart_guard); + return sysfs_emit(buf, "%u\n", READ_ONCE(sep_diag.xart_guard)); } static DEVICE_ATTR_RO(xart_guard); @@ -298,9 +298,9 @@ static ssize_t xart_guard_list_show(struct device *dev, { const char *state; - if (!sep_diag.xart_guard) + if (!READ_ONCE(sep_diag.xart_guard)) state = "none"; - else if (sep_diag.xart_guard_list) + else if (READ_ONCE(sep_diag.xart_guard_list)) state = "loaded"; else state = "unavailable"; @@ -481,8 +481,8 @@ void sep_diag_set_restore(int state) void sep_diag_set_xart_guard(unsigned int level, bool list_loaded, int matching) { - sep_diag.xart_guard = level; - sep_diag.xart_guard_list = list_loaded; + WRITE_ONCE(sep_diag.xart_guard, level); + WRITE_ONCE(sep_diag.xart_guard_list, list_loaded); atomic_set(&sep_diag.xart_refusals, 0); atomic_set(&sep_diag.matching, matching); } @@ -499,10 +499,20 @@ void sep_diag_xart_guard_refused(int matching) * and it is not repeated. Whatever it left undecided is final for this boot: * the key store stays closed and Touch ID cannot start. */ -void sep_diag_bringup_ended(void) +void sep_diag_set_xart(bool writable) +{ + WRITE_ONCE(sep_diag.xart, writable); +} + +void sep_diag_keystore_ended(void) { atomic_cmpxchg(&sep_diag.keystore, SEP_DIAG_KEYSTORE_UNKNOWN, SEP_DIAG_KEYSTORE_CLOSED); +} + +void sep_diag_bringup_ended(void) +{ + sep_diag_keystore_ended(); atomic_cmpxchg(&sep_diag.touchid, SEP_DIAG_TOUCHID_PENDING, SEP_DIAG_TOUCHID_UNAVAILABLE); } diff --git a/drivers/soc/apple/refkey.rs b/drivers/soc/apple/refkey.rs index e505fb8d03bea..048b40bbb7ab0 100644 --- a/drivers/soc/apple/refkey.rs +++ b/drivers/soc/apple/refkey.rs @@ -14,6 +14,28 @@ use kernel::sync::atomic::Relaxed; pub(crate) const REFKEY_ENVELOPE_VERSION: u32 = 2; impl SepData { + /// Keep only decoded bytes across switch-root, never an open initrd file. + fn with_identity_keybag( + &self, + f: impl FnOnce(&keybag::StoredKeyBag) -> Result, + ) -> Result { + let mut cache = self.identity_keybag.lock(); + if self.keystore_only.load(Relaxed) { + if cache.is_none() { + let keybag::State::Present(stored) = keybag::read(keybag::Slot::Identity)? else { + return Err(ENODEV); + }; + *cache = Some(stored); + } + return f(cache.as_ref().ok_or(ENODEV)?); + } + drop(cache); + let keybag::State::Present(stored) = keybag::read(keybag::Slot::Identity)? else { + return Err(ENODEV); + }; + f(&stored) + } + fn sks_req_refkey(&self, keybag: i32, der_set: &[u8]) -> Result { let (keybag, envelope) = match self.profile.key_store { // The 13.5 operation envelope is 3. Use the designated identity @@ -197,6 +219,10 @@ impl SepData { return; } + if self.keystore_only.load(Relaxed) { + // A missing initrd copy is never permission to replace an identity. + return; + } let Some(blob) = self.sks_create_refkey(handle, secret) else { dev_warn!( self.dev, @@ -234,12 +260,12 @@ impl SepData { if !self.sks_ready() { return Err(ENODEV); } - let keybag::State::Present(stored) = keybag::read(keybag::Slot::Identity)? else { - return Err(ENODEV); - }; - let (handle, _uuid) = self.sks_recover(&stored).ok_or(EIO)?; - self.sks_machine_refkey(handle, stored.secret()); - let _ = self.sks_send(self.sks_req_unload_keybag(handle)); + self.with_identity_keybag(|stored| { + let (handle, _uuid) = self.sks_recover(stored).ok_or(EIO)?; + self.sks_machine_refkey(handle, stored.secret()); + let _ = self.sks_send(self.sks_req_unload_keybag(handle)); + Ok(()) + })?; if self.machine_refkey.lock().is_some() { Ok(()) } else { @@ -269,6 +295,16 @@ impl SepData { } pub(crate) fn refkey_unseal_trusted(&self, sealed: &[u8]) -> Result> { + let refusals = self.keystore_write_refusals.load(Relaxed); + let recovered = self.refkey_unseal_cached(sealed)?; + if !crate::attach::unseal_allowed(refusals, self.keystore_write_refusals.load(Relaxed)) { + drop(crate::Secret(recovered)); + return Err(EIO); // trusted.rs maps every unseal rejection to EBADMSG. + } + Ok(recovered) + } + + fn refkey_unseal_cached(&self, sealed: &[u8]) -> Result> { self.wait_for_trusted_ready()?; self.ensure_machine_refkey()?; let blob = { @@ -278,34 +314,39 @@ impl SepData { b.extend_from_slice(&mk.blob, GFP_KERNEL)?; b }; - let keybag::State::Present(stored) = keybag::read(keybag::Slot::Identity)? else { - return Err(ENODEV); - }; - let (handle, _uuid) = self.sks_recover(&stored).ok_or(EIO)?; - // The first unlock under a changed boot policy rewrites one session - // record; see crate::xart_guard. - let unlocked = crate::xart_guard::policy_window(|| { - if self.profile.key_store == profile::KeyStore::Variant5 { - if let Some(healthy) = self.sks_health_check(c"trusted-key unseal") { - let _ = self.sks_send(self.sks_req_change_lock_state( - handle, - LockState::Unlocked, - stored.secret(), - healthy, - )); + self.with_identity_keybag(|stored| { + let (handle, _uuid) = self.sks_recover(stored).ok_or(EIO)?; + // The first unlock under a changed boot policy rewrites one session + // record; see crate::xart_guard. + let unlock = || { + if self.profile.key_store == profile::KeyStore::Variant5 { + if let Some(healthy) = self.sks_health_check(c"trusted-key unseal") { + let _ = self.sks_send(self.sks_req_change_lock_state( + handle, + LockState::Unlocked, + stored.secret(), + healthy, + )); + } + true + } else { + self.sks_prepare_identity_refkey(handle, stored.secret()) } - true + }; + // Do not consume the real root's one policy window in the initrd. + let unlocked = if self.keystore_only.load(Relaxed) { + unlock() } else { - self.sks_prepare_identity_refkey(handle, stored.secret()) + crate::xart_guard::policy_window(unlock) + }; + if !unlocked { + let _ = self.sks_send(self.sks_req_unload_keybag(handle)); + return Err(EIO); } - }); - if !unlocked { + let recovered = self.sks_refkey_unseal(handle, &blob, sealed); let _ = self.sks_send(self.sks_req_unload_keybag(handle)); - return Err(EIO); - } - let recovered = self.sks_refkey_unseal(handle, &blob, sealed); - let _ = self.sks_send(self.sks_req_unload_keybag(handle)); - recovered.ok_or(EIO) + recovered.ok_or(EIO) + }) } /// Op `osgn`: ECDSA-P256 over `challenge` as the pre-computed digest. diff --git a/drivers/soc/apple/sbio.rs b/drivers/soc/apple/sbio.rs index 58582dc6e1b8b..405cf94ac4b83 100644 --- a/drivers/soc/apple/sbio.rs +++ b/drivers/soc/apple/sbio.rs @@ -1851,18 +1851,29 @@ impl SepData { if self.bringup_started.xchg(true, Relaxed) { return; } + if !self.run_keystore_bringup() { + return; + } + self.finish_keystore_bringup(); + } + + pub(crate) fn run_keystore_bringup(&self) -> bool { if let Err(e) = self.enable_sks() { dev_warn!(self.dev, "bringup: could not enable the key store ({:?}); keybag and ref-key operations are unavailable\n", e); - return; + return false; } if !self.sks_init_endpoint() { dev_err!( self.dev, "bringup: key-store endpoint init failed; the key store stays closed\n" ); - return; + return false; } diag::keystore_open(); + true + } + + pub(crate) fn finish_keystore_bringup(&self) { if module_parameters::provision_keybag.value() != 0 { if module_parameters::xart_writes.value() == 0 { dev_err!( diff --git a/drivers/soc/apple/sep.rs b/drivers/soc/apple/sep.rs index fd75957ddeca2..ab01c9048ae6f 100644 --- a/drivers/soc/apple/sep.rs +++ b/drivers/soc/apple/sep.rs @@ -9,6 +9,7 @@ #[cfg(not(CONFIG_OF_DYNAMIC))] compile_error!("apple_sep requires CONFIG_OF_DYNAMIC: the SEP and DART nodes ship disabled and are enabled with a device-tree changeset"); +mod attach; mod bio; mod catacomb; mod control; @@ -600,6 +601,15 @@ struct SepData { profile: &'static profile::PlatformProfile, + // Serialises completion, including concurrent sysfs writers and retries. + #[pin] + attach_phase: Mutex, + keystore_only: Atomic, + keystore_write_refusals: Atomic, + #[pin] + identity_keybag: Mutex>, + + #[pin] fw_map: Mutex>, @@ -924,7 +934,8 @@ impl SepData { )?; let dma_ring = dma::Coherent::::zeroed_slice(dev, DMA_RING_SIZE, GFP_KERNEL)?; - let xart_writes = module_parameters::xart_writes.value() != 0; + let keystore_only = attach::requested(); + let xart_writes = !keystore_only && module_parameters::xart_writes.value() != 0; let xart_start = module_parameters::xart_start_sector.value(); let xart_guard = xart_guard::level(module_parameters::xart_guard.value(), profile.xart_guard); @@ -965,11 +976,15 @@ impl SepData { } }; - let mut host_store = match store::Store::open() { - Ok(store) => Some(store), - Err(e) => { - dev_warn!(dev, "Linux host-state store unavailable: {:?}\n", e); - None + let mut host_store = if keystore_only { + None + } else { + match store::Store::open() { + Ok(store) => Some(store), + Err(e) => { + dev_warn!(dev, "Linux host-state store unavailable: {:?}\n", e); + None + } } }; @@ -983,6 +998,10 @@ impl SepData { try_pin_init!(SepData { dev: ARef::::from(dev), profile, + attach_phase <- new_mutex!(attach::Phase::new(keystore_only)), + keystore_only: Atomic::new(keystore_only), + keystore_write_refusals: Atomic::new(0), + identity_keybag <- new_mutex!(None), fw_map <- new_mutex!(None), mbox <- new_mutex!(None), shmem <- new_mutex!(Some(buf)), @@ -1714,6 +1733,13 @@ impl SepData { return; }; + if !writable + && xarm::is_write(req.opcode) + && done.reply.status != xarm::STATUS_OK + && self.keystore_only.load(Relaxed) + { + self.keystore_write_refusals.fetch_add(1, Relaxed); + } if !writable && xarm::is_write(req.opcode) && done.reply.status != xarm::STATUS_OK @@ -1738,7 +1764,10 @@ impl SepData { } if done.reply_bytes > 0 { - if self.ool_write(&self.ool_xarm, 0, &staging[..done.reply_bytes]).is_err() { + if self + .ool_write(&self.ool_xarm, 0, &staging[..done.reply_bytes]) + .is_err() + { self.fail_xarm(req.tag); return; } @@ -2471,8 +2500,13 @@ impl SepData { } } - this.run_bringup(); - diag::bringup_ended(); + if this.keystore_only.load(Relaxed) { + this.run_keystore_bringup(); + diag::keystore_ended(); + } else { + this.run_bringup(); + diag::bringup_ended(); + } this.finish_bringup(); Self::arm_settle(this); } @@ -2485,6 +2519,72 @@ impl SepData { self.trusted_ready.notify_all(); } + /// Completes the original attach without sending a second registration. + fn complete_attach(&self) -> Result<()> { + self.attach_phase.lock().complete(|| self.finish_attach()) + } + + fn finish_attach(&self) -> Result<()> { + self.wait_for_trusted_ready()?; + if module_parameters::provision_keybag.value() != 0 + && module_parameters::xart_writes.value() == 0 + { + return Err(EINVAL); + } + + // Wait out a cached-keybag consumer before enabling persistent writes. + // All opens below resolve against PID 1's current (real) root. + let mut cached = self.identity_keybag.lock(); + let mut host = store::Store::open()?; + let index = bio::IdentityIndex::load(&mut host)?; + let mut store = self.store.lock(); + // Drop the read-only block handle before reopening the owner writable. + // Failure leaves a usable read-only store when it can be reopened. + *store = None; + let reopened = (|| { + let mut new = xart_store::Store::open_owner( + module_parameters::xart_writes.value() != 0, + module_parameters::xart_start_sector.value(), + )?; + if new.writes_enabled() { + if let Some(level) = xart_guard::level( + module_parameters::xart_guard.value(), + self.profile.xart_guard, + ) { + new.set_guard(level)?; + } + } + Ok(new) + })(); + let new = match reopened { + Ok(new) => new, + Err(e) => { + *store = xart_store::Store::open_owner( + false, + module_parameters::xart_start_sector.value(), + ) + .ok(); + return Err(e); + } + }; + let writable = new.writes_enabled(); + let guard = new.guard_state(); + *store = Some(new); + drop(store); + *self.host_store.lock() = Some(host); + *self.bio_index.lock() = index; + diag::xart_state(writable, guard); + self.keystore_only.store(false, Relaxed); + *cached = None; + drop(cached); + + // The default path's ordering stays in run_bringup. SKS has already + // been initialised, so completion resumes immediately after that step. + self.finish_keystore_bringup(); + diag::bringup_ended(); + Ok(()) + } + /// Trusted-key callbacks can run as soon as probe publishes the source, /// while endpoint discovery and key-store initialisation are still running. /// Wait only in those consumers, leaving bringup free to establish the key. @@ -2880,7 +2980,7 @@ impl WorkItem for SepData { } -struct SepDriver(Arc); +struct SepDriver(Arc, attach::Attribute); // Deliberately not exported as a module alias: udev must not load the driver // on its own. The loader service loads it once the iBoot system container's @@ -2900,7 +3000,8 @@ impl platform::Driver for SepDriver { _info: Option<&'bound Self::IdInfo>, ) -> impl PinInit, Error> + 'bound { let dev: &device::Device> = pdev.as_ref(); - if module_parameters::provision_keybag.value() != 0 + if !attach::requested() + && module_parameters::provision_keybag.value() != 0 && module_parameters::xart_writes.value() == 0 { dev_err!(dev, "provision_keybag=1 requires xart_writes=1\n"); @@ -2942,7 +3043,11 @@ impl platform::Driver for SepDriver { data.attach(&sep_node)?; if let Err(e) = data.register_fv_kernel() { - dev_err!(dev, "could not register the FileVault kernel API: {:?}\n", e); + dev_err!( + dev, + "could not register the FileVault kernel API: {:?}\n", + e + ); } if let Err(e) = trusted::register(data.clone()) { @@ -2950,19 +3055,31 @@ impl platform::Driver for SepDriver { } if let Err(e) = pm::register(data.clone()) { - dev_warn!(data.dev, "sleep notifier registration failed ({:?}); a capture may run into a suspend\n", e); + dev_warn!( + data.dev, + "sleep notifier registration failed ({:?}); a capture may run into a suspend\n", + e + ); } if data.registered.load(Relaxed) { SepData::arm_settle(&data); } - Ok(SepDriver(data)) + let attribute = match attach::Attribute::register(data.clone()) { + Ok(attribute) => attribute, + Err(e) => { + data.remove(); + return Err(e); + } + }; + Ok(SepDriver(data, attribute)) } } impl Drop for SepDriver { fn drop(&mut self) { + self.1.unregister(); self.0.remove(); } } diff --git a/drivers/soc/apple/shim.h b/drivers/soc/apple/shim.h index ca0d3d8a797ee..87bbfdf7de416 100644 --- a/drivers/soc/apple/shim.h +++ b/drivers/soc/apple/shim.h @@ -244,4 +244,13 @@ unsigned char *sep_tk_blob_ptr(struct trusted_key_payload *p); unsigned int sep_tk_blob_len(const struct trusted_key_payload *p); void sep_tk_set_blob_len(struct trusted_key_payload *p, unsigned int n); + +/* Initrd attach and completion; sysfs removal drains the callback. */ +bool sep_keystore_only(void); +int sep_complete_attach_register(struct device *dev, void *ctx, + int (*complete)(void *ctx)); +void sep_complete_attach_unregister(struct device *dev); +void sep_diag_keystore_ended(void); +void sep_diag_set_xart(bool writable); + #endif /* SEP_SHIM_H */ diff --git a/tools/aurora-sep/load-driver b/tools/aurora-sep/load-driver index b3da54da2a440..e0431168e828b 100755 --- a/tools/aurora-sep/load-driver +++ b/tools/aurora-sep/load-driver @@ -39,7 +39,22 @@ if [ "${AURORA_SEP_SESSION:-0}" != 1 ]; then exec keyctl session aurora-sep-load env AURORA_SEP_SESSION=1 "$0" "$@" fi -modprobe apple_sep xart_writes=1 provision_keybag=1 +case "$(cat /sys/module/apple_sep/parameters/keystore_only 2>/dev/null || true)" in + 1 | Y) + completed=false + for attribute in /sys/bus/platform/devices/*.sep/complete_attach; do + [ -e "$attribute" ] || continue + printf '1\n' >"$attribute" + completed=true + break + done + if [ "$completed" != true ]; then + log "keystore-only driver has no complete_attach attribute" + exit 1 + fi + ;; + *) modprobe apple_sep xart_writes=1 provision_keybag=1 ;; +esac work=$(mktemp -d /run/aurora-sep-load.XXXXXX) cleanup() { diff --git a/tools/aurora-sep/tests/keystore-only.config b/tools/aurora-sep/tests/keystore-only.config new file mode 100644 index 0000000000000..1a7b3f16c67bd --- /dev/null +++ b/tools/aurora-sep/tests/keystore-only.config @@ -0,0 +1,21 @@ +# Minimal arm64 QEMU KUnit kernel, not a hardware boot configuration. +CONFIG_KUNIT=y +CONFIG_RUST=y +CONFIG_PRINTK=y +CONFIG_ARCH_APPLE=y +CONFIG_PM=y +CONFIG_OF=y +CONFIG_OF_DYNAMIC=y +CONFIG_DMA_SHARED_BUFFER=y +CONFIG_SPI=y +CONFIG_RUST_APPLE_MAILBOX=y +CONFIG_HW_RANDOM=y +CONFIG_KEYS=y +CONFIG_TRUSTED_KEYS=y +CONFIG_CRYPTO=y +CONFIG_APPLE_SEP=y +CONFIG_APPLE_SEP_KUNIT_TEST=y +# AOP selects the hidden Rust mailbox abstraction used by SEP. +CONFIG_APPLE_AOP=y +CONFIG_BLOCK=y +CONFIG_SYSFS=y diff --git a/tools/aurora-sep/tests/test_keystore_only.py b/tools/aurora-sep/tests/test_keystore_only.py new file mode 100644 index 0000000000000..e7482462b2f79 --- /dev/null +++ b/tools/aurora-sep/tests/test_keystore_only.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Exercise production initrd cache and refusal paths without SEP hardware.""" +from pathlib import Path +import os +import shutil +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[3] + + +def function(path, name): + source = (ROOT / path).read_text() + start = source.index("fn " + name) + start = source.rfind("\n", 0, start) + 1 + opening = source.index("{", start) + end, depth = opening + 1, 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[start:end] + + +@unittest.skipUnless(shutil.which("rustc"), "rustc required") +class KeyStoreCacheTests(unittest.TestCase): + def test_production_cache_and_unseal_refusal(self): + harness = r''' +use std::cell::{Cell, RefCell}; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering::Relaxed}; +type Result = std::result::Result; +type KVec = Vec; +const EIO: u8 = 5; +const ENODEV: u8 = 19; +thread_local! { + static READS: Cell = const { Cell::new(0) }; + static FILE: Cell>> = const { Cell::new(Ok(Some(7))) }; + static WIPED: Cell = const { Cell::new(false) }; +} +struct Secret(Vec); +impl Drop for Secret { + fn drop(&mut self) { self.0.fill(0); WIPED.with(|w| w.set(true)); } +} +struct Cache(RefCell>); +impl Cache { + fn lock(&self) -> std::cell::RefMut<'_, Option> { + self.0.borrow_mut() + } +} +mod keybag { + use super::*; + pub struct StoredKeyBag(pub u8); + pub enum Slot { Identity } + pub enum State { Present(StoredKeyBag), Absent(()) } + pub fn read(_: Slot) -> Result { + READS.with(|n| n.set(n.get()+1)); + FILE.with(|f| f.get()).map(|v| match v { + Some(v) => State::Present(StoredKeyBag(v)), None => State::Absent(()), + }) + } +} +mod attach { +ALLOW +} +struct SepData { + identity_keybag: Cache, + keystore_only: AtomicBool, + keystore_write_refusals: AtomicU32, + refuse: bool, +} +impl SepData { +CACHE +UNSEAL + fn refkey_unseal_cached(&self, _: &[u8]) -> Result> { + if self.refuse { self.keystore_write_refusals.fetch_add(1, Relaxed); } + Ok(vec![0x5a; 32]) + } +} +fn device(only: bool, refuse: bool) -> SepData { + SepData { identity_keybag: Cache(RefCell::new(None)), + keystore_only: AtomicBool::new(only), + keystore_write_refusals: AtomicU32::new(0), refuse } +} +#[test] fn cache_survives_loss_of_initrd_files() { + let d = device(true, false); + assert_eq!(d.with_identity_keybag(|k| Ok(k.0)), Ok(7)); + FILE.with(|f| f.set(Err(2))); + for _ in 0..5 { assert_eq!(d.with_identity_keybag(|k| Ok(k.0)), Ok(7)); } + READS.with(|n| assert_eq!(n.get(), 1)); +} +#[test] fn consumer_failure_does_not_discard_initrd_material() { + let d = device(true, false); + assert_eq!(d.with_identity_keybag::<()>(|_| Err(EIO)), Err(EIO)); + FILE.with(|f| f.set(Err(2))); + assert_eq!(d.with_identity_keybag(|k| Ok(k.0)), Ok(7)); + READS.with(|n| assert_eq!(n.get(), 1)); +} +#[test] fn absent_or_invalid_files_are_not_provisioned_or_cached() { + let d = device(true, false); + FILE.with(|f| f.set(Ok(None))); + assert_eq!(d.with_identity_keybag(|k| Ok(k.0)), Err(ENODEV)); + FILE.with(|f| f.set(Err(22))); + assert_eq!(d.with_identity_keybag(|k| Ok(k.0)), Err(22)); + FILE.with(|f| f.set(Ok(Some(9)))); + assert_eq!(d.with_identity_keybag(|k| Ok(k.0)), Ok(9)); + READS.with(|n| assert_eq!(n.get(), 3)); +} +#[test] fn default_attach_keeps_reading_current_real_root_bag() { + let d = device(false, false); + assert_eq!(d.with_identity_keybag(|k| Ok(k.0)), Ok(7)); + FILE.with(|f| f.set(Ok(Some(11)))); + assert_eq!(d.with_identity_keybag(|k| Ok(k.0)), Ok(11)); + READS.with(|n| assert_eq!(n.get(), 2)); +} +#[test] fn completion_switches_to_real_root_material() { + let d = device(true, false); + assert_eq!(d.with_identity_keybag(|k| Ok(k.0)), Ok(7)); + d.keystore_only.store(false, Relaxed); + *d.identity_keybag.lock() = None; + FILE.with(|f| f.set(Ok(Some(11)))); + assert_eq!(d.with_identity_keybag(|k| Ok(k.0)), Ok(11)); +} +#[test] fn refused_write_overrides_even_enclave_success_and_wipes_key() { + assert_eq!(device(true, true).refkey_unseal_trusted(&[]), Err(EIO)); + WIPED.with(|w| assert!(w.get())); +} +#[test] fn previous_refusal_does_not_poison_later_read_only_unseal() { + let d = device(true, false); + d.keystore_write_refusals.store(3, Relaxed); + assert_eq!(d.refkey_unseal_trusted(&[]), Ok(vec![0x5a; 32])); +} +''' + harness = harness.replace("CACHE", function("drivers/soc/apple/refkey.rs", "with_identity_keybag")) + harness = harness.replace("UNSEAL", function("drivers/soc/apple/refkey.rs", "refkey_unseal_trusted")) + harness = harness.replace("ALLOW", function("drivers/soc/apple/attach.rs", "unseal_allowed")) + with tempfile.TemporaryDirectory(prefix="sep-keystore-") as tmp: + path = Path(tmp) + (path / "test.rs").write_text(harness) + subprocess.run(["rustc", "--edition=2021", "--test", str(path / "test.rs"), + "-o", str(path / "test")], check=True) + subprocess.run([str(path / "test"), "--test-threads=1"], check=True) + + +@unittest.skipUnless(shutil.which("rustc"), "rustc required") +class MachineRefkeyTests(unittest.TestCase): + def test_production_read_only_refkey_cache(self): + harness = r''' +use std::cell::{Cell, RefCell}; +use std::ffi::CStr; +use std::ops::{Deref, DerefMut}; +use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; +type Result = std::result::Result; +const GFP_KERNEL: u8 = 0; +macro_rules! dev_err { ($($arg:tt)*) => {}; } +macro_rules! dev_warn { ($($arg:tt)*) => {}; } +thread_local! { + static READABLE: Cell = const { Cell::new(true) }; + static VALID: Cell = const { Cell::new(true) }; + static READS: Cell = const { Cell::new(0) }; + static WRITES: Cell = const { Cell::new(0) }; + static CREATES: Cell = const { Cell::new(0) }; +} +struct KVec(Vec); +impl KVec { + fn new() -> Self { Self(Vec::new()) } + fn resize(&mut self, len: usize, value: T, _: u8) -> Result<()> { + self.0.resize(len, value); Ok(()) + } +} +impl Deref for KVec { + type Target = [T]; + fn deref(&self) -> &[T] { &self.0 } +} +impl DerefMut for KVec { + fn deref_mut(&mut self) -> &mut [T] { &mut self.0 } +} +mod sks { pub type KeyBagHandle = u8; } +mod module_parameters { + pub struct Param; + impl Param { pub fn value(&self) -> u8 { 0 } } + #[allow(non_upper_case_globals)] + pub const refkey_v2: Param = Param; +} +mod shim { + use super::*; + pub struct StoreFile; + impl StoreFile { + pub fn open_readonly(_: &CStr) -> Result { + READS.with(|n| n.set(n.get()+1)); + if READABLE.with(|v| v.get()) { Ok(Self) } else { Err(2) } + } + pub fn open(_: &CStr) -> Result { + WRITES.with(|n| n.set(n.get()+1)); Ok(Self) + } + pub fn size(&self) -> Result { Ok(3) } + pub fn read_exact(&self, _: u64, out: &mut [u8]) -> Result<()> { + out.fill(0xa5); Ok(()) + } + pub fn write_all(&self, _: u64, _: &[u8]) -> Result<()> { Ok(()) } + pub fn sync(&self) -> Result<()> { Ok(()) } + } +} +struct Cache(RefCell>); +impl Cache { + fn lock(&self) -> std::cell::RefMut<'_, Option<()>> { self.0.borrow_mut() } +} +struct SepData { machine_refkey: Cache, keystore_only: AtomicBool } +impl SepData { +MACHINE + fn sks_create_refkey(&self, _: u8, _: &[u8]) -> Option> { + CREATES.with(|n| n.set(n.get()+1)); Some(KVec(vec![1,2,3])) + } + fn cache_machine_refkey(&self, _: KVec) -> bool { + if !VALID.with(|v| v.get()) { return false; } + *self.machine_refkey.lock() = Some(()); true + } +} +fn device(only: bool) -> SepData { + SepData { machine_refkey: Cache(RefCell::new(None)), keystore_only: AtomicBool::new(only) } +} +fn no_writes() { + WRITES.with(|n| assert_eq!(n.get(), 0)); + CREATES.with(|n| assert_eq!(n.get(), 0)); +} +#[test] fn cached_refkey_never_reopens_initrd_file() { + let d = device(true); d.sks_machine_refkey(1, &[]); + READABLE.with(|v| v.set(false)); + for _ in 0..5 { d.sks_machine_refkey(1, &[]); } + READS.with(|n| assert_eq!(n.get(), 1)); no_writes(); +} +#[test] fn missing_initrd_refkey_never_creates_or_persists() { + READABLE.with(|v| v.set(false)); + let d = device(true); d.sks_machine_refkey(1, &[]); + assert!(d.machine_refkey.lock().is_none()); no_writes(); +} +#[test] fn invalid_existing_refkey_is_never_replaced_in_either_phase() { + VALID.with(|v| v.set(false)); + for only in [false,true] { device(only).sks_machine_refkey(1, &[]); } + no_writes(); +} +#[test] fn default_attach_retains_missing_refkey_provisioning() { + READABLE.with(|v| v.set(false)); + let d = device(false); d.sks_machine_refkey(1, &[]); + assert!(d.machine_refkey.lock().is_some()); + CREATES.with(|n| assert_eq!(n.get(), 1)); + WRITES.with(|n| assert_eq!(n.get(), 1)); +} +''' + harness = harness.replace("MACHINE", function("drivers/soc/apple/refkey.rs", "sks_machine_refkey")) + with tempfile.TemporaryDirectory(prefix="sep-machine-refkey-") as tmp: + path = Path(tmp) + (path / "test.rs").write_text(harness) + subprocess.run(["rustc", "--edition=2021", "--test", str(path / "test.rs"), + "-o", str(path / "test")], check=True) + subprocess.run([str(path / "test"), "--test-threads=1"], check=True) + + +class LoaderTests(unittest.TestCase): + def test_completion_replaces_modprobe(self): + source = (ROOT / "tools/aurora-sep/load-driver").read_text().split("work=$(mktemp", 1)[0] + for parameter in (None, "0", "1", "Y"): + with self.subTest(parameter=parameter), tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) + param = path / "keystore_only" + if parameter is not None: + param.write_text(parameter + "\n") + devices = path / "devices" + attribute = devices / "1234.sep/complete_attach" + attribute.parent.mkdir(parents=True) + attribute.touch() + binary = path / "modprobe" + binary.write_text('#!/bin/sh\nprintf "%s\\n" "$*" >"$CALLS"\n') + binary.chmod(0o755) + script = path / "load-driver" + script.write_text(source.replace('/sys/module/apple_sep/parameters/keystore_only', str(param)) + .replace('/sys/bus/platform/devices', str(devices))) + calls = path / "calls" + env = dict(os.environ, AURORA_SEP_SESSION="1", CALLS=str(calls), + PATH=str(path) + os.pathsep + os.environ['PATH']) + done = subprocess.run(["sh", str(script)], env=env, capture_output=True, text=True) + self.assertEqual(done.returncode, 0, done.stderr) + self.assertEqual(attribute.read_text(), "1\n" if parameter in ("1", "Y") else "") + self.assertEqual(calls.exists(), parameter not in ("1", "Y")) + + +if __name__ == "__main__": + unittest.main() From 9f63416f825a89db586d7a5932a36cecd804c4e9 Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Sun, 8 Nov 2026 04:02:32 +1100 Subject: [PATCH 2/9] selftests: apple-sep: enable shared buffers in the KUnit config SYNC_FILE selects the hidden DMA_SHARED_BUFFER symbol required by SEP. Setting the hidden symbol alone is ignored by Kconfig. Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Assisted-by: LLM --- tools/aurora-sep/tests/keystore-only.config | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/aurora-sep/tests/keystore-only.config b/tools/aurora-sep/tests/keystore-only.config index 1a7b3f16c67bd..c8b6c9ebc8e78 100644 --- a/tools/aurora-sep/tests/keystore-only.config +++ b/tools/aurora-sep/tests/keystore-only.config @@ -19,3 +19,5 @@ CONFIG_APPLE_SEP_KUNIT_TEST=y CONFIG_APPLE_AOP=y CONFIG_BLOCK=y CONFIG_SYSFS=y +# Select the hidden DMA_SHARED_BUFFER symbol through a public option. +CONFIG_SYNC_FILE=y From 669455b592ccb1fdcef79416c5a3f2a4d8c25335 Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 10 Nov 2026 00:45:29 +1100 Subject: [PATCH 3/9] soc: apple: sep: userbag: seal a disk-encryption secret to a user's bag FileVault ties the volume key to the user's password through the enclave: the password goes to the enclave, which entangles it with its UID key and counts failed attempts, and only then releases the key. The per-user bags give Linux the same gate; this adds the key release. SEP_BIO_FDE_CREATE (0x37) makes a P-256 ref-key under the designated, unlocked bag of a uid (protection class 9, no access-control list), so the enclave uses it only while that bag is unlocked. SEP_BIO_FDE_SEAL (0x38) ECIES-encrypts up to 64 bytes to it and has the enclave open the result once before returning it. SEP_BIO_FDE_UNSEAL (0x39) asks the enclave to open a sealed blob. Root only, on the re-arm node, variant-5 key store only, like the other user-bag ioctls. A LUKS2 token keeps the wrapped bag, the record and the sealed key-slot secret in the header, none of which opens without the password on this Mac. Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Assisted-by: LLM --- drivers/soc/apple/refkey.rs | 52 ++++++++++ drivers/soc/apple/userbag.rs | 173 ++++++++++++++++++++++++++++++++- include/uapi/linux/apple-sep.h | 41 ++++++++ 3 files changed, 265 insertions(+), 1 deletion(-) diff --git a/drivers/soc/apple/refkey.rs b/drivers/soc/apple/refkey.rs index 3f360a20c0fe6..6c04d70e78176 100644 --- a/drivers/soc/apple/refkey.rs +++ b/drivers/soc/apple/refkey.rs @@ -511,6 +511,58 @@ impl SepData { self.with_identity_bag(|handle, _secret| self.sks_refkey_sign(handle, blob, digest, acm)) } + /// Volume-key sealing: a ref-key without an access-control list under + /// the designated user bag `user` (outer handle `-uid`), protection + /// class 9, so the enclave uses it only while that bag is unlocked. The + /// caller has unlocked the bag with the user's password. Variant-5 key + /// store only. + pub(crate) fn fde_key_create(&self, user: crate::sks::DesignateUser) -> Result> { + if self.profile.key_store != profile::KeyStore::Variant5 { + return Err(EOPNOTSUPP); + } + crate::xart_guard::close_policy_window(); + let blob = self + .sks_create_refkey_at(user.special_handle().value(), 9, 5, None) + .ok_or(EACCES)?; + Self::refkey_pub(&blob).ok_or(EIO)?; + Ok(blob) + } + + /// ECIES-encrypts `secret` to the sealing key in `record` and has the + /// enclave open the result once before returning it, so a sealed blob + /// this bag cannot open is never handed out. + pub(crate) fn fde_seal( + &self, + user: crate::sks::DesignateUser, + record: &[u8], + secret: &[u8], + ) -> Result> { + let sealed = refkey_seal::ecies_seal(Self::refkey_pub(record).ok_or(EINVAL)?, secret)?; + let recovered = crate::Secret(self.fde_unseal(user, record, &sealed)?); + if &*recovered != secret { + dev_err!(self.dev, "fde: refusing a sealed volume secret whose round trip failed\n"); + return Err(EIO); + } + Ok(sealed) + } + + /// Has the enclave open `sealed` with the sealing key in `record`. It + /// does so only on the Mac whose enclave made the key and only while + /// `user`'s bag is unlocked; any refusal is `EKEYREJECTED`. + pub(crate) fn fde_unseal( + &self, + user: crate::sks::DesignateUser, + record: &[u8], + sealed: &[u8], + ) -> Result> { + if self.profile.key_store != profile::KeyStore::Variant5 { + return Err(EOPNOTSUPP); + } + Self::refkey_pub(record).ok_or(EINVAL)?; + let handle = crate::sks::KeyBagHandle::from_load_reply(user.special_handle().value()); + self.sks_refkey_unseal(handle, record, sealed).ok_or(EKEYREJECTED) + } + /// Enclave ECIES-decrypt (o = "oecd"); takes the ephemeral from the front of `d`. fn sks_refkey_unseal( &self, diff --git a/drivers/soc/apple/userbag.rs b/drivers/soc/apple/userbag.rs index 7e994ba3d0768..8af0db36339a5 100644 --- a/drivers/soc/apple/userbag.rs +++ b/drivers/soc/apple/userbag.rs @@ -770,10 +770,87 @@ unsafe impl kernel::transmute::FromBytes for BagBlob {} // SAFETY: as above. unsafe impl kernel::transmute::AsBytes for BagBlob {} +/// The largest sealing-key record `SEP_BIO_FDE_*` carry. +pub(crate) const FDE_RECORD_MAX: usize = 2048; +/// The largest volume secret the enclave seals (a LUKS2 key-slot secret). +pub(crate) const FDE_SECRET_MAX: usize = 64; +/// `ephemeral point (65) || ciphertext || tag (16)` for the largest secret. +pub(crate) const FDE_SEALED_MAX: usize = 256; + +/// The UAPI `struct sep_fde_key`: a sealing key made under `uid`'s bag. +#[repr(C)] +pub(crate) struct FdeKey { + pub(crate) uid: u32, + pub(crate) record_len: u32, + pub(crate) record: [u8; FDE_RECORD_MAX], +} + +/// The UAPI `struct sep_fde_seal`, for both SEAL and UNSEAL. +#[repr(C)] +pub(crate) struct FdeSeal { + pub(crate) uid: u32, + pub(crate) record_len: u32, + pub(crate) secret_len: u32, + pub(crate) sealed_len: u32, + pub(crate) secret: [u8; FDE_SECRET_MAX], + pub(crate) sealed: [u8; FDE_SEALED_MAX], + pub(crate) record: [u8; FDE_RECORD_MAX], +} + +impl Drop for FdeSeal { + fn drop(&mut self) { + crate::image::wipe(&mut self.secret); + } +} + +static_assert!(core::mem::size_of::() == 8 + FDE_RECORD_MAX); +static_assert!( + core::mem::size_of::() == 16 + FDE_SECRET_MAX + FDE_SEALED_MAX + FDE_RECORD_MAX +); +// SAFETY: C layout, no padding, every bit pattern valid. +unsafe impl kernel::transmute::FromBytes for FdeKey {} +// SAFETY: as above. +unsafe impl kernel::transmute::AsBytes for FdeKey {} +// SAFETY: as above. +unsafe impl kernel::transmute::FromBytes for FdeSeal {} +// SAFETY: as above. +unsafe impl kernel::transmute::AsBytes for FdeSeal {} + +pub(crate) const IOC_FDE_CREATE: u32 = kernel::ioctl::_IOWR::(IOC_MAGIC, 0x37); +pub(crate) const IOC_FDE_SEAL: u32 = kernel::ioctl::_IOWR::(IOC_MAGIC, 0x38); +pub(crate) const IOC_FDE_UNSEAL: u32 = kernel::ioctl::_IOWR::(IOC_MAGIC, 0x39); + +/// Checks a SEAL/UNSEAL request: the user, the record, and on SEAL the +/// secret, on UNSEAL the sealed blob. +pub(crate) fn parse_fde(req: &FdeSeal, seal: bool) -> Result { + let record_len = req.record_len as usize; + let secret_len = req.secret_len as usize; + let sealed_len = req.sealed_len as usize; + if req.uid < MIN_UID || req.uid > i32::MAX as u32 || record_len == 0 || record_len > FDE_RECORD_MAX { + return Err(EINVAL); + } + if seal && (secret_len == 0 || secret_len > FDE_SECRET_MAX || sealed_len != 0) { + return Err(EINVAL); + } + if !seal && (sealed_len == 0 || sealed_len > FDE_SEALED_MAX || secret_len != 0) { + return Err(EINVAL); + } + crate::sks::DesignateUser::new(req.uid as i32).ok_or(EINVAL) +} + pub(crate) fn is_ioctl(cmd: u32) -> bool { matches!( cmd, - IOC_STATE | IOC_UNLOCK | IOC_LOCK | IOC_CHANGE | IOC_CREATE | IOC_EXPORT | IOC_IMPORT + IOC_STATE + | IOC_UNLOCK + | IOC_LOCK + | IOC_CHANGE + | IOC_CREATE + | IOC_EXPORT + | IOC_IMPORT + | IOC_FDE_CREATE + | IOC_FDE_SEAL + | IOC_FDE_UNSEAL ) } @@ -1139,6 +1216,59 @@ impl crate::SepData { self.userbag_import(user, &blob.data[..len])?; crate::bio::ok() } + IOC_FDE_CREATE => { + let mut key: FdeKey = + UserSlice::new(user_ptr, core::mem::size_of::()).reader().read()?; + if key.uid < MIN_UID || key.uid > i32::MAX as u32 { + return Err(EINVAL); + } + let user = crate::sks::DesignateUser::new(key.uid as i32).ok_or(EINVAL)?; + let record = self.fde_key_create(user)?; + if record.len() > FDE_RECORD_MAX { + return Err(E2BIG); + } + key.record.fill(0); + key.record[..record.len()].copy_from_slice(&record); + key.record_len = record.len() as u32; + UserSlice::new(user_ptr, core::mem::size_of::()).writer().write(&key)?; + crate::bio::ok() + } + IOC_FDE_SEAL | IOC_FDE_UNSEAL => { + let seal = cmd == IOC_FDE_SEAL; + let mut req: FdeSeal = + UserSlice::new(user_ptr, core::mem::size_of::()).reader().read()?; + let user = parse_fde(&req, seal)?; + let record_len = req.record_len as usize; + if seal { + let sealed = self.fde_seal( + user, + &req.record[..record_len], + &req.secret[..req.secret_len as usize], + )?; + if sealed.len() > FDE_SEALED_MAX { + return Err(E2BIG); + } + req.sealed.fill(0); + req.sealed[..sealed.len()].copy_from_slice(&sealed); + req.sealed_len = sealed.len() as u32; + crate::image::wipe(&mut req.secret); + req.secret_len = 0; + } else { + let secret = crate::Secret(self.fde_unseal( + user, + &req.record[..record_len], + &req.sealed[..req.sealed_len as usize], + )?); + if secret.len() > FDE_SECRET_MAX { + return Err(E2BIG); + } + req.secret.fill(0); + req.secret[..secret.len()].copy_from_slice(&secret); + req.secret_len = secret.len() as u32; + } + UserSlice::new(user_ptr, core::mem::size_of::()).writer().write(&req)?; + crate::bio::ok() + } IOC_CREATE | IOC_UNLOCK => { let request: UnlockRequest = UserSlice::new(user_ptr, core::mem::size_of::()) @@ -1628,4 +1758,45 @@ mod tests { assert_eq!(r.since_password_s, 5); assert_eq!(r.enclave_lock_state, 0); } + + fn fde_request(uid: u32, record_len: u32, secret_len: u32, sealed_len: u32) -> FdeSeal { + FdeSeal { + uid, + record_len, + secret_len, + sealed_len, + secret: [0; FDE_SECRET_MAX], + sealed: [0; FDE_SEALED_MAX], + record: [0; FDE_RECORD_MAX], + } + } + + #[test] + fn fde_parse_bounds() { + let ok = MIN_UID + 1; + assert!(parse_fde(&fde_request(ok, 600, 32, 0), true).is_ok()); + assert!(parse_fde(&fde_request(ok, 600, 0, 113), false).is_ok()); + // SEAL needs a secret and no sealed blob, UNSEAL the reverse. + assert!(parse_fde(&fde_request(ok, 600, 0, 0), true).is_err()); + assert!(parse_fde(&fde_request(ok, 600, 32, 113), true).is_err()); + assert!(parse_fde(&fde_request(ok, 600, 32, 113), false).is_err()); + assert!(parse_fde(&fde_request(ok, 600, 0, 0), false).is_err()); + // Lengths past the arrays and system uids are refused. + assert!(parse_fde(&fde_request(ok, 600, FDE_SECRET_MAX as u32 + 1, 0), true).is_err()); + assert!(parse_fde(&fde_request(ok, 600, 0, FDE_SEALED_MAX as u32 + 1), false).is_err()); + assert!(parse_fde(&fde_request(ok, 0, 32, 0), true).is_err()); + assert!(parse_fde(&fde_request(ok, FDE_RECORD_MAX as u32 + 1, 32, 0), true).is_err()); + assert!(parse_fde(&fde_request(MIN_UID - 1, 600, 32, 0), true).is_err()); + assert!(parse_fde(&fde_request(u32::MAX, 600, 32, 0), true).is_err()); + } + + #[test] + fn fde_ioctls_are_userbag_ioctls() { + assert!(is_ioctl(IOC_FDE_CREATE)); + assert!(is_ioctl(IOC_FDE_SEAL)); + assert!(is_ioctl(IOC_FDE_UNSEAL)); + assert_ne!(IOC_FDE_SEAL, IOC_FDE_UNSEAL); + // The largest sealed secret fits: point || ciphertext || tag. + assert!(65 + FDE_SECRET_MAX + 16 <= FDE_SEALED_MAX); + } } diff --git a/include/uapi/linux/apple-sep.h b/include/uapi/linux/apple-sep.h index bcd017ca3e943..d60a3c761e123 100644 --- a/include/uapi/linux/apple-sep.h +++ b/include/uapi/linux/apple-sep.h @@ -437,4 +437,45 @@ struct sep_keybag_blob { #define SEP_BIO_KEYBAG_EXPORT _IOWR(SEP_BIO_IOC_MAGIC, 0x35, struct sep_keybag_blob) #define SEP_BIO_KEYBAG_IMPORT _IOW(SEP_BIO_IOC_MAGIC, 0x36, struct sep_keybag_blob) +/* + * DESIGN DRAFT, not a stable ABI: sealing a disk-encryption secret (a LUKS2 + * key-slot secret) to a user's key bag, as FileVault ties the volume key to + * the user's password. Root only, on the re-arm node. + * + * FDE_CREATE makes a P-256 sealing key inside the enclave under the + * designated, unlocked bag of 'uid' and returns its record. The record holds + * no secret and works only with this Mac's enclave. FDE_SEAL encrypts + * 'secret' to that key and has the enclave open the result once before + * returning 'sealed'. FDE_UNSEAL returns the secret; the enclave opens it + * only while the bag of 'uid' is unlocked (SEP_BIO_KEYBAG_UNLOCK with the + * user's password, which the enclave checks and counts). + * + * Errors: EINVAL for a malformed request, EKEYREJECTED when the enclave + * refuses (bag locked, a record or blob from another Mac or another bag), + * EOPNOTSUPP on a key store without user bags. + */ +#define SEP_FDE_RECORD_MAX 2048 +#define SEP_FDE_SECRET_MAX 64 +#define SEP_FDE_SEALED_MAX 256 + +struct sep_fde_key { + __u32 uid; /* in */ + __u32 record_len; /* out */ + __u8 record[SEP_FDE_RECORD_MAX]; /* out */ +}; + +struct sep_fde_seal { + __u32 uid; /* in */ + __u32 record_len; /* in */ + __u32 secret_len; /* SEAL: in; UNSEAL: out (in 0) */ + __u32 sealed_len; /* SEAL: out (in 0); UNSEAL: in */ + __u8 secret[SEP_FDE_SECRET_MAX]; + __u8 sealed[SEP_FDE_SEALED_MAX]; + __u8 record[SEP_FDE_RECORD_MAX]; /* in */ +}; + +#define SEP_BIO_FDE_CREATE _IOWR(SEP_BIO_IOC_MAGIC, 0x37, struct sep_fde_key) +#define SEP_BIO_FDE_SEAL _IOWR(SEP_BIO_IOC_MAGIC, 0x38, struct sep_fde_seal) +#define SEP_BIO_FDE_UNSEAL _IOWR(SEP_BIO_IOC_MAGIC, 0x39, struct sep_fde_seal) + #endif /* _UAPI_LINUX_APPLE_SEP_H */ From a138583856b70fa87d4418793cc8a457edb591c2 Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 10 Nov 2026 12:00:58 +1100 Subject: [PATCH 4/9] soc: apple: sep: userbag: keep FDE requests off the kernel stack struct sep_fde_seal is 2.4 KiB and struct sep_fde_key 2 KiB. Copy them to the heap in the FDE_CREATE/SEAL/UNSEAL handlers, as EXPORT/IMPORT do for the wrapped bag. The SEAL/UNSEAL buffer carries the volume secret and is wiped when it is dropped. --- drivers/soc/apple/userbag.rs | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/drivers/soc/apple/userbag.rs b/drivers/soc/apple/userbag.rs index 703d779d0e209..9729dda22a81e 100644 --- a/drivers/soc/apple/userbag.rs +++ b/drivers/soc/apple/userbag.rs @@ -23,6 +23,7 @@ #![allow(dead_code)] use kernel::prelude::*; +use kernel::transmute::FromBytes; use kernel::uaccess::{UserPtr, UserSlice}; const CLIENT: u64 = crate::sks::SKS_CLIENT_ID; @@ -1237,8 +1238,12 @@ impl crate::SepData { crate::bio::ok() } IOC_FDE_CREATE => { - let mut key: FdeKey = - UserSlice::new(user_ptr, core::mem::size_of::()).reader().read()?; + // 2 KiB: keep the request off the kernel stack. + let (reader, mut writer) = + UserSlice::new(user_ptr, core::mem::size_of::()).reader_writer(); + let mut buf = KVec::::with_capacity(core::mem::size_of::(), GFP_KERNEL)?; + reader.read_all(&mut buf, GFP_KERNEL)?; + let key = FdeKey::from_bytes_mut(&mut buf).ok_or(EINVAL)?; if key.uid < MIN_UID || key.uid > i32::MAX as u32 { return Err(EINVAL); } @@ -1250,14 +1255,22 @@ impl crate::SepData { key.record.fill(0); key.record[..record.len()].copy_from_slice(&record); key.record_len = record.len() as u32; - UserSlice::new(user_ptr, core::mem::size_of::()).writer().write(&key)?; + writer.write_slice(&buf)?; crate::bio::ok() } IOC_FDE_SEAL | IOC_FDE_UNSEAL => { let seal = cmd == IOC_FDE_SEAL; - let mut req: FdeSeal = - UserSlice::new(user_ptr, core::mem::size_of::()).reader().read()?; - let user = parse_fde(&req, seal)?; + // 2.4 KiB: keep the request off the kernel stack. It carries the + // volume secret, so the buffer is wiped when it is dropped. + let (reader, mut writer) = + UserSlice::new(user_ptr, core::mem::size_of::()).reader_writer(); + let mut buf = crate::Secret(KVec::::with_capacity( + core::mem::size_of::(), + GFP_KERNEL, + )?); + reader.read_all(&mut buf.0, GFP_KERNEL)?; + let req = FdeSeal::from_bytes_mut(&mut buf.0).ok_or(EINVAL)?; + let user = parse_fde(req, seal)?; let record_len = req.record_len as usize; if seal { let sealed = self.fde_seal( @@ -1286,7 +1299,7 @@ impl crate::SepData { req.secret[..secret.len()].copy_from_slice(&secret); req.secret_len = secret.len() as u32; } - UserSlice::new(user_ptr, core::mem::size_of::()).writer().write(&req)?; + writer.write_slice(&buf)?; crate::bio::ok() } IOC_CREATE | IOC_UNLOCK => { From 5c7ae60c864c958a3d5afcff79bddf90a4d7acdb Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 10 Nov 2026 12:16:06 +1100 Subject: [PATCH 5/9] soc: apple: sep: userbag: lock at once on logout and suspend After the lock transition the key store holds the bag in its locking phase, class keys still usable, until the grace timer fires. The LOCK ioctl fired it only for a screen lock with no grace, so a logout or suspend lock left the bag's keys open: on the Neo a key sealed under a user bag still unsealed after SEP_BIO_KEYBAG_LOCK with the logout reason, and was refused once the timer was fired. Fire it for every lock but a screen lock with a grace period. Assisted-by: LLM --- drivers/soc/apple/userbag.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/drivers/soc/apple/userbag.rs b/drivers/soc/apple/userbag.rs index 9729dda22a81e..e54a56b860b2e 100644 --- a/drivers/soc/apple/userbag.rs +++ b/drivers/soc/apple/userbag.rs @@ -896,6 +896,14 @@ pub(crate) fn parse_lock(req: &LockRequest) -> Result<(u32, u32, u32)> { Ok((req.uid, req.reason, req.grace_s.min(MAX_GRACE_S))) } +/// Whether a lock request ends the bag's grace at once. After the +/// transition the key store keeps the bag "locking" (its class keys still +/// usable) until the grace timer fires, so only a screen lock with a grace +/// period may leave it there; suspend and logout lock at once. +pub(crate) fn lock_expires_now(reason: u32, grace_s: u32) -> bool { + reason != LOCK_REASON_SCREEN || grace_s == 0 +} + pub(crate) fn parse_change( req: &ChangeRequest, ) -> Result<(crate::sks::DesignateUser, &[u8], &[u8])> { @@ -1326,7 +1334,7 @@ impl crate::SepData { let (uid, reason, grace_s) = parse_lock(&request)?; let user = crate::sks::DesignateUser::new(uid as i32).ok_or(EINVAL)?; let screen = reason == LOCK_REASON_SCREEN; - self.userbag_lock(user, screen, screen && grace_s == 0)?; + self.userbag_lock(user, screen, lock_expires_now(reason, grace_s))?; crate::bio::ok() } IOC_CHANGE => { @@ -1771,6 +1779,15 @@ mod tests { assert!(parse_lock(&lock).is_err()); } + #[test] + fn only_a_screen_lock_with_grace_leaves_the_bag_locking() { + assert!(!lock_expires_now(LOCK_REASON_SCREEN, 300)); + assert!(lock_expires_now(LOCK_REASON_SCREEN, 0)); + assert!(lock_expires_now(LOCK_REASON_SUSPEND, 300)); + assert!(lock_expires_now(LOCK_REASON_LOGOUT, 0)); + assert!(lock_expires_now(LOCK_REASON_LOGOUT, 300)); + } + #[test] fn the_user_bag_handle_is_minus_the_uid() { let user = crate::sks::DesignateUser::new(501).expect("user"); From 251b5682016ebe37f5571fe01afb3c8e8ad644b5 Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 10 Nov 2026 12:34:38 +1100 Subject: [PATCH 6/9] Revert "soc: apple: sep: userbag: lock at once on logout and suspend" Firing the grace timer does not end the key store's locking phase early: on the Neo the store answers the timer with no handle until its own deadline (about 40 s after the lock transition, for every lock reason), so the change only turned logout and suspend locks into EAGAIN. The bag reaches the locked phase by itself at that deadline, and a key sealed under it is refused from then on. Assisted-by: LLM --- drivers/soc/apple/userbag.rs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/drivers/soc/apple/userbag.rs b/drivers/soc/apple/userbag.rs index e54a56b860b2e..9729dda22a81e 100644 --- a/drivers/soc/apple/userbag.rs +++ b/drivers/soc/apple/userbag.rs @@ -896,14 +896,6 @@ pub(crate) fn parse_lock(req: &LockRequest) -> Result<(u32, u32, u32)> { Ok((req.uid, req.reason, req.grace_s.min(MAX_GRACE_S))) } -/// Whether a lock request ends the bag's grace at once. After the -/// transition the key store keeps the bag "locking" (its class keys still -/// usable) until the grace timer fires, so only a screen lock with a grace -/// period may leave it there; suspend and logout lock at once. -pub(crate) fn lock_expires_now(reason: u32, grace_s: u32) -> bool { - reason != LOCK_REASON_SCREEN || grace_s == 0 -} - pub(crate) fn parse_change( req: &ChangeRequest, ) -> Result<(crate::sks::DesignateUser, &[u8], &[u8])> { @@ -1334,7 +1326,7 @@ impl crate::SepData { let (uid, reason, grace_s) = parse_lock(&request)?; let user = crate::sks::DesignateUser::new(uid as i32).ok_or(EINVAL)?; let screen = reason == LOCK_REASON_SCREEN; - self.userbag_lock(user, screen, lock_expires_now(reason, grace_s))?; + self.userbag_lock(user, screen, screen && grace_s == 0)?; crate::bio::ok() } IOC_CHANGE => { @@ -1779,15 +1771,6 @@ mod tests { assert!(parse_lock(&lock).is_err()); } - #[test] - fn only_a_screen_lock_with_grace_leaves_the_bag_locking() { - assert!(!lock_expires_now(LOCK_REASON_SCREEN, 300)); - assert!(lock_expires_now(LOCK_REASON_SCREEN, 0)); - assert!(lock_expires_now(LOCK_REASON_SUSPEND, 300)); - assert!(lock_expires_now(LOCK_REASON_LOGOUT, 0)); - assert!(lock_expires_now(LOCK_REASON_LOGOUT, 300)); - } - #[test] fn the_user_bag_handle_is_minus_the_uid() { let user = crate::sks::DesignateUser::new(501).expect("user"); From 771db99f7a32f8c85ef5a41744bd836373fa0d13 Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 10 Nov 2026 17:54:34 +1100 Subject: [PATCH 7/9] soc: apple: sep: unlock per-user key bags in the initrd (keystore_only=2) A root volume sealed to a user's password (SEP_BIO_FDE_*) has to open before the real root is mounted, the way FileVault asks for the password once after a cold boot. keystore_only=1 attaches the key store for trusted keys only and keeps xART read-only, so a saved user bag can neither be loaded nor unlocked there. keystore_only=2 adds that unlock to the initrd phase: - /dev/sep-bio-rearm is published as soon as the key store is open. Until complete_attach it serves only what a password unlock needs: state, import, unlock, lock and FDE unseal. Create, change, export and the FDE create/seal requests return EAGAIN; enrolment stays on the real root. - Loading and unlocking a bag needs enclave writes to the gigalocker, so xART follows xart_writes in this mode, but only while the xART guard is off: the guard's record list lives in host files on the real root, which the initrd must not write. With a guard, xART stays read-only, the bag requests fail with EROFS and the volume falls back to its passphrase. - Nothing changes for keystore_only=0 or 1. complete_attach is unchanged and publishes the rest of the node's requests. load-driver completes the attach for either initrd mode. KUnit: the initrd request subset and the initrd xART write rule. Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Assisted-by: LLM --- .../ABI/testing/sysfs-driver-apple-sep | 10 +++++++ drivers/soc/apple/attach.rs | 26 +++++++++++++++++++ drivers/soc/apple/attach_shim.c | 7 ++++- drivers/soc/apple/sbio.rs | 18 +++++++++++++ drivers/soc/apple/sep.rs | 21 ++++++++++++++- drivers/soc/apple/shim.h | 1 + drivers/soc/apple/userbag.rs | 23 ++++++++++++++++ tools/aurora-sep/load-driver | 2 +- 8 files changed, 105 insertions(+), 3 deletions(-) diff --git a/Documentation/ABI/testing/sysfs-driver-apple-sep b/Documentation/ABI/testing/sysfs-driver-apple-sep index 6049df1852470..6576373c3c661 100644 --- a/Documentation/ABI/testing/sysfs-driver-apple-sep +++ b/Documentation/ABI/testing/sysfs-driver-apple-sep @@ -382,3 +382,13 @@ Description: /var/lib paths; both are cached on first trusted-key use. An unseal needing an xART write fails with EBADMSG so userspace can use its passphrase fallback. + + With apple_sep.keystore_only=2 the initrd phase also serves a + password unlock of a per-user key bag on /dev/sep-bio-rearm: + only the state, import, unlock, lock and FDE unseal requests; + every other request returns EAGAIN until complete_attach. + Loading and unlocking a bag needs enclave writes to xART, so + in this mode xART follows xart_writes, but only when the xART + guard is off (its record list lives in host files on the real + root). With a guard, xART stays read-only, the bag requests + fail with EROFS and the volume falls back to its passphrase. diff --git a/drivers/soc/apple/attach.rs b/drivers/soc/apple/attach.rs index 7e628c16fb0bc..fd1632181ab97 100644 --- a/drivers/soc/apple/attach.rs +++ b/drivers/soc/apple/attach.rs @@ -47,6 +47,7 @@ pub(crate) fn unseal_allowed(before: u32, after: u32) -> bool { extern "C" { fn sep_keystore_only() -> bool; + fn sep_keystore_user_bags() -> bool; fn sep_complete_attach_register( dev: *mut c_void, ctx: *mut c_void, @@ -60,6 +61,22 @@ pub(crate) fn requested() -> bool { unsafe { sep_keystore_only() } } +/// `keystore_only=2`: the initrd also unlocks per-user key bags, so that a +/// volume sealed to a user's password can open before the real root. +pub(crate) fn user_bags_requested() -> bool { + // SAFETY: the parameter is immutable after module loading. + unsafe { sep_keystore_user_bags() } +} + +/// Whether the initrd phase may let the enclave write the gigalocker. A +/// per-user bag cannot be loaded or unlocked without those writes, but the +/// xART guard keeps its record list in host files on the real root, which +/// the initrd must not write. So writes are allowed only for user bags and +/// only when no guard runs; with a guard the bags wait for complete_attach. +pub(crate) fn initrd_xart_writes(xart_writes: bool, user_bags: bool, guard: Option) -> bool { + xart_writes && user_bags && guard.is_none() +} + /// Holds the callback context alive until sysfs has drained all writers. pub(crate) struct Attribute { sep: Arc, @@ -147,6 +164,15 @@ mod tests { assert_eq!(phase.needs_completion(), Ok(false)); } + #[test] + fn initrd_writes_only_for_user_bags_without_a_guard() { + assert!(initrd_xart_writes(true, true, None)); + assert!(!initrd_xart_writes(false, true, None)); + assert!(!initrd_xart_writes(true, false, None)); + assert!(!initrd_xart_writes(true, true, Some(1))); + assert!(!initrd_xart_writes(true, true, Some(2))); + } + #[test] fn refused_initrd_write_fails_unseal_closed() { assert!(unseal_allowed(0, 0)); diff --git a/drivers/soc/apple/attach_shim.c b/drivers/soc/apple/attach_shim.c index 1fa8f9cdea620..b24a4bfdd012a 100644 --- a/drivers/soc/apple/attach_shim.c +++ b/drivers/soc/apple/attach_shim.c @@ -10,13 +10,18 @@ static unsigned char keystore_only; module_param(keystore_only, byte, 0444); MODULE_PARM_DESC(keystore_only, - "Attach for trusted keys only; defer persistent host state and Touch ID until complete_attach"); + "Initrd attach: 1 = trusted keys only, 2 = also per-user key bags for a password unlock; defer persistent host state and Touch ID until complete_attach"); bool sep_keystore_only(void) { return keystore_only != 0; } +bool sep_keystore_user_bags(void) +{ + return keystore_only == 2; +} + /* One SEP per machine. device_remove_file drains callbacks before release. */ static void *sep_attach_ctx; static int (*sep_attach_complete)(void *ctx); diff --git a/drivers/soc/apple/sbio.rs b/drivers/soc/apple/sbio.rs index 77e55e5f3e55c..eab5e4486a6d6 100644 --- a/drivers/soc/apple/sbio.rs +++ b/drivers/soc/apple/sbio.rs @@ -2261,6 +2261,19 @@ impl SepData { ); return false; } + if self.keystore_user_bags { + // keystore_only=2: a volume sealed to a user's password opens + // here, before the real root. rearm_ioctl limits the node to the + // unlock requests until complete_attach. + if let Err(e) = self.register_rearm() { + dev_err!( + self.dev, + "bringup: could not publish /dev/{} for the initrd password unlock: {:?}\n", + bio::REARM_DEVICE_NAME, + e + ); + } + } diag::keystore_open(); true } @@ -5186,6 +5199,11 @@ impl SepData { if self.shutting_down.load(Relaxed) { return Err(ENODEV); } + // In the initrd phase the node serves only a password unlock; Touch + // ID and the host state are not attached yet. + if self.keystore_only.load(Relaxed) && !crate::userbag::is_initrd_ioctl(cmd) { + return Err(EAGAIN); + } // The per-user key bag carries login passwords: root only, on this // node, never on /dev/sep-bio, which fprintd holds. if crate::userbag::is_ioctl(cmd) { diff --git a/drivers/soc/apple/sep.rs b/drivers/soc/apple/sep.rs index dd654b0a2dc16..80e141bf9afda 100644 --- a/drivers/soc/apple/sep.rs +++ b/drivers/soc/apple/sep.rs @@ -611,6 +611,9 @@ struct SepData { #[pin] attach_phase: Mutex, keystore_only: Atomic, + // keystore_only=2: /dev/sep-bio-rearm is published in the initrd phase + // for the password-unlock subset of the per-user key bag requests. + keystore_user_bags: bool, keystore_write_refusals: Atomic, #[pin] identity_keybag: Mutex>, @@ -969,10 +972,25 @@ impl SepData { let dma_ring = dma::Coherent::::zeroed_slice(dev, DMA_RING_SIZE, GFP_KERNEL)?; let keystore_only = attach::requested(); - let xart_writes = !keystore_only && module_parameters::xart_writes.value() != 0; + let keystore_user_bags = keystore_only && attach::user_bags_requested(); let xart_start = module_parameters::xart_start_sector.value(); let xart_guard = xart_guard::level(module_parameters::xart_guard.value(), profile.xart_guard); + let xart_writes = if keystore_only { + attach::initrd_xart_writes( + module_parameters::xart_writes.value() != 0, + keystore_user_bags, + xart_guard, + ) + } else { + module_parameters::xart_writes.value() != 0 + }; + if keystore_user_bags && !xart_writes { + dev_info!( + dev, + "initrd: per-user key bags need xart_writes=1 and no xART guard; they wait for the real root\n" + ); + } let store = match xart_store::Store::open_owner(xart_writes, xart_start) { Ok(mut store) => { match xart_guard { @@ -1034,6 +1052,7 @@ impl SepData { profile, attach_phase <- new_mutex!(attach::Phase::new(keystore_only)), keystore_only: Atomic::new(keystore_only), + keystore_user_bags, keystore_write_refusals: Atomic::new(0), identity_keybag <- new_mutex!(None), fw_map <- new_mutex!(None), diff --git a/drivers/soc/apple/shim.h b/drivers/soc/apple/shim.h index 87bbfdf7de416..44a68920ddc35 100644 --- a/drivers/soc/apple/shim.h +++ b/drivers/soc/apple/shim.h @@ -247,6 +247,7 @@ void sep_tk_set_blob_len(struct trusted_key_payload *p, unsigned int n); /* Initrd attach and completion; sysfs removal drains the callback. */ bool sep_keystore_only(void); +bool sep_keystore_user_bags(void); int sep_complete_attach_register(struct device *dev, void *ctx, int (*complete)(void *ctx)); void sep_complete_attach_unregister(struct device *dev); diff --git a/drivers/soc/apple/userbag.rs b/drivers/soc/apple/userbag.rs index 9729dda22a81e..9056740efa31d 100644 --- a/drivers/soc/apple/userbag.rs +++ b/drivers/soc/apple/userbag.rs @@ -868,6 +868,18 @@ pub(crate) fn is_ioctl(cmd: u32) -> bool { ) } +/// The requests a password unlock in the initrd needs: load the bag saved in +/// the volume's token, unlock it with the typed password, open the sealed +/// key-slot secret, lock the bag again, and read its state. Enrolment and +/// password changes (create, change, export, new or re-sealed keys) wait for +/// the real root, where the host state and Touch ID are attached. +pub(crate) fn is_initrd_ioctl(cmd: u32) -> bool { + matches!( + cmd, + IOC_STATE | IOC_IMPORT | IOC_UNLOCK | IOC_LOCK | IOC_FDE_UNSEAL + ) +} + /// Checks an unlock/create request and returns the user and the secret. pub(crate) fn parse_unlock( req: &UnlockRequest, @@ -1387,6 +1399,17 @@ mod tests { p } + #[test] + fn initrd_allows_only_the_password_unlock() { + for cmd in [IOC_STATE, IOC_IMPORT, IOC_UNLOCK, IOC_LOCK, IOC_FDE_UNSEAL] { + assert!(is_initrd_ioctl(cmd)); + assert!(is_ioctl(cmd)); + } + for cmd in [IOC_CREATE, IOC_CHANGE, IOC_EXPORT, IOC_FDE_CREATE, IOC_FDE_SEAL] { + assert!(!is_initrd_ioctl(cmd)); + } + } + const USER_HANDLE: i32 = -501; const CLIENT_LE: [u8; 8] = [0x53, 0x4b, 0x53, 0x58, 0x55, 0x4e, 0x49, 0x4c]; diff --git a/tools/aurora-sep/load-driver b/tools/aurora-sep/load-driver index e0431168e828b..3961a1e638216 100755 --- a/tools/aurora-sep/load-driver +++ b/tools/aurora-sep/load-driver @@ -40,7 +40,7 @@ if [ "${AURORA_SEP_SESSION:-0}" != 1 ]; then fi case "$(cat /sys/module/apple_sep/parameters/keystore_only 2>/dev/null || true)" in - 1 | Y) + 1 | 2 | Y) completed=false for attribute in /sys/bus/platform/devices/*.sep/complete_attach; do [ -e "$attribute" ] || continue From 24e66c388113f24150bf7e0a54b6c556de7550a8 Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:35:46 +1100 Subject: [PATCH 8/9] soc: apple: sep: userbag: bind FDE secrets to the OS install A sealed volume secret opened for any Linux install on the Mac whose user bag it was sealed under. FileVault ties a volume key to its volume, so do the same here: FDE_SEAL frames the secret with the kernel's OS identity (the os_uuid_hi/lo parameters, else /chosen/apfs-preboot-uuid) inside the ECIES envelope, and FDE_UNSEAL returns the secret only when the identity inside matches. Another install gets EXDEV, so user space can tell it from a refused unseal (EKEYREJECTED); a kernel without an identity gets ENOKEY. A plaintext without the frame is refused. Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Assisted-by: LLM --- drivers/soc/apple/refkey.rs | 30 +++++++++++++++---- drivers/soc/apple/userbag.rs | 55 ++++++++++++++++++++++++++++++++++ include/uapi/linux/apple-sep.h | 7 +++++ 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/drivers/soc/apple/refkey.rs b/drivers/soc/apple/refkey.rs index 6c04d70e78176..33701cf44c640 100644 --- a/drivers/soc/apple/refkey.rs +++ b/drivers/soc/apple/refkey.rs @@ -528,16 +528,26 @@ impl SepData { Ok(blob) } - /// ECIES-encrypts `secret` to the sealing key in `record` and has the - /// enclave open the result once before returning it, so a sealed blob - /// this bag cannot open is never handed out. + /// The OS install this kernel runs as (the explicit `os_uuid_hi/lo` + /// identity, else `/chosen/apfs-preboot-uuid`), which FDE secrets are + /// bound to, as FileVault binds a volume key to its volume. + fn fde_os_identity(&self) -> Result<[u8; 16]> { + self.xarm.lock().os_uuid.ok_or(ENOKEY) + } + + /// ECIES-encrypts `secret`, framed with this OS install's identity, to the + /// sealing key in `record` and has the enclave open the result once before + /// returning it, so a sealed blob this bag cannot open is never handed + /// out. `ENOKEY` when this kernel has no OS identity. pub(crate) fn fde_seal( &self, user: crate::sks::DesignateUser, record: &[u8], secret: &[u8], ) -> Result> { - let sealed = refkey_seal::ecies_seal(Self::refkey_pub(record).ok_or(EINVAL)?, secret)?; + let os = self.fde_os_identity()?; + let framed = crate::Secret(crate::userbag::fde_bind(&os, secret)?); + let sealed = refkey_seal::ecies_seal(Self::refkey_pub(record).ok_or(EINVAL)?, &framed)?; let recovered = crate::Secret(self.fde_unseal(user, record, &sealed)?); if &*recovered != secret { dev_err!(self.dev, "fde: refusing a sealed volume secret whose round trip failed\n"); @@ -548,7 +558,9 @@ impl SepData { /// Has the enclave open `sealed` with the sealing key in `record`. It /// does so only on the Mac whose enclave made the key and only while - /// `user`'s bag is unlocked; any refusal is `EKEYREJECTED`. + /// `user`'s bag is unlocked; any refusal is `EKEYREJECTED`. The secret is + /// returned only to the OS install it was sealed under: `EXDEV` for + /// another install, `ENOKEY` when this kernel has no OS identity. pub(crate) fn fde_unseal( &self, user: crate::sks::DesignateUser, @@ -559,8 +571,14 @@ impl SepData { return Err(EOPNOTSUPP); } Self::refkey_pub(record).ok_or(EINVAL)?; + let os = self.fde_os_identity()?; let handle = crate::sks::KeyBagHandle::from_load_reply(user.special_handle().value()); - self.sks_refkey_unseal(handle, record, sealed).ok_or(EKEYREJECTED) + let framed = + crate::Secret(self.sks_refkey_unseal(handle, record, sealed).ok_or(EKEYREJECTED)?); + let secret = crate::userbag::fde_unbind(&os, &framed)?; + let mut out = KVec::with_capacity(secret.len(), GFP_KERNEL)?; + out.extend_from_slice(secret, GFP_KERNEL)?; + Ok(out) } /// Enclave ECIES-decrypt (o = "oecd"); takes the ephemeral from the front of `d`. diff --git a/drivers/soc/apple/userbag.rs b/drivers/soc/apple/userbag.rs index 49ccfb8fa17b4..db6d11a36016d 100644 --- a/drivers/soc/apple/userbag.rs +++ b/drivers/soc/apple/userbag.rs @@ -823,6 +823,40 @@ pub(crate) const FDE_SECRET_MAX: usize = 64; /// `ephemeral point (65) || ciphertext || tag (16)` for the largest secret. pub(crate) const FDE_SEALED_MAX: usize = 256; +/// Magic and version at the front of every sealed FDE plaintext. +const FDE_BINDING_MAGIC: [u8; 8] = *b"AURFDEO1"; +/// Magic followed by the 16-byte OS install identity, then the secret. +const FDE_BINDING_LEN: usize = FDE_BINDING_MAGIC.len() + 16; + +/// Frames a volume secret with the OS install identity it is sealed under. +/// The frame sits inside the ECIES envelope, so the enclave's AEAD covers it. +pub(crate) fn fde_bind(os: &[u8; 16], secret: &[u8]) -> Result> { + if secret.is_empty() || secret.len() > FDE_SECRET_MAX { + return Err(EINVAL); + } + let mut v = KVec::with_capacity(FDE_BINDING_LEN + secret.len(), GFP_KERNEL)?; + v.extend_from_slice(&FDE_BINDING_MAGIC, GFP_KERNEL)?; + v.extend_from_slice(os, GFP_KERNEL)?; + v.extend_from_slice(secret, GFP_KERNEL)?; + Ok(v) +} + +/// The secret inside a frame made by [`fde_bind`], if it was bound to `os`: +/// `EXDEV` for a secret sealed under another OS install, `EKEYREJECTED` for a +/// plaintext without the frame (not sealed by this driver). +pub(crate) fn fde_unbind<'a>(os: &[u8; 16], framed: &'a [u8]) -> Result<&'a [u8]> { + if framed.len() <= FDE_BINDING_LEN + || framed.len() > FDE_BINDING_LEN + FDE_SECRET_MAX + || framed[..FDE_BINDING_MAGIC.len()] != FDE_BINDING_MAGIC + { + return Err(EKEYREJECTED); + } + if framed[FDE_BINDING_MAGIC.len()..FDE_BINDING_LEN] != os[..] { + return Err(EXDEV); + } + Ok(&framed[FDE_BINDING_LEN..]) +} + /// The UAPI `struct sep_fde_key`: a sealing key made under `uid`'s bag. #[repr(C)] pub(crate) struct FdeKey { @@ -1873,6 +1907,27 @@ mod tests { } } + #[test] + fn fde_secret_is_bound_to_its_os_install() { + let a = [0x11u8; 16]; + let b = [0x22u8; 16]; + let secret = [0x5au8; 32]; + let framed = fde_bind(&a, &secret).expect("bind"); + assert_eq!(framed.len(), FDE_BINDING_LEN + secret.len()); + assert_eq!(fde_unbind(&a, &framed).expect("same install"), &secret[..]); + assert_eq!(fde_unbind(&b, &framed), Err(EXDEV)); + // A plaintext without the frame (an older draft's blob) is refused. + assert_eq!(fde_unbind(&a, &secret), Err(EKEYREJECTED)); + let mut bad = [0u8; FDE_BINDING_LEN + 32]; + bad.copy_from_slice(&framed); + bad[0] ^= 1; + assert_eq!(fde_unbind(&a, &bad), Err(EKEYREJECTED)); + assert_eq!(fde_unbind(&a, &framed[..FDE_BINDING_LEN]), Err(EKEYREJECTED)); + assert!(fde_bind(&a, &[]).is_err()); + assert!(fde_bind(&a, &[0u8; FDE_SECRET_MAX + 1]).is_err()); + assert!(fde_bind(&a, &[0u8; FDE_SECRET_MAX]).is_ok()); + } + #[test] fn fde_parse_bounds() { let ok = MIN_UID + 1; diff --git a/include/uapi/linux/apple-sep.h b/include/uapi/linux/apple-sep.h index cac5c2952a639..94e31b6b199c1 100644 --- a/include/uapi/linux/apple-sep.h +++ b/include/uapi/linux/apple-sep.h @@ -450,8 +450,15 @@ struct sep_keybag_blob { * only while the bag of 'uid' is unlocked (SEP_BIO_KEYBAG_UNLOCK with the * user's password, which the enclave checks and counts). * + * The secret is bound to the OS install that sealed it (the apple_sep + * os_uuid_hi/lo identity, else /chosen/apfs-preboot-uuid): the identity is + * sealed with the secret and FDE_UNSEAL returns the secret only under the + * same identity, as FileVault binds a volume key to its volume. + * * Errors: EINVAL for a malformed request, EKEYREJECTED when the enclave * refuses (bag locked, a record or blob from another Mac or another bag), + * EXDEV on FDE_UNSEAL of a secret sealed under another OS install, ENOKEY + * when the kernel has no OS identity (FDE_SEAL and FDE_UNSEAL), * EOPNOTSUPP on a key store without user bags. */ #define SEP_FDE_RECORD_MAX 2048 From 1f15b401847c99b10eef81cb793099c09f9b2af1 Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Wed, 11 Nov 2026 21:01:25 +1100 Subject: [PATCH 9/9] tools: aurora-sep: tests: keystore_only in the machine ref-key fixture sks_machine_refkey() now returns without creating a ref-key in the key-store-only initrd phase. Give the fixture the keystore_only flag and check that phase never creates one, whatever the store open returns. --- tools/aurora-sep/tests/test_identity_preservation.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/aurora-sep/tests/test_identity_preservation.py b/tools/aurora-sep/tests/test_identity_preservation.py index f420ec74b167f..d30dce71c9897 100644 --- a/tools/aurora-sep/tests/test_identity_preservation.py +++ b/tools/aurora-sep/tests/test_identity_preservation.py @@ -47,6 +47,7 @@ def body(source, signature): ''' REFKEY = r''' +use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; thread_local! {static OPEN_ERROR:Cell=const{Cell::new(0)};} mod sks {pub type KeyBagHandle=u32;} mod module_parameters { @@ -67,7 +68,7 @@ def body(source, signature): } struct Cache; impl Cache {fn lock(&self)->Option<()>{None}} -struct SepData {machine_refkey:Cache,creates:Cell} +struct SepData {machine_refkey:Cache,creates:Cell,keystore_only:AtomicBool} impl SepData { fn sks_create_refkey(&self,_:u32,_:&[u8],_:Option<&[u8]>)->Option>{self.creates.set(self.creates.get()+1);None} fn cache_machine_refkey(&self,_:KVec)->bool{true} @@ -76,9 +77,12 @@ def body(source, signature): fn main(){ for error in [0,ENOENT,EIO,13,12,30,40] { OPEN_ERROR.with(|e|e.set(error)); - let s=SepData{machine_refkey:Cache,creates:Cell::new(0)}; + let s=SepData{machine_refkey:Cache,creates:Cell::new(0),keystore_only:AtomicBool::new(false)}; s.sks_machine_refkey(0,&[]); assert_eq!(s.creates.get(),usize::from(error==ENOENT),"only absence permits ref-key creation"); + let s=SepData{machine_refkey:Cache,creates:Cell::new(0),keystore_only:AtomicBool::new(true)}; + s.sks_machine_refkey(0,&[]); + assert_eq!(s.creates.get(),0,"the key-store-only initrd never creates a ref-key"); } println!("refkey cases passed"); }