diff --git a/drivers/soc/apple/der.rs b/drivers/soc/apple/der.rs index fcd198561872f..28ac3f9d42799 100644 --- a/drivers/soc/apple/der.rs +++ b/drivers/soc/apple/der.rs @@ -8,6 +8,7 @@ use kernel::prelude::*; const TAG_SET: u8 = 0x31; const TAG_SEQUENCE: u8 = 0x30; const TAG_UTF8: u8 = 0x0c; +const TAG_BOOLEAN: u8 = 0x01; pub(crate) const TAG_INTEGER: u8 = 0x02; pub(crate) const TAG_OCTET_STRING: u8 = 0x04; @@ -51,6 +52,8 @@ pub(crate) enum RefKeyValue<'a> { Integer(u32), Octets(&'a [u8]), Der(&'a [u8]), + /// BOOLEAN, encoded as the key store writes it (true = 0x01). + Bool(bool), } /// Canonical DER `SET OF SEQUENCE`, members sorted by key — the shape the @@ -85,6 +88,10 @@ pub(crate) fn encode_refkey_set(items: &[(&[u8], RefKeyValue<'_>)]) -> Result inner.extend_from_slice(d, GFP_KERNEL)?, + RefKeyValue::Bool(b) => { + put_header(&mut inner, TAG_BOOLEAN, 1)?; + inner.push(u8::from(*b), GFP_KERNEL)?; + } } let mut seq = KVec::new(); put_header(&mut seq, TAG_SEQUENCE, inner.len())?; @@ -158,3 +165,29 @@ pub(crate) fn octet_string_body(tlv: &[u8]) -> Option<&[u8]> { _ => None, } } + +fn integer_value(tlv: &[u8]) -> Option { + let body = match take_tlv(tlv) { + Some((TAG_INTEGER, body, [])) if !body.is_empty() && body.len() <= 5 => body, + _ => return None, + }; + // DER integers are minimal, signed, big-endian values. + if body.len() > 1 + && ((body[0] == 0 && body[1] & 0x80 == 0) || (body[0] == 0xff && body[1] & 0x80 != 0)) + { + return None; + } + let mut value = if body[0] & 0x80 != 0 { -1i64 } else { 0 }; + for byte in body { + value = (value << 8) | i64::from(*byte); + } + Some(value) +} + +pub(crate) fn integer_u32(tlv: &[u8]) -> Option { + u32::try_from(integer_value(tlv)?).ok() +} + +pub(crate) fn integer_i32(tlv: &[u8]) -> Option { + i32::try_from(integer_value(tlv)?).ok() +} diff --git a/drivers/soc/apple/key.rs b/drivers/soc/apple/key.rs new file mode 100644 index 0000000000000..38f423ad71e60 --- /dev/null +++ b/drivers/soc/apple/key.rs @@ -0,0 +1,1250 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +// Copyright 2026 Ryan Murray +//! `/dev/sep-key`: P-256 keys created inside the SEP. +//! +//! Each key is an enclave ref-key bound to this SEP and to the Linux identity +//! key bag. Its private half never leaves the enclave; userspace holds only a +//! key handle, the wrapped ref-key record behind a small versioned header. +//! A handle works on the Mac that created it and nowhere else. Signing takes a +//! 32-byte digest and returns a DER `ECDSA-Sig-Value`. +//! +//! A key made with `CREATE_BIOMETRY_CURRENT_SET` carries an access-control +//! list in its record, the one macOS gives a Secure Enclave key made with +//! `.biometryCurrentSet`: signing (and key agreement) needs a credential +//! context in which the enclave itself saw a match of one of the fingers +//! enrolled now. The kernel opens that context inside the `SIGN` call, runs a +//! Touch ID match in it, signs, and deletes it before the call returns. No +//! handle to it ever reaches userspace, and every signature needs its own +//! touch. The enclave refuses the signature for any other context; the +//! header's flag only tells the kernel to ask for the touch. +//! +//! `CREATE_BIOMETRY_ANY` is macOS's `.biometryAny`: the same list without +//! the enrolled-set hash, so any finger enrolled in this catacomb matches, +//! including fingers enrolled after the key was made. +//! +//! macOS's `.userPresence` (Touch ID or the password) and `.devicePasscode` +//! need a passcode credential the enclave checks against the user's own +//! secret. On Linux the only passcode the enclave knows today is the identity +//! key bag's secret, which root holds, so those policies would be +//! root-satisfiable. Their `CREATE` bits are reserved and refused with +//! `EOPNOTSUPP` until a per-user, password-keyed bag exists; they are never +//! emulated in software. + +use crate::der::RefKeyValue as RV; +use crate::SepData; +use kernel::ioctl::{_IOR, _IOWR}; +use kernel::prelude::*; +use kernel::sync::atomic::Relaxed; +use kernel::uaccess::{UserPtr, UserSlice}; + +pub(crate) const DEVICE_NAME: &CStr = c"sep-key"; +/// Root only by default; distributions grant a group with a udev rule. +pub(crate) const DEVICE_MODE: u16 = 0o600; + +pub(crate) const IFACE_VERSION: u32 = 2; + +pub(crate) const HANDLE_MAX: usize = 2048; +pub(crate) const PUB_LEN: usize = 65; +pub(crate) const DIGEST_LEN: usize = 32; +pub(crate) const SIG_MAX: usize = 72; + +/// `CREATE` flag: signing needs a Touch ID match of a currently enrolled +/// finger, enforced by the enclave. +pub(crate) const CREATE_BIOMETRY_CURRENT_SET: u32 = 1; +/// `CREATE` flag: signing needs a Touch ID match of any finger enrolled in +/// this catacomb, now or later, enforced by the enclave. +pub(crate) const CREATE_BIOMETRY_ANY: u32 = 2; +/// Reserved `CREATE` flags for macOS's `.userPresence` and `.devicePasscode`. +/// Refused with `EOPNOTSUPP` until the enclave can check the user's own +/// password (see the module comment). +pub(crate) const CREATE_USER_PRESENCE: u32 = 4; +pub(crate) const CREATE_DEVICE_PASSCODE: u32 = 8; +const CREATE_FLAGS: u32 = CREATE_BIOMETRY_CURRENT_SET | CREATE_BIOMETRY_ANY; +const CREATE_RESERVED: u32 = CREATE_USER_PRESENCE | CREATE_DEVICE_PASSCODE; + +/// The access policy a `CREATE` request asks for: at most one policy bit. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum Policy { + None, + BiometryCurrentSet, + BiometryAny, +} + +pub(crate) fn create_policy(flags: u32) -> Result { + if flags & !(CREATE_FLAGS | CREATE_RESERVED) != 0 || flags.count_ones() > 1 { + return Err(EINVAL); + } + match flags { + 0 => Ok(Policy::None), + CREATE_BIOMETRY_CURRENT_SET => Ok(Policy::BiometryCurrentSet), + CREATE_BIOMETRY_ANY => Ok(Policy::BiometryAny), + _ => Err(EOPNOTSUPP), + } +} + +/// A touch-gated `SIGN` asks for a finger again after a capture that saw +/// none until this long has passed; one capture itself waits up to a minute. +const TOUCH_WAIT_MS: u64 = 60_000; + +const MAGIC: u32 = 0xB1; + +/// Handle header: `"SEPK"`, version, flags, reserved u16, blob length (LE u32). +const HANDLE_MAGIC: &[u8; 4] = b"SEPK"; +const HANDLE_VERSION: u8 = 1; +/// Handle flag: the record carries a biometric access-control list. +pub(crate) const HANDLE_BIOMETRY: u8 = 0x01; +/// Handle flag, with `HANDLE_BIOMETRY` only: the list names no enrolled-set +/// hash (biometry-any). +pub(crate) const HANDLE_BIOMETRY_ANY: u8 = 0x02; +pub(crate) const HANDLE_HEADER: usize = 12; +/// The largest enclave record a handle can carry. +pub(crate) const BLOB_MAX: usize = HANDLE_MAX - HANDLE_HEADER; + +#[repr(C)] +pub(crate) struct Info { + pub(crate) version: u32, + /// `flags` bits `CREATE` accepts. + pub(crate) flags_supported: u32, + pub(crate) handle_max: u32, + pub(crate) reserved: u32, +} + +#[repr(C)] +pub(crate) struct Create { + pub(crate) flags: u32, + pub(crate) handle_len: u32, + pub(crate) public_key: [u8; PUB_LEN], + pub(crate) reserved: [u8; 3], + pub(crate) handle: [u8; HANDLE_MAX], +} + +#[repr(C)] +pub(crate) struct Sign { + pub(crate) flags: u32, + pub(crate) handle_len: u32, + pub(crate) digest: [u8; DIGEST_LEN], + pub(crate) sig_len: u32, + pub(crate) signature: [u8; SIG_MAX], + pub(crate) handle: [u8; HANDLE_MAX], +} + +#[repr(C)] +pub(crate) struct Public { + pub(crate) flags: u32, + pub(crate) handle_len: u32, + pub(crate) public_key: [u8; PUB_LEN], + pub(crate) reserved: [u8; 3], + pub(crate) handle: [u8; HANDLE_MAX], +} + +/// Test only (module parameter `key_selftest`, root): sign with a +/// biometric key under a deliberately unsuitable context. +#[repr(C)] +pub(crate) struct Selftest { + pub(crate) mode: u32, + pub(crate) handle_len: u32, + pub(crate) digest: [u8; DIGEST_LEN], + /// 0 when the enclave signed, else the negative errno. + pub(crate) result: i32, + /// The enclave's status for that signature. + pub(crate) enclave_status: i32, + /// `SELFTEST_TOUCH_THEN_DELETED`: the second signature. + pub(crate) result2: i32, + pub(crate) enclave_status2: i32, + pub(crate) handle: [u8; HANDLE_MAX], +} + +/// No credential context at all. +pub(crate) const SELFTEST_NO_CONTEXT: u32 = 1; +/// A fresh context in which nothing has matched. +pub(crate) const SELFTEST_UNSATISFIED: u32 = 2; +/// A context holding only the key bag's secret (Linux's "passcode"). +pub(crate) const SELFTEST_PASSCODE: u32 = 3; +/// A fresh context, deleted before the signature. +pub(crate) const SELFTEST_DELETED: u32 = 4; +/// One touch: sign in the matched context, delete it, sign again with it +/// (needs a finger). +pub(crate) const SELFTEST_TOUCH_THEN_DELETED: u32 = 5; +/// Research probe, no finger, ignores the handle: make a throwaway key with +/// macOS's `.userPresence` list (`cpo` "DeviceOwnerAuthentication") and sign +/// with a context holding only the key bag's secret (`result`) and with an +/// unsatisfied context (`result2`). Shows whether the enclave enforces that +/// policy and whether a root-held secret satisfies it; `CREATE` refuses the +/// policy either way. +pub(crate) const SELFTEST_PRESENCE_PROBE: u32 = 6; +/// Research probe, no finger: the `.userPresence` throwaway key (identity +/// bag) signed with a context whose passcode credential came from another +/// user's bag. In: `enclave_status` = that user's uid, the handle buffer = +/// that user's bag password (`handle_len` bytes). Out: `result2` = the +/// verify-secret status, `result`/`enclave_status` = the signature. Shows +/// whether a credential from one bag satisfies a key under another. +pub(crate) const SELFTEST_PRESENCE_OTHER_BAG: u32 = 7; +/// Research probe, no finger: a throwaway `.userPresence` key made under +/// the (unlocked) bag of the user in `enclave_status` (outer handle -uid). +/// `result`: signed with a context holding a passcode credential from that +/// same bag (the password in the handle buffer). `result2`: signed with a +/// fresh, unsatisfied context. Statuses in `enclave_status`/`2`. +pub(crate) const SELFTEST_USERBAG_KEY_SAME_BAG: u32 = 8; +/// As 8, but `result` signs with the Linux identity bag's secret as the +/// credential (what root holds) and `result2` with the handle buffer's +/// password used as given (pass a wrong one). +pub(crate) const SELFTEST_USERBAG_KEY_ROOT: u32 = 9; +/// Research probe, no finger (T2: a locked bag refuses without its +/// password): a throwaway `.userPresence` key under the unlocked bag of the +/// uid in `enclave_status`, password in the handle buffer. The kernel signs +/// (r0) with a context holding that bag's password credential, locks the bag +/// for good (screen lock, grace expired now), then signs with the same, now +/// stale, context (r1), with no credential (r2) and with a fresh password +/// credential (r3), unlocks the bag with the password and signs with a fresh +/// credential again (r4). The digest buffer returns, as LE i32: r0..r4 (0 = +/// signed, else the enclave status), the lock phase before the lock, after +/// it and after the unlock. +pub(crate) const SELFTEST_USERBAG_LOCKED: u32 = 10; +/// Research: create a throwaway `.userPresence` key under the unlocked bag +/// of the uid in `enclave_status` and return its record in the handle +/// buffer (`handle_len`), so a later boot can use it after the bag is +/// imported again. +pub(crate) const SELFTEST_USERBAG_KEY_MAKE: u32 = 11; +/// Research: sign the record in the handle buffer under the bag of the uid +/// in `enclave_status`, with a password credential from that bag; the +/// password is the NUL-padded `digest` (at most 32 bytes). `result` = the +/// signature, `result2` = the verify-secret status. +pub(crate) const SELFTEST_USERBAG_KEY_USE: u32 = 12; + +static_assert!(core::mem::size_of::() == 16); +static_assert!(core::mem::size_of::() == 2124); +static_assert!(core::mem::size_of::() == 2164); +static_assert!(core::mem::size_of::() == 2124); +static_assert!(core::mem::size_of::() == 2104); + +// SAFETY: `repr(C)` with no padding (the asserts above), so each value is a +// faithful byte image and every bit pattern is a valid value. +unsafe impl kernel::transmute::AsBytes for Info {} +// SAFETY: as above. +unsafe impl kernel::transmute::AsBytes for Create {} +// SAFETY: as above. +unsafe impl kernel::transmute::FromBytes for Create {} +// SAFETY: as above. +unsafe impl kernel::transmute::AsBytes for Sign {} +// SAFETY: as above. +unsafe impl kernel::transmute::FromBytes for Sign {} +// SAFETY: as above. +unsafe impl kernel::transmute::AsBytes for Public {} +// SAFETY: as above. +unsafe impl kernel::transmute::FromBytes for Public {} +// SAFETY: as above. +unsafe impl kernel::transmute::AsBytes for Selftest {} +// SAFETY: as above. +unsafe impl kernel::transmute::FromBytes for Selftest {} + +pub(crate) const IOC_INFO: u32 = _IOR::(MAGIC, 0x20); +pub(crate) const IOC_CREATE: u32 = _IOWR::(MAGIC, 0x21); +pub(crate) const IOC_SIGN: u32 = _IOWR::(MAGIC, 0x22); +pub(crate) const IOC_PUBLIC: u32 = _IOWR::(MAGIC, 0x23); +pub(crate) const IOC_SELFTEST: u32 = _IOWR::(MAGIC, 0x24); + +pub(crate) fn info() -> Info { + Info { + version: IFACE_VERSION, + flags_supported: CREATE_FLAGS, + handle_max: HANDLE_MAX as u32, + reserved: 0, + } +} + +/// No flags, biometry (current set), or biometry plus biometry-any. +fn handle_flags_valid(flags: u8) -> bool { + matches!(flags, 0 | HANDLE_BIOMETRY | 0x03) +} + +/// Writes the handle for `blob` with `flags` into `out`; returns its length. +pub(crate) fn encode_handle(blob: &[u8], flags: u8, out: &mut [u8; HANDLE_MAX]) -> Result { + if blob.is_empty() || blob.len() > BLOB_MAX || !handle_flags_valid(flags) { + return Err(EINVAL); + } + let len = u32::try_from(blob.len()).map_err(|_| EINVAL)?; + out.fill(0); + out[..4].copy_from_slice(HANDLE_MAGIC); + out[4] = HANDLE_VERSION; + out[5] = flags; + out[8..12].copy_from_slice(&len.to_le_bytes()); + out[HANDLE_HEADER..HANDLE_HEADER + blob.len()].copy_from_slice(blob); + Ok(HANDLE_HEADER + blob.len()) +} + +/// The enclave record inside the first `len` bytes of `handle`, and the +/// handle's flags. Refuses an unknown version or flag, a non-zero reserved +/// field or a length that does not match exactly. The flags are not +/// authenticated; the enclave enforces the record's own list either way. +pub(crate) fn decode_handle(handle: &[u8; HANDLE_MAX], len: u32) -> Result<(&[u8], u8)> { + let len = len as usize; + if !(HANDLE_HEADER + 1..=HANDLE_MAX).contains(&len) { + return Err(EINVAL); + } + if &handle[..4] != HANDLE_MAGIC || handle[4] != HANDLE_VERSION || !handle_flags_valid(handle[5]) { + return Err(EINVAL); + } + if handle[6] != 0 || handle[7] != 0 { + return Err(EINVAL); + } + let blob_len = u32::from_le_bytes([handle[8], handle[9], handle[10], handle[11]]) as usize; + if blob_len != len - HANDLE_HEADER { + return Err(EINVAL); + } + Ok((&handle[HANDLE_HEADER..len], handle[5])) +} + +/// Copies a DER signature into the reply; refuses one that does not fit. +pub(crate) fn put_signature(sig: &[u8], out: &mut Sign) -> Result<()> { + if sig.is_empty() || sig.len() > SIG_MAX { + return Err(EIO); + } + out.signature.fill(0); + out.signature[..sig.len()].copy_from_slice(sig); + out.sig_len = sig.len() as u32; + Ok(()) +} + +/// macOS's `.userPresence` list: every constrained operation names the +/// `DeviceOwnerAuthentication` policy (Touch ID or the password). Used only +/// by `SELFTEST_PRESENCE_PROBE`. +pub(crate) fn presence_acl() -> Result> { + let constraint = + crate::der::encode_refkey_set(&[(b"cpo".as_slice(), RV::Utf8(b"DeviceOwnerAuthentication"))])?; + let acl = crate::der::encode_refkey_set(&[ + (b"oa".as_slice(), RV::Bool(true)), + (b"ock".as_slice(), RV::Der(&constraint)), + (b"odel".as_slice(), RV::Bool(true)), + (b"okd".as_slice(), RV::Der(&constraint)), + (b"osgn".as_slice(), RV::Der(&constraint)), + ])?; + crate::der::encode_refkey_set(&[(b"acl".as_slice(), RV::Der(&acl))]) +} + +/// The access-control list of a biometry-current-set key, in the key +/// store's DER, and the constraint its signing operation carries. +/// +/// As macOS writes it for `.privateKeyUsage | .biometryCurrentSet`: sign, +/// key agreement (`ock`) and derivation (`okd`) need a biometric credential +/// (`cbio`) for this catacomb (`pbioc`) and enrolled set (`pbioh`); delete +/// (`odel`) and attest (`oa`) are free. With no `enrolled_hash` it is the +/// `.biometryAny` list: `cbio` names the catacomb only. +pub(crate) struct BioAcl { + pub(crate) ed: KVec, + pub(crate) constraint: KVec, +} + +pub(crate) fn bio_acl(catacomb: &[u8; 16], enrolled_hash: Option<&[u8; 32]>) -> Result { + let cbio = match enrolled_hash { + Some(hash) => crate::der::encode_refkey_set(&[ + (b"pbioc".as_slice(), RV::Octets(catacomb)), + (b"pbioh".as_slice(), RV::Octets(hash)), + ])?, + None => crate::der::encode_refkey_set(&[(b"pbioc".as_slice(), RV::Octets(catacomb))])?, + }; + let constraint = crate::der::encode_refkey_set(&[(b"cbio".as_slice(), RV::Der(&cbio))])?; + let acl = crate::der::encode_refkey_set(&[ + (b"oa".as_slice(), RV::Bool(true)), + (b"ock".as_slice(), RV::Der(&constraint)), + (b"odel".as_slice(), RV::Bool(true)), + (b"okd".as_slice(), RV::Der(&constraint)), + (b"osgn".as_slice(), RV::Der(&constraint)), + ])?; + let ed = crate::der::encode_refkey_set(&[(b"acl".as_slice(), RV::Der(&acl))])?; + Ok(BioAcl { ed, constraint }) +} + +/// What one verify run of a touch-gated signature ended with. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum TouchOutcome { + Matched, + NoMatch, + Failed(u32), +} + +/// The touch-gated signature a `SIGN` call is waiting for. Lives only for +/// that call; the verify it runs fills it in. +pub(crate) struct KeyTouch { + blob: KVec, + digest: [u8; DIGEST_LEN], + constraint: KVec, + sign_again_after_delete: bool, + outcome: Option, + sig: Option>>, + status: i32, + again: Option>>, + again_status: i32, +} + +impl KeyTouch { + pub(crate) fn set_outcome(&mut self, outcome: &crate::bio::VerifyOutcome) { + self.outcome = Some(match outcome { + crate::bio::VerifyOutcome::Matched(_) => TouchOutcome::Matched, + crate::bio::VerifyOutcome::NoMatch => TouchOutcome::NoMatch, + crate::bio::VerifyOutcome::Failed(s) => TouchOutcome::Failed(*s), + }); + } +} + +fn errno_of(r: &Option>) -> i32 { + match r { + Some(Ok(_)) => 0, + Some(Err(e)) => e.to_errno(), + None => EIO.to_errno(), + } +} + +#[::kernel::macros::kunit_tests(apple_sep_key_handle)] +mod tests { + use super::*; + + fn blob() -> [u8; 100] { + let mut b = [0u8; 100]; + for (i, v) in b.iter_mut().enumerate() { + *v = i as u8 ^ 0x5a; + } + b + } + + #[test] + fn round_trip() { + let mut h = [0u8; HANDLE_MAX]; + let n = encode_handle(&blob(), 0, &mut h).unwrap(); + assert_eq!(n, HANDLE_HEADER + 100); + assert_eq!(&h[..4], b"SEPK"); + assert_eq!(decode_handle(&h, n as u32).unwrap(), (&blob()[..], 0)); + } + + #[test] + fn length_must_match_exactly() { + let mut h = [0u8; HANDLE_MAX]; + let n = encode_handle(&blob(), 0, &mut h).unwrap() as u32; + assert!(decode_handle(&h, n - 1).is_err()); + assert!(decode_handle(&h, n + 1).is_err()); + assert!(decode_handle(&h, 0).is_err()); + assert!(decode_handle(&h, HANDLE_MAX as u32 + 1).is_err()); + } + + #[test] + fn header_fields_are_checked() { + let mut h = [0u8; HANDLE_MAX]; + let n = encode_handle(&blob(), 0, &mut h).unwrap() as u32; + for at in [0usize, 4, 6, 7] { + let mut bad = h; + bad[at] ^= 0x01; + assert!(decode_handle(&bad, n).is_err()); + } + } + + #[test] + fn empty_and_oversize_blobs_are_refused() { + let mut h = [0u8; HANDLE_MAX]; + assert!(encode_handle(&[], 0, &mut h).is_err()); + let big = [1u8; BLOB_MAX + 1]; + assert!(encode_handle(&big, 0, &mut h).is_err()); + let max = [1u8; BLOB_MAX]; + assert_eq!(encode_handle(&max, 0, &mut h).unwrap(), HANDLE_MAX); + } + + #[test] + fn signature_bounds() { + let mut s = Sign { + flags: 0, + handle_len: 0, + digest: [0; DIGEST_LEN], + sig_len: 0, + signature: [0; SIG_MAX], + handle: [0; HANDLE_MAX], + }; + assert!(put_signature(&[], &mut s).is_err()); + assert!(put_signature(&[0x30; SIG_MAX + 1], &mut s).is_err()); + put_signature(&[0x30, 0x02, 0x01, 0x01], &mut s).unwrap(); + assert_eq!(s.sig_len, 4); + } + + #[test] + fn biometry_flag_round_trips_and_unknown_flags_are_refused() { + let mut h = [0u8; HANDLE_MAX]; + let n = encode_handle(&blob(), HANDLE_BIOMETRY, &mut h).unwrap() as u32; + assert_eq!(decode_handle(&h, n).unwrap().1, HANDLE_BIOMETRY); + assert!(encode_handle(&blob(), 0x02, &mut h).is_err()); + assert!(encode_handle(&blob(), 0x04, &mut h).is_err()); + let mut bad = h; + bad[5] = 0x02; + assert!(decode_handle(&bad, n).is_err()); + let n = encode_handle(&blob(), HANDLE_BIOMETRY | HANDLE_BIOMETRY_ANY, &mut h).unwrap() as u32; + assert_eq!(decode_handle(&h, n).unwrap().1, HANDLE_BIOMETRY | HANDLE_BIOMETRY_ANY); + } + + #[test] + fn create_takes_one_policy_and_refuses_the_password_ones() { + assert_eq!(create_policy(0).unwrap(), Policy::None); + assert_eq!(create_policy(CREATE_BIOMETRY_CURRENT_SET).unwrap(), Policy::BiometryCurrentSet); + assert_eq!(create_policy(CREATE_BIOMETRY_ANY).unwrap(), Policy::BiometryAny); + assert_eq!(create_policy(CREATE_USER_PRESENCE).unwrap_err(), EOPNOTSUPP); + assert_eq!(create_policy(CREATE_DEVICE_PASSCODE).unwrap_err(), EOPNOTSUPP); + assert_eq!(create_policy(CREATE_BIOMETRY_CURRENT_SET | CREATE_BIOMETRY_ANY).unwrap_err(), EINVAL); + assert_eq!(create_policy(0x10).unwrap_err(), EINVAL); + assert_eq!(info().flags_supported, CREATE_BIOMETRY_CURRENT_SET | CREATE_BIOMETRY_ANY); + } + + #[test] + fn biometry_any_list_names_the_catacomb_only() { + let acl = bio_acl(&[0x11; 16], None).unwrap(); + // SET { SEQ { "cbio", SET { SEQ { "pbioc", OCTETS[16] } } } } + assert_eq!(acl.constraint.len(), 2 + 2 + 6 + 2 + (2 + 7 + 18)); + assert!(!acl.ed.windows(5).any(|w| w == b"pbioh")); + assert!(acl.ed.windows(5).any(|w| w == b"pbioc")); + } + + #[test] + fn presence_list_has_the_captured_shape() { + let ed = presence_acl().unwrap(); + // macOS: each constrained op is SET(34) { SEQ { "cpo", "DeviceOwnerAuthentication" } }. + assert_eq!(ed.windows(2).filter(|w| *w == [0x31u8, 34].as_slice()).count(), 3); + assert_eq!(ed.windows(25).filter(|w| *w == b"DeviceOwnerAuthentication").count(), 3); + } + + #[test] + fn biometric_acl_has_the_key_store_shape() { + let acl = bio_acl(&[0x11; 16], Some(&[0x22; 32])).unwrap(); + // SET { SEQ { "cbio", SET { SEQ { "pbioc", OCTETS[16] }, SEQ { "pbioh", OCTETS[32] } } } } + assert_eq!(acl.constraint.len(), 2 + 2 + 6 + 2 + (2 + 7 + 18) + (2 + 7 + 34)); + assert_eq!(&acl.constraint[..2], &[0x31, 0x50]); + assert_eq!(&acl.constraint[4..10], b"\x0c\x04cbio"); + let ed = &acl.ed; + assert_eq!(ed[0], 0x31); + // The list names all five operations; delete and attest are free. + for op in [b"oa".as_slice(), b"ock", b"odel", b"okd", b"osgn"] { + assert!(ed.windows(op.len() + 2).any(|w| w[0] == 0x0c && w[1] as usize == op.len() && &w[2..] == op)); + } + assert!(ed.windows(3).any(|w| w == [0x01u8, 0x01, 0x01].as_slice())); + } + + #[test] + fn acl_constraint_command_has_the_captured_layout() { + let acl = bio_acl(&[0x11; 16], Some(&[0x22; 32])).unwrap(); + let cmd = crate::scrd::scrd_verify_acl_constraint(&[0x33; 16], &acl.constraint, b"ock").unwrap(); + // macOS: 138 bytes for a 94-byte constraint and "ock"; this one is 80 bytes. + assert_eq!(cmd.len(), 8 + 16 + 4 + acl.constraint.len() + 4 + 3 + 1 + 8); + assert_eq!(&cmd[..8], b"DRCS\x20\x00\x00\x02"); + assert_eq!(&cmd[24..28], &(acl.constraint.len() as u32).to_le_bytes()); + } +} + +impl SepData { + pub(crate) fn register_key(&self) -> Result<()> { + let mut guard = self.key_dev.lock(); + if guard.is_some() { + return Ok(()); + } + let ctx = core::ptr::from_ref(self).cast_mut().cast::(); + // SAFETY: as in `register_bio`: `remove()` drops the registration + // before the `Arc` that keeps `ctx` alive, and the shim's unregister + // clears the stored context under the lock every callback takes. + let dev = unsafe { + crate::shim::BioChardev::register( + DEVICE_NAME, + DEVICE_MODE, + ctx, + key_open_trampoline, + key_release_trampoline, + key_ioctl_trampoline, + key_ready_trampoline, + ) + }?; + *guard = Some(dev); + Ok(()) + } + + /// Whether the capture a verify is waiting on is still wanted: the + /// touch-gated signature that owns it, or /dev/sep-bio's live session. + pub(crate) fn capture_live(&self) -> bool { + (self.key_touch_active.load(Relaxed) && !self.shutting_down.load(Relaxed)) + || crate::bio::capture_is_live(&self.bio_session.lock(), self.capture_epoch.load(Relaxed)) + } + + fn key_bio_acl(&self, any: bool) -> Result { + if !self.touchid_started.load(Relaxed) { + return Err(ENODEV); + } + let (catacomb, enrolled) = self.catacomb_binding()?; + bio_acl(&catacomb, (!any).then_some(&enrolled)) + } + + /// Evaluates the signing constraint on the fresh context before the + /// match, as LocalAuthentication does. Best effort: logged only. + pub(crate) fn key_touch_preflight(&self, evaluation: &[u8; crate::scrd::SCRD_ACM_HANDLE_LEN]) { + let constraint = { + let guard = self.key_touch.lock(); + let Some(job) = guard.as_ref() else { + return; + }; + let mut c = KVec::new(); + if c.extend_from_slice(&job.constraint, GFP_KERNEL).is_err() { + return; + } + c + }; + let Ok(payload) = crate::scrd::scrd_verify_acl_constraint(evaluation, &constraint, b"osgn") else { + return; + }; + match self.scrd_transact( + c"SCRD_VERIFY_ACL_CONSTRAINT", + crate::scrd::SCRD_REQUEST_COMMAND, + &payload, + crate::SCRD_TIMEOUT_MS, + ) { + Some((reply, out)) => { + let word = |at: usize| { + out.get(at..at + 4) + .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]])) + }; + dev_info!( + self.dev, + "sep-key: signing constraint on the new context: status {}, {} reply bytes, satisfied {:?}, first requirement {:?}\n", + reply.status, + out.len(), + word(0), + word(4) + ); + } + None => dev_info!(self.dev, "sep-key: signing constraint check got no answer\n"), + } + } + + /// Signs the waiting job in `evaluation`, the context the match just + /// satisfied. Called from the verify, at most once with success. + pub(crate) fn key_touch_sign(&self, evaluation: &[u8; crate::scrd::SCRD_ACM_HANDLE_LEN], when: &CStr) { + let (blob, digest) = { + let guard = self.key_touch.lock(); + let Some(job) = guard.as_ref() else { + return; + }; + if matches!(job.sig, Some(Ok(_))) { + return; + } + let mut b = KVec::new(); + if b.extend_from_slice(&job.blob, GFP_KERNEL).is_err() { + return; + } + (b, job.digest) + }; + let r = self.user_key_sign(&blob, &digest, Some(evaluation)); + let status = self.last_key_status.load(Relaxed) as i32; + dev_info!( + self.dev, + "sep-key: touch-gated signature {}: {} (enclave status {})\n", + when, + (if r.is_ok() { "signed" } else { "refused" }), + status + ); + if let Some(job) = &mut *self.key_touch.lock() { + job.sig = Some(r); + job.status = status; + } + } + + /// Self-test only: after the matched context is deleted, sign once more + /// with its old handle. + pub(crate) fn key_touch_after_close(&self, evaluation: &[u8; crate::scrd::SCRD_ACM_HANDLE_LEN]) { + let (blob, digest) = { + let guard = self.key_touch.lock(); + let Some(job) = guard.as_ref() else { + return; + }; + if !job.sign_again_after_delete || !matches!(job.sig, Some(Ok(_))) { + return; + } + let mut b = KVec::new(); + if b.extend_from_slice(&job.blob, GFP_KERNEL).is_err() { + return; + } + (b, job.digest) + }; + let r = self.user_key_sign(&blob, &digest, Some(evaluation)); + let status = self.last_key_status.load(Relaxed) as i32; + dev_info!( + self.dev, + "sep-key: self-test: signature with the deleted matched context: {} (enclave status {})\n", + (if r.is_ok() { "SIGNED" } else { "refused" }), + status + ); + if let Some(job) = &mut *self.key_touch.lock() { + job.again = Some(r); + job.again_status = status; + } + } + + /// A touch-gated signature: one fresh credential context, one Touch ID + /// match in it, the signature, and the context deleted, all inside this + /// call. Waits about a minute for a finger. + fn key_sign_with_touch(&self, blob: &[u8], flags: u8, digest: &[u8; DIGEST_LEN], again: bool) -> Result { + let acl = self.key_bio_acl(flags & HANDLE_BIOMETRY_ANY != 0)?; + let mut owned = KVec::new(); + owned.extend_from_slice(blob, GFP_KERNEL)?; + // Serialises with /dev/sep-bio's enrol and verify work, as they do + // with each other. + let _writer = self.catacomb_writer.lock(); + *self.key_touch.lock() = Some(KeyTouch { + blob: owned, + digest: *digest, + constraint: acl.constraint, + sign_again_after_delete: again, + outcome: None, + sig: None, + status: 0, + again: None, + again_status: 0, + }); + self.key_touch_active.store(true, Relaxed); + dev_info!(self.dev, "sep-key: waiting for a Touch ID match to sign\n"); + let deadline = crate::shim::boottime_ns().saturating_add(TOUCH_WAIT_MS * 1_000_000); + let ended = loop { + if self.shutting_down.load(Relaxed) { + break Err(ENODEV); + } + if !self.capture_begin() { + break Err(EAGAIN); + } + self.run_verify(); + self.capture_end(); + let outcome = match &mut *self.key_touch.lock() { + Some(job) => job.outcome.take(), + None => None, + }; + match outcome { + Some(TouchOutcome::Matched) => break Ok(()), + Some(TouchOutcome::NoMatch) => break Err(EACCES), + Some(TouchOutcome::Failed(s)) if s == crate::ENROL_STATUS_RETRY => { + if crate::shim::boottime_ns() >= deadline { + break Err(ETIMEDOUT); + } + } + Some(TouchOutcome::Failed(s)) if s == crate::ENROL_STATUS_LOCKOUT => break Err(EACCES), + Some(TouchOutcome::Failed(_)) | None => break Err(EIO), + } + }; + self.key_touch_active.store(false, Relaxed); + let job = self.key_touch.lock().take().ok_or(EIO)?; + dev_info!( + self.dev, + "sep-key: touch-gated signature ended: {:?}, signature {}\n", + ended, + errno_of(&job.sig) + ); + ended?; + Ok(job) + } + + /// A credential context holding only the identity key bag's secret, the + /// Linux stand-in for a passcode (self-test). + fn passcode_context(&self) -> Result<[u8; crate::scrd::SCRD_ACM_HANDLE_LEN]> { + let du = crate::sks::DesignateUser::new(crate::SBIO_PROBE_USER_ID).ok_or(EINVAL)?; + let crate::keybag::State::Present(stored) = crate::keybag::read(crate::keybag::Slot::Identity)? else { + return Err(ENODEV); + }; + let (status, _) = self.scrd_command(&crate::scrd::scrd_initialize()).ok_or(EIO)?; + if status != 0 { + return Err(EIO); + } + let (status, out) = self + .scrd_command(&crate::scrd::scrd_context_create_tracked(crate::SBIO_PROBE_USER_ID)) + .ok_or(EIO)?; + let handle = crate::scrd::tracked_create_reference(&out) + .filter(|_| status == 0) + .ok_or(EIO)?; + let ok = (|| -> Option<()> { + let (status, _) = self.scrd_command(&crate::scrd::scrd_context_externalize(&handle))?; + if status != 0 { + return None; + } + let out = self.sks_send(self.sks_req_verify_secret(du.special_handle(), stored.secret(), &handle))?; + dev_info!(self.dev, "sep-key: self-test: passcode credential added, status {}\n", out.reply.status); + (out.reply.status == 0).then_some(()) + })(); + if ok.is_none() { + self.close_unlock_evaluation(&handle); + return Err(EIO); + } + Ok(handle) + } + + /// A fresh tracked context for `uid`; with `secret`, a passcode + /// credential from `bag`'s verify-secret is added. Returns the context + /// and the verify status (0 when none was asked). + fn key_context_for( + &self, + uid: i32, + cred: Option<(crate::sks::SpecialHandle, &[u8])>, + ) -> Result<([u8; crate::scrd::SCRD_ACM_HANDLE_LEN], i32)> { + let (status, _) = self.scrd_command(&crate::scrd::scrd_initialize()).ok_or(EIO)?; + if status != 0 { + return Err(EIO); + } + let (status, out) = self.scrd_command(&crate::scrd::scrd_context_create_tracked(uid)).ok_or(EIO)?; + let ctx = crate::scrd::tracked_create_reference(&out).filter(|_| status == 0).ok_or(EIO)?; + let (status, _) = self.scrd_command(&crate::scrd::scrd_context_externalize(&ctx)).ok_or(EIO)?; + if status != 0 { + self.close_unlock_evaluation(&ctx); + return Err(EIO); + } + let mut verify = 0; + if let Some((bag, secret)) = cred { + verify = match self.sks_send(self.sks_req_verify_secret(bag, secret, &ctx)) { + Some(out) => out.reply.status as i32, + None => -(EIO.to_errno()), + }; + } + Ok((ctx, verify)) + } + + fn key_userbag_record(&self, req: &mut Selftest) -> Result<()> { + let uid = req.enclave_status; + if uid < crate::sks::SKS_DESIGNATE_USER_MIN || uid == crate::SBIO_PROBE_USER_ID { + return Err(EINVAL); + } + let user = crate::sks::DesignateUser::new(uid).ok_or(EINVAL)?; + if req.mode == SELFTEST_USERBAG_KEY_MAKE { + let ed = presence_acl()?; + let blob = self.user_key_create_in_bag(user, Some(&ed))?; + if blob.len() > HANDLE_MAX { + return Err(E2BIG); + } + req.handle.fill(0); + req.handle[..blob.len()].copy_from_slice(&blob); + req.handle_len = blob.len() as u32; + req.result = 0; + dev_info!(self.dev, "sep-key: self-test 11: key under uid {}'s bag, record {} bytes\n", uid, blob.len()); + return Ok(()); + } + let len = req.handle_len as usize; + if len == 0 || len > HANDLE_MAX { + return Err(EINVAL); + } + let mut pw = req.digest; + req.digest.fill(0); + let plen = pw.iter().position(|&b| b == 0).unwrap_or(pw.len()); + if plen == 0 { + return Err(EINVAL); + } + let mut record = KVec::new(); + record.extend_from_slice(&req.handle[..len], GFP_KERNEL)?; + let made = self.key_context_for(uid, Some((user.special_handle(), &pw[..plen]))); + pw.fill(0); + let (ctx, v) = made?; + req.result2 = v; + self.last_key_status.store(0, Relaxed); + let r = self.user_key_sign_in_bag(user, &record, &[0x5a; DIGEST_LEN], Some(&ctx)); + self.close_unlock_evaluation(&ctx); + req.result = r.err().map(|e| e.to_errno()).unwrap_or(0); + req.enclave_status = self.last_key_status.load(Relaxed) as i32; + dev_info!( + self.dev, + "sep-key: self-test 12: saved key under uid {}'s bag: verify {} -> {} ({})\n", + uid, + v, + (if req.result == 0 { "SIGNED" } else { "refused" }), + req.enclave_status + ); + Ok(()) + } + + fn key_userbag_locked(&self, req: &mut Selftest) -> Result<()> { + let uid = req.enclave_status; + let len = req.handle_len as usize; + if len == 0 || len > 256 || uid < crate::sks::SKS_DESIGNATE_USER_MIN || uid == crate::SBIO_PROBE_USER_ID { + return Err(EINVAL); + } + let mut pw = [0u8; 256]; + pw[..len].copy_from_slice(&req.handle[..len]); + req.handle.fill(0); + let user = crate::sks::DesignateUser::new(uid).ok_or(EINVAL)?; + let out = (|| -> Result<[i32; 8]> { + let ed = presence_acl()?; + let blob = self.user_key_create_in_bag(user, Some(&ed))?; + let digest = [0x5au8; DIGEST_LEN]; + let sign = |ctx: Option<&[u8; crate::scrd::SCRD_ACM_HANDLE_LEN]>| -> i32 { + self.last_key_status.store(0, Relaxed); + match self.user_key_sign_in_bag(user, &blob, &digest, ctx) { + Ok(_) => 0, + Err(_) => match self.last_key_status.load(Relaxed) as i32 { + 0 => -(EIO.to_errno()), + s => s, + }, + } + }; + let mut r = [0i32; 8]; + r[5] = self.userbag_lock_phase(user).map(|p| p as i32).unwrap_or(-1); + let (stale, _) = self.key_context_for(uid, Some((user.special_handle(), &pw[..len])))?; + r[0] = sign(Some(&stale)); + // Lock for good: the screen lock, then grace-timer fires (as the + // Mac's kernel sends them) until the store locks the bag, for at + // most LOCK_WAIT_S. The store sets the grace period itself. + const LOCK_WAIT_S: u64 = 330; + self.userbag_lock(user, true, false)?; + let start = crate::shim::boottime_ns(); + let mut locked = false; + while crate::shim::boottime_ns() - start < LOCK_WAIT_S * 1_000_000_000 { + kernel::time::delay::fsleep(kernel::time::Delta::from_millis(5000)); + match self.userbag_timer_fire(user) { + Ok(true) => { + locked = true; + break; + } + Ok(false) => {} + Err(e) => { + dev_info!(self.dev, "sep-key: self-test 10: timer fire returned {:?}\n", e); + break; + } + } + } + dev_info!( + self.dev, + "sep-key: self-test 10: bag locked by the store: {} after {} s\n", + locked, + (crate::shim::boottime_ns() - start) / 1_000_000_000 + ); + r[6] = self.userbag_lock_phase(user).map(|p| p as i32).unwrap_or(-1); + r[1] = sign(Some(&stale)); + self.close_unlock_evaluation(&stale); + r[2] = sign(None); + let (fresh, v) = self.key_context_for(uid, Some((user.special_handle(), &pw[..len])))?; + r[3] = if v == 0 { sign(Some(&fresh)) } else { v }; + self.close_unlock_evaluation(&fresh); + self.userbag_unlock(user, &pw[..len])?; + r[7] = self.userbag_lock_phase(user).map(|p| p as i32).unwrap_or(-1); + let (fresh2, v2) = self.key_context_for(uid, Some((user.special_handle(), &pw[..len])))?; + r[4] = if v2 == 0 { sign(Some(&fresh2)) } else { v2 }; + self.close_unlock_evaluation(&fresh2); + Ok(r) + })(); + pw.fill(0); + let r = out?; + for (i, v) in r.iter().enumerate() { + req.digest[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes()); + } + req.result = 0; + dev_info!( + self.dev, + "sep-key: self-test 10, uid {}'s bag: phase {} -> sign {}; locked (phase {}): stale credential {}, none {}, fresh password credential {}; unlocked (phase {}): {}\n", + uid, r[5], r[0], r[6], r[1], r[2], r[3], r[7], r[4] + ); + Ok(()) + } + + fn key_userbag_probe(&self, req: &mut Selftest) -> Result<()> { + let uid = req.enclave_status; + let len = req.handle_len as usize; + if len == 0 || len > 256 || uid < crate::sks::SKS_DESIGNATE_USER_MIN || uid == crate::SBIO_PROBE_USER_ID { + return Err(EINVAL); + } + let mut pw = [0u8; 256]; + pw[..len].copy_from_slice(&req.handle[..len]); + req.handle.fill(0); + let user = crate::sks::DesignateUser::new(uid).ok_or(EINVAL)?; + let ed = presence_acl()?; + let made = self.user_key_create_in_bag(user, Some(&ed)); + let blob = match made { + Ok(b) => b, + Err(e) => { + pw.fill(0); + dev_info!(self.dev, "sep-key: self-test: key under uid {}'s bag not created ({:?})\n", uid, e); + return Err(e); + } + }; + let digest = req.digest; + let sign = |ctx: &[u8; crate::scrd::SCRD_ACM_HANDLE_LEN]| -> (i32, i32) { + self.last_key_status.store(0, Relaxed); + let r = self.user_key_sign_in_bag(user, &blob, &digest, Some(ctx)); + (r.err().map(|e| e.to_errno()).unwrap_or(0), self.last_key_status.load(Relaxed) as i32) + }; + let first = if req.mode == SELFTEST_USERBAG_KEY_SAME_BAG { + self.key_context_for(uid, Some((user.special_handle(), &pw[..len]))) + } else { + let du = crate::sks::DesignateUser::new(crate::SBIO_PROBE_USER_ID).ok_or(EINVAL)?; + match crate::keybag::read(crate::keybag::Slot::Identity) { + Ok(crate::keybag::State::Present(stored)) => { + self.key_context_for(crate::SBIO_PROBE_USER_ID, Some((du.special_handle(), stored.secret()))) + } + _ => Err(ENODEV), + } + }; + let (ctx, v1) = match first { + Ok(c) => c, + Err(e) => { + pw.fill(0); + return Err(e); + } + }; + (req.result, req.enclave_status) = sign(&ctx); + self.close_unlock_evaluation(&ctx); + let second = if req.mode == SELFTEST_USERBAG_KEY_SAME_BAG { + self.key_context_for(uid, None) + } else { + self.key_context_for(uid, Some((user.special_handle(), &pw[..len]))) + }; + pw.fill(0); + let (ctx2, v2) = second?; + (req.result2, req.enclave_status2) = sign(&ctx2); + self.close_unlock_evaluation(&ctx2); + dev_info!( + self.dev, + "sep-key: self-test {}: key under uid {}'s bag: first ({}) verify {} -> {} ({}); second ({}) verify {} -> {} ({})\n", + req.mode, + uid, + (if req.mode == SELFTEST_USERBAG_KEY_SAME_BAG { "same-bag password" } else { "identity-bag secret" }), + v1, + (if req.result == 0 { "SIGNED" } else { "refused" }), + req.enclave_status, + (if req.mode == SELFTEST_USERBAG_KEY_SAME_BAG { "unsatisfied" } else { "given password" }), + v2, + (if req.result2 == 0 { "SIGNED" } else { "refused" }), + req.enclave_status2 + ); + Ok(()) + } + + fn key_presence_other_bag(&self, req: &mut Selftest) -> Result<()> { + let uid = req.enclave_status; + let len = req.handle_len as usize; + if len == 0 || len > 256 || uid < crate::sks::SKS_DESIGNATE_USER_MIN || uid == crate::SBIO_PROBE_USER_ID { + return Err(EINVAL); + } + let du = crate::sks::DesignateUser::new(uid).ok_or(EINVAL)?; + let ed = presence_acl()?; + let (blob, _) = self.user_key_create(Some(&ed))?; + let digest = req.digest; + let (status, _) = self.scrd_command(&crate::scrd::scrd_initialize()).ok_or(EIO)?; + if status != 0 { + return Err(EIO); + } + let (status, out) = self.scrd_command(&crate::scrd::scrd_context_create_tracked(uid)).ok_or(EIO)?; + let ctx = crate::scrd::tracked_create_reference(&out).filter(|_| status == 0).ok_or(EIO)?; + let verify = (|| -> Option { + let (status, _) = self.scrd_command(&crate::scrd::scrd_context_externalize(&ctx))?; + if status != 0 { + return None; + } + let out = self.sks_send(self.sks_req_verify_secret(du.special_handle(), &req.handle[..len], &ctx))?; + Some(out.reply.status as i32) + })(); + req.handle.fill(0); + req.result2 = verify.unwrap_or(-(EIO.to_errno())); + self.last_key_status.store(0, Relaxed); + let r = self.user_key_sign(&blob, &digest, Some(&ctx)); + req.result = r.err().map(|e| e.to_errno()).unwrap_or(0); + req.enclave_status = self.last_key_status.load(Relaxed) as i32; + self.close_unlock_evaluation(&ctx); + dev_info!( + self.dev, + "sep-key: self-test presence key with a credential from uid {}'s bag: verify status {}, signature {} (enclave status {})\n", + uid, + req.result2, + (if req.result == 0 { "SIGNED" } else { "refused" }), + req.enclave_status + ); + Ok(()) + } + + fn key_presence_probe(&self, req: &mut Selftest) -> Result<()> { + let ed = presence_acl()?; + let (blob, _) = self.user_key_create(Some(&ed))?; + let digest = req.digest; + let user = crate::sbio::UserId::new(crate::SBIO_PROBE_USER_ID).ok_or(EINVAL)?; + let record = |r: Result>, s: &SepData| -> (i32, i32) { + (r.err().map(|e| e.to_errno()).unwrap_or(0), s.last_key_status.load(Relaxed) as i32) + }; + self.last_key_status.store(0, Relaxed); + let ctx = self.passcode_context()?; + (req.result, req.enclave_status) = record(self.user_key_sign(&blob, &digest, Some(&ctx)), self); + self.close_unlock_evaluation(&ctx); + self.last_key_status.store(0, Relaxed); + let ctx = self.open_unlock_evaluation(user).ok_or(EIO)?; + (req.result2, req.enclave_status2) = record(self.user_key_sign(&blob, &digest, Some(&ctx)), self); + self.close_unlock_evaluation(&ctx); + dev_info!( + self.dev, + "sep-key: self-test presence probe: bag-secret context {} (enclave status {}), unsatisfied context {} (enclave status {})\n", + (if req.result == 0 { "SIGNED" } else { "refused" }), + req.enclave_status, + (if req.result2 == 0 { "SIGNED" } else { "refused" }), + req.enclave_status2 + ); + Ok(()) + } + + fn key_selftest(&self, req: &mut Selftest) -> Result<()> { + if crate::module_parameters::key_selftest.value() == 0 { + return Err(EPERM); + } + crate::bio::require_admin()?; + if req.mode == SELFTEST_PRESENCE_PROBE { + return self.key_presence_probe(req); + } + if req.mode == SELFTEST_PRESENCE_OTHER_BAG { + return self.key_presence_other_bag(req); + } + if req.mode == SELFTEST_USERBAG_KEY_MAKE || req.mode == SELFTEST_USERBAG_KEY_USE { + return self.key_userbag_record(req); + } + if req.mode == SELFTEST_USERBAG_LOCKED { + return self.key_userbag_locked(req); + } + if req.mode == SELFTEST_USERBAG_KEY_SAME_BAG || req.mode == SELFTEST_USERBAG_KEY_ROOT { + return self.key_userbag_probe(req); + } + let (blob, flags) = decode_handle(&req.handle, req.handle_len)?; + if flags & HANDLE_BIOMETRY == 0 { + return Err(EINVAL); + } + let mut owned = KVec::new(); + owned.extend_from_slice(blob, GFP_KERNEL)?; + let digest = req.digest; + let user = crate::sbio::UserId::new(crate::SBIO_PROBE_USER_ID).ok_or(EINVAL)?; + let record = |r: Result>, s: &SepData| -> (i32, i32) { + (r.err().map(|e| e.to_errno()).unwrap_or(0), s.last_key_status.load(Relaxed) as i32) + }; + self.last_key_status.store(0, Relaxed); + let (result, status) = match req.mode { + SELFTEST_NO_CONTEXT => record(self.user_key_sign(&owned, &digest, None), self), + SELFTEST_UNSATISFIED => { + let ctx = self.open_unlock_evaluation(user).ok_or(EIO)?; + let r = record(self.user_key_sign(&owned, &digest, Some(&ctx)), self); + self.close_unlock_evaluation(&ctx); + r + } + SELFTEST_PASSCODE => { + let ctx = self.passcode_context()?; + let r = record(self.user_key_sign(&owned, &digest, Some(&ctx)), self); + self.close_unlock_evaluation(&ctx); + r + } + SELFTEST_DELETED => { + let ctx = self.open_unlock_evaluation(user).ok_or(EIO)?; + self.close_unlock_evaluation(&ctx); + record(self.user_key_sign(&owned, &digest, Some(&ctx)), self) + } + SELFTEST_TOUCH_THEN_DELETED => { + let job = self.key_sign_with_touch(&owned, flags, &digest, true)?; + req.result2 = errno_of(&job.again); + req.enclave_status2 = job.again_status; + (errno_of(&job.sig), job.status) + } + _ => return Err(EINVAL), + }; + req.result = result; + req.enclave_status = status; + dev_info!( + self.dev, + "sep-key: self-test mode {}: {} (enclave status {})\n", + req.mode, + (if result == 0 { "SIGNED" } else { "refused" }), + status + ); + Ok(()) + } + + fn key_ioctl(&self, cmd: u32, arg: usize) -> Result<()> { + if self.shutting_down.load(Relaxed) { + return Err(ENODEV); + } + let user = UserPtr::from_addr(arg); + match cmd { + IOC_INFO => UserSlice::new(user, core::mem::size_of::()) + .writer() + .write(&info()), + IOC_CREATE => { + let slice = UserSlice::new(user, core::mem::size_of::()); + let (mut reader, mut writer) = slice.reader_writer(); + let mut req: Create = reader.read()?; + if req.reserved != [0; 3] { + return Err(EINVAL); + } + let (acl, flags) = match create_policy(req.flags)? { + Policy::None => (None, 0), + Policy::BiometryCurrentSet => (Some(self.key_bio_acl(false)?), HANDLE_BIOMETRY), + Policy::BiometryAny => (Some(self.key_bio_acl(true)?), HANDLE_BIOMETRY | HANDLE_BIOMETRY_ANY), + }; + let (blob, point) = self.user_key_create(acl.as_ref().map(|a| &a.ed[..]))?; + req.handle_len = encode_handle(&blob, flags, &mut req.handle)? as u32; + req.public_key = point; + writer.write(&req) + } + IOC_SIGN => { + let slice = UserSlice::new(user, core::mem::size_of::()); + let (mut reader, mut writer) = slice.reader_writer(); + let mut req: Sign = reader.read()?; + if req.flags != 0 { + return Err(EINVAL); + } + let sig = { + let (blob, flags) = decode_handle(&req.handle, req.handle_len)?; + if flags & HANDLE_BIOMETRY != 0 { + let digest = req.digest; + let job = self.key_sign_with_touch(blob, flags, &digest, false)?; + job.sig.ok_or(EIO)?? + } else { + self.user_key_sign(blob, &req.digest, None)? + } + }; + put_signature(&sig, &mut req)?; + writer.write(&req) + } + IOC_PUBLIC => { + let slice = UserSlice::new(user, core::mem::size_of::()); + let (mut reader, mut writer) = slice.reader_writer(); + let mut req: Public = reader.read()?; + if req.flags != 0 || req.reserved != [0; 3] { + return Err(EINVAL); + } + req.public_key = + SepData::user_key_public(decode_handle(&req.handle, req.handle_len)?.0)?; + writer.write(&req) + } + IOC_SELFTEST => { + let slice = UserSlice::new(user, core::mem::size_of::()); + let (mut reader, mut writer) = slice.reader_writer(); + let mut req: Selftest = reader.read()?; + self.key_selftest(&mut req)?; + writer.write(&req) + } + _ => Err(ENOTTY), + } + } +} + +// /dev/sep-key has no session: any number of openers, no poll. + +/// # Safety +/// `ctx` is the `SepData` pointer given to `register_key`, valid while the +/// registration lives (see `register_key`). +unsafe extern "C" fn key_open_trampoline(ctx: *mut c_void) -> c_int { + // SAFETY: per the contract above. + let this = unsafe { &*ctx.cast::() }; + if this.shutting_down.load(Relaxed) { + return ENODEV.to_errno(); + } + 0 +} + +/// # Safety +/// As for `key_open_trampoline`. +unsafe extern "C" fn key_release_trampoline(_ctx: *mut c_void) {} + +/// # Safety +/// As for `key_open_trampoline`. +unsafe extern "C" fn key_ioctl_trampoline(ctx: *mut c_void, cmd: c_uint, arg: c_ulong) -> c_long { + // SAFETY: per the contract above. + let this = unsafe { &*ctx.cast::() }; + match this.key_ioctl(cmd, arg) { + Ok(()) => 0, + Err(e) => e.to_errno() as c_long, + } +} + +/// # Safety +/// As for `key_open_trampoline`. +unsafe extern "C" fn key_ready_trampoline(_ctx: *mut c_void) -> c_int { + 0 +} diff --git a/drivers/soc/apple/refkey.rs b/drivers/soc/apple/refkey.rs index e505fb8d03bea..3f360a20c0fe6 100644 --- a/drivers/soc/apple/refkey.rs +++ b/drivers/soc/apple/refkey.rs @@ -87,9 +87,8 @@ impl SepData { &self, handle: crate::sks::KeyBagHandle, secret: &[u8], + acl: Option<&[u8]>, ) -> Option> { - use crate::der::RefKeyValue as RV; - let (protection_class, key_type) = match self.profile.key_store { // J313 accepted this non-class-F class and 13.5 EC key type for // creation, same-boot unseal, and post-reboot unseal. The exact @@ -114,6 +113,21 @@ impl SepData { return None; } + self.sks_create_refkey_at(handle.value(), protection_class, key_type, acl) + } + + /// The ref-key create itself, with `handle` as the outer key-bag handle: + /// a loaded bag, or a designated user's special handle `-uid` (the bag a + /// macOS Secure Enclave key lives under). The bag must already be + /// unlocked. + fn sks_create_refkey_at( + &self, + handle: i32, + protection_class: u32, + key_type: u32, + acl: Option<&[u8]>, + ) -> Option> { + use crate::der::RefKeyValue as RV; let create_der = { let mut items: KVec<(&[u8], RV<'_>)> = KVec::new(); if items.push((b"o", RV::Utf8(b"oc")), GFP_KERNEL).is_err() @@ -126,9 +140,14 @@ impl SepData { { return None; } + // `ed` carries the key's access-control list; the enclave keeps it + // in the record and enforces it on every later operation. + if let Some(acl) = acl { + items.push((b"ed", RV::Der(acl)), GFP_KERNEL).ok()?; + } crate::der::encode_refkey_set(&items).ok()? }; - let cout = match self.sks_send(self.sks_req_refkey(handle.value(), &create_der)) { + let cout = match self.sks_send(self.sks_req_refkey(handle, &create_der)) { Some(out) => out, None => { dev_warn!(self.dev, "sks: ref-key create got no usable reply\n"); @@ -197,7 +216,7 @@ impl SepData { return; } - let Some(blob) = self.sks_create_refkey(handle, secret) else { + let Some(blob) = self.sks_create_refkey(handle, secret, None) else { dev_warn!( self.dev, "sks: could not create the machine ref-key; trusted-key sealing is unavailable this boot.\n" @@ -309,37 +328,54 @@ impl SepData { } /// Op `osgn`: ECDSA-P256 over `challenge` as the pre-computed digest. + /// `acm` is the credential context an access-controlled key's operation + /// is checked against (`acmh`). `EKEYREJECTED` when the enclave refuses + /// the operation (a record another SEP or key bag made, or an access- + /// control list the context does not satisfy), `EIO` for no or a + /// malformed reply. The enclave's status is logged and kept in + /// `last_key_status` for the self-test. fn sks_refkey_sign( &self, handle: crate::sks::KeyBagHandle, refkey_blob: &[u8], challenge: &[u8], - ) -> Option> { + acm: Option<&[u8; crate::scrd::SCRD_ACM_HANDLE_LEN]>, + ) -> Result> { use crate::der::RefKeyValue as RV; let mut items: KVec<(&[u8], RV<'_>)> = KVec::new(); - items.push((b"o", RV::Utf8(b"osgn")), GFP_KERNEL).ok()?; - items.push((b"d", RV::Octets(challenge)), GFP_KERNEL).ok()?; - items.push((b"rk", RV::Der(refkey_blob)), GFP_KERNEL).ok()?; - let der = crate::der::encode_refkey_set(&items).ok()?; - let out = self.sks_send(self.sks_req_refkey(handle.value(), &der))?; + items.push((b"o", RV::Utf8(b"osgn")), GFP_KERNEL)?; + items.push((b"d", RV::Octets(challenge)), GFP_KERNEL)?; + items.push((b"rk", RV::Der(refkey_blob)), GFP_KERNEL)?; + if let Some(acm) = acm { + items.push((b"acmh", RV::Octets(acm)), GFP_KERNEL)?; + } + let der = crate::der::encode_refkey_set(&items)?; + let Some(out) = self.sks_send(self.sks_req_refkey(handle.value(), &der)) else { + self.last_key_status.store(i32::MIN as u32, Relaxed); + return Err(EIO); + }; + self.last_key_status.store(i32::from(out.reply.status) as u32, Relaxed); if out.reply.status != 0 { dev_warn!( self.dev, - "sks: ref-key attest sign failed (status {})\n", - out.reply.status + "sks: ref-key sign refused by the enclave (status {}{})\n", + out.reply.status, + (if acm.is_some() { ", with a credential context" } else { "" }) ); - return None; + return Err(EKEYREJECTED); } - let body = self.sks_report_response(crate::sks::SKS_PERFORM_OP_NAME, &out)?; + let body = self + .sks_report_response(crate::sks::SKS_PERFORM_OP_NAME, &out) + .ok_or(EIO)?; let mut f = proto::FieldCursor::new(body); let (Some(0), Some(sig)) = (f.i32(), f.blob()) else { - return None; + return Err(EIO); }; // Enclave wraps the sig in OCTET STRING { SEQUENCE { r, s } }; strip to inner DER. let der_sig = crate::der::octet_string_body(sig).unwrap_or(sig); let mut owned: KVec = KVec::new(); - owned.extend_from_slice(der_sig, GFP_KERNEL).ok()?; - Some(owned) + owned.extend_from_slice(der_sig, GFP_KERNEL)?; + Ok(owned) } /// Attestation of key possession by signing proof; op `oa` (attestation-chained) @@ -369,9 +405,110 @@ impl SepData { )); } }); - let sig = self.sks_refkey_sign(handle, &blob, challenge); + let sig = self.sks_refkey_sign(handle, &blob, challenge, None); + let _ = self.sks_send(self.sks_req_unload_keybag(handle)); + Ok((sig?, pubk)) + } + + /// Runs `op` with the identity key bag loaded and unlocked, as a trusted + /// key unseal does, and unloads the bag afterwards whatever `op` returns. + fn with_identity_bag( + &self, + op: impl FnOnce(crate::sks::KeyBagHandle, &[u8]) -> Result, + ) -> Result { + self.wait_for_trusted_ready()?; + let keybag::State::Present(stored) = keybag::read(keybag::Slot::Identity)? else { + return Err(ENODEV); + }; + let (handle, _uuid) = self.sks_recover(&stored).ok_or(EIO)?; + let unlocked = crate::xart_guard::policy_window(|| { + if self.profile.key_store == profile::KeyStore::Variant5 { + if let Some(healthy) = self.sks_health_check(c"sep-key unlock") { + 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()) + } + }); + let result = if unlocked { + op(handle, stored.secret()) + } else { + Err(EIO) + }; let _ = self.sks_send(self.sks_req_unload_keybag(handle)); - Ok((sig.ok_or(EIO)?, pubk)) + result + } + + /// `/dev/sep-key` create: a new enclave P-256 ref-key under the identity + /// key bag. Returns its record and public point; the private half stays + /// in the enclave. With `acl` the record carries that access-control + /// list (see `bio_acl`). + pub(crate) fn user_key_create( + &self, + acl: Option<&[u8]>, + ) -> Result<(KVec, [u8; refkey_seal::POINT_LEN])> { + let blob = self.with_identity_bag(|handle, secret| { + self.sks_create_refkey(handle, secret, acl).ok_or(EIO) + })?; + let mut point = [0u8; refkey_seal::POINT_LEN]; + point.copy_from_slice(Self::refkey_pub(&blob).ok_or(EIO)?); + Ok((blob, point)) + } + + /// Test only (`key_selftest`): a ref-key under the designated user bag + /// `user` (outer handle `-uid`), which the caller has unlocked. Variant-5 + /// key store only. + pub(crate) fn user_key_create_in_bag( + &self, + user: crate::sks::DesignateUser, + acl: Option<&[u8]>, + ) -> Result> { + if self.profile.key_store != profile::KeyStore::Variant5 { + return Err(EOPNOTSUPP); + } + crate::xart_guard::close_policy_window(); + self.sks_create_refkey_at(user.special_handle().value(), 9, 5, acl).ok_or(EIO) + } + + /// Test only: sign with a ref-key under the designated user bag `user`. + pub(crate) fn user_key_sign_in_bag( + &self, + user: crate::sks::DesignateUser, + blob: &[u8], + digest: &[u8; 32], + acm: Option<&[u8; crate::scrd::SCRD_ACM_HANDLE_LEN]>, + ) -> Result> { + let handle = crate::sks::KeyBagHandle::from_load_reply(user.special_handle().value()); + self.sks_refkey_sign(handle, blob, digest, acm) + } + + /// The public point recorded in a ref-key record; no enclave round trip. + pub(crate) fn user_key_public(blob: &[u8]) -> Result<[u8; refkey_seal::POINT_LEN]> { + let mut point = [0u8; refkey_seal::POINT_LEN]; + point.copy_from_slice(Self::refkey_pub(blob).ok_or(EINVAL)?); + Ok(point) + } + + /// `/dev/sep-key` sign: the enclave's ECDSA-P256 over `digest` with the + /// key in `blob`, checked against the credential context `acm` when one + /// is given. A record this SEP and key bag did not make, or an access- + /// control list `acm` does not satisfy, is refused by the enclave + /// (`EKEYREJECTED`). + pub(crate) fn user_key_sign( + &self, + blob: &[u8], + digest: &[u8; 32], + acm: Option<&[u8; crate::scrd::SCRD_ACM_HANDLE_LEN]>, + ) -> Result> { + // A record without a public point is not a ref-key record at all. + Self::refkey_pub(blob).ok_or(EINVAL)?; + self.with_identity_bag(|handle, _secret| self.sks_refkey_sign(handle, blob, digest, acm)) } /// Enclave ECIES-decrypt (o = "oecd"); takes the ephemeral from the front of `d`. diff --git a/drivers/soc/apple/sbio.rs b/drivers/soc/apple/sbio.rs index 2464fcb028c9a..70af8f94cf89f 100644 --- a/drivers/soc/apple/sbio.rs +++ b/drivers/soc/apple/sbio.rs @@ -585,6 +585,30 @@ impl SepData { } } + /// The Linux enclave user's catacomb id and enrolled-set hash, which a + /// key's biometric access-control list names (`pbioc`, `pbioh`). Read + /// only. + pub(crate) fn catacomb_binding( + &self, + ) -> Result<([u8; crate::sbio::SBIO_CATACOMB_ID_LEN], [u8; crate::sbio::SBIO_CATACOMB_HASH_LEN])> { + let user = crate::sbio::UserId::new(SBIO_PROBE_USER_ID).ok_or(EINVAL)?; + let id_reply = self.sbio_expect_ok(&crate::sbio::sbio_catacomb_id(user)).ok_or(EIO)?; + let hash_reply = self.sbio_expect_ok(&crate::sbio::sbio_catacomb_hash(user)).ok_or(EIO)?; + dev_info!( + self.dev, + "sep-key: catacomb id {} bytes, hash {} bytes\n", + id_reply.len(), + hash_reply.len() + ); + if id_reply.len() != crate::sbio::SBIO_CATACOMB_ID_LEN { + return Err(EIO); + } + let mut id = [0u8; crate::sbio::SBIO_CATACOMB_ID_LEN]; + id.copy_from_slice(&id_reply); + let hash = crate::sbio::catacomb_hash_from_reply(&hash_reply).ok_or(EIO)?; + Ok((id, hash)) + } + fn sbio_expect_ok(&self, op: &crate::sbio::SbioOp) -> Option> { match self.sbio_call(op) { SbioOutcome::Ok(payload) => Some(payload), @@ -892,7 +916,7 @@ impl SepData { let mut told = false; let mut first: Option<[u8; sensor::STATUS_LEN]> = None; loop { - if !bio::capture_is_live(&self.bio_session.lock(), self.capture_epoch.load(Relaxed)) { + if !self.capture_live() { return false; } let Ok(st) = sensor::status() else { @@ -2266,6 +2290,14 @@ impl SepData { e ); } + if let Err(e) = self.register_key() { + dev_err!( + self.dev, + "bringup: could not publish /dev/{}: {:?}\n", + crate::key::DEVICE_NAME, + e + ); + } } fn activate_touchid(&self) -> Result<()> { @@ -2443,7 +2475,8 @@ impl SepData { /// not ended by a suspend and not closed. fn verify_wanted(&self) -> bool { !self.shutting_down.load(Relaxed) - && bio::verify_is_live(&self.bio_session.lock(), self.capture_epoch.load(Relaxed)) + && (self.key_touch_active.load(Relaxed) + || bio::verify_is_live(&self.bio_session.lock(), self.capture_epoch.load(Relaxed))) } // Every verify registers the sensor again. On a profile with a nonce @@ -2583,7 +2616,10 @@ impl SepData { // MATCH_CANCEL only after the third). Here a prompt is one open of the // Touch ID device: fprintd claims it for one PAM authentication. let open_gen = self.bio_open_gen.load(Relaxed); - let current = if self.match_scope_gen.load(Relaxed) == open_gen { + let key_mode = self.key_touch_active.load(Relaxed); + // A /dev/sep-key signature is its own prompt: a fresh evaluation that + // never outlives this run, whatever prompt /dev/sep-bio has open. + let current = if !key_mode && self.match_scope_gen.load(Relaxed) == open_gen { *self.match_context.lock() } else { None @@ -2624,6 +2660,9 @@ impl SepData { return; } *self.match_context.lock() = Some(evaluation); + if key_mode { + self.key_touch_preflight(&evaluation); + } self.match_scope_gen.store(open_gen, Relaxed); self.match_scope_touches.store(1, Relaxed); self.match_scope_misses.store(0, Relaxed); @@ -2660,7 +2699,7 @@ impl SepData { let total_misses = self.bio_nomatch_streak.load(Relaxed).saturating_add(miss); self.match_scope_misses.store(prompt_misses, Relaxed); let step = prompt_step(no_match, capture_retry, prompt_misses, total_misses); - let keep_open = !self.enclave_reports_lockout() && step == PromptStep::KeepOpen; + let keep_open = !key_mode && !self.enclave_reports_lockout() && step == PromptStep::KeepOpen; if no_match && !keep_open && prompt_misses >= PROMPT_RETRY_LIMIT @@ -2699,15 +2738,26 @@ impl SepData { } } } - if matches!(outcome, bio::VerifyOutcome::Matched(_)) - && !self.complete_unlock_evaluation(&evaluation, user) - { + let matched = matches!(outcome, bio::VerifyOutcome::Matched(_)); + // The signature runs in the evaluation the match just satisfied, + // before it is completed (as macOS signs after MATCH_CANCEL) and, + // if the enclave wants the completed evaluation, once more after. + if key_mode && matched { + self.key_touch_sign(&evaluation, c"after the match"); + } + if matched && !self.complete_unlock_evaluation(&evaluation, user) { dev_err!(self.dev, "verify: the enclave matched but the unlock policy was not satisfied\n"); outcome = bio::VerifyOutcome::Failed(ENROL_STATUS_ENCLAVE); } + if key_mode && matched { + self.key_touch_sign(&evaluation, c"after the completed evaluation"); + } *self.match_context.lock() = None; self.match_scope_gen.store(0, Relaxed); self.close_unlock_evaluation(&evaluation); + if key_mode && matched { + self.key_touch_after_close(&evaluation); + } } // macOS saves the enclave's lockout record once after every answered // result (SAVE_LOCKOUT, kept on disk for the boot's LOAD_LOCKOUT), @@ -2879,9 +2929,9 @@ impl SepData { } CaptureWait::Abandon => { // A cancelled verify also abandons; only a live one is a fault. - if bio::verify_is_live( - &self.bio_session.lock(), self.capture_epoch.load(Relaxed), - ) { + if self.key_touch_active.load(Relaxed) + || bio::verify_is_live(&self.bio_session.lock(), self.capture_epoch.load(Relaxed)) + { dev_err!( self.dev, "verify: the sensor status became unreadable during the capture\n" @@ -4142,7 +4192,7 @@ impl SepData { let deadline_ns = crate::shim::monotonic_ns().saturating_add(wait_ms.saturating_mul(1_000_000)); let mut attempt: u32 = 0; loop { - if !bio::capture_is_live(&self.bio_session.lock(), self.capture_epoch.load(Relaxed)) { + if !self.capture_live() { return CaptureWait::Abandon; } @@ -4885,6 +4935,14 @@ impl SepData { } fn finish_verify(&self, outcome: bio::VerifyOutcome, token_bytes: [u8; bio::TOKEN_LEN]) { + // A touch-gated /dev/sep-key signature owns this verify: its result + // goes to that ioctl, never to /dev/sep-bio's session. + if self.key_touch_active.load(Relaxed) { + if let Some(job) = &mut *self.key_touch.lock() { + job.set_outcome(&outcome); + } + return; + } let filed = bio::verify_finish( &mut self.bio_session.lock(), self.capture_epoch.load(Relaxed), outcome, token_bytes, ); @@ -5117,6 +5175,11 @@ impl SepData { if self.shutting_down.load(Relaxed) { return Err(ENODEV); } + // 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) { + return self.userbag_ioctl(cmd, arg).map(|_| ()); + } match cmd { bio::IOC_LOCKOUT_STATE => self.write_lockout_state(arg), bio::IOC_REARM => { @@ -6512,6 +6575,45 @@ pub(crate) fn sbio_protected_config(id: UserId) -> SbioOp { } } +/// The 16-byte id of `id`'s Touch ID catacomb (the enrolled-template set), +/// as a key access-control list names it. Read only. +const OP_SBIO_CATACOMB_ID: u16 = 0x33; +pub(crate) const SBIO_CATACOMB_ID_LEN: usize = 16; +pub(crate) fn sbio_catacomb_id(id: UserId) -> SbioOp { + SbioOp { + opcode: OP_SBIO_CATACOMB_ID, + payload: pad_payload(&id.value().to_le_bytes()), + payload_len: 4, + name: c"CATACOMB_ID", + } +} + +/// The 32-byte hash of `id`'s enrolled set, which changes when a finger is +/// added or removed. Read only. The reply may lead with one version byte. +const OP_SBIO_CATACOMB_HASH: u16 = 0x36; +pub(crate) const SBIO_CATACOMB_HASH_LEN: usize = 32; +pub(crate) fn sbio_catacomb_hash(id: UserId) -> SbioOp { + SbioOp { + opcode: OP_SBIO_CATACOMB_HASH, + payload: pad_payload(&id.value().to_le_bytes()), + payload_len: 4, + name: c"CATACOMB_HASH", + } +} + +/// The 32-byte hash in a CATACOMB_HASH reply: the bare hash, or one leading +/// version byte and the hash. +pub(crate) fn catacomb_hash_from_reply(reply: &[u8]) -> Option<[u8; SBIO_CATACOMB_HASH_LEN]> { + let raw = match reply.len() { + SBIO_CATACOMB_HASH_LEN => reply, + n if n == SBIO_CATACOMB_HASH_LEN + 1 => &reply[1..], + _ => return None, + }; + let mut out = [0u8; SBIO_CATACOMB_HASH_LEN]; + out.copy_from_slice(raw); + Some(out) +} + const OP_SBIO_BEGIN_ENROL: u16 = 0x03; pub(crate) const BE_AUTH_TYPE_ACM_CONTEXT: u32 = 0; diff --git a/drivers/soc/apple/scrd.rs b/drivers/soc/apple/scrd.rs index fea9b390b6466..bb03acb4a10c7 100644 --- a/drivers/soc/apple/scrd.rs +++ b/drivers/soc/apple/scrd.rs @@ -130,6 +130,35 @@ pub(crate) fn scrd_verify_touchid_enrollment(handle: &[u8; SCRD_ACM_HANDLE_LEN]) } } +const SCRD_CMD_VERIFY_ACL_CONSTRAINT: u8 = 0x20; +const SCRD_ACL_VERSION: u8 = 2; + +/// Command 0x20: evaluate one operation's access-control constraint (a DER +/// set, as in a key's ACL) on the context, as LocalAuthentication does before +/// and after a Touch ID match for a key operation. Laid out as macOS sends it: +/// header, context, u32 constraint length, constraint, u32 operation length, +/// operation, a 1 byte, two zero u32 words. +pub(crate) fn scrd_verify_acl_constraint( + handle: &[u8; SCRD_ACM_HANDLE_LEN], + constraint: &[u8], + operation: &[u8], +) -> Result> { + let mut out = KVec::new(); + let mut header = [0u8; SCRD_MAX_LOGICAL]; + scrd_header(&mut header, SCRD_CMD_VERIFY_ACL_CONSTRAINT, 0, SCRD_ACL_VERSION); + out.extend_from_slice(&header[..8], GFP_KERNEL)?; + out.extend_from_slice(handle, GFP_KERNEL)?; + let clen = u32::try_from(constraint.len()).map_err(|_| EINVAL)?; + out.extend_from_slice(&clen.to_le_bytes(), GFP_KERNEL)?; + out.extend_from_slice(constraint, GFP_KERNEL)?; + let olen = u32::try_from(operation.len()).map_err(|_| EINVAL)?; + out.extend_from_slice(&olen.to_le_bytes(), GFP_KERNEL)?; + out.extend_from_slice(operation, GFP_KERNEL)?; + out.push(1, GFP_KERNEL)?; + out.extend_from_slice(&[0u8; 8], GFP_KERNEL)?; + Ok(out) +} + /// The reference of a tracked context-create reply, or `None` for any /// other shape. pub(crate) fn tracked_create_reference(reply: &[u8]) -> Option<[u8; SCRD_ACM_HANDLE_LEN]> { diff --git a/drivers/soc/apple/sep.rs b/drivers/soc/apple/sep.rs index 5528a771f1893..70511f67337d7 100644 --- a/drivers/soc/apple/sep.rs +++ b/drivers/soc/apple/sep.rs @@ -18,6 +18,7 @@ mod dt; mod fv; mod hwrng; mod image; +mod key; mod keybag; mod keybag_identity; mod os_identity; @@ -37,6 +38,7 @@ mod sks; mod store; mod transfer; mod trusted; +mod userbag; mod xarm; mod xart_apfs; mod xart_guard; @@ -770,6 +772,19 @@ struct SepData { #[pin] rearm_dev: Mutex>, + // /dev/sep-key: P-256 keys created inside the SEP (see `key`). + #[pin] + key_dev: Mutex>, + + // /dev/sep-key: the signature a touch-gated SEP_KEY_SIGN is waiting for. + // Set only for the length of that ioctl; while set, a verify run belongs + // to it and not to /dev/sep-bio's session. + #[pin] + key_touch: Mutex>, + key_touch_active: Atomic, + // The enclave status of the last /dev/sep-key signature (self-test). + last_key_status: Atomic, + // The enclave refused to match (MATCH_RESULT answered 0x1) after failed // attempts, or the lockout survived a restart. Every verify is refused // without a capture until a password authentication re-arms Touch ID. @@ -1060,6 +1075,10 @@ impl SepData { bio_index <- new_mutex!(bio_index), bio_dev <- new_mutex!(None), rearm_dev <- new_mutex!(None), + key_dev <- new_mutex!(None), + key_touch <- new_mutex!(None), + key_touch_active: Atomic::new(false), + last_key_status: Atomic::new(0), bio_lockout: Atomic::new(false), bio_after_boot: Atomic::new(false), bio_lockout_logged: Atomic::new(false), @@ -2641,6 +2660,8 @@ impl SepData { drop(dev); let dev = self.rearm_dev.lock().take(); drop(dev); + let dev = self.key_dev.lock().take(); + drop(dev); self.rng_shutdown.store(true, Relaxed); self.control_wq.notify_all(); @@ -3075,6 +3096,10 @@ module! { default: 0, description: "Opt-in J414s diagnostic: with zero live identities, select the system context and try saving the missing owner Catacomb", }, + key_selftest: u8 { + default: 0, + description: "Test only: let root run SEP_KEY_SELFTEST, which signs with an access-controlled /dev/sep-key key under deliberately unsuitable credential contexts to show the enclave refuses", + }, refkey_v2: u8 { default: 0, description: "Explicit recovery slot for a new identity-bag context; preserves the original machine ref-key", diff --git a/drivers/soc/apple/sks.rs b/drivers/soc/apple/sks.rs index d3d30eb015df1..a4cac9071a108 100644 --- a/drivers/soc/apple/sks.rs +++ b/drivers/soc/apple/sks.rs @@ -548,7 +548,7 @@ impl SepData { } /// `0x02` copy the designated biometric identity bag. - fn sks_req_copy_keybag_special(&self, handle: crate::sks::SpecialHandle) -> Result { + pub(crate) fn sks_req_copy_keybag_special(&self, handle: crate::sks::SpecialHandle) -> Result { let op = crate::sks::sks_copy_keybag(); let mut body = image::Body::new(); body.put_u32(0)?; @@ -557,6 +557,24 @@ impl SepData { self.sks_seal(&op, &body) } + /// Loads a wrapped bag (`0x03`) and returns its temporary handle. Used for + /// per-user bags, whose wrapped container userspace keeps. + pub(crate) fn sks_load_wrapped(&self, wrapped: &[u8]) -> Option { + let out = self.sks_send(self.sks_req_load_keybag(wrapped))?; + let body = self.sks_report_response(crate::sks::SKS_LOAD_NAME, &out)?; + if out.reply.status != 0 || body.len() != SKS_LOAD_REPLY_LEN { + dev_info!(self.dev, "sks: LOAD_KEYBAG (user bag): status {}, {} bytes\n", out.reply.status, body.len()); + return None; + } + let status = i32::from_le_bytes([body[0], body[1], body[2], body[3]]); + let handle = i32::from_le_bytes([body[4], body[5], body[6], body[7]]); + if status != 0 || handle < 0 { + dev_info!(self.dev, "sks: LOAD_KEYBAG (user bag): operation status {}, handle {}\n", status, handle); + return None; + } + Some(crate::sks::KeyBagHandle::from_load_reply(handle)) + } + /// `0x03` load key bag. fn sks_req_load_keybag(&self, wrapped: &[u8]) -> Result { let mut body = image::Body::new(); @@ -1354,6 +1372,13 @@ const OP_SKS_LOAD_KEYBAG: u8 = 0x03; pub(crate) const SKS_LOAD_NAME: &CStr = c"LOAD_KEYBAG"; +const OP_SKS_CHANGE_SECRET: u8 = 0x07; +pub(crate) const SKS_CHANGE_SECRET_NAME: &CStr = c"CHANGE_SECRET"; + +pub(crate) fn encode_sks_change_secret(seq: Sequence, len: ImageLen) -> Message { + encode_sks_raw(OP_SKS_CHANGE_SECRET, seq.value(), len.value()) +} + const OP_SKS_CHANGE_LOCK_STATE: u8 = 0x04; pub(crate) const SKS_LOCK_STATE_NAME: &CStr = c"CHANGE_LOCK_STATE"; @@ -1365,6 +1390,27 @@ pub(crate) fn encode_sks_device_state_transition(seq: Sequence, len: ImageLen) - encode_sks_raw(OP_SKS_DEVICE_STATE_TRANSITION, seq.value(), len.value()) } +const OP_SKS_GET_DEVICE_STATE: u8 = 0x19; +pub(crate) const SKS_GET_DEVICE_STATE_NAME: &CStr = c"GET_DEVICE_STATE"; + +pub(crate) fn encode_sks_get_device_state(seq: Sequence, len: ImageLen) -> Message { + encode_sks_raw(OP_SKS_GET_DEVICE_STATE, seq.value(), len.value()) +} + +const OP_SKS_GET_CONFIGURATION: u8 = 0x23; +pub(crate) const SKS_GET_CONFIGURATION_NAME: &CStr = c"GET_CONFIGURATION"; + +pub(crate) fn encode_sks_get_configuration(seq: Sequence, len: ImageLen) -> Message { + encode_sks_raw(OP_SKS_GET_CONFIGURATION, seq.value(), len.value()) +} + +const OP_SKS_HANDLE_TIMER_FIRE: u8 = 0x2f; +pub(crate) const SKS_HANDLE_TIMER_FIRE_NAME: &CStr = c"HANDLE_TIMER_FIRE"; + +pub(crate) fn encode_sks_handle_timer_fire(seq: Sequence, len: ImageLen) -> Message { + encode_sks_raw(OP_SKS_HANDLE_TIMER_FIRE, seq.value(), len.value()) +} + const OP_SKS_VERIFY_SECRET: u8 = 0x21; pub(crate) const SKS_VERIFY_SECRET_NAME: &CStr = c"VERIFY_SECRET"; pub(crate) const SKS_VERIFY_SECRET_VARIANT: u32 = 1; diff --git a/drivers/soc/apple/userbag.rs b/drivers/soc/apple/userbag.rs new file mode 100644 index 0000000000000..d06f48cf7260f --- /dev/null +++ b/drivers/soc/apple/userbag.rs @@ -0,0 +1,1720 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Per-user key-bag protocol and host policy. +//! +//! Today the driver unlocks the designated identity bag at boot with a secret +//! it keeps on disk, and never locks it. This module sketches the macOS model +//! instead: +//! +//! * the machine bag (ref-key, trusted keys, system Catacomb) stays as it is; +//! * each Unix user who enrols gets a second bag, created with that user's +//! password as the secret, and designated as the biometric user. The +//! enclave entangles the secret with its own device key, so the bag cannot +//! be opened off the machine, and root on the machine cannot open it +//! without the password; +//! * the bag stays locked from boot until the first password unlock, locks +//! on screen lock after a grace period and at once on suspend; +//! * Touch ID is offered only while [`Policy::biometry_allowed`] says so. +//! +//! The ioctls (UAPI `SEP_BIO_KEYBAG_*`) are meant for the root-only re-arm +//! node, so fprintd never sees a password. Request images are wiped on drop, +//! and the ioctl request copies holding passwords are wiped before return. + +#![allow(dead_code)] + +use kernel::prelude::*; +use kernel::uaccess::{UserPtr, UserSlice}; + +const CLIENT: u64 = crate::sks::SKS_CLIENT_ID; +const CREATE_VARIANT: u32 = 4; +const CREATE_BAG_TYPE: u32 = 0; +const CREATE_ARG: i32 = -1; +const DESIGNATE_VARIANT: u32 = 1; +const CHANGE_VARIANT: u32 = 3; +const LOCK_STATE_VARIANT: u32 = 1; +const DEVICE_STATE_VARIANT: u32 = 0; +const READ_VARIANT: u32 = 0; +const CHANGE_TRAILER: u32 = 0xff; + +#[derive(Debug, PartialEq, Eq)] +struct Create<'a> { + secret: &'a [u8], +} + +#[derive(Debug, PartialEq, Eq)] +struct Designate { + source_handle: i32, + user_handle: i32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum TransitionState { + Unlocked, + Locked, +} + +impl TransitionState { + const fn wire(self) -> u32 { + match self { + TransitionState::Unlocked => 0, + TransitionState::Locked => 1, + } + } +} + +/// State passed to the key store's explicit change-lock-state operation. +/// +/// Device-state transitions model screen/sleep transitions and may enter the +/// bag's configured grace period. This operation is the direct key-bag lock +/// used when the caller asked for no grace (logout, suspend, or a zero-grace +/// screen lock). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum LockStateChange { + Query, + Locked, +} + +impl LockStateChange { + const fn wire(self) -> i32 { + match self { + LockStateChange::Query => -1, + LockStateChange::Locked => 1, + } + } +} + +#[derive(Debug, PartialEq, Eq)] +struct Transition<'a> { + user_handle: i32, + state: TransitionState, + flags: u64, + secret: &'a [u8], +} + +#[derive(Debug, PartialEq, Eq)] +struct ChangeSecret<'a> { + user_handle: i32, + old: &'a [u8], + new: &'a [u8], +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct GetState { + state_word: u32, + lock_phase: u32, + failed_attempts: u32, + attempt_limit: u32, + handle: i32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Configuration { + grace_period_s: u32, + expiry_time_s: u32, + extended_period_s: u32, + attempt_limit: u32, + biometric_disable_limit: u32, +} + +struct Cursor<'a> { + bytes: &'a [u8], + at: usize, +} + +impl<'a> Cursor<'a> { + fn new(bytes: &'a [u8]) -> Self { + Self { bytes, at: 0 } + } + + fn u32(&mut self) -> Option { + let end = self.at.checked_add(4)?; + let value = u32::from_le_bytes(self.bytes.get(self.at..end)?.try_into().ok()?); + self.at = end; + Some(value) + } + + fn i32(&mut self) -> Option { + Some(self.u32()? as i32) + } + + fn u64(&mut self) -> Option { + let end = self.at.checked_add(8)?; + let value = u64::from_le_bytes(self.bytes.get(self.at..end)?.try_into().ok()?); + self.at = end; + Some(value) + } + + fn blob(&mut self) -> Option<&'a [u8]> { + let (blob, next) = crate::image::read_blob(self.bytes, self.at)?; + self.at = next; + Some(blob) + } + + fn done(self) -> bool { + self.at == self.bytes.len() + } +} + +fn encode_create(secret: &[u8]) -> Result { + let mut body = crate::image::Body::new(); + body.put_u32(CREATE_VARIANT)?; + body.put_u64(CLIENT)?; + body.put_u32(CREATE_BAG_TYPE)?; + body.put_i32(CREATE_ARG)?; + body.put_blob(secret)?; + // Five absent trailing fields in the captured variant-4 structure. + for _ in 0..5 { + body.put_u32(0)?; + } + Ok(body) +} + +fn decode_create(body: &[u8]) -> Option> { + let mut c = Cursor::new(body); + if c.u32()? != CREATE_VARIANT + || c.u64()? != CLIENT + || c.u32()? != CREATE_BAG_TYPE + || c.i32()? != CREATE_ARG + { + return None; + } + let secret = c.blob()?; + for _ in 0..5 { + if c.u32()? != 0 { + return None; + } + } + c.done().then_some(Create { secret }) +} + +fn decode_create_reply(body: &[u8]) -> Option { + let mut c = Cursor::new(body); + if c.u32()? != CREATE_VARIANT { + return None; + } + let handle = c.i32()?; + if handle < 0 || c.u32()? != 0 || !c.done() { + return None; + } + Some(crate::sks::KeyBagHandle::from_create_reply(handle)) +} + +fn encode_designate(source_handle: i32, user_handle: i32) -> Result { + if source_handle < 0 || user_handle >= 0 { + return Err(EINVAL); + } + let mut body = crate::image::Body::new(); + body.put_u32(DESIGNATE_VARIANT)?; + body.put_u64(CLIENT)?; + body.put_i32(source_handle)?; + body.put_i32(user_handle)?; + body.put_blob(&[])?; + body.put_u64(0)?; + Ok(body) +} + +fn decode_designate(body: &[u8]) -> Option { + let mut c = Cursor::new(body); + if c.u32()? != DESIGNATE_VARIANT || c.u64()? != CLIENT { + return None; + } + let source_handle = c.i32()?; + let user_handle = c.i32()?; + if source_handle < 0 || user_handle >= 0 || !c.blob()?.is_empty() || c.u64()? != 0 || !c.done() + { + return None; + } + Some(Designate { + source_handle, + user_handle, + }) +} + +fn decode_designate_reply(body: &[u8]) -> bool { + body == DESIGNATE_VARIANT.to_le_bytes() +} + +fn encode_transition( + user_handle: i32, + state: TransitionState, + flags: u64, + secret: &[u8], +) -> Result { + if user_handle >= 0 + || (state == TransitionState::Unlocked && flags != 0) + || (state == TransitionState::Locked && (!secret.is_empty() || flags > 1)) + { + return Err(EINVAL); + } + let mut body = crate::image::Body::new(); + body.put_u32(DEVICE_STATE_VARIANT)?; + body.put_u64(CLIENT)?; + body.put_i32(user_handle)?; + body.put_u32(state.wire())?; + body.put_u64(flags)?; + body.put_blob(secret)?; + Ok(body) +} + +fn decode_transition(body: &[u8]) -> Option> { + let mut c = Cursor::new(body); + if c.u32()? != DEVICE_STATE_VARIANT || c.u64()? != CLIENT { + return None; + } + let user_handle = c.i32()?; + let state = match c.u32()? { + 0 => TransitionState::Unlocked, + 1 => TransitionState::Locked, + _ => return None, + }; + let flags = c.u64()?; + let secret = c.blob()?; + if user_handle >= 0 + || !c.done() + || (state == TransitionState::Unlocked && flags != 0) + || (state == TransitionState::Locked && (!secret.is_empty() || flags > 1)) + { + return None; + } + Some(Transition { + user_handle, + state, + flags, + secret, + }) +} + +fn encode_timer_fire() -> Result { + let mut body = crate::image::Body::new(); + body.put_u32(0)?; + body.put_u32(1)?; + body.put_u32(0)?; + Ok(body) +} + +fn decode_timer_fire(body: &[u8]) -> bool { + body == [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0] +} + +/// The grace-expiry answer, as the Neo's key store gave it on Linux on +/// 11-09: `u32 0`, the handle whose grace period ended (0 when none has +/// yet), the timer kind (1), `u32 0`, then two opaque words (a token, and a +/// small counter that was 0 while grace ran and 3 when the bag locked). +fn decode_timer_fire_reply(body: &[u8]) -> Option { + let mut c = Cursor::new(body); + let lead = c.u32()?; + let handle = c.i32()?; + let kind = c.u32()?; + let reserved = c.u32()?; + let _token = c.u32()?; + let _count = c.u32()?; + (lead == 0 && handle <= 0 && kind == 1 && reserved == 0 && c.done()).then_some(handle) +} + +fn encode_change_secret(user_handle: i32, old: &[u8], new: &[u8]) -> Result { + if user_handle >= 0 || old.is_empty() || new.is_empty() { + return Err(EINVAL); + } + let mut body = crate::image::Body::new(); + body.put_u32(CHANGE_VARIANT)?; + body.put_u64(CLIENT)?; + body.put_i32(user_handle)?; + body.put_blob(old)?; + body.put_blob(new)?; + body.put_u64(0)?; + body.put_u32(CHANGE_TRAILER)?; + body.put_u64(0)?; + Ok(body) +} + +fn decode_change_secret(body: &[u8]) -> Option> { + let mut c = Cursor::new(body); + if c.u32()? != CHANGE_VARIANT || c.u64()? != CLIENT { + return None; + } + let user_handle = c.i32()?; + let old = c.blob()?; + let new = c.blob()?; + if user_handle >= 0 + || old.is_empty() + || new.is_empty() + || c.u64()? != 0 + || c.u32()? != CHANGE_TRAILER + || c.u64()? != 0 + || !c.done() + { + return None; + } + Some(ChangeSecret { + user_handle, + old, + new, + }) +} + +fn decode_change_secret_reply(body: &[u8]) -> bool { + body == [3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] +} + +fn encode_lock_state_change( + user_handle: i32, + state: LockStateChange, +) -> Result { + if user_handle >= 0 { + return Err(EINVAL); + } + let mut body = crate::image::Body::new(); + body.put_u32(LOCK_STATE_VARIANT)?; + body.put_u64(CLIENT)?; + body.put_i32(user_handle)?; + body.put_i32(state.wire())?; + body.put_blob(&[])?; + body.put_u64(0)?; + Ok(body) +} + +fn encode_lock_query(user_handle: i32) -> Result { + encode_lock_state_change(user_handle, LockStateChange::Query) +} + +fn encode_explicit_lock(user_handle: i32) -> Result { + encode_lock_state_change(user_handle, LockStateChange::Locked) +} + +fn decode_lock_query(body: &[u8]) -> Option { + let mut c = Cursor::new(body); + if c.u32()? != LOCK_STATE_VARIANT || c.u64()? != CLIENT { + return None; + } + let handle = c.i32()?; + if handle >= 0 || c.i32()? != -1 || !c.blob()?.is_empty() || c.u64()? != 0 || !c.done() { + return None; + } + Some(handle) +} + +fn decode_lock_query_reply(body: &[u8]) -> Option { + let mut c = Cursor::new(body); + if c.u32()? != LOCK_STATE_VARIANT { + return None; + } + let state = c.u32()?; + if c.u32()? != 0 || c.u32()? != 0 || !c.done() { + return None; + } + Some(state) +} + +fn encode_get_state(user_handle: i32) -> Result { + if user_handle >= 0 { + return Err(EINVAL); + } + let mut body = crate::image::Body::new(); + body.put_u32(READ_VARIANT)?; + body.put_u64(CLIENT)?; + body.put_i32(user_handle)?; + body.put_u32(0)?; + Ok(body) +} + +fn decode_get_state_request(body: &[u8]) -> Option { + let mut c = Cursor::new(body); + if c.u32()? != READ_VARIANT || c.u64()? != CLIENT { + return None; + } + let handle = c.i32()?; + if handle >= 0 || c.u32()? != 0 || !c.done() { + return None; + } + Some(handle) +} + +/// A DER-dictionary reply: `u32 0`, then the dictionary as a blob (padded to +/// 4), as the Neo's key store answered on Linux on 11-08. +fn response_der(body: &[u8]) -> Option<&[u8]> { + let mut c = Cursor::new(body); + if c.u32()? != 0 { + return None; + } + let der = c.blob()?; + if !c.done() { + return None; + } + Some(der) +} + +fn der_u32(der: &[u8], key: &[u8]) -> Option { + crate::der::refkey_find(der, key).and_then(crate::der::integer_u32) +} + +fn decode_get_state_reply(body: &[u8]) -> Option { + let der = response_der(body)?; + Some(GetState { + state_word: der_u32(der, b"ss")?, + lock_phase: der_u32(der, b"sls")?, + failed_attempts: der_u32(der, b"sfa")?, + attempt_limit: der_u32(der, b"mua")?, + handle: crate::der::refkey_find(der, b"bh").and_then(crate::der::integer_i32)?, + }) +} + +fn encode_get_configuration(user_handle: i32) -> Result { + if user_handle >= 0 { + return Err(EINVAL); + } + let mut body = crate::image::Body::new(); + body.put_u32(READ_VARIANT)?; + body.put_u64(CLIENT)?; + body.put_i32(user_handle)?; + Ok(body) +} + +fn decode_get_configuration_request(body: &[u8]) -> Option { + let mut c = Cursor::new(body); + if c.u32()? != READ_VARIANT || c.u64()? != CLIENT { + return None; + } + let handle = c.i32()?; + (handle < 0 && c.done()).then_some(handle) +} + +fn decode_get_configuration_reply(body: &[u8]) -> Option { + let der = response_der(body)?; + Some(Configuration { + grace_period_s: der_u32(der, b"gp")?, + expiry_time_s: der_u32(der, b"etp")?, + extended_period_s: der_u32(der, b"epp")?, + attempt_limit: der_u32(der, b"mua")?, + biometric_disable_limit: der_u32(der, b"bd")?, + }) +} + +/// macOS: "five unsuccessful biometric authentication match attempts". +pub(crate) const MAX_FAILED_MATCHES: u32 = 5; +/// macOS: "hasn't unlocked their device for more than 48 hours". +pub(crate) const NO_UNLOCK_LIMIT_S: u64 = 48 * 3600; +/// macOS: no password unlock for 156 hours (six and a half days) ... +pub(crate) const NO_PASSWORD_LIMIT_S: u64 = 156 * 3600; +/// ... and no biometric unlock in four hours. +pub(crate) const NO_BIOMETRY_LIMIT_S: u64 = 4 * 3600; +/// Longest grace period a lock request may ask for. The default comes from +/// the user's desktop setting, not from here. +pub(crate) const MAX_GRACE_S: u32 = 3600; + +/// Longest secret accepted by the draft UAPI. The enclave's own limit remains +/// unknown; macOS passes the password bytes as typed. +pub(crate) const SECRET_MAX: usize = 256; + +/// The lowest designated user the key store accepts (`DesignateUser`). +pub(crate) const MIN_UID: u32 = crate::sks::SKS_DESIGNATE_USER_MIN as u32; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum BagState { + /// Since boot (or logout) no password has opened the bag. + LockedSinceBoot, + Unlocked, + /// Locked by the screen; class keys stay until `deadline_s`. + Locking { + deadline_s: u64, + }, + Locked, +} + +impl BagState { + pub(crate) const fn uapi(self) -> u32 { + match self { + BagState::LockedSinceBoot => 0, + BagState::Unlocked => 1, + BagState::Locking { .. } => 2, + BagState::Locked => 3, + } + } +} + +/// Why Touch ID is not offered; the reason the lock screen shows. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum Refusal { + PasswordSinceBoot, + NoUnlockIn48h, + NoPasswordIn156hAndNoBiometryIn4h, + FailedMatches, +} + +impl Refusal { + pub(crate) const fn uapi(self) -> u32 { + match self { + Refusal::PasswordSinceBoot => 1, + Refusal::NoUnlockIn48h => 2, + Refusal::NoPasswordIn156hAndNoBiometryIn4h => 3, + Refusal::FailedMatches => 4, + } + } +} + +/// What the driver must tell the enclave after a policy step. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum EnclaveAction { + Nothing, + /// Send the bag lock. + LockBag, +} + +/// Host-side copy of the macOS rules for one user's bag. Times are +/// CLOCK_BOOTTIME seconds, so time asleep counts, as it does on macOS. +/// +/// Once the user bag is real the enclave enforces the part that matters (no +/// keys without the password after boot). This copy decides when to offer +/// Touch ID at all and when to send the lock. +#[derive(Clone, Copy, Debug)] +pub(crate) struct Policy { + state: BagState, + last_unlock_s: Option, + last_password_s: Option, + last_biometry_s: Option, + failed_matches: u32, +} + +impl Policy { + pub(crate) const fn at_boot() -> Policy { + Policy { + state: BagState::LockedSinceBoot, + last_unlock_s: None, + last_password_s: None, + last_biometry_s: None, + failed_matches: 0, + } + } + + pub(crate) fn state(&self) -> BagState { + self.state + } + + pub(crate) fn failed_matches(&self) -> u32 { + self.failed_matches + } + + pub(crate) fn since_password_s(&self, now_s: u64) -> Option { + self.last_password_s.map(|t| now_s.saturating_sub(t)) + } + + pub(crate) fn since_unlock_s(&self, now_s: u64) -> Option { + self.last_unlock_s.map(|t| now_s.saturating_sub(t)) + } + + /// The macOS rule set, in the order the lock screen should report it. + pub(crate) fn biometry_allowed(&self, now_s: u64) -> core::result::Result<(), Refusal> { + let Some(password) = self.last_password_s else { + return Err(Refusal::PasswordSinceBoot); + }; + if self.failed_matches >= MAX_FAILED_MATCHES { + return Err(Refusal::FailedMatches); + } + let unlock = self.last_unlock_s.unwrap_or(password); + if now_s.saturating_sub(unlock) > NO_UNLOCK_LIMIT_S { + return Err(Refusal::NoUnlockIn48h); + } + let no_password = now_s.saturating_sub(password) > NO_PASSWORD_LIMIT_S; + let no_biometry = self + .last_biometry_s + .map_or(true, |t| now_s.saturating_sub(t) > NO_BIOMETRY_LIMIT_S); + if no_password && no_biometry { + return Err(Refusal::NoPasswordIn156hAndNoBiometryIn4h); + } + Ok(()) + } + + /// The enclave accepted the password: the only way out of every refusal, + /// the lockout included (macOS sends no separate clear). + pub(crate) fn password_unlocked(&mut self, now_s: u64) { + self.state = BagState::Unlocked; + self.last_unlock_s = Some(now_s); + self.last_password_s = Some(now_s); + self.failed_matches = 0; + } + + /// A match unlocked the bag. Refused (and nothing changes) when the + /// rules do not allow Touch ID now. + pub(crate) fn biometry_unlocked(&mut self, now_s: u64) -> core::result::Result<(), Refusal> { + self.biometry_allowed(now_s)?; + self.state = BagState::Unlocked; + self.last_unlock_s = Some(now_s); + self.last_biometry_s = Some(now_s); + self.failed_matches = 0; + Ok(()) + } + + pub(crate) fn match_failed(&mut self) { + self.failed_matches = self.failed_matches.saturating_add(1); + } + + /// Screen lock. A zero grace period locks at once. + pub(crate) fn screen_locked(&mut self, now_s: u64, grace_s: u32) -> EnclaveAction { + match self.state { + BagState::Unlocked if grace_s == 0 => { + self.state = BagState::Locked; + EnclaveAction::LockBag + } + BagState::Unlocked => { + let grace = u64::from(grace_s.min(MAX_GRACE_S)); + self.state = BagState::Locking { + deadline_s: now_s.saturating_add(grace), + }; + EnclaveAction::Nothing + } + _ => EnclaveAction::Nothing, + } + } + + /// Called from a timer and before every verify. + pub(crate) fn tick(&mut self, now_s: u64) -> EnclaveAction { + match self.state { + BagState::Locking { deadline_s } if now_s >= deadline_s => { + self.state = BagState::Locked; + EnclaveAction::LockBag + } + _ => EnclaveAction::Nothing, + } + } + + /// Suspend locks at once, grace or not ("locks ... every time it goes to + /// sleep" on a Mac laptop with Touch ID). + pub(crate) fn suspending(&mut self) -> EnclaveAction { + match self.state { + BagState::Unlocked | BagState::Locking { .. } => { + self.state = BagState::Locked; + EnclaveAction::LockBag + } + _ => EnclaveAction::Nothing, + } + } + + /// Logout: as after a restart, only the password opens the bag again. + pub(crate) fn logged_out(&mut self) -> EnclaveAction { + let was_open = matches!(self.state, BagState::Unlocked | BagState::Locking { .. }); + *self = Policy::at_boot(); + if was_open { + EnclaveAction::LockBag + } else { + EnclaveAction::Nothing + } + } +} + +/// The UAPI `struct sep_keybag_unlock` (also used to create the bag). +#[repr(C)] +pub(crate) struct UnlockRequest { + pub(crate) uid: u32, + pub(crate) secret_len: u32, + pub(crate) secret: [u8; SECRET_MAX], +} + +impl Drop for UnlockRequest { + fn drop(&mut self) { + crate::image::wipe(&mut self.secret); + } +} + +/// The UAPI `struct sep_keybag_lock`. +#[repr(C)] +pub(crate) struct LockRequest { + pub(crate) uid: u32, + pub(crate) reason: u32, + pub(crate) grace_s: u32, + pub(crate) reserved: u32, +} + +pub(crate) const LOCK_REASON_SCREEN: u32 = 1; +pub(crate) const LOCK_REASON_SUSPEND: u32 = 2; +pub(crate) const LOCK_REASON_LOGOUT: u32 = 3; + +/// The UAPI `struct sep_keybag_state`. +#[repr(C)] +pub(crate) struct StateReply { + pub(crate) uid: u32, + pub(crate) bag_state: u32, + /// 0 when Touch ID may be offered, otherwise a `Refusal`. + pub(crate) biometry: u32, + pub(crate) failed_matches: u32, + /// `u64::MAX` when there was none since boot. + pub(crate) since_password_s: u64, + pub(crate) since_unlock_s: u64, + /// The enclave's own state word from GET_DEVICE_STATE. + pub(crate) enclave_lock_state: u32, + /// The enclave's lock phase from GET_DEVICE_STATE. + pub(crate) lock_phase: u32, +} + +/// The UAPI `struct sep_keybag_change`. +#[repr(C)] +pub(crate) struct ChangeRequest { + pub(crate) uid: u32, + pub(crate) old_len: u32, + pub(crate) new_len: u32, + pub(crate) reserved: u32, + pub(crate) old_secret: [u8; SECRET_MAX], + pub(crate) new_secret: [u8; SECRET_MAX], +} + +impl Drop for ChangeRequest { + fn drop(&mut self) { + crate::image::wipe(&mut self.old_secret); + crate::image::wipe(&mut self.new_secret); + } +} + +static_assert!(core::mem::size_of::() == 8 + SECRET_MAX); +static_assert!(core::mem::size_of::() == 16); +static_assert!(core::mem::size_of::() == 40); +static_assert!(core::mem::size_of::() == 16 + 2 * SECRET_MAX); + +// SAFETY: these C-layout structures contain no padding with uninitialised +// bytes, and every bit pattern of each integer/byte member is valid. +unsafe impl kernel::transmute::FromBytes for UnlockRequest {} +unsafe impl kernel::transmute::FromBytes for LockRequest {} +unsafe impl kernel::transmute::FromBytes for StateReply {} +unsafe impl kernel::transmute::FromBytes for ChangeRequest {} +unsafe impl kernel::transmute::AsBytes for StateReply {} + +const IOC_MAGIC: u32 = 0xB1; +pub(crate) const IOC_STATE: u32 = kernel::ioctl::_IOWR::(IOC_MAGIC, 0x30); +pub(crate) const IOC_UNLOCK: u32 = kernel::ioctl::_IOW::(IOC_MAGIC, 0x31); +pub(crate) const IOC_LOCK: u32 = kernel::ioctl::_IOW::(IOC_MAGIC, 0x32); +pub(crate) const IOC_CHANGE: u32 = kernel::ioctl::_IOW::(IOC_MAGIC, 0x33); +pub(crate) const IOC_CREATE: u32 = kernel::ioctl::_IOW::(IOC_MAGIC, 0x34); +pub(crate) const IOC_EXPORT: u32 = kernel::ioctl::_IOWR::(IOC_MAGIC, 0x35); +pub(crate) const IOC_IMPORT: u32 = kernel::ioctl::_IOW::(IOC_MAGIC, 0x36); + +/// The largest wrapped user bag `EXPORT`/`IMPORT` carry. A fresh variant-5 +/// user bag wraps to 1504 bytes; the bound leaves room for the class keys a +/// bag gains as it is used. +pub(crate) const BAG_BLOB_MAX: usize = 4096; + +/// The UAPI `struct sep_keybag_blob`: a user's wrapped bag. It holds no +/// secret: the enclave wrapped it under a key entangled with its own UID +/// key and the bag opens only with the user's password. +/// Only its layout is used: the ioctl handlers copy it to the heap. +#[repr(C)] +#[allow(dead_code)] +pub(crate) struct BagBlob { + pub(crate) uid: u32, + pub(crate) len: u32, + pub(crate) data: [u8; BAG_BLOB_MAX], +} +static_assert!(core::mem::size_of::() == 8 + BAG_BLOB_MAX); +// SAFETY: C layout, no padding, every bit pattern valid. +unsafe impl kernel::transmute::FromBytes for BagBlob {} +// SAFETY: as above. +unsafe impl kernel::transmute::AsBytes for BagBlob {} + +/// `struct sep_keybag_blob`'s uid and len, read from a copy of the whole +/// struct (the ioctl handlers keep the 4 KiB blob on the heap). +fn blob_uid(blob: &[u8]) -> u32 { + u32::from_ne_bytes([blob[0], blob[1], blob[2], blob[3]]) +} +fn blob_len(blob: &[u8]) -> usize { + u32::from_ne_bytes([blob[4], blob[5], blob[6], blob[7]]) as usize +} + +pub(crate) fn is_ioctl(cmd: u32) -> bool { + matches!( + cmd, + IOC_STATE | IOC_UNLOCK | IOC_LOCK | IOC_CHANGE | IOC_CREATE | IOC_EXPORT | IOC_IMPORT + ) +} + +/// Checks an unlock/create request and returns the user and the secret. +pub(crate) fn parse_unlock( + req: &UnlockRequest, + allow_empty: bool, +) -> Result<(crate::sks::DesignateUser, &[u8])> { + let len = req.secret_len as usize; + if (!allow_empty && len == 0) + || len > SECRET_MAX + || req.uid < MIN_UID + || req.uid > i32::MAX as u32 + { + return Err(EINVAL); + } + let user = crate::sks::DesignateUser::new(req.uid as i32).ok_or(EINVAL)?; + Ok((user, &req.secret[..len])) +} + +pub(crate) fn parse_lock(req: &LockRequest) -> Result<(u32, u32, u32)> { + if req.reserved != 0 || req.uid < MIN_UID || req.uid > i32::MAX as u32 { + return Err(EINVAL); + } + match req.reason { + LOCK_REASON_SCREEN | LOCK_REASON_SUSPEND | LOCK_REASON_LOGOUT => {} + _ => return Err(EINVAL), + } + Ok((req.uid, req.reason, req.grace_s.min(MAX_GRACE_S))) +} + +/// Whether this UAPI request is an explicit, no-grace key-bag lock. +/// +/// Only a screen lock may retain keys for a requested grace period. Apple +/// documents sleep and logout as immediate/password-required boundaries, and +/// the UAPI promises the same for suspend and logout. +pub(crate) fn lock_is_immediate(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])> { + let old_len = req.old_len as usize; + let new_len = req.new_len as usize; + if req.reserved != 0 + || old_len == 0 + || old_len > SECRET_MAX + || new_len == 0 + || new_len > SECRET_MAX + || req.uid < MIN_UID + || req.uid > i32::MAX as u32 + { + return Err(EINVAL); + } + let user = crate::sks::DesignateUser::new(req.uid as i32).ok_or(EINVAL)?; + Ok((user, &req.old_secret[..old_len], &req.new_secret[..new_len])) +} + +pub(crate) fn state_reply(uid: u32, p: &Policy, now_s: u64) -> StateReply { + StateReply { + uid, + bag_state: p.state().uapi(), + biometry: match p.biometry_allowed(now_s) { + Ok(()) => 0, + Err(r) => r.uapi(), + }, + failed_matches: p.failed_matches(), + since_password_s: p.since_password_s(now_s).unwrap_or(u64::MAX), + since_unlock_s: p.since_unlock_s(now_s).unwrap_or(u64::MAX), + enclave_lock_state: 0, + lock_phase: 0, + } +} + +impl crate::SepData { + fn userbag_request( + &self, + name: &'static CStr, + body: crate::image::Body, + encode: impl FnOnce( + crate::sks::Sequence, + crate::sks::ImageLen, + ) -> Result, + ) -> Result { + let img = + crate::image::build_request(self.sks_ipc_version(), self.sks_timestamp_us(), &body)?; + let len = self.sks_image_len(&img)?; + Ok(crate::SksRequest { + name, + msg: encode(self.sks_next_seq(), len)?, + img, + }) + } + + fn userbag_body<'a>(&self, name: &CStr, out: &'a crate::SksOutcome) -> Result<&'a [u8]> { + let body = self.sks_report_response(name, out); + if body.is_none() { + dev_info!(self.dev, "userbag: {}: no usable response (status {})\n", name, out.reply.status); + } + body.ok_or(EPROTO) + } + + /// Logs a reply the decoders refused: which step, its length and its + /// first bytes. Replies to these queries carry no secret. + fn userbag_refused(&self, step: &str, body: &[u8]) -> Error { + let n = body.len().min(96); + dev_info!(self.dev, "userbag: {}: reply refused, {} bytes: {:02x?}\n", step, body.len(), &body[..n]); + EPROTO + } + + fn userbag_transition( + &self, + user: crate::sks::DesignateUser, + state: TransitionState, + flags: u64, + secret: &[u8], + ) -> Result<()> { + let handle = user.special_handle().value(); + self.sks_step(crate::sks::SKS_DEVICE_STATE_NAME, |_healthy| { + self.userbag_request( + crate::sks::SKS_DEVICE_STATE_NAME, + encode_transition(handle, state, flags, secret)?, + |seq, len| Ok(crate::sks::encode_sks_device_state_transition(seq, len)), + ) + }) + .map(|_| ()) + .ok_or(EACCES) + } + + /// Opens a user's bag through the captured device-state transition. + pub(crate) fn userbag_unlock( + &self, + user: crate::sks::DesignateUser, + secret: &[u8], + ) -> Result<()> { + self.userbag_transition(user, TransitionState::Unlocked, 0, secret) + } + + pub(crate) fn userbag_lock( + &self, + user: crate::sks::DesignateUser, + screen_lock: bool, + immediate: bool, + ) -> Result<()> { + if immediate { + let handle = user.special_handle().value(); + return self + .sks_step(crate::sks::SKS_LOCK_STATE_NAME, |_healthy| { + self.userbag_request( + crate::sks::SKS_LOCK_STATE_NAME, + encode_explicit_lock(handle)?, + |seq, len| { + crate::sks::encode_sks_change_lock_state(seq, len).ok_or(EINVAL) + }, + ) + }) + .map(|_| ()) + .ok_or(EACCES); + } + + self.userbag_transition(user, TransitionState::Locked, u64::from(screen_lock), &[])?; + Ok(()) + } + + pub(crate) fn userbag_change_secret( + &self, + user: crate::sks::DesignateUser, + old: &[u8], + new: &[u8], + ) -> Result<()> { + let handle = user.special_handle().value(); + let out = self + .sks_step(crate::sks::SKS_CHANGE_SECRET_NAME, |_healthy| { + self.userbag_request( + crate::sks::SKS_CHANGE_SECRET_NAME, + encode_change_secret(handle, old, new)?, + |seq, len| Ok(crate::sks::encode_sks_change_secret(seq, len)), + ) + }) + .ok_or(EACCES)?; + let body = self.userbag_body(crate::sks::SKS_CHANGE_SECRET_NAME, &out)?; + if !decode_change_secret_reply(body) { + return Err(EPROTO); + } + Ok(()) + } + + /// The designated user bag's wrapped container (`0x02` against `-uid`), + /// for userspace to keep across reboots, as macOS saves it. + pub(crate) fn userbag_export(&self, user: crate::sks::DesignateUser) -> Result> { + let out = self.sks_send(self.sks_req_copy_keybag_special(user.special_handle())).ok_or(EACCES)?; + let wrapped = self.wrapped_from_copy_reply(&out, c"a user bag").ok_or(EACCES)?; + if wrapped.len() > BAG_BLOB_MAX { + dev_info!(self.dev, "userbag: wrapped bag is {} bytes, more than {}\n", wrapped.len(), BAG_BLOB_MAX); + return Err(E2BIG); + } + Ok(wrapped) + } + + /// Loads a saved wrapped user bag and designates it as `-uid` again (it + /// stays locked until the password unlocks it), then drops the temporary + /// handle, in macOS's order. + pub(crate) fn userbag_import(&self, user: crate::sks::DesignateUser, wrapped: &[u8]) -> Result<()> { + let handle = self.sks_load_wrapped(wrapped).ok_or(EACCES)?; + let designated = self.sks_step(crate::sks::SKS_DESIGNATE_NAME, |_healthy| { + self.userbag_request( + crate::sks::SKS_DESIGNATE_NAME, + encode_designate(handle.value(), user.special_handle().value())?, + |seq, len| crate::sks::encode_sks_designate(seq, len).ok_or(EINVAL), + ) + }); + let valid = designated.as_ref().is_some_and(|out| { + self.sks_report_response(crate::sks::SKS_DESIGNATE_NAME, out) + .is_some_and(decode_designate_reply) + }); + let _ = self.sks_send(self.sks_req_unload_keybag(handle)); + if !valid { + dev_info!(self.dev, "userbag: designating the imported bag as {} failed\n", user.special_handle().value()); + return Err(EACCES); + } + Ok(()) + } + + pub(crate) fn userbag_create( + &self, + user: crate::sks::DesignateUser, + secret: &[u8], + ) -> Result<()> { + let _healthy = self + .sks_health_check(crate::sks::SKS_CREATE_NAME) + .ok_or(EACCES)?; + let request = self.userbag_request( + crate::sks::SKS_CREATE_NAME, + encode_create(secret)?, + |seq, len| crate::sks::encode_sks_create(seq, len).ok_or(EINVAL), + )?; + let created = crate::xart_guard::keybag_create_window(|| self.sks_send(Ok(request))) + .ok_or(EACCES)?; + if created.reply.status != 0 { + return Err(EACCES); + } + let body = self.userbag_body(crate::sks::SKS_CREATE_NAME, &created)?; + let handle = decode_create_reply(body).ok_or(EPROTO)?; + + let designated = self.sks_step(crate::sks::SKS_DESIGNATE_NAME, |_healthy| { + self.userbag_request( + crate::sks::SKS_DESIGNATE_NAME, + encode_designate(handle.value(), user.special_handle().value())?, + |seq, len| crate::sks::encode_sks_designate(seq, len).ok_or(EINVAL), + ) + }); + let valid = designated.as_ref().is_some_and(|out| { + self.sks_report_response(crate::sks::SKS_DESIGNATE_NAME, out) + .is_some_and(decode_designate_reply) + }); + if !valid { + let _ = self.sks_send(self.sks_req_unload_keybag(handle)); + return Err(EACCES); + } + // Copying the wrapped bag into a per-user durable store is phase 2. + // Until then this ioctl establishes only the live designated bag. + Ok(()) + } + + /// Tells the key store a grace timer is due (`0x2f`), as the Mac's own + /// kernel does. True when the store locked `user`'s bag; false while its + /// grace period has not ended (the store names no handle). + pub(crate) fn userbag_timer_fire(&self, user: crate::sks::DesignateUser) -> Result { + let out = self + .sks_step(crate::sks::SKS_HANDLE_TIMER_FIRE_NAME, |_healthy| { + self.userbag_request( + crate::sks::SKS_HANDLE_TIMER_FIRE_NAME, + encode_timer_fire()?, + |seq, len| Ok(crate::sks::encode_sks_handle_timer_fire(seq, len)), + ) + }) + .ok_or(EACCES)?; + let body = self.userbag_body(crate::sks::SKS_HANDLE_TIMER_FIRE_NAME, &out)?; + match decode_timer_fire_reply(body) { + Some(0) => Ok(false), + Some(h) if h == user.special_handle().value() => Ok(true), + fired => { + dev_info!(self.dev, "userbag: timer fire answered {:?}, expected {}\n", fired, user.special_handle().value()); + Err(self.userbag_refused("timer fire", body)) + } + } + } + + /// The enclave's lock phase for `user`'s bag (0 unlocked, 1 locked, + /// 2 locking, 5 asleep in grace), for the key self-tests. + pub(crate) fn userbag_lock_phase(&self, user: crate::sks::DesignateUser) -> Result { + Ok(self.userbag_state(user)?.lock_phase) + } + + fn userbag_state(&self, user: crate::sks::DesignateUser) -> Result { + let handle = user.special_handle().value(); + let lock = self + .sks_step(crate::sks::SKS_LOCK_STATE_NAME, |_healthy| { + self.userbag_request( + crate::sks::SKS_LOCK_STATE_NAME, + encode_lock_query(handle)?, + |seq, len| crate::sks::encode_sks_change_lock_state(seq, len).ok_or(EINVAL), + ) + }) + .ok_or(EACCES)?; + let lock_body = self.userbag_body(crate::sks::SKS_LOCK_STATE_NAME, &lock)?; + let _lock_state = decode_lock_query_reply(lock_body).ok_or_else(|| self.userbag_refused("lock-state query", lock_body))?; + + let state = self + .sks_step(crate::sks::SKS_GET_DEVICE_STATE_NAME, |_healthy| { + self.userbag_request( + crate::sks::SKS_GET_DEVICE_STATE_NAME, + encode_get_state(handle)?, + |seq, len| Ok(crate::sks::encode_sks_get_device_state(seq, len)), + ) + }) + .ok_or(EACCES)?; + let body = self.userbag_body(crate::sks::SKS_GET_DEVICE_STATE_NAME, &state)?; + let state = decode_get_state_reply(body).ok_or_else(|| self.userbag_refused("device state", body))?; + if state.handle != handle { + dev_info!(self.dev, "userbag: device state names bag handle {}, asked about {}\n", state.handle, handle); + return Err(self.userbag_refused("device state (handle)", body)); + } + Ok(state) + } + + fn userbag_configuration(&self, user: crate::sks::DesignateUser) -> Result { + let handle = user.special_handle().value(); + let out = self + .sks_step(crate::sks::SKS_GET_CONFIGURATION_NAME, |_healthy| { + self.userbag_request( + crate::sks::SKS_GET_CONFIGURATION_NAME, + encode_get_configuration(handle)?, + |seq, len| Ok(crate::sks::encode_sks_get_configuration(seq, len)), + ) + }) + .ok_or(EACCES)?; + let body = self.userbag_body(crate::sks::SKS_GET_CONFIGURATION_NAME, &out)?; + decode_get_configuration_reply(body).ok_or_else(|| self.userbag_refused("configuration", body)) + } + + pub(crate) fn userbag_ioctl(&self, cmd: u32, arg: usize) -> Result { + crate::bio::require_admin()?; + if self.profile.key_store != crate::profile::KeyStore::Variant5 { + // The user-bag layouts were captured on the variant-5 key store + // (macOS 26 on the Neo). The 13.x key store of M1/M2-generation + // Macs has not been captured, so nothing is sent there. + dev_info!( + self.dev, + "userbag: per-user key bags are not supported on this key store yet (only variant 5 has been captured)\n" + ); + return Err(EOPNOTSUPP); + } + let user_ptr = UserPtr::from_addr(arg); + if cmd != IOC_STATE && !crate::bio::idle_for_mutation(&self.bio_session.lock()) { + return Err(EBUSY); + } + match cmd { + IOC_EXPORT => { + // The blob is 4 KiB: keep it off the kernel stack. + let (reader, mut writer) = UserSlice::new(user_ptr, core::mem::size_of::()).reader_writer(); + let mut blob = KVec::::with_capacity(core::mem::size_of::(), GFP_KERNEL)?; + reader.read_all(&mut blob, GFP_KERNEL)?; + let uid = blob_uid(&blob); + let user = crate::sks::DesignateUser::new(i32::try_from(uid).map_err(|_| EINVAL)?).ok_or(EINVAL)?; + if uid < MIN_UID { + return Err(EINVAL); + } + let wrapped = self.userbag_export(user)?; + blob.fill(0); + blob[..4].copy_from_slice(&uid.to_ne_bytes()); + blob[4..8].copy_from_slice(&(wrapped.len() as u32).to_ne_bytes()); + blob[8..8 + wrapped.len()].copy_from_slice(&wrapped); + writer.write_slice(&blob)?; + crate::bio::ok() + } + IOC_IMPORT => { + let mut blob = KVec::::with_capacity(core::mem::size_of::(), GFP_KERNEL)?; + UserSlice::new(user_ptr, core::mem::size_of::()).reader().read_all(&mut blob, GFP_KERNEL)?; + let uid = blob_uid(&blob); + let len = blob_len(&blob); + if uid < MIN_UID || len == 0 || len > BAG_BLOB_MAX { + return Err(EINVAL); + } + let user = crate::sks::DesignateUser::new(i32::try_from(uid).map_err(|_| EINVAL)?).ok_or(EINVAL)?; + if !self.xart_writable() { + return Err(EROFS); + } + self.userbag_import(user, &blob[8..8 + len])?; + crate::bio::ok() + } + IOC_CREATE | IOC_UNLOCK => { + let request: UnlockRequest = + UserSlice::new(user_ptr, core::mem::size_of::()) + .reader() + .read()?; + let (user, secret) = parse_unlock(&request, cmd == IOC_UNLOCK)?; + if cmd == IOC_CREATE { + if !self.xart_writable() { + return Err(EROFS); + } + self.userbag_create(user, secret)?; + } else { + self.userbag_unlock(user, secret)?; + } + crate::bio::ok() + } + IOC_LOCK => { + let request: LockRequest = + UserSlice::new(user_ptr, core::mem::size_of::()) + .reader() + .read()?; + 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_is_immediate(reason, grace_s))?; + crate::bio::ok() + } + IOC_CHANGE => { + let request: ChangeRequest = + UserSlice::new(user_ptr, core::mem::size_of::()) + .reader() + .read()?; + let (user, old, new) = parse_change(&request)?; + self.userbag_change_secret(user, old, new)?; + crate::bio::ok() + } + IOC_STATE => { + let mut reply: StateReply = + UserSlice::new(user_ptr, core::mem::size_of::()) + .reader() + .read()?; + let user = + crate::sks::DesignateUser::new(i32::try_from(reply.uid).map_err(|_| EINVAL)?) + .ok_or(EINVAL)?; + let state = self.userbag_state(user)?; + let configuration = self.userbag_configuration(user)?; + if state.attempt_limit != configuration.attempt_limit { + dev_info!(self.dev, "userbag: attempt limit {} in the state, {} in the configuration\n", state.attempt_limit, configuration.attempt_limit); + return Err(EPROTO); + } + reply.bag_state = match state.lock_phase { + 0 => BagState::Unlocked.uapi(), + 2 | 5 => BagState::Locking { deadline_s: 0 }.uapi(), + _ => BagState::Locked.uapi(), + }; + reply.biometry = 0; + reply.failed_matches = state.failed_attempts; + reply.since_password_s = u64::MAX; + reply.since_unlock_s = u64::MAX; + reply.enclave_lock_state = state.state_word; + reply.lock_phase = state.lock_phase; + UserSlice::new(user_ptr, core::mem::size_of::()) + .writer() + .write(&reply)?; + crate::bio::ok() + } + _ => Err(ENOTTY), + } + } +} + +#[cfg(CONFIG_APPLE_SEP_KUNIT_TEST)] +#[::kernel::macros::kunit_tests(apple_sep_userbag_policy)] +mod tests { + use super::*; + + const H: u64 = 3600; + const T0: u64 = 1000; + + fn unlocked_at(t: u64) -> Policy { + let mut p = Policy::at_boot(); + p.password_unlocked(t); + p + } + + const USER_HANDLE: i32 = -501; + const CLIENT_LE: [u8; 8] = [0x53, 0x4b, 0x53, 0x58, 0x55, 0x4e, 0x49, 0x4c]; + + #[test] + fn create_layout_golden_and_round_trip() { + let body = encode_create(b"pw").expect("create"); + let mut golden = [0u8; 48]; + golden[0..4].copy_from_slice(&4u32.to_le_bytes()); + golden[4..12].copy_from_slice(&CLIENT_LE); + golden[16..20].copy_from_slice(&(-1i32).to_le_bytes()); + golden[20..24].copy_from_slice(&2u32.to_le_bytes()); + golden[24..26].copy_from_slice(b"pw"); + assert_eq!(body.as_slice(), &golden); + assert_eq!( + decode_create(body.as_slice()), + Some(Create { secret: b"pw" }) + ); + assert_eq!( + decode_create_reply(&[4, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0]) + .unwrap() + .value(), + 7 + ); + } + + #[test] + fn designate_layout_golden_and_round_trip() { + let body = encode_designate(1, USER_HANDLE).expect("designate"); + let mut golden = [0u8; 32]; + golden[0..4].copy_from_slice(&1u32.to_le_bytes()); + golden[4..12].copy_from_slice(&CLIENT_LE); + golden[12..16].copy_from_slice(&1i32.to_le_bytes()); + golden[16..20].copy_from_slice(&USER_HANDLE.to_le_bytes()); + assert_eq!(body.as_slice(), &golden); + assert_eq!( + decode_designate(body.as_slice()), + Some(Designate { + source_handle: 1, + user_handle: USER_HANDLE + }) + ); + assert!(decode_designate_reply(&1u32.to_le_bytes())); + } + + #[test] + fn device_state_layouts_golden_and_round_trip() { + let lock = + encode_transition(USER_HANDLE, TransitionState::Locked, 1, &[]).expect("screen lock"); + let mut golden = [0u8; 32]; + golden[4..12].copy_from_slice(&CLIENT_LE); + golden[12..16].copy_from_slice(&USER_HANDLE.to_le_bytes()); + golden[16..20].copy_from_slice(&1u32.to_le_bytes()); + golden[20..28].copy_from_slice(&1u64.to_le_bytes()); + assert_eq!(lock.as_slice(), &golden); + assert_eq!( + decode_transition(lock.as_slice()), + Some(Transition { + user_handle: USER_HANDLE, + state: TransitionState::Locked, + flags: 1, + secret: &[], + }) + ); + let sleep = + encode_transition(USER_HANDLE, TransitionState::Locked, 0, &[]).expect("sleep lock"); + assert_eq!(decode_transition(sleep.as_slice()).unwrap().flags, 0); + + let unlock = + encode_transition(USER_HANDLE, TransitionState::Unlocked, 0, b"pw").expect("unlock"); + let mut golden = [0u8; 36]; + golden[4..12].copy_from_slice(&CLIENT_LE); + golden[12..16].copy_from_slice(&USER_HANDLE.to_le_bytes()); + golden[28..32].copy_from_slice(&2u32.to_le_bytes()); + golden[32..34].copy_from_slice(b"pw"); + assert_eq!(unlock.as_slice(), &golden); + assert_eq!(decode_transition(unlock.as_slice()).unwrap().secret, b"pw"); + let cancel = encode_transition(USER_HANDLE, TransitionState::Unlocked, 0, &[]) + .expect("grace cancel"); + assert!(decode_transition(cancel.as_slice()).unwrap().secret.is_empty()); + } + + #[test] + fn grace_expiry_layout_golden_and_round_trip() { + let body = encode_timer_fire().expect("timer"); + let golden = [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]; + assert_eq!(body.as_slice(), &golden); + assert!(decode_timer_fire(body.as_slice())); + let mut reply = [0u8; 24]; + reply[4..8].copy_from_slice(&USER_HANDLE.to_le_bytes()); + reply[8..12].copy_from_slice(&1u32.to_le_bytes()); + reply[16..20].copy_from_slice(&0x027e_4cb2u32.to_le_bytes()); + assert_eq!(decode_timer_fire_reply(&reply), Some(USER_HANDLE)); + // Before the grace period ends the store names no handle. + reply[4..8].copy_from_slice(&0i32.to_le_bytes()); + assert_eq!(decode_timer_fire_reply(&reply), Some(0)); + // When the bag locks, the store names it and the last word is not 0. + reply[4..8].copy_from_slice(&USER_HANDLE.to_le_bytes()); + reply[20..24].copy_from_slice(&3u32.to_le_bytes()); + assert_eq!(decode_timer_fire_reply(&reply), Some(USER_HANDLE)); + reply[0] = 1; + assert_eq!(decode_timer_fire_reply(&reply), None); + } + + #[test] + fn change_secret_layout_golden_and_round_trip() { + let body = encode_change_secret(USER_HANDLE, b"old", b"new").expect("change"); + let mut golden = [0u8; 52]; + golden[0..4].copy_from_slice(&3u32.to_le_bytes()); + golden[4..12].copy_from_slice(&CLIENT_LE); + golden[12..16].copy_from_slice(&USER_HANDLE.to_le_bytes()); + golden[16..20].copy_from_slice(&3u32.to_le_bytes()); + golden[20..23].copy_from_slice(b"old"); + golden[24..28].copy_from_slice(&3u32.to_le_bytes()); + golden[28..31].copy_from_slice(b"new"); + golden[40..44].copy_from_slice(&0xffu32.to_le_bytes()); + assert_eq!(body.as_slice(), &golden); + assert_eq!( + decode_change_secret(body.as_slice()), + Some(ChangeSecret { + user_handle: USER_HANDLE, + old: b"old", + new: b"new" + }) + ); + assert!(decode_change_secret_reply(&[ + 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ])); + } + + #[test] + fn lock_query_layout_golden_and_round_trip() { + let body = encode_lock_query(USER_HANDLE).expect("lock query"); + let mut golden = [0u8; 32]; + golden[0..4].copy_from_slice(&1u32.to_le_bytes()); + golden[4..12].copy_from_slice(&CLIENT_LE); + golden[12..16].copy_from_slice(&USER_HANDLE.to_le_bytes()); + golden[16..20].copy_from_slice(&(-1i32).to_le_bytes()); + assert_eq!(body.as_slice(), &golden); + assert_eq!(decode_lock_query(body.as_slice()), Some(USER_HANDLE)); + assert_eq!( + decode_lock_query_reply(&[1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + Some(1) + ); + + let lock = encode_explicit_lock(USER_HANDLE).expect("explicit lock"); + let mut golden = [0u8; 32]; + golden[0..4].copy_from_slice(&LOCK_STATE_VARIANT.to_le_bytes()); + golden[4..12].copy_from_slice(&CLIENT_LE); + golden[12..16].copy_from_slice(&USER_HANDLE.to_le_bytes()); + golden[16..20].copy_from_slice(&LockStateChange::Locked.wire().to_le_bytes()); + assert_eq!(lock.as_slice(), &golden); + } + + #[test] + fn get_state_layout_and_der_reply() { + let body = encode_get_state(USER_HANDLE).expect("get state"); + let mut golden = [0u8; 20]; + golden[4..12].copy_from_slice(&CLIENT_LE); + golden[12..16].copy_from_slice(&USER_HANDLE.to_le_bytes()); + assert_eq!(body.as_slice(), &golden); + assert_eq!(decode_get_state_request(body.as_slice()), Some(USER_HANDLE)); + + // Canonical SET members, sorted by key: bh, mua, sfa, sls, ss. + let der = [ + 0x31, 0x34, 0x30, 0x08, 0x0c, 0x02, b'b', b'h', 0x02, 0x02, 0xfe, 0x0b, 0x30, 0x08, + 0x0c, 0x03, b'm', b'u', b'a', 0x02, 0x01, 10, 0x30, 0x08, 0x0c, 0x03, b's', b'f', b'a', + 0x02, 0x01, 3, 0x30, 0x08, 0x0c, 0x03, b's', b'l', b's', 0x02, 0x01, 2, 0x30, 0x0a, + 0x0c, 0x02, b's', b's', 0x02, 0x04, 0x06, 0, 0, 0x15, + ]; + let mut response = crate::image::Body::new(); + response.put_u32(0).unwrap(); + response.put_blob(&der).unwrap(); + assert_eq!( + decode_get_state_reply(response.as_slice()), + Some(GetState { + state_word: 0x0600_0015, + lock_phase: 2, + failed_attempts: 3, + attempt_limit: 10, + handle: USER_HANDLE, + }) + ); + } + + #[test] + fn configuration_layout_and_der_reply() { + let body = encode_get_configuration(USER_HANDLE).expect("get config"); + let mut golden = [0u8; 16]; + golden[4..12].copy_from_slice(&CLIENT_LE); + golden[12..16].copy_from_slice(&USER_HANDLE.to_le_bytes()); + assert_eq!(body.as_slice(), &golden); + assert_eq!( + decode_get_configuration_request(body.as_slice()), + Some(USER_HANDLE) + ); + + // Canonical SET members, sorted by key: bd, epp, etp, gp, mua. + let der = [ + 0x31, 0x35, 0x30, 0x07, 0x0c, 0x02, b'b', b'd', 0x02, 0x01, 5, 0x30, 0x0a, 0x0c, 0x03, + b'e', b'p', b'p', 0x02, 0x03, 0x09, 0x3a, 0x80, 0x30, 0x0a, 0x0c, 0x03, b'e', b't', + b'p', 0x02, 0x03, 0x02, 0xa3, 0x00, 0x30, 0x08, 0x0c, 0x02, b'g', b'p', 0x02, 0x02, + 0x01, 0x2c, 0x30, 0x08, 0x0c, 0x03, b'm', b'u', b'a', 0x02, 0x01, 10, + ]; + let mut response = crate::image::Body::new(); + response.put_u32(0).unwrap(); + response.put_blob(&der).unwrap(); + assert_eq!( + decode_get_configuration_reply(response.as_slice()), + Some(Configuration { + grace_period_s: 300, + expiry_time_s: 172800, + extended_period_s: 604800, + attempt_limit: 10, + biometric_disable_limit: 5, + }) + ); + } + + #[test] + fn protocol_decoders_reject_trailing_or_nonzero_reserved_data() { + let create = encode_create(b"pw").expect("create"); + let mut bad = [0u8; 48]; + bad.copy_from_slice(create.as_slice()); + bad[47] = 1; + assert!(decode_create(&bad).is_none()); + assert!(decode_lock_query_reply(&[1, 0, 0, 0, 1, 0, 0, 0]).is_none()); + assert!(encode_transition(USER_HANDLE, TransitionState::Locked, 2, &[]).is_err()); + } + + #[test] + fn password_is_required_after_boot() { + let mut p = Policy::at_boot(); + assert_eq!(p.state(), BagState::LockedSinceBoot); + assert_eq!(p.biometry_allowed(T0), Err(Refusal::PasswordSinceBoot)); + assert_eq!(p.biometry_unlocked(T0), Err(Refusal::PasswordSinceBoot)); + assert_eq!(p.state(), BagState::LockedSinceBoot); + p.password_unlocked(T0); + assert_eq!(p.biometry_allowed(T0 + 1), Ok(())); + } + + #[test] + fn five_failed_matches_lock_out_until_the_password() { + let mut p = unlocked_at(T0); + for _ in 0..MAX_FAILED_MATCHES - 1 { + p.match_failed(); + } + assert_eq!(p.biometry_allowed(T0 + 1), Ok(())); + p.match_failed(); + assert_eq!(p.biometry_allowed(T0 + 1), Err(Refusal::FailedMatches)); + // A match cannot clear it; only the password does. + assert_eq!(p.biometry_unlocked(T0 + 2), Err(Refusal::FailedMatches)); + p.password_unlocked(T0 + 3); + assert_eq!(p.failed_matches(), 0); + assert_eq!(p.biometry_allowed(T0 + 4), Ok(())); + } + + #[test] + fn a_successful_match_resets_the_failure_count() { + let mut p = unlocked_at(T0); + p.match_failed(); + p.match_failed(); + assert_eq!(p.biometry_unlocked(T0 + 1), Ok(())); + assert_eq!(p.failed_matches(), 0); + } + + #[test] + fn no_unlock_for_48_hours_needs_the_password() { + let mut p = unlocked_at(T0); + assert_eq!(p.biometry_allowed(T0 + 48 * H), Ok(())); + assert_eq!( + p.biometry_allowed(T0 + 48 * H + 1), + Err(Refusal::NoUnlockIn48h) + ); + // Touch ID unlocks restart the 48 h window. + assert_eq!(p.biometry_unlocked(T0 + 47 * H), Ok(())); + assert_eq!(p.biometry_allowed(T0 + 94 * H), Ok(())); + } + + #[test] + fn the_156_hour_rule_needs_both_conditions() { + let mut p = unlocked_at(T0); + // Keep unlocking by finger every 3 h: the 4 h condition never holds. + let mut t = T0; + while t < T0 + 160 * H { + t += 3 * H; + assert_eq!(p.biometry_unlocked(t), Ok(())); + } + assert!(t - T0 > NO_PASSWORD_LIMIT_S); + assert_eq!(p.biometry_allowed(t + 4 * H), Ok(())); + // Over 156 h since the password and over 4 h since a finger. + assert_eq!( + p.biometry_allowed(t + 4 * H + 1), + Err(Refusal::NoPasswordIn156hAndNoBiometryIn4h) + ); + } + + #[test] + fn screen_lock_waits_for_the_grace_period() { + let mut p = unlocked_at(T0); + assert_eq!(p.screen_locked(T0, 10), EnclaveAction::Nothing); + assert_eq!( + p.state(), + BagState::Locking { + deadline_s: T0 + 10 + } + ); + assert_eq!(p.tick(T0 + 9), EnclaveAction::Nothing); + assert_eq!(p.tick(T0 + 10), EnclaveAction::LockBag); + assert_eq!(p.state(), BagState::Locked); + assert_eq!(p.tick(T0 + 11), EnclaveAction::Nothing); + } + + #[test] + fn a_match_inside_the_grace_period_keeps_the_bag_open() { + let mut p = unlocked_at(T0); + p.screen_locked(T0, 10); + assert_eq!(p.biometry_unlocked(T0 + 4), Ok(())); + assert_eq!(p.state(), BagState::Unlocked); + assert_eq!(p.tick(T0 + 20), EnclaveAction::Nothing); + } + + #[test] + fn zero_grace_and_suspend_lock_at_once() { + let mut p = unlocked_at(T0); + assert_eq!(p.screen_locked(T0, 0), EnclaveAction::LockBag); + let mut q = unlocked_at(T0); + q.screen_locked(T0, 300); + assert_eq!(q.suspending(), EnclaveAction::LockBag); + assert_eq!(q.state(), BagState::Locked); + assert_eq!(q.suspending(), EnclaveAction::Nothing); + // Touch ID may still open a bag locked by sleep. + assert_eq!(q.biometry_unlocked(T0 + 60), Ok(())); + } + + #[test] + fn grace_is_capped() { + let mut p = unlocked_at(T0); + p.screen_locked(T0, u32::MAX); + assert_eq!( + p.state(), + BagState::Locking { + deadline_s: T0 + u64::from(MAX_GRACE_S) + } + ); + } + + #[test] + fn logout_behaves_like_a_restart() { + let mut p = unlocked_at(T0); + assert_eq!(p.logged_out(), EnclaveAction::LockBag); + assert_eq!(p.biometry_allowed(T0 + 1), Err(Refusal::PasswordSinceBoot)); + assert_eq!(p.logged_out(), EnclaveAction::Nothing); + } + + #[test] + fn requests_are_checked() { + let mut req = UnlockRequest { + uid: 1000, + secret_len: 4, + secret: [0x61; SECRET_MAX], + }; + let (user, secret) = parse_unlock(&req, false).expect("valid request"); + assert_eq!(user.special_handle().value(), -1000); + assert_eq!(secret.len(), 4); + req.secret_len = 0; + assert!(parse_unlock(&req, false).is_err()); + assert!(parse_unlock(&req, true).is_ok()); + req.secret_len = SECRET_MAX as u32 + 1; + assert!(parse_unlock(&req, false).is_err()); + req.secret_len = 4; + req.uid = 0; + assert!(parse_unlock(&req, false).is_err()); + req.uid = u32::MAX; + assert!(parse_unlock(&req, false).is_err()); + + let mut lock = LockRequest { + uid: 1000, + reason: LOCK_REASON_SCREEN, + grace_s: 99999, + reserved: 0, + }; + assert_eq!( + parse_lock(&lock).ok(), + Some((1000, LOCK_REASON_SCREEN, MAX_GRACE_S)) + ); + lock.reason = 0; + assert!(parse_lock(&lock).is_err()); + lock.reason = LOCK_REASON_SUSPEND; + lock.reserved = 1; + assert!(parse_lock(&lock).is_err()); + } + + #[test] + fn only_a_screen_lock_with_grace_uses_the_grace_transition() { + assert!(!lock_is_immediate(LOCK_REASON_SCREEN, 300)); + assert!(lock_is_immediate(LOCK_REASON_SCREEN, 0)); + assert!(lock_is_immediate(LOCK_REASON_SUSPEND, 300)); + assert!(lock_is_immediate(LOCK_REASON_LOGOUT, 0)); + assert!(lock_is_immediate(LOCK_REASON_LOGOUT, 300)); + } + + #[test] + fn the_user_bag_handle_is_minus_the_uid() { + let user = crate::sks::DesignateUser::new(501).expect("user"); + assert_eq!(user.special_handle().value(), -501); + } + + #[test] + fn state_reply_reports_the_refusal() { + let p = Policy::at_boot(); + let r = state_reply(1000, &p, T0); + assert_eq!(r.bag_state, 0); + assert_eq!(r.biometry, Refusal::PasswordSinceBoot.uapi()); + assert_eq!(r.since_password_s, u64::MAX); + let q = unlocked_at(T0); + let r = state_reply(1000, &q, T0 + 5); + assert_eq!(r.bag_state, 1); + assert_eq!(r.biometry, 0); + assert_eq!(r.since_password_s, 5); + assert_eq!(r.enclave_lock_state, 0); + } +} diff --git a/include/uapi/linux/apple-sep.h b/include/uapi/linux/apple-sep.h index ece6cedd4b299..97aab2a94dbfc 100644 --- a/include/uapi/linux/apple-sep.h +++ b/include/uapi/linux/apple-sep.h @@ -230,4 +230,211 @@ struct sep_bio_rearm { */ #define SEP_BIO_REQUIRE_PASSWORD _IO(SEP_BIO_IOC_MAGIC, 0x10) +/* + * /dev/sep-key: P-256 keys created inside the SEP. + * + * Each key is made by the enclave's key store and bound to this SEP and to + * the Linux identity key bag. The private key never leaves the enclave. + * Userspace holds only an opaque handle (at most SEP_KEY_HANDLE_MAX bytes) + * that works on the Mac that made it and nowhere else; keep it like a key + * file. Signing takes a 32-byte digest (for ES256/ecdsa-sha2-nistp256, the + * SHA-256 of the message) and returns a DER ECDSA-Sig-Value. + * + * A key made without flags has no access control: whoever can open the node + * and holds its handle can sign with it. A key made with + * SEP_KEY_CREATE_BIOMETRY_CURRENT_SET (version 2) carries an access-control + * list the enclave enforces: every signature needs a Touch ID match of a + * finger enrolled when the key was made. SEP_KEY_SIGN then waits (about a minute) + * for a touch; the kernel opens a credential context, runs the match in it, + * signs and deletes the context inside that one call. Enrolling or removing a + * finger makes the key unusable (macOS .biometryCurrentSet). + * SEP_KEY_CREATE_BIOMETRY_ANY is macOS .biometryAny: the same, but any finger + * enrolled on this install matches, including ones enrolled later. At most one + * policy bit may be set; flags_supported lists the ones this kernel accepts. + * + * SEP_KEY_CREATE_USER_PRESENCE (.userPresence: Touch ID or the password) and + * SEP_KEY_CREATE_DEVICE_PASSCODE (.devicePasscode) are reserved. They need + * the enclave to check the user's own password; on Linux today the enclave's + * only passcode is a key bag secret root holds, so CREATE refuses them with + * EOPNOTSUPP rather than check a password in software. + * + * Keep a second key (a backup) wherever a biometric key is trusted, as with + * hardware security keys: no key can be exported or recovered. + * + * The node is created 0600; distributions may grant a group. Unknown 'flags' + * bits and 'reserved' fields must be zero. + * + * Errors: EINVAL for a malformed request or handle, EKEYREJECTED when the + * enclave refuses the key (a handle from another Mac or an earlier Linux + * install, or an access-control list not satisfied), EACCES when the touch + * did not match (or Touch ID is locked out), ETIMEDOUT when no finger came, + * ENODEV while the key store or Touch ID is not up, EOPNOTSUPP for a + * reserved policy, EIO for any other enclave failure. + */ +#define SEP_KEY_IFACE_VERSION 2 +#define SEP_KEY_CREATE_BIOMETRY_CURRENT_SET 0x1 +#define SEP_KEY_CREATE_BIOMETRY_ANY 0x2 +#define SEP_KEY_CREATE_USER_PRESENCE 0x4 /* reserved: EOPNOTSUPP */ +#define SEP_KEY_CREATE_DEVICE_PASSCODE 0x8 /* reserved: EOPNOTSUPP */ +#define SEP_KEY_HANDLE_MAX 2048 +#define SEP_KEY_PUB_LEN 65 /* uncompressed point, 04 || X || Y */ +#define SEP_KEY_DIGEST_LEN 32 +#define SEP_KEY_SIG_MAX 72 /* DER SEQUENCE { INTEGER r, INTEGER s } */ + +struct sep_key_info { + __u32 version; /* SEP_KEY_IFACE_VERSION */ + __u32 flags_supported; /* SEP_KEY_CREATE_* bits this kernel accepts */ + __u32 handle_max; /* SEP_KEY_HANDLE_MAX */ + __u32 reserved; +}; + +struct sep_key_create { + __u32 flags; /* in: SEP_KEY_CREATE_* */ + __u32 handle_len; /* out */ + __u8 public_key[SEP_KEY_PUB_LEN]; /* out */ + __u8 reserved[3]; /* in: 0 */ + __u8 handle[SEP_KEY_HANDLE_MAX]; /* out */ +}; + +struct sep_key_sign { + __u32 flags; /* in: 0 */ + __u32 handle_len; /* in */ + __u8 digest[SEP_KEY_DIGEST_LEN]; /* in */ + __u32 sig_len; /* out */ + __u8 signature[SEP_KEY_SIG_MAX]; /* out, zero-padded */ + __u8 handle[SEP_KEY_HANDLE_MAX]; /* in */ +}; + +struct sep_key_public { + __u32 flags; /* in: 0 */ + __u32 handle_len; /* in */ + __u8 public_key[SEP_KEY_PUB_LEN]; /* out */ + __u8 reserved[3]; /* in: 0 */ + __u8 handle[SEP_KEY_HANDLE_MAX]; /* in */ +}; + +#define SEP_KEY_GET_INFO _IOR(SEP_BIO_IOC_MAGIC, 0x20, struct sep_key_info) +#define SEP_KEY_CREATE _IOWR(SEP_BIO_IOC_MAGIC, 0x21, struct sep_key_create) +#define SEP_KEY_SIGN _IOWR(SEP_BIO_IOC_MAGIC, 0x22, struct sep_key_sign) +#define SEP_KEY_PUBLIC _IOWR(SEP_BIO_IOC_MAGIC, 0x23, struct sep_key_public) + +/* + * Test only, refused unless apple_sep.key_selftest=1, root: sign with a + * biometric key under a deliberately unsuitable credential context, to show + * the enclave refuses. Modes: 1 no context, 2 a context with no match, + * 3 a context holding only the key bag secret, 4 a deleted context, 5 one + * touch: sign in the matched context, delete it, sign again with it. + * 6 (handle ignored): make a throwaway key with the .userPresence list and + * sign with a key-bag-secret context ('result') and with a context with no + * match ('result2'). + * 7 (research): the same throwaway key signed with a context whose passcode + * credential comes from another user's key bag. In: 'enclave_status' = that + * uid, 'handle'/'handle_len' = that bag's password. Out: 'result2' = the + * verify-secret status, 'result'/'enclave_status' = the signature. + * 8 (research): a throwaway .userPresence key under the unlocked bag of the + * uid in 'enclave_status'; 'result' = signed with that bag's password + * credential (password in 'handle'), 'result2' = with no credential. + * 9 (research): as 8, 'result' = with the identity bag's secret as the + * credential, 'result2' = with the password given in 'handle'. + * 10 (research): as 8, but the kernel also locks the bag for good and signs + * with a stale, an empty and a fresh credential, then unlocks and signs + * again; the results come back in 'digest' (see key.rs). + * 11 (research): make such a key under that uid's unlocked bag and return its + * record in 'handle'. 12: sign that record with a password credential from + * the bag; the password is the NUL-padded 'digest'. + * 'result' is 0 when the enclave signed, else a negative errno. + */ +struct sep_key_selftest { + __u32 mode; + __u32 handle_len; + __u8 digest[SEP_KEY_DIGEST_LEN]; + __s32 result; + __s32 enclave_status; + __s32 result2; /* mode 5: the signature after the delete */ + __s32 enclave_status2; + __u8 handle[SEP_KEY_HANDLE_MAX]; +}; + +#define SEP_KEY_SELFTEST _IOWR(SEP_BIO_IOC_MAGIC, 0x24, struct sep_key_selftest) + +/* + * DESIGN DRAFT, not a stable ABI: the per-user key bag whose secret is the + * login password. Root only, on the re-arm node, never on /dev/sep-bio. The + * password passes through once per call and the kernel keeps no copy. + * 0x0e/0x0f are the lockout re-arm and 0x10 SEP_BIO_REQUIRE_PASSWORD; these + * use 0x30-0x36. + */ +#define SEP_KEYBAG_SECRET_MAX 256 + +struct sep_keybag_unlock { /* UNLOCK and CREATE */ + __u32 uid; + __u32 secret_len; /* UNLOCK permits zero to cancel pending lock */ + __u8 secret[SEP_KEYBAG_SECRET_MAX]; +}; + +#define SEP_KEYBAG_LOCK_SCREEN 1 /* after grace_s */ +#define SEP_KEYBAG_LOCK_SUSPEND 2 /* at once */ +#define SEP_KEYBAG_LOCK_LOGOUT 3 /* at once; the password is needed again */ + +struct sep_keybag_lock { + __u32 uid; + __u32 reason; + __u32 grace_s; + __u32 reserved; +}; + +#define SEP_KEYBAG_STATE_LOCKED_SINCE_BOOT 0 +#define SEP_KEYBAG_STATE_UNLOCKED 1 +#define SEP_KEYBAG_STATE_LOCKING 2 +#define SEP_KEYBAG_STATE_LOCKED 3 + +#define SEP_KEYBAG_BIO_ALLOWED 0 +#define SEP_KEYBAG_BIO_PASSWORD_SINCE_BOOT 1 +#define SEP_KEYBAG_BIO_NO_UNLOCK_48H 2 +#define SEP_KEYBAG_BIO_NO_PASSWORD_156H 3 +#define SEP_KEYBAG_BIO_FAILED_MATCHES 4 + +struct sep_keybag_state { + __u32 uid; /* in */ + __u32 bag_state; /* SEP_KEYBAG_STATE_* */ + __u32 biometry; /* SEP_KEYBAG_BIO_* */ + __u32 failed_matches; + __u64 since_password_s; /* ~0 when none since boot */ + __u64 since_unlock_s; + __u32 enclave_lock_state; /* raw enclave state word */ + __u32 lock_phase; /* enclave lock phase */ +}; + +struct sep_keybag_change { + __u32 uid; + __u32 old_len; + __u32 new_len; + __u32 reserved; + __u8 old_secret[SEP_KEYBAG_SECRET_MAX]; + __u8 new_secret[SEP_KEYBAG_SECRET_MAX]; +}; + +#define SEP_BIO_KEYBAG_STATE _IOWR(SEP_BIO_IOC_MAGIC, 0x30, struct sep_keybag_state) +#define SEP_BIO_KEYBAG_UNLOCK _IOW(SEP_BIO_IOC_MAGIC, 0x31, struct sep_keybag_unlock) +#define SEP_BIO_KEYBAG_LOCK _IOW(SEP_BIO_IOC_MAGIC, 0x32, struct sep_keybag_lock) +#define SEP_BIO_KEYBAG_CHANGE _IOW(SEP_BIO_IOC_MAGIC, 0x33, struct sep_keybag_change) +#define SEP_BIO_KEYBAG_CREATE _IOW(SEP_BIO_IOC_MAGIC, 0x34, struct sep_keybag_unlock) + +/* + * A user's wrapped bag, for userspace to keep across reboots (as macOS + * saves it). It holds no secret: it opens only with the user's password, + * on this Mac. EXPORT copies the designated bag; IMPORT loads a saved one + * and designates it again, locked until SEP_BIO_KEYBAG_UNLOCK. + */ +#define SEP_KEYBAG_BLOB_MAX 4096 + +struct sep_keybag_blob { + __u32 uid; + __u32 len; + __u8 data[SEP_KEYBAG_BLOB_MAX]; +}; + +#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) + #endif /* _UAPI_LINUX_APPLE_SEP_H */