Skip to content

soc: apple: sep: open the boot policy window for Touch ID's designate - #1340

Merged
DjDeveloperr merged 2 commits into
integration/aurorafrom
satchlj/touchid-designate-window-1109
Nov 11, 2026
Merged

DjDeveloperr merged 2 commits into
integration/aurorafrom
satchlj/touchid-designate-window-1109

Conversation

@satchlj

@satchlj satchlj commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

Closes #1190.

After a boot policy change the enclave rewrites one existing kind-4 session
record the first time Linux's identity keybag is used under the new policy.
e89d4a2 lets that rewrite through during the first unlock
(CHANGE_LOCK_STATE in refkey.rs). When Touch ID comes up before any trusted
key is unsealed, the keybag's first use is the DESIGNATE_KEYBAG in
activate_touchid() instead, and that runs outside the window. The guard
refuses the rewrite, the designate is never answered, and Touch ID stays off on
every boot until aurora-sep-policy-change is run.

Seen on

  • J616s (T6040), in apple_sep: Touch ID is lost after a boot policy change when DESIGNATE_KEYBAG comes before the trusted-key unlock #1190: after every Recovery enrolment of a Stage 1 on
    2026-11-07, raw and Mach-O alike.

  • J613 (MacBook Air M3, T8122), Omarchy (Arch): after a change from raw
    m1n1 to the Aura 1.0.0b5 Mach-O Stage 1 on 2026-11-07, on a 7.3 kernel built on
    integration/aurora 11eccbe (which has e89d4a2):

    sks: LOAD_KEYBAG reply mailbox status 0, ...
    apple_sep: xART guard: refused to rewrite kind-4 record <id>, which this Linux install did not create
    sks: DESIGNATE_KEYBAG got no reply after 2037 ms
    sks: DESIGNATE_KEYBAG did not complete; enrolment will refuse
    hwrng: read produced nothing (ETIMEDOUT)
    matching unavailable this boot ...
    

On both, the refusal comes after LOAD_KEYBAG has answered and before
DESIGNATE_KEYBAG times out. Both use the variant-5 key store (T8122 takes
T8112's, T6040 takes T8140's), where activate_touchid() is the only sender of
DESIGNATE_KEYBAG. sks_prepare_identity_refkey() designates too, but only on
the 13.5 key store, and always inside a ref-key path's window.

On the J613, aurora-sep-policy-change with no arguments armed the refused
record. The next boot logged "allowed one rewrite of kind-4 record for a
boot policy change; its previous value is in
/var/lib/aurora-sep-xart-policy-previous.bin", restored the Catacomb, and
"Touch ID: restored 1 enrolled identity/identities; enrolment survived reboot".

The change

  • sbio.rs: activate_touchid() runs its sks_designate_user_keybag() in
    crate::xart_guard::policy_window(). That is the whole code change.
  • xart_guard.rs: doc comments only. The window is described as the first
    unlock or designate of an identity keybag that already existed.
  • tools/aurora-sep/tests/test_policy_window.py, also added to the SEP source
    checks: activate_touchid() designates a keybag it recovered, inside the
    window, before the machine ref-key (whose creation closes the window), and
    the only other designate is in a ref-key path that holds the window or has
    closed it. It fails on the current tree.
  • XART-SAFETY.md and the tool's help: the designate case, the J613 report,
    and "Changing Stage 1 on purpose" (the first commit, which stands without the
    fix).

The window's terms don't change. There is one window per boot, opened by
whichever comes first of this designate and the first unlock, and kept shut by
a keybag or ref-key created earlier in the boot. Inside it the guard still lets
through one rewrite of one kind-4 record that existed at probe, never a delete
or a shared root, only once the previous value is durable, and without listing
the record as Linux's. Guard::check() is untouched.

Under an unchanged policy this designate rewrites no existing record. The guard
is on by default on both Macs, and a refused write there leaves the designate
unanswered, yet every Touch ID boot without a policy change brings the enrolment
up.

Limit

The window stays one per boot, as you designed it. If some Mac designates
first but the enclave only asks at a later unlock, this change would close the
window before that unlock, where today's kernel would have let the rewrite
through. Neither Mac does that: on both, the first use got the request (your
J616s at the loader's unlock on 10-28, the #1190 J616s and the J613 at the
designate). The one-time allowance still covers such a case. Giving the
designate its own latch would cover it too, at the cost of two short windows
per boot. I kept to one.

Planned changes

The first commit documents what worked on the J613's second Stage 1 change
(2026-11-08, Aura 1.0.0b5-hsusb1): run aurora-sep-policy-change --expect
before the 1TR enrolment, on whatever kernel is installed, then make the first
boot afterwards a kernel with the allowance code. Touch ID survived that first
boot with no refusal. A kernel without the allowance code doesn't read the
file. With the guard on, it refuses and leaves the allowance for the next boot.
Its refusal line also names no session id, so the no-argument form can't arm
from there.

Tested

  • Host tests (neo-build, Ubuntu arm64 VM): the SEP source job's list, plus test_policy_window and
    test_policy_change: all pass. test_policy_window fails on the current tree.
  • drivers/soc/apple/apple-sep.o, CI recipe (defconfig + asahi.config +
    KUNIT + APPLE_SEP_KUNIT_TEST, LLVM 18, Rust 1.85.0, W=1): builds. Its one warning (SCRD_UNLOCK_POLICY_LEN
    unused, in scrd.rs) is already on the base.
  • kunit.py run --arch=arm64 --kunitconfig=drivers/soc/apple/sep.kunitconfig 'apple_sep*' on QEMU: 193 of 193 pass (rustc 1.85.0, clang 18, QEMU 8.2).
  • rustfmt 1.99: the added lines come out unchanged. Both files already differ
    from rustfmt elsewhere, base and head alike.
  • Not tested: a boot across a policy change with this kernel. That takes
    another Stage 1 enrolment; I can run one on the J613 if you want hardware
    proof first.

🤖 Generated with Claude Code

@RyanTheTide RyanTheTide left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code change looks right: activate_touchid() now runs DESIGNATE_KEYBAG for the recovered keybag inside xart_guard::policy_window(), before sks_machine_refkey(), and the window is still one per boot. The new test_policy_window.py is wired into sep-source.yml and passes.

One thing to change before merge: the text refers to the issue as a bare #1190, which on this repository resolves to another project. Please replace it with #1190 (or drop it, since the Link: trailers already carry the URL) in:

  • the body of commit 5c0e33e (This is what #1190 reports on a J616s)
  • the body of commit 4858214 (This happened on a J616s (#1190))
  • tools/aurora-sep/XART-SAFETY.md (enrolment of a Stage 1 that day (#1190))
  • the docstring of tools/aurora-sep/tests/test_policy_window.py (when Touch ID comes up first (#1190))

This is a reword only, so the code does not change.

The boot policy window covers the first unlock of the identity keybag.
When Touch ID brings the keybag up before any trusted key is unsealed,
its first use under a new policy is the DESIGNATE_KEYBAG in
activate_touchid() instead, and the enclave asks for its one rewrite
there. On a MacBook Air M3 (J613) that offers Touch ID at the login
screen, the first boot of integration/aurora 11eccbe after a change from
a raw m1n1 Stage 1 to a Mach-O one logged, after LOAD_KEYBAG answered:

  xART guard: refused to rewrite kind-4 record <id>, which this Linux install did not create
  sks: DESIGNATE_KEYBAG got no reply after 2037 ms
  sks: DESIGNATE_KEYBAG did not complete; enrolment will refuse
  hwrng: read produced nothing (ETIMEDOUT)

This is what #1190 reports on a J616s. aurora-sep-policy-change with no
arguments armed the refused record, and the next boot rewrote it and
restored the enrolment.

A planned change does not need the lost boot. Say how to avoid it: arm
--expect before the change, on whatever kernel is running, then make the
first boot afterwards a kernel with the allowance code. A kernel without
that code does not read the allowance; with the guard on it refuses the
rewrite and leaves the allowance for a later boot. On a second Stage 1
change on the same J613, armed this way from a kernel without the
allowance code, Touch ID survived the first boot with no refusal.

Also say that the tool finds nothing to allow on a kernel whose refusal
line has no session id, and that the keybag's first use can be the
designate.

Link: #1190
Signed-off-by: Satya Benson <satya@benson.earth>
After a boot policy change the enclave rewrites one existing kind-4
session record the first time Linux's identity keybag is used under the
new policy. Commit e89d4a2 ("soc: apple: sep: let a boot policy
change update its xART record") lets that rewrite through in a window
around the first unlock, CHANGE_LOCK_STATE in refkey.rs. When Touch ID
comes up before any trusted key is unsealed, the keybag's first use is
the DESIGNATE_KEYBAG in activate_touchid() instead, which runs outside
the window. The guard refuses the rewrite, the designate is never
answered, and Touch ID stays off on every boot until
aurora-sep-policy-change is run.

This happened on a J616s (#1190) and on a MacBook Air M3 (J613), each
after a Recovery enrolment of a new Stage 1. On both, the refusal comes
after LOAD_KEYBAG has answered and before DESIGNATE_KEYBAG times out:

  xART guard: refused to rewrite kind-4 record <id>, which this Linux install did not create
  sks: DESIGNATE_KEYBAG got no reply after 2037 ms
  sks: DESIGNATE_KEYBAG did not complete; enrolment will refuse

Both use the variant-5 key store, where activate_touchid() is the only
sender of DESIGNATE_KEYBAG.

Run that designate in policy_window() too. The window itself is
unchanged: one per boot, opened by whichever comes first of that
designate and the first unlock of an identity keybag that already
existed, and kept shut by a keybag or ref-key created earlier in the
boot. activate_touchid() designates before it can create the machine
ref-key. Inside the window the guard still lets through one rewrite of
one kind-4 record that existed at probe, never a delete or a shared
root, only once its previous value is saved, and without adding it to
Linux's list.

Under an unchanged policy that designate rewrites no existing record.
The guard is on by default on both Macs, and a refused write there
leaves the designate unanswered, yet every Touch ID boot without a
policy change brings the enrolment up.

A host test checks that activate_touchid() designates a keybag it
recovered, inside the window, before the machine ref-key, and that the
only other designate runs in a ref-key path that holds the window or has
closed it. Run it in the SEP source checks.

Fixes: e89d4a2 ("soc: apple: sep: let a boot policy change update its xART record")
Link: #1190
Signed-off-by: Satya Benson <satya@benson.earth>
@satchlj
satchlj force-pushed the satchlj/touchid-designate-window-1109 branch from 4858214 to 0f3fda0 Compare November 10, 2026 19:46
@satchlj

satchlj commented Nov 10, 2026

Copy link
Copy Markdown
Member Author

Replaced all four bare issue references with #1190, including both commit messages. This is text-only; the policy-window test still passes (4/4).

@satchlj

satchlj commented Nov 11, 2026

Copy link
Copy Markdown
Member Author

GPT-6.1 Sol on @satchlj's behalf

2026-11-10 J613 boot/no-finger re-test with #1340 head 0f3fda0. RTC set correct time before userspace; fprintd and aurora73-touchid-gate reported success. Persistent Claim/VerifyStart/VerifyStop/Release succeeded, sensor registration completed and all traced SPI messages completed at full length with #1491 included. The new-default reboot repeated the service/gate and sensor-start checks. No finger match or enrollment was attempted; protected fingerprint/SEP state directories were not read.

J613/T8122 M3 Air, integration dc8f6f7 plus #971/#973/#974/#978/#1340/#1486/#1491, composed source 112007b70271. Hardware-tested release 7.3.0-aurora73-upstream-2-ARCH; UKI SHA256 248283ad7750793cf60f8530e3e2331f7c1a58d733d2f3e8880f8a959e87becc. Composition evidence, not an isolated PR kernel. The same binary passed a one-shot trial and the normal new-default reboot. Installed package revision 30.10 corrects only the installer's pre-extraction space estimate; all 2054 Linux-package usr regular files match the qualified 30.9 package. Stage1/Stage2 unchanged, four boot hooks masked.

@DjDeveloperr DjDeveloperr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex (GPT-6.1 Sol), reviewing on @DjDeveloperr’s behalf.

No blocking findings. DESIGNATE_KEYBAG for the recovered identity bag now runs inside the existing one-per-boot policy window before machine-refkey creation can close it. Guard::check and its rewrite/delete/shared-root protections are unchanged. The maintainer-requested issue references are now full URLs. Exact-head host/source/Rust/KUnit checks pass; the reported 193/193 SEP KUnit result is attributed evidence. An attended policy-change boot remains needed before claiming hardware proof of this specific policy-change case; unchanged-policy sensor startup alone does not prove it.

@DjDeveloperr
DjDeveloperr merged commit cf42f82 into integration/aurora Nov 11, 2026
4 checks passed
@DjDeveloperr

Copy link
Copy Markdown
Contributor

Since this merged into integration/aurora (cf42f82), the source check fails on every linux PR. The cause is the new static test, not any PR:

tools/aurora-sep/tests/test_policy_window.py::test_no_other_path_designates_outside_a_window
  refkey.rs: refkey_unseal_cached

refkey_unseal_cached comes from 519d09b ("support a trusted-key-only initrd attach", 11-08). It defines sks_prepare_identity_refkey() inside an unlock closure and runs it as policy_window(unlock). In keystore_only (initrd) mode, though, it calls unlock() directly with no window, on purpose: "Do not consume the real root's one policy window in the initrd". #1340's branch didn't carry 519d09b, so its CI passed (4/4) before the merge.

@satchlj @RyanTheTide, which one is intended?

  1. The initrd path is a deliberate exception. The test should allowlist the documented keystore_only path, with no runtime change.
  2. The initrd designate should also happen inside a policy window, which changes refkey.rs.

All other linux PRs are held until source is green again. Happy to send the test-only change for option 1 if you confirm.

Assisted by Claude Code

@satchlj

satchlj commented Nov 11, 2026

Copy link
Copy Markdown
Member Author

Claude Fable 5.2 on @satchlj's behalf

@DjDeveloperr thanks for tracking that down. Option 1 from our side: the test was written without knowledge of 519d09b, and the bare unlock() under keystore_only is a documented, deliberate choice. It also cannot cause what #1340 guards against, because with the window shut the guard does not let an existing session record be rewritten.

To unblock source without waiting, the test-only change is up as #1561: it admits exactly that one shape (the closure in refkey_unseal_cached, bare under keystore_only, in the window otherwise) and keeps the rule for every other caller. 4/4 locally on integration/aurora 17efdf6; two deliberate regressions still fail it.

@RyanTheTide if you would rather have the initrd designate inside a window (option 2), that is your design to change; the test can be tightened again afterwards.

DjDeveloperr pushed a commit that referenced this pull request Nov 11, 2026
test_no_other_path_designates_outside_a_window fails on integration/aurora
since #1340 merged, which fails the source check of every pull request.
The test was written on a branch without 519d09b ("support a
trusted-key-only initrd attach"). That commit builds the ref-key unlock
as a closure in refkey_unseal_cached and runs it inside
crate::xart_guard::policy_window on the real root, but bare in the
trusted-key-only initrd (keystore_only), on purpose: "Do not consume the
real root's one policy window in the initrd". With the window shut the
guard lets no existing session record be rewritten, so that path cannot
make the rewrite the test protects against.

Accept exactly that shape: the one deferred call in
refkey_unseal_cached, whose closure is used twice, bare under
keystore_only and in the window otherwise. Any other caller still has
to open or close the window before the call. No runtime change.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: Satya Benson <satya@benson.earth>
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.

apple_sep: Touch ID is lost after a boot policy change when DESIGNATE_KEYBAG comes before the trusted-key unlock

3 participants