Repository navigation
apple_sep: password-keyed Touch ID user key bag - #921
Conversation
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
|
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 What macOS does (our own captures 11-04 and 11-08, public Apple Platform Security Guide):
What Linux has today
Gaps
Order
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
|
Phase 2, first sends from Linux (11-08, lane SEPKEY, combined branch 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.
|
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
|
Phase 2 on the Neo: per-user key bag lifecycle works from Linux (11-08 23:57,
|
…'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
|
Cross-bag credential check (Neo, The test bag for uid 60123 is created and unlocked with its password. Then a throwaway
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
|
Keys under a user's bag: only that bag's password satisfies them (Neo, Following the macOS recipe (the ref-key's outer key-store handle is the designated user's
With the cross-bag check before it (a 60123 credential does not satisfy an identity-bag key), this is the property Still open before these policies go live on Every SEP file and the small NVMe namespaces hash the same before and after; both enrolled fingers restore on fleet36. |
|
Branch update (11-09 01:0x): |
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
|
T2: a locked user bag refuses its keys, even with the password (Neo, 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
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 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
…licon/linux into sepkey/userbag-keys
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
|
Hardware, MacBook Neo (variant-5 key store), 7.3.0-aurora-secdec1 = fleet59 (e6381bd) + this branch at 34ac189 + #1339 + #1471, one-shot boots:
|
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).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:
EOPNOTSUPPwithTODO(capture …)notes. No wire value is guessed.Tests: KUnit on arm64/qemu ran 165 tests: 161 passed, 4 failed. All 13 new tests pass. The 4 failures are
apple_sep_host_storeand fail the same way on integration/aurora.Lock (measured on the Neo): after
SEP_BIO_KEYBAG_LOCKthe key store keeps the bag in its locking phase (lock_phase2, class keys still usable) for about 40 s for every lock reason, then reports it locked (lock_phase1) 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