Skip to content

soc: apple: sep: seal a disk-encryption secret to a user's key bag, per OS install, unlockable from the initrd - #1339

Merged
RyanTheTide merged 15 commits into
integration/aurorafrom
sepfde/fde-seal
Nov 11, 2026
Merged

RyanTheTide merged 15 commits into
integration/aurorafrom
sepfde/fde-seal

Conversation

@RyanTheTide

@RyanTheTide RyanTheTide commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

FileVault ties a volume key to the user's password through the Secure Enclave. The password goes to the enclave, which checks it, entangles it with its UID key, counts failed attempts and then releases the key. The per-user key bags give Linux the same gate. This PR adds the key release, binds each release to one OS install, and adds the key-store-only initrd attach so the root volume can unlock this way.

FDE key release. Three ioctls on the root-only re-arm node, variant-5 key store only. They are built from the existing ref-key and user-bag primitives, with no new enclave wire protocol.

  • SEP_BIO_FDE_CREATE (0x37): creates a P-256 sealing key under the designated, unlocked bag of a uid (class 9, no ACL). The enclave uses it only while that bag is unlocked.
  • SEP_BIO_FDE_SEAL (0x38): ECIES-encrypts up to 64 bytes to that key, then has the enclave open the result once before returning it.
  • SEP_BIO_FDE_UNSEAL (0x39): opens a sealed blob. The enclave does this only while the bag is unlocked, and only on the Mac that made the key.

Per-OS-install binding, like FileVault's per-volume key:

  • The sealed plaintext carries the 16-byte OS identity the key store serves: the os_uuid_hi/os_uuid_lo pin, else /chosen/apfs-preboot-uuid.
  • An unseal under another identity returns EXDEV. A kernel with no identity returns ENOKEY.

Key-store-only initrd attach (apple_sep.keystore_only):

  • 1: the initrd attaches the SEP key store for trusted keys only. Touch ID and the host-state store wait until the real root writes complete_attach.
  • 2: as 1, plus /dev/sep-bio-rearm serves a password unlock of a per-user key bag in the initrd. Only state, import, unlock, lock and FDE unseal are served. Create, change, export and FDE create/seal return EAGAIN until complete_attach.
  • In mode 2, xART follows xart_writes only while the xART guard is off. With a guard, the bag requests fail with EROFS and the volume falls back to its passphrase or recovery key.

Userspace (aurora-sep-userspace aurora-sep-fde and its LUKS2 token) keeps the wrapped bag, the key record and the sealed key-slot secret in an aurora-sep LUKS2 token.

Tests

  • KUnit drivers/soc/apple/sep.kunitconfig on this branch: 509 tests, 507 pass, 2 skipped (not SEP tests).
  • tools/aurora-sep/tests source checks pass (the ref-key fixture now covers the key-store-only phase).
  • Neo (t8140), test kernel = this branch + the boot chain record driver, fleet release config, on a LUKS2 container file with a throwaway uid:
    • enrol a password authority; the right password opens, a wrong one is refused and counted by the enclave (failed attempts 1);
    • with the bag locked the enclave refuses the unseal (it reports the bag locked 0 s after the lock); after the password unlocks it, the unseal opens;
    • a recovery key opens through the enclave, and its offline keyslot opens without it;
    • after a reboot the bag reloads from the token: wrong refused, right opens;
    • one boot with a different os_uuid pin: the unseal is refused with EXDEV ("belongs to another OS install"), and back on the pinned install it opens;
    • SELinux enforcing, 0 AVCs, 0 failed units.

Assisted-by: LLM

Keep host state, identity-index loading and biometric device publication off
until the real root completes the original attach. Open xART read-only in the
initrd, suppress keybag and ref-key provisioning, and cache trusted-key material
without retaining initrd file handles. Refuse an unseal when persistent-state
writes were denied, while preserving the real root's boot-policy window.

Add the root-only complete_attach attribute and let load-driver finish an
initrd-loaded module without another SEP registration. Cover completion
rejection, retries and idempotence in KUnit, plus production cache, refusal and
loader paths in buildbox host tests.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
SYNC_FILE selects the hidden DMA_SHARED_BUFFER symbol required by SEP.
Setting the hidden symbol alone is ignored by Kconfig.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
FileVault ties the volume key to the user's password through the
enclave: the password goes to the enclave, which entangles it with its
UID key and counts failed attempts, and only then releases the key. The
per-user bags give Linux the same gate; this adds the key release.

SEP_BIO_FDE_CREATE (0x37) makes a P-256 ref-key under the designated,
unlocked bag of a uid (protection class 9, no access-control list), so
the enclave uses it only while that bag is unlocked. SEP_BIO_FDE_SEAL
(0x38) ECIES-encrypts up to 64 bytes to it and has the enclave open the
result once before returning it. SEP_BIO_FDE_UNSEAL (0x39) asks the
enclave to open a sealed blob. Root only, on the re-arm node, variant-5
key store only, like the other user-bag ioctls. A LUKS2 token keeps the
wrapped bag, the record and the sealed key-slot secret in the header,
none of which opens without the password on this Mac.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
# Conflicts:
#	drivers/soc/apple/userbag.rs
struct sep_fde_seal is 2.4 KiB and struct sep_fde_key 2 KiB. Copy them
to the heap in the FDE_CREATE/SEAL/UNSEAL handlers, as EXPORT/IMPORT do
for the wrapped bag. The SEAL/UNSEAL buffer carries the volume secret
and is wiped when it is dropped.
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

RyanTheTide commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Hardware rows on the Neo (variant-5 key store), kernel 7.3.0-aurora-sepfde3 (5c7ae60c; the tree equals the current head 251b5682 apart from the reverted lock change, which these rows do not use), tool and plugin from aurora-sep-userspace#21 (5067539). Every row runs on a 64 MiB LUKS2 container file in /var/tmp, with throwaway uids 60250/60251 and generated passwords. SELinux enforcing.

Row Result
enroll PASS. Bag 1504 B, FDE record 258 B, sealed secret 145 B. The right password opens keyslot 1. A wrong one is refused and the enclave counts it (failed attempts 1).
gate PASS. Unlock the bag, lock it, and the unseal opens until the key store reports lock_phase 1, 41 s later. From then on the SEP refuses it (ref-key unseal failed (status -2)). After the password unlocks the bag, the same unseal opens.
reboot PASS. After a real reboot the bag is not loaded. It is imported from the token; a wrong password is refused and the right one opens.
recovery PASS. A recovery-key bag (uid 60251) opens keyslot 2.
change PASS. The new password opens keyslot 1 and the old one is refused.
replay FAIL. After a reboot, the pre-change header (the old wrapped bag) with the old password opens keyslot 1. With that old bag loaded, the current header with the new password is refused until the next boot, where it opens again. IMPORT has no rollback protection.
reset PASS. The recovery key sets a new password, which opens keyslot 3; the previous password is refused.
other OS lane FAIL. One boot with apple_sep.os_uuid_hi/lo set to other values still opens with the password, so the bag and the FDE key are not bound to the OS identity.

The 1504-byte bag needed #921's 4 KiB SEP_KEYBAG_BLOB_MAX (6177e64). Against the old 1024-byte bound, EXPORT failed with E2BIG. This branch now merges 6177e64 and moves the FDE requests to the heap (a138583). KUnit sep.kunitconfig on 5c7ae60: 447 run, 445 pass, 2 skipped.

Still open:

  • Rollback protection for imported bags, so an old header and an old password stop working after a change.
  • OS-identity binding.
  • An ioctl to delete a bag. An enrol that fails after CREATE leaves the bag in the enclave until reboot, and re-creating it with another password returns EACCES.

@RyanTheTide

Copy link
Copy Markdown
Contributor Author

Cross-Mac row on the J616s, kernel 7.3.0-aurora-sepfdej1 (fleet46 base plus this branch). The Neo's container file and its current password are presented to the J616s enclave. Its Secure Enclave does not accept the token (EPERM on import), and the volume does not open: the bag and the sealing key work only on the Mac that made them. PASS.

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

A root volume sealed to a user's password (SEP_BIO_FDE_*) has to open
before the real root is mounted, the way FileVault asks for the password
once after a cold boot. keystore_only=1 attaches the key store for trusted
keys only and keeps xART read-only, so a saved user bag can neither be
loaded nor unlocked there.

keystore_only=2 adds that unlock to the initrd phase:

- /dev/sep-bio-rearm is published as soon as the key store is open. Until
  complete_attach it serves only what a password unlock needs: state,
  import, unlock, lock and FDE unseal. Create, change, export and the FDE
  create/seal requests return EAGAIN; enrolment stays on the real root.
- Loading and unlocking a bag needs enclave writes to the gigalocker, so
  xART follows xart_writes in this mode, but only while the xART guard is
  off: the guard's record list lives in host files on the real root, which
  the initrd must not write. With a guard, xART stays read-only, the bag
  requests fail with EROFS and the volume falls back to its passphrase.
- Nothing changes for keystore_only=0 or 1. complete_attach is unchanged
  and publishes the rest of the node's requests.

load-driver completes the attach for either initrd mode.

KUnit: the initrd request subset and the initrd xART write rule.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
A sealed volume secret opened for any Linux install on the Mac whose
user bag it was sealed under. FileVault ties a volume key to its volume,
so do the same here: FDE_SEAL frames the secret with the kernel's OS
identity (the os_uuid_hi/lo parameters, else /chosen/apfs-preboot-uuid)
inside the ECIES envelope, and FDE_UNSEAL returns the secret only when
the identity inside matches. Another install gets EXDEV, so user space
can tell it from a refused unseal (EKEYREJECTED); a kernel without an
identity gets ENOKEY. A plaintext without the frame is refused.

Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com>
Assisted-by: LLM
@RyanTheTide
RyanTheTide changed the base branch from design/sep-user-keybag to integration/aurora November 11, 2026 06:40
@RyanTheTide

Copy link
Copy Markdown
Contributor Author

24e66c3 binds the sealed secret to the OS install: the sealed plaintext is AURFDEO1 + the 16-byte OS identity (os_uuid_hi/lo, else /chosen/apfs-preboot-uuid) + the secret, inside the ECIES envelope. FDE_UNSEAL under another identity returns EXDEV, without an identity ENOKEY (FDE_SEAL too), an unframed plaintext EKEYREJECTED. KUnit case fde_secret_is_bound_to_its_os_install.

Hardware, MacBook Neo, 7.3.0-aurora-secdec1 (fleet59 + #921 + this branch + #1471), aurora-sep-userspace#23 tools, LUKS2 test container file:

  • enrol: sealed blob 169 B; right password opens, wrong refused and counted;
  • gate: logout lock -> bag locked at once, unseal refused, opens after the password unlock;
  • reboot: the same install opens after a reboot;
  • other OS install (same kernel, another os_uuid): refused with EXDEV ("belongs to another OS install on this Mac");
  • 0 failed units, enforcing, 0 AVC. KUnit sep 452 passed, 2 skipped.
    Not covered here: IMPORT rollback (an old wrapped bag after a password change still imports).

sks_machine_refkey() now returns without creating a ref-key in the
key-store-only initrd phase. Give the fixture the keystore_only flag and
check that phase never creates one, whatever the store open returns.
@RyanTheTide RyanTheTide changed the title soc: apple: sep: seal a disk-encryption secret to a user's key bag (SEP FDE) soc: apple: sep: seal a disk-encryption secret to a user's key bag, per OS install, unlockable from the initrd Nov 11, 2026
@RyanTheTide
RyanTheTide marked this pull request as ready for review November 11, 2026 12:24
@RyanTheTide
RyanTheTide merged commit 7851a25 into integration/aurora Nov 11, 2026
4 checks passed
@RyanTheTide
RyanTheTide deleted the sepfde/fde-seal branch November 11, 2026 12:25
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