diff --git a/Documentation/ABI/testing/sysfs-driver-apple-sep b/Documentation/ABI/testing/sysfs-driver-apple-sep index e81af09585c76..2f55f1ddef613 100644 --- a/Documentation/ABI/testing/sysfs-driver-apple-sep +++ b/Documentation/ABI/testing/sysfs-driver-apple-sep @@ -382,6 +382,39 @@ Description: 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. + + 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. + What: /sys/bus/platform/devices/.sep/diag/policy Date: November 2026 KernelVersion: 7.2 diff --git a/drivers/soc/apple/Makefile b/drivers/soc/apple/Makefile index c57ed9ccf7749..8b36ffee52eae 100644 --- a/drivers/soc/apple/Makefile +++ b/drivers/soc/apple/Makefile @@ -49,7 +49,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..fd1632181ab97 --- /dev/null +++ b/drivers/soc/apple/attach.rs @@ -0,0 +1,183 @@ +// 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_keystore_user_bags() -> 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() } +} + +/// `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, + 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 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)); + 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..b24a4bfdd012a --- /dev/null +++ b/drivers/soc/apple/attach_shim.c @@ -0,0 +1,55 @@ +// 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, + "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); + +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 77a31f2e00ffb..324b03c03110a 100644 --- a/drivers/soc/apple/diag.rs +++ b/drivers/soc/apple/diag.rs @@ -42,6 +42,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); @@ -176,6 +178,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 38ffecc753d95..d8d515d2c39b3 100644 --- a/drivers/soc/apple/diag_shim.c +++ b/drivers/soc/apple/diag_shim.c @@ -150,7 +150,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); @@ -371,7 +371,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); @@ -380,9 +380,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"; @@ -784,8 +784,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); } @@ -802,11 +802,21 @@ 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) { atomic64_cmpxchg(&sep_diag.ready_ns, 0, ktime_get_boottime_ns()); 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 e6dee4b2ade55..3c7573cbde28d 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 @@ -228,6 +250,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, None) else { dev_warn!( self.dev, @@ -270,12 +296,12 @@ impl SepData { // concurrently, and an unload here must not end the handle one of // them is using. let _bag = self.key_bag_lock.lock(); - 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 { @@ -305,6 +331,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 = { @@ -316,34 +352,39 @@ impl SepData { }; // See `ensure_machine_refkey`. let _bag = self.key_bag_lock.lock(); - 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. @@ -533,6 +574,76 @@ 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) + } + + /// 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 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"); + 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`. 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, + record: &[u8], + sealed: &[u8], + ) -> Result> { + if self.profile.key_store != profile::KeyStore::Variant5 { + 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()); + 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) + } + /// `/dev/sep-key` unseal of `sealed` (from `refkey_seal::ecies_seal` to /// this key's public point), checked against the credential context `acm` /// when one is given, as `user_key_sign` is. The enclave refuses its own diff --git a/drivers/soc/apple/sbio.rs b/drivers/soc/apple/sbio.rs index ff692a1edc49e..24a044625ff7d 100644 --- a/drivers/soc/apple/sbio.rs +++ b/drivers/soc/apple/sbio.rs @@ -2256,18 +2256,42 @@ 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; + } + 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 + } + + pub(crate) fn finish_keystore_bringup(&self) { if module_parameters::provision_keybag.value() != 0 { if module_parameters::xart_writes.value() == 0 { dev_err!( @@ -5261,6 +5285,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 328ed781b2aac..5f863afc63eca 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; @@ -612,6 +613,18 @@ struct SepData { profile: &'static profile::PlatformProfile, + // Serialises completion, including concurrent sysfs writers and retries. + #[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>, + + #[pin] fw_map: Mutex>, @@ -984,10 +997,26 @@ 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 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" + ); + } // The qualified 26.6.2 lane uses only the boot-forwarded identity. // Check before opening the shared xART store writable. if profile::j613_26_6_2() && xart_writes { @@ -1037,15 +1066,19 @@ impl SepData { } }; - let mut host_store = match store::Store::open() { - Ok(store) => Some(store), - Err(e) => { - if profile::j613_26_6_2() { - dev_err!(dev, "J613 26.6.2 host-state store unavailable: {:?}\n", e); - return Err(e); + let mut host_store = if keystore_only { + None + } else { + match store::Store::open() { + Ok(store) => Some(store), + Err(e) => { + if profile::j613_26_6_2() { + dev_err!(dev, "J613 26.6.2 host-state store unavailable: {:?}\n", e); + return Err(e); + } + dev_warn!(dev, "Linux host-state store unavailable: {:?}\n", e); + None } - dev_warn!(dev, "Linux host-state store unavailable: {:?}\n", e); - None } }; @@ -1060,6 +1093,11 @@ 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_user_bags, + 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)), @@ -1820,6 +1858,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 @@ -1844,7 +1889,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; } @@ -2590,8 +2638,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(); + } diag::log_summary(&this.dev); this.finish_bringup(); Self::arm_settle(this); @@ -2605,6 +2658,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. @@ -3014,7 +3133,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 @@ -3034,7 +3153,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"); @@ -3097,19 +3217,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 cd312d2ede080..12d6fe7d1b214 100644 --- a/drivers/soc/apple/shim.h +++ b/drivers/soc/apple/shim.h @@ -270,4 +270,14 @@ 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); +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); +void sep_diag_keystore_ended(void); +void sep_diag_set_xart(bool writable); + #endif /* SEP_SHIM_H */ diff --git a/drivers/soc/apple/userbag.rs b/drivers/soc/apple/userbag.rs index d06f48cf7260f..bb94793472a8c 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; @@ -815,10 +816,133 @@ fn blob_len(blob: &[u8]) -> usize { u32::from_ne_bytes([blob[4], blob[5], blob[6], blob[7]]) as usize } +/// 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; + +/// 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 { + 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 + ) +} + +/// 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 ) } @@ -1211,6 +1335,71 @@ impl crate::SepData { self.userbag_import(user, &blob[8..8 + len])?; crate::bio::ok() } + IOC_FDE_CREATE => { + // 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); + } + 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; + writer.write_slice(&buf)?; + crate::bio::ok() + } + IOC_FDE_SEAL | IOC_FDE_UNSEAL => { + let seal = cmd == IOC_FDE_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( + 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; + } + writer.write_slice(&buf)?; + crate::bio::ok() + } IOC_CREATE | IOC_UNLOCK => { let request: UnlockRequest = UserSlice::new(user_ptr, core::mem::size_of::()) @@ -1296,6 +1485,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]; @@ -1717,4 +1917,66 @@ 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_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; + 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 8bf6db5769657..4841c2d4eee47 100644 --- a/include/uapi/linux/apple-sep.h +++ b/include/uapi/linux/apple-sep.h @@ -475,4 +475,52 @@ 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). + * + * 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 +#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 */ diff --git a/tools/aurora-sep/load-driver b/tools/aurora-sep/load-driver index 984354341bf40..8e45fd4511850 100755 --- a/tools/aurora-sep/load-driver +++ b/tools/aurora-sep/load-driver @@ -37,7 +37,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 | 2 | 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..c8b6c9ebc8e78 --- /dev/null +++ b/tools/aurora-sep/tests/keystore-only.config @@ -0,0 +1,23 @@ +# 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 +# Select the hidden DMA_SHARED_BUFFER symbol through a public option. +CONFIG_SYNC_FILE=y 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"); } 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()