Skip to content

apple_sep: password-keyed Touch ID user key bag - #921

Merged
RyanTheTide merged 25 commits into
integration/aurorafrom
design/sep-user-keybag
Nov 11, 2026
Merged

RyanTheTide merged 25 commits into
integration/aurorafrom
design/sep-user-keybag

Conversation

@RyanTheTide

@RyanTheTide RyanTheTide commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Design draft. Do not merge. This is the kernel half of a proposal to make Linux Touch ID follow the macOS security model: the key bag behind Touch ID is keyed by the user's login password, it locks on screen lock (after a grace period) and on suspend, and it stays closed from boot until the first password login.

What this adds:

  • drivers/soc/apple/userbag.rs, the biometric policy macOS documents. The password is required after boot and after logout, after 48 h without an unlock, after 156 h without the password combined with 4 h without a fingerprint, and after five failed matches. It also covers the screen-lock grace period and the immediate lock on suspend. 13 KUnit tests cover it (apple_sep_userbag_policy).
  • userbag_unlock(). It reuses the bag unlock the driver already sends for the identity bag, addressed to the user's handle (-uid).
  • Draft UAPI SEP_BIO_KEYBAG_{STATE,UNLOCK,LOCK,CHANGE,CREATE} (0x10-0x14), meant for the root-only re-arm node from soc: apple: sep: Touch ID lockout that a password re-arms, matched like a macOS prompt #904.

What is deliberately not here:

  • The bag lock, the secret change and the lock-state read. Their wire format is not known yet, so they return EOPNOTSUPP with TODO(capture …) notes. No wire value is guessed.
  • Any hookup to a device node, and any change to existing behaviour. The module is not called yet.

Tests: KUnit on arm64/qemu ran 165 tests: 161 passed, 4 failed. All 13 new tests pass. The 4 failures are apple_sep_host_store and fail the same way on integration/aurora.

Lock (measured on the Neo): after SEP_BIO_KEYBAG_LOCK the key store keeps the bag in its locking phase (lock_phase 2, class keys still usable) for about 40 s for every lock reason, then reports it locked (lock_phase 1) and refuses its keys. Firing the grace timer earlier does not shorten this (the store answers with no handle), so cb12edf is reverted by 5816d7d.

Userspace counterpart: aurora-silicon/aurora-sep-userspace design/touchid-userbag.

🤖 Generated with Claude Code

RyanTheTide and others added 2 commits November 4, 2026 09:57
Design prototype for a macOS-style Touch ID security model: a per-user
key bag whose secret is the login password, locked on screen lock after
a grace period and on suspend, and closed from boot until the first
password unlock.

This adds:
- userbag.rs: the biometric policy macOS documents (password after boot
  and logout, after 48 h without an unlock, after 156 h without the
  password and 4 h without a fingerprint, after five failed matches),
  the screen-lock grace period and the suspend lock, with KUnit tests;
- the unlock request, which reuses the identity-bag unlock the driver
  already sends, addressed to the user's handle (-uid);
- draft UAPI for the root-only node: state, unlock, lock, change, create.

The lock, the secret change and the lock-state read are stubs that
return EOPNOTSUPP: their wire format is not known yet. Nothing is wired
to a device node, and no existing behaviour changes.

Assisted-by: LLM
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add strict SKS request encoders and response decoders for the per-user keybag operations observed on the variant-5 key store. Cover every captured layout with synthetic golden vectors and round-trip KUnit tests, expose live state through the draft keybag ioctls, and wipe password-bearing ioctl copies and request images.

Assisted-by: LLM
Add a character device that creates P-256 keys inside the Secure
Enclave's key store and signs digests with them. Each key is an enclave
ref-key bound to this SEP and to the Linux identity key bag; the private
key never leaves the enclave. Userspace gets an opaque handle (a short
versioned header plus the enclave's wrapped record) that only this Mac
can use, and the public point.

ioctls (magic 0xB1, nr 0x20-0x23): SEP_KEY_GET_INFO, SEP_KEY_CREATE,
SEP_KEY_SIGN (ECDSA-P256 over a caller-supplied 32-byte digest, DER
signature) and SEP_KEY_PUBLIC (point from a handle, no enclave round
trip). Each enclave operation loads and unlocks the identity key bag the
way a trusted-key unseal does and unloads it afterwards.

Version 1 has no access control: every flags field must be zero and is
reserved for an enclave-enforced policy. A handle the enclave does not
accept (another Mac, another key bag) fails with EKEYREJECTED, so the
ref-key sign helper now reports the enclave's refusal separately from a
missing or malformed reply.

KUnit covers the handle format and the reply bounds.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
Add SEP_KEY_CREATE_BIOMETRY_CURRENT_SET (interface version 2). Such a
key's record carries the access-control list macOS gives a Secure
Enclave key made with .biometryCurrentSet: signing, key agreement and
derivation need a biometric credential for this catacomb and enrolled
set; delete and attest are free. The kernel reads the catacomb id and
enrolled-set hash with SBIO 0x33 and 0x36 (read only) and puts the list
in the create request (`ed`); the enclave keeps it in the record and
enforces it on every later operation. Adding or removing a finger
changes the hash and makes the key unusable.

SEP_KEY_SIGN on such a key waits up to 30 s for a touch. Inside that one
call the kernel opens a fresh credential context, evaluates the signing
constraint on it (ACM command 0x20, best effort), runs one Touch ID
match in it through the normal verify path (ACM match mode, MATCH_CANCEL,
the same lockout counting and template persistence as any verify),
signs with the context (`acmh`) and deletes the context before
returning. No handle to the context ever reaches userspace, so a
satisfied context cannot outlive the signature it was opened for: every
signature needs its own touch. While it runs, the verify belongs to the
ioctl, not to /dev/sep-bio's session; it serialises with that session's
work on the catacomb writer lock and never reuses its open prompt.

SEP_KEY_SELFTEST (test only: apple_sep.key_selftest=1 and root) signs a
biometric key under deliberately unsuitable contexts: none, one with no
match, one holding only the key-bag secret (the Linux passcode), a
deleted one, and, with one touch, the matched context after it was
deleted. It reports the enclave's answer, so the refusal is seen to
come from the enclave.

The request layouts follow our own trace of macOS 26.6.2 creating and
using such keys through public APIs.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
One verify capture already waits up to a minute for a finger, so the
30 s overall budget only ever allowed one capture. Make the budget a
minute and say "about a minute" in the UAPI comment and the log.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
…icies

Add SEP_KEY_CREATE_BIOMETRY_ANY, macOS's .biometryAny: the key's
access-control list names the Touch ID catacomb (pbioc) but no enrolled-set
hash, so any finger enrolled on this install matches, including fingers
enrolled after the key was made. SIGN runs the same touch-gated path as a
biometry-current-set key; handle flag 0x02 (with 0x01) picks the matching
preflight constraint. The enclave enforces the list in the record either way.

Reserve SEP_KEY_CREATE_USER_PRESENCE (.userPresence, Touch ID or the
password) and SEP_KEY_CREATE_DEVICE_PASSCODE (.devicePasscode). Both need
the enclave to check the user's own password. On Linux the only passcode the
enclave knows today is the identity key bag's secret, which root holds, so
such a key would be root-satisfiable. CREATE refuses them with EOPNOTSUPP;
they are not emulated in software. At most one policy bit may be set.

Add self-test mode 6 (key_selftest=1, root, no finger): a throwaway key
with the .userPresence list as macOS writes it (cpo
"DeviceOwnerAuthentication"), signed with a key-bag-secret context and with
an unsatisfied context, to measure what the enclave enforces for that
policy on Linux.

KUnit: policy parsing, handle flags, the biometry-any and presence lists.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
@RyanTheTide

Copy link
Copy Markdown
Contributor Author

Plan 11-08: one SEP key hierarchy, as macOS (Ryan's direction, 11-08 ~22:00)

Target: a system key bag plus one key bag per Unix user. A user's bag is created and unlocked only with that user's password, which goes to the enclave as typed; the enclave entangles it with the hardware UID key, so root never holds the secret. Touch ID works only after the first password unlock since boot. The disk key is released only by a password at boot, never by Touch ID. The same per-user bag is what makes macOS's .userPresence (Touch ID or password) and .devicePasscode key policies root-proof on /dev/sep-key (#1196).

What macOS does (our own captures 11-04 and 11-08, public Apple Platform Security Guide):

  • Each user has a bag, designated as enclave user -uid; created with the password (0x01 variant 4), unlocked at login (0x04/0x18 state 0 + password), locked at screen lock after the grace period and at sleep (0x18 state 1, host timer 0x2f), re-keyed only by a password change that knows the old password (0x07).
  • The enclave keeps the failed-password counter (sfa, max mua 10) and the 48 h rule (etp in the bag's configuration).
  • Key ACLs name biometry (cbio{pbioc[,pbioh]}) or the owner policy (cpo "DeviceOwnerAuthentication"); a password credential enters an ACM context through verify-secret (0x21) against the user's bag.
  • FileVault's volume key is handled by separate requests (0x41/0x43/0x47) that carry the password; Touch ID never releases it.

What Linux has today

  • One machine identity bag (/var/lib/aurora-sep-keybag.bin). Its random secret is stored on disk and root holds it. It is designated as enclave user 1000 for every account (3 prints per Mac in total).
  • /dev/sep-key keys live under that bag. Biometry policies (current-set, any) are enclave-enforced and hardware-proven (one touch, one signature). The password policies are reserved (EOPNOTSUPP) because the only passcode the enclave knows is that root-held secret.
  • This draft: the macOS biometric rules (userbag.rs, 13 KUnit), draft UAPI; the wire layouts captured but not yet encoded.

Gaps

  1. Per-user bags on Linux: create, unlock, designate -uid, per-uid Catacomb, never-stored password (DESIGN X5, X12).
  2. Keys bound to a user bag; what the enclave does while it is locked (protection classes) -> Daybreak t-5756.
  3. The password credential path for cpo/passcode ACLs, and whether a credential from another bag satisfies it; lockout semantics per policy -> Daybreak t-5753.
  4. "Touch ID only after the first unlock" and the 48 h / 156 h+4 h rules: which side enforces them (X7) -> t-5756 item 2.
  5. Volume key: password-only release at boot through the enclave, compatible with a block-level (non-LUKS) volume key -> Daybreak t-5757. The block-level encryption itself is DJ's work; this side only provides the key release.
  6. Lock events (X4b), the grace timer vs gp (X6), and the per-user / per-Mac print limits (X8): still open from t-4607.

Order

  1. Kernel encoders and KUnit for the captured layouts (0x01 v4, 0x18, 0x2f, 0x04 -1, 0x07, 0x19/0x23 DER parse). No hardware. Started 11-08 (lane SEPKEY).
  2. M1 Pro, throwaway test uid and test password only: create a user bag, unlock it, designate it, lock it, change its secret. Then create a /dev/sep-key key under it and prove the policies: a .userPresence key signs with that user's password credential, and is refused with the identity bag secret, a wrong password, or no credential.
  3. Then SEP_KEY_CREATE_USER_PRESENCE / DEVICE_PASSCODE go live (bound to the caller's own bag); the default policy in the OpenSSH provider becomes presence, as on macOS.
  4. PAM/logind wiring (pam_aurora_userbag, lockd), migration of the shared bag via 0x07, and a password-first Touch ID after restart (lane TIDMSG has already made that the default).
  5. Volume key release at boot by password (after t-5757), handed to the block-level volume work as a key-release interface.
  6. Distros and SELinux, then default on, once proven on all four Macs.

No software-checked password stands in for the enclave anywhere, and no secret derived from the password is stored on disk.

# Conflicts:
#	include/uapi/linux/apple-sep.h
The draft UAPI puts SEP_BIO_KEYBAG_* on the root-only re-arm node so that
fprintd, which holds /dev/sep-bio, never handles a login password, but
the dispatch sat in bio_ioctl. Move it to rearm_ioctl and drop it from
/dev/sep-bio.

The user-bag layouts were captured on the variant-5 key store (macOS 26,
Neo generation). On the 13.x key store of M1/M2-generation Macs nothing
is sent; say so in the log before returning EOPNOTSUPP.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
The first Linux user-bag create on the Neo worked, but the state query
after it returned EPROTO with nothing in the log. Log which step refused
which reply (length and first bytes; these replies carry no secret), a
bag-handle mismatch, and an attempt-limit mismatch.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
@RyanTheTide

Copy link
Copy Markdown
Contributor Author

Phase 2, first sends from Linux (11-08, lane SEPKEY, combined branch sepkey/userbag-keys)

Throwaway test uid 60123 with a generated test password. Read-only rollback copies and hashes were taken before each run; the run stops on the first unexpected status; the Mac returns to its default kernel afterwards.

  • c15306a: the SEP_BIO_KEYBAG_* ioctls now go only to the root-only re-arm node, as the UAPI says (the draft had them on /dev/sep-bio, which fprintd holds). On the 13.x key store (M1/M2 generation) the driver logs that user bags are not supported there yet and returns EOPNOTSUPP; only the variant-5 layouts were captured.
  • M1 Pro (13.5 key store): EOPNOTSUPP, nothing sent.
  • Neo (variant 5), 7.3.0-aurora-ubag2:
    1. state query before create -> EACCES (no bag), as expected;
    2. create (0x01 variant 4 with the test password, then designate -60123) -> OK. This is the first user-bag create from Linux;
    3. state query after create -> EPROTO: the driver refused the reply, so the run stopped there.
      After a reboot the state query is EACCES again: the bag was live only. Every SEP file and the small NVMe namespaces hash the same before and after, and both enrolled fingers restore.
  • 55c5101: log which reply a decoder refuses (step, length, first bytes; these replies carry no secret). Next: the same steps with that kernel, then lock/unlock, change secret, keys under the bag.

On the Neo the key store answers a device-state request (0x19) with u32 0
followed by the DER dictionary as a padded blob. The draft expected the
blob first and refused the reply. Read the observed order, and keep the
golden vectors in that order.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
@RyanTheTide

Copy link
Copy Markdown
Contributor Author

Phase 2 on the Neo: per-user key bag lifecycle works from Linux (11-08 23:57, 7.3.0-aurora-ubag4 = sepkey/userbag-keys 09752b7, throwaway uid 60123, generated test password, one-shot boot)

  • ubag3 (55c5101) logged the reply the decoder refused. The device-state reply (0x19) is u32 0 followed by the DER dictionary as a padded blob; the draft expected the blob first. Fixed in 09752b7, and the golden vectors now use the observed order.
  • ubag4, 16/16:
step result
state before create EACCES (no bag)
create (0x01 v4 + designate -60123) OK
state locked until the first password unlock (word 0x1, phase 1), as at boot
unlock, wrong password EACCES, enclave failed-attempt count 1
unlock, password OK (0x6000014, phase 0, count back to 0)
lock (suspend: at once) OK (phase 2)
unlock, password OK
change secret (0x07, old -> new) OK
lock, then unlock with the old password EACCES
unlock, new password OK (0x6000004)
lock (logout) OK
  • After a reboot the bag is gone (the live bag is not persisted yet: no durable per-user store in phase 2). Ryan's two enrolled fingers restore on every boot. Every SEP file and the small NVMe namespaces hash the same before and after.
  • So the enclave holds the password-keyed bag, counts failures and refuses the old password after a change. The secret is never stored on Linux.
  • Next: a durable per-user store (copy 0x02 + reload at login), then keys bound to the user's bag and .userPresence/.devicePasscode on /dev/sep-key (soc: apple: sep: add /dev/sep-key for P-256 keys that stay in the SEP #1196), proven root-proof the same way.

…'s bag

Research probe (key_selftest=1, root, no finger): sign the throwaway
.userPresence key, made under the identity bag, with a context whose
passcode credential came from verify-secret on another user's key bag.
It shows whether the enclave binds a passcode credential to the bag the
key lives under, which decides how /dev/sep-key keys must be bound to
per-user bags before .userPresence can go live.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
@RyanTheTide

Copy link
Copy Markdown
Contributor Author

Cross-bag credential check (Neo, 7.3.0-aurora-ubag5 = ccdbdc4, self-test 7, no finger, 11-09 00:33)

The test bag for uid 60123 is created and unlocked with its password. Then a throwaway .userPresence key (made under the Linux identity bag) is signed with an ACM context whose passcode credential comes from verify-secret on the 60123 bag:

  • with a wrong password: verify fails (-5), and the signature is refused (-8);
  • with the right password: verify succeeds (0, credential added), but the signature is still refused (-8).

So a passcode credential from one user's bag does not satisfy a key under another bag. Root cannot satisfy someone else's password-guarded key by making a bag of its own. Caveat: the context was created for session uid 60123, so this may be a context-user check rather than a bag check. A key created under the 60123 bag will tell them apart; that needs the create parameter that selects the bag (Daybreak SEPKEY-E, in progress).

Every SEP file and the small NVMe namespaces hash the same before and after. Both enrolled fingers restore on fleet35.

Research probes (key_selftest=1, root, no finger) for binding
/dev/sep-key keys to per-user key bags, per the macOS recipe: the key is
created with the designated user's special handle (-uid) as the outer
key-store handle, and every later operation repeats it.

8: a throwaway .userPresence key under the unlocked bag of a given uid,
signed with a passcode credential from that same bag, and with no
credential. 9: the same key signed with the Linux identity bag's secret
(what root holds) and with a given (wrong) password.

ref-key create is split so the create request can name either a loaded
bag or a designated user's handle.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
@RyanTheTide

Copy link
Copy Markdown
Contributor Author

Keys under a user's bag: only that bag's password satisfies them (Neo, 7.3.0-aurora-ubag6 = ff19f17, self-tests 8/9, no finger, 11-09 00:44)

Following the macOS recipe (the ref-key's outer key-store handle is the designated user's -uid; ag/acmh do not pick the bag), a throwaway .userPresence key is created under the unlocked test bag of uid 60123 (class 9). Then:

credential in the sign context verify-secret signature
verify-secret on the same bag with its password 0 SIGNED
none - refused (-8)
verify-secret on the Linux identity bag with its stored secret (what root holds) 0 refused (-8)
verify-secret on the same bag with a wrong password -5 refused (-8)

With the cross-bag check before it (a 60123 credential does not satisfy an identity-bag key), this is the property .userPresence/.devicePasscode need on Linux: the password path is enforced by the enclave against the user's own bag, and root's stored secret does not open it.

Still open before these policies go live on /dev/sep-key (#1196): a durable per-user store (the bag is live-only so far), the login/PAM path that hands the password to the kernel once, binding CREATE/SIGN to the caller's bag, the locked-bag behaviour of class 9 (one observation: a fresh same-bag password credential still signs after the logout lock), and the "Touch ID only after the first unlock" gate.

Every SEP file and the small NVMe namespaces hash the same before and after; both enrolled fingers restore on fleet36.

@RyanTheTide

Copy link
Copy Markdown
Contributor Author

Branch update (11-09 01:0x): design/sep-user-keybag was fast-forwarded to ff19f17, the combined branch from the comments above. That adds the merge of #1196 (sepkey/sep-key-chardev 50ba8b0) and the phase 2 fixes and self-tests: c15306a (re-arm node only; explicit 13.x EOPNOTSUPP), 55c5101 (log refused replies), 09752b7 (DER reply order), ccdbdc4 and ff19f17 (self-tests 7-9: keys under a user's bag). No force push. This PR now stacks on #1196, which merges first. KUnit apple_sep* 213/213, W=1 clean. Still a draft: neither merges until a fleet kernel carries them and a second Mac passes.

Research probe (key_selftest=1, root, no finger) for the property that
matters most before per-user bags carry real keys: a key under a user's
bag must not be usable while the bag is locked unless the user's
password is presented. Sign with a password credential while unlocked,
lock the bag for good, sign with that now stale credential, with none and
with a fresh password credential, unlock and sign again; return each
enclave status and the lock phases.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
…uses

The locked-bag self-test stopped with EPROTO after the grace-expiry
timer fire (0x2f): its reply did not match the layout the draft checks,
and nothing was logged. Log it, and let self-test 10 measure the bag's
lock phase and the signatures even when that answer is not understood.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
…s it

The key store answers the grace-timer fire (0x2f) with u32 0, the handle
whose grace period ended (0 while none has), the timer kind, u32 0, an
opaque token and u32 0. The draft read the handle first and refused it.
Split the fire into userbag_timer_fire(), which tells a locked bag from
a grace period still running; a lock that wants the expiry now returns
EAGAIN in the latter case. Self-test 10 now fires the timer every 5 s
until the store locks the bag (at most 330 s) before it measures.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
…nter

When the store locks the bag at grace expiry it names the bag and puts 3
in the last word (0 while grace ran). Treat that word as opaque instead
of requiring 0.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
@RyanTheTide

Copy link
Copy Markdown
Contributor Author

T2: a locked user bag refuses its keys, even with the password (Neo, 7.3.0-aurora-ubag9 = cc1836d, self-test 10, throwaway uid 60123, 11-09 01:21)

After a screen lock, the host sends the grace-timer fire every 5 s. The store locks the bag about 10 s after the lock (phase 1), the same grace period macOS shows. Results for a class-9 .userPresence key under that bag:

bag state credential signature
unlocked the bag's password credential SIGNED
locked the same credential, now stale refused (-2)
locked none refused (-2)
locked a fresh password credential from that bag refused (-2)
unlocked again (with the password) fresh password credential SIGNED

During the grace period (ubag8) the bag is "locking" (phase 2) and its keys still sign, as on macOS. The status for a locked bag is -2, distinct from the -8 an unsatisfied ACL gives. A password credential on its own does not open a key of a locked bag: the bag has to be unlocked, which needs the password.

The grace-timer answer is u32 0, handle (0 while grace runs, -uid once locked), kind 1, 0, token, counter; decoders fixed in cc1836d and the commit after it. Every SEP file and the small NVMe namespaces hash the same before and after, and both enrolled fingers restore.

Next: the durable per-user store (save the wrapped bag, reload it at login), then the PAM path.

A per-user bag lives only until reboot unless its wrapped container is
kept. macOS saves it and loads it again at login; the container holds
no secret and opens only with the user's password on this Mac.

Add SEP_BIO_KEYBAG_EXPORT (0x15: copy the designated -uid bag) and
SEP_BIO_KEYBAG_IMPORT (0x16: load a saved bag, designate it as -uid
again, drop the temporary handle), both root-only on the re-arm node, so
userspace keeps the file. Self-tests 11/12 make a key under a user's bag
and use its saved record later, to prove keys survive with the bag.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
…n/linux into sepkey/sep-key-chardev

# Conflicts:
#	include/uapi/linux/apple-sep.h
integration/aurora now uses nr 0x10 for SEP_BIO_REQUIRE_PASSWORD. Move
the draft key-bag ioctls out of its way.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
A fresh variant-5 user bag wraps to 1504 bytes, so EXPORT failed with
E2BIG against the 1024-byte SEP_KEYBAG_BLOB_MAX. Raise the bound to
4096 and keep the blob on the heap in the EXPORT/IMPORT handlers
instead of reading the whole struct onto the kernel stack.

Assisted-by: LLM
After the lock transition the key store holds the bag in its locking
phase, class keys still usable, until the grace timer fires. The LOCK
ioctl fired it only for a screen lock with no grace, so a logout or
suspend lock left the bag's keys open: on the Neo a key sealed under a
user bag still unsealed after SEP_BIO_KEYBAG_LOCK with the logout
reason, and was refused once the timer was fired. Fire it for every
lock but a screen lock with a grace period.

Assisted-by: LLM
Firing the grace timer does not end the key store's locking phase early:
on the Neo the store answers the timer with no handle until its own
deadline (about 40 s after the lock transition, for every lock reason),
so the change only turned logout and suspend locks into EAGAIN. The bag
reaches the locked phase by itself at that deadline, and a key sealed
under it is refused from then on.

Assisted-by: LLM
@RyanTheTide

Copy link
Copy Markdown
Contributor Author

Hardware, MacBook Neo (variant-5 key store), 7.3.0-aurora-secdec1 = fleet59 (e6381bd) + this branch at 34ac189 + #1339 + #1471, one-shot boots:

  • Two-boot durable test (throwaway uid 60123): create, unlock, a key under the bag signs with the bag's password credential, EXPORT 1504 B, logout lock; after a reboot: no live bag, the saved key is refused before IMPORT, a tampered IMPORT is refused, IMPORT of the saved bag, a wrong password is refused and counted, the locked bag refuses the saved key, unlock, the saved key signs, a wrong-password credential is refused. Ran B twice across reboots: all pass. SEP host files and the SEP partitions hash identical before and after.
  • 34ac189 (explicit locks): a logout lock leaves the bag locked at once (state polled 0 s after the lock; it stayed in the locking phase for ~41 s before), and an unseal under the locked bag is refused until the password unlocks it.
  • KUnit sep.kunitconfig: 452 passed, 2 skipped.
  • 0 failed units, SELinux enforcing, 0 AVC.

@RyanTheTide RyanTheTide changed the title [DESIGN DRAFT] apple_sep: password-keyed Touch ID user key bag apple_sep: password-keyed Touch ID user key bag Nov 11, 2026
@RyanTheTide
RyanTheTide marked this pull request as ready for review November 11, 2026 06:40
@RyanTheTide
RyanTheTide merged commit b6ae14d into integration/aurora Nov 11, 2026
4 checks passed
@RyanTheTide
RyanTheTide deleted the design/sep-user-keybag branch November 11, 2026 06:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant