Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/sep-source.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ jobs:
python3 -m unittest tools/aurora-sep/tests/test_orphan_delete.py -v
python3 -m unittest tools/aurora-sep/tests/test_enrol_admission.py -v
python3 -m unittest tools/aurora-sep/tests/test_owner_preparation.py -v
python3 -m unittest tools/aurora-sep/tests/test_policy_window.py -v
python3 -m unittest discover -s tools/aurora-sep/tests -p test_packaging.py -v

rust-build:
Expand Down
8 changes: 7 additions & 1 deletion drivers/soc/apple/sbio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2388,7 +2388,13 @@ impl SepData {
EIO
})?;
diag::keybag(diag::Keybag::Present);
self.sks_designate_user_keybag(handle, stored.secret());
// When Touch ID comes up before a trusted key is unsealed, this is
// the keybag's first use this boot, and after a boot policy change
// the enclave rewrites one session record here; see
// crate::xart_guard.
crate::xart_guard::policy_window(|| {
self.sks_designate_user_keybag(handle, stored.secret())
});
self.sks_machine_refkey(handle, stored.secret());
let prepared = self.cold_match_continue(handle, uuid, stored.uuid_provenance());
self.ensure_restored_after(prepared);
Expand Down
26 changes: 14 additions & 12 deletions drivers/soc/apple/xart_guard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@
//!
//! A boot policy change (a new Stage 1, a different boot object) makes the
//! enclave rewrite one existing session record the first time Linux's
//! identity keybag is unlocked under the new policy. Linux did not create
//! that record. On a boot under an unchanged policy that unlock rewrites no
//! session record at all, so a rewrite inside it is the sign of a policy
//! identity keybag is used under the new policy: its first unlock, or
//! Touch ID's `DESIGNATE_KEYBAG` when that comes first. Linux did not create
//! that record. On a boot under an unchanged policy neither rewrites an
//! existing session record, so a rewrite inside them is the sign of a policy
//! change. The guard lets exactly one kind-4 rewrite through in that window
//! ([`policy_window`]): the first unlock of an identity keybag that already
//! existed, at most once per boot, never a delete, and only once the
//! record's previous value is saved in a host file ([`PREVIOUS_PATH`]).
//! The record is not added to Linux's list.
//! ([`policy_window`]): the first unlock or designate of an identity keybag
//! that already existed, at most once per boot, never a delete, and only
//! once the record's previous value is saved in a host file
//! ([`PREVIOUS_PATH`]). The record is not added to Linux's list.
//!
//! For a policy change the window did not cover, `aurora-sep-policy-change`
//! backs up the gigalocker and leaves a one-time allowance in a host file
Expand Down Expand Up @@ -122,9 +123,9 @@ fn with_window<T>(why: Rewrite, f: impl FnOnce() -> T) -> T {
result
}

/// Runs `unlock`, the unlock of an identity keybag that already existed,
/// with the boot policy window open if it is this boot's first. Later
/// unlocks run with it closed.
/// Runs `unlock`, the unlock or Touch ID designate of an identity keybag
/// that already existed, with the boot policy window open if it is this
/// boot's first. Later ones run with it closed.
pub(crate) fn policy_window<T>(unlock: impl FnOnce() -> T) -> T {
with_window(Rewrite::PolicyChange, unlock)
}
Expand Down Expand Up @@ -326,8 +327,9 @@ pub(crate) enum Allowance {
Record(Key),
/// The first existing record of this kind the enclave asks to rewrite.
/// An installer that changes the boot policy cannot know the record's
/// session id beforehand; the first identity unlock under the new policy
/// comes before anything else can rewrite a session record.
/// session id beforehand; the keybag's first use under the new policy
/// (an unlock or Touch ID's designate) comes before anything else can
/// rewrite a session record.
FirstOfKind(u8),
}

Expand Down
78 changes: 69 additions & 9 deletions tools/aurora-sep/XART-SAFETY.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,36 @@ The same kernel, under the old policy, unlocked the keybag without any
session record write. Linux had never created a record there: the guard
ran from Linux's first boot and its list stayed empty.

Under an unchanged policy that first unlock rewrites no session record at
all (every J616s boot before the change). So the driver treats a rewrite
inside it as the policy change and lets exactly one through, by itself, on
the first boot after the change:

- only during the first unlock of an identity keybag that already existed,
once per boot; a keybag or ref-key created this boot closes the window;
Touch ID can use the keybag first instead. When fprintd opens the sensor
before any trusted key is unsealed (a login screen that offers Touch ID,
for example), the first use of the keybag under the new policy is the
`DESIGNATE_KEYBAG` in `activate_touchid` (`sbio.rs`), and the enclave asks
for the rewrite there, after LOAD_KEYBAG has answered. The guard refuses
it, and the designate is never 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)
matching unavailable this boot: ...

Touch ID and the enclave's RNG stay down for that boot, and the refusal
repeats on every boot until the record is allowed. This was seen on the
MacBook Air M3 (T8122, J613) on 2026-11-07, after a change from a raw
m1n1 Stage 1 to a Mach-O one, and on a J616s after every Recovery
enrolment of a Stage 1 that day (https://github.com/aurora-silicon/linux/issues/1190). Kernels from before that
designate ran in the window below need `aurora-sep-policy-change` for this
case.

Under an unchanged policy neither that first unlock nor that designate
rewrites an existing session record (every J616s boot before the change,
and every guarded Touch ID boot on the J613 and J616s). So the driver
treats a rewrite inside them as the policy change and lets exactly one
through, by itself, on the first boot after the change:

- only during the first unlock or Touch ID designate of an identity keybag
that already existed, whichever comes first, once per boot; a keybag or
ref-key created this boot closes the window;
- only one kind-4 session record, only a rewrite, never a delete or a
shared root, and the record is not added to Linux's list;
- only once the record's previous value is durable in
Expand All @@ -160,8 +183,12 @@ boot:
run it with no arguments. It takes the session id from the refusal.
- Before a planned change, run it with `--expect`. The record cannot be
named in advance, so the allowance covers the first existing kind-4
record the enclave asks to rewrite. The identity unlock is the first
thing that asks.
record the enclave asks to rewrite. The keybag's first use, Touch ID's
designate or the first unlock, is the first thing that asks. See
"Changing Stage 1 on purpose" below.
- On a kernel whose refusal line names no session id (one from before the
allowance code), the tool with no arguments finds nothing to allow. Use
`--expect` there, and boot a kernel with the allowance code next.

Either way it first copies the whole 6 MiB gigalocker extent, the driver's
host files and `/var/lib/fprint` to
Expand All @@ -174,6 +201,30 @@ log names the record either way. `--status`
shows what is armed and what this boot refused; `--cancel` removes an
allowance that was not used yet.

#### Changing Stage 1 on purpose

Arm the allowance before the change, so the first boot under the new
policy keeps Touch ID:

1. On the Linux install, run `aurora-sep-policy-change --expect` as root.
The running kernel only has to load the driver, which logs where the
gigalocker is; it need not have the allowance code.
2. Change Stage 1 (for example, enrol it from 1TR).
3. Make the first boot afterwards a kernel with the allowance code, the
one that reads `/var/lib/aurora-sep-xart-allow-once.bin`. It empties
the file at probe and logs "this boot may rewrite the first existing
kind-4 record the enclave asks for, once", then "allowed one rewrite of
kind-4 record ... for a boot policy change" when the enclave asks.
A kernel without that code does not read the file. With the guard on,
it refuses the rewrite as above and leaves the allowance for a later
boot; with the guard off, it lets the rewrite through without saving
the previous value.
4. Later boots under the same policy need nothing, on either kernel.

If the first boot was the wrong kernel, Touch ID is down for that boot
only: boot one with the allowance code next, and it uses the allowance
still in the file.

`aurora-sep-load` recognises a refusal and stops at once with a message
that points here, instead of retrying for two minutes.

Expand All @@ -182,6 +233,15 @@ rewrote one kind-4 record and one shared root. The other six kind-4
records and the kind-3 record kept their revisions, Ryan's enrolled
finger matched afterwards, and it kept matching across later reboots.

On the J613 (2026-11-07) the tool with no arguments armed the record the
designate refusal named. The next boot logged "allowed one rewrite of
kind-4 record ... for a boot policy change", restored the Catacomb and the
enrolment ("Touch ID: restored 1 enrolled identity/identities; enrolment
survived reboot"), and a kernel without the allowance code brought Touch
ID up on later boots. A second Stage 1 change on that Mac (2026-11-08),
armed beforehand as in "Changing Stage 1 on purpose", kept Touch ID on
the first boot with no refusal.

The copy of the gigalocker is evidence and a last resort, not an undo:
writing an old copy back does not roll back the enclave's anti-replay
counters, and the enclave may refuse it.
Expand Down
16 changes: 11 additions & 5 deletions tools/aurora-sep/aurora-sep-policy-change
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

A new Stage 1, another boot object or a boot mode conversion changes the
Mac's boot policy for Linux. The first time Linux's identity keybag is
unlocked under the new policy, the enclave rewrites one session record in
the shared xART gigalocker. Linux did not create that record, so the
driver's xART guard refuses the write, the machine ref-key stays down and
Touch ID is unavailable. The keybag, the ref-key and the enrolled
fingerprints are all intact: the refused write changed nothing.
used under the new policy (unlocked, or designated for Touch ID), the
enclave rewrites one session record in the shared xART gigalocker. Linux
did not create that record, so the driver's xART guard refuses the write,
the enclave stops answering and Touch ID is unavailable. The keybag, the
ref-key and the enrolled fingerprints are all intact: the refused write
changed nothing.

This lets the enclave make that one write on the next boot:

Expand All @@ -20,6 +21,11 @@ This lets the enclave make that one write on the next boot:
aurora-sep-policy-change --status what is armed, and this boot's refusals
aurora-sep-policy-change --cancel remove an allowance not used yet

Only a kernel with the xART guard's allowance code reads the allowance.
After a planned change, make the first boot such a kernel; any other
kernel leaves the allowance for a later boot. See "Changing Stage 1 on
purpose" in XART-SAFETY.md.

Before it arms anything it backs up the whole gigalocker extent, the
driver's host files and the fingerprint store to
/var/lib/aurora-rollback/sep-policy-change-<time>/, with SHA256SUMS.
Expand Down
109 changes: 109 additions & 0 deletions tools/aurora-sep/tests/test_policy_window.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# SPDX-License-Identifier: GPL-2.0-only
"""The first use of an existing identity keybag runs in the boot policy window.

After a boot policy change the enclave rewrites one existing session record
at the keybag's first use: the first unlock, or Touch ID's DESIGNATE_KEYBAG
when Touch ID comes up first (https://github.com/aurora-silicon/linux/issues/1190). The xART guard lets that rewrite
through only inside crate::xart_guard::policy_window, so every designate of
a keybag that already existed has to run in it.
"""
from pathlib import Path
import re
import unittest

APPLE = Path(__file__).resolve().parents[3] / "drivers/soc/apple"
DESIGNATE = "sks_designate_user_keybag("
PREPARE = "sks_prepare_identity_refkey("


def function(text, name):
"""The source of `fn name`, from its signature to its closing brace."""
match = re.search(rf"\bfn {name}\b", text)
if match is None:
raise AssertionError(f"fn {name} not found")
opening = text.index("{", match.end())
depth, end = 1, opening + 1
while depth:
depth += (text[end] == "{") - (text[end] == "}")
end += 1
return text[match.start():end]


def functions(text):
"""(name, start, end) of every fn with a body."""
found = []
for match in re.finditer(r"\bfn (\w+)\b[^;{]*\{", text):
depth, end = 1, match.end()
while depth:
depth += (text[end] == "{") - (text[end] == "}")
end += 1
found.append((match.group(1), match.start(), end))
return found


def calls(name):
"""(file, caller, caller's source, offset of the call in it) per call."""
found = []
for path in sorted(APPLE.glob("*.rs")):
text = path.read_text()
spans = functions(text)
for match in re.finditer(re.escape(name), text):
if text[max(0, match.start() - 3):match.start()] == "fn ":
continue
# The innermost function around the call.
caller, start, end = max(
(s for s in spans if s[1] < match.start() < s[2]),
key=lambda s: s[1])
found.append((path.name, caller, text[start:end],
match.start() - start))
return found


def statement(body, at):
"""The statement around offset `at`: from the previous `;`, `{` or `}`."""
start = max(body.rfind(c, 0, at) for c in ";{}")
# A closure opened inside the statement does not end it.
while body[start] == "{" and body[start - 3:start].strip().endswith("||"):
start = max(body.rfind(c, 0, start) for c in ";{}")
return body[start + 1:at]


class PolicyWindowTests(unittest.TestCase):
def setUp(self):
self.sbio = (APPLE / "sbio.rs").read_text()
self.activate = function(self.sbio, "activate_touchid")

def test_touchid_designates_inside_the_window(self):
at = self.activate.index(DESIGNATE)
self.assertIn("crate::xart_guard::policy_window(", statement(self.activate, at))

def test_touchid_designates_before_the_ref_key_can_be_created(self):
# Creating the machine ref-key closes the window for the boot.
self.assertLess(self.activate.index(DESIGNATE),
self.activate.index("sks_machine_refkey("))

def test_touchid_designates_a_keybag_that_already_existed(self):
recovered = self.activate.index("sks_recover(&stored)")
self.assertLess(recovered, self.activate.index(DESIGNATE))
self.assertNotIn("sks_provision_identity_keybag(", self.activate)

def test_no_other_path_designates_outside_a_window(self):
# A new designate of an existing keybag needs the window too; add it
# here once it runs in one.
sites = {(f, outer) for f, outer, _body, _at in calls(DESIGNATE)}
self.assertEqual(sites, {("sbio.rs", "activate_touchid"),
("refkey.rs", "sks_prepare_identity_refkey")})
# The ref-key path designates inside its own unlock's window, or
# after a ref-key create has closed the window for the boot.
callers = calls(PREPARE)
self.assertTrue(callers)
for f, outer, body, at in callers:
with self.subTest(caller=outer):
before = body[:at]
self.assertTrue("crate::xart_guard::policy_window(" in before
or "crate::xart_guard::close_policy_window(" in before,
f"{f}: {outer}")


if __name__ == "__main__":
unittest.main()
Loading