Skip to content

soc: apple: sep: add /dev/sep-key for P-256 keys that stay in the SEP - #1196

Merged
RyanTheTide merged 6 commits into
integration/aurorafrom
sepkey/sep-key-chardev
Nov 8, 2026
Merged

RyanTheTide merged 6 commits into
integration/aurorafrom
sepkey/sep-key-chardev

Conversation

@RyanTheTide

@RyanTheTide RyanTheTide commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Adds /dev/sep-key: P-256 keys that the Secure Enclave creates and keeps. Userspace never sees the private key; it gets an opaque handle that only this Mac (and this Linux install) can use, plus the public point. Signatures are ECDSA-P256 over a 32-byte digest the caller supplies. Keys can carry an access policy the enclave enforces, the same ones macOS offers through SecAccessControl.

Stages (commits)

  • Stage 0 (8c6f003): the node, create/sign/public, no access control.
  • Stage 1 (ed3d647, ac6c191): SEP_KEY_CREATE_BIOMETRY_CURRENT_SET, Touch ID keys the enclave guards. The kernel opens a fresh credential context inside SEP_KEY_SIGN, runs the Touch ID match in it, signs with it and deletes it before the call returns. One touch, one signature; no context handle reaches userspace.
  • Policies (50ba8b0): SEP_KEY_CREATE_BIOMETRY_ANY; the password policies reserved (below); self-test mode 6.

Access policies (macOS semantics)

SEP_KEY_CREATE_* macOS who can sign enrol/remove a finger status
(none) no ACL whoever holds the handle and can open the node n/a yes
BIOMETRY_CURRENT_SET 0x1 .biometryCurrentSet the fingers enrolled when the key was made key dead for good yes, hardware-proven
BIOMETRY_ANY 0x2 .biometryAny any finger enrolled on this install, now or later still works yes (KUnit; hardware row pending)
USER_PRESENCE 0x4 .userPresence (Touch ID or password) reserved, EOPNOTSUPP
DEVICE_PASSCODE 0x8 .devicePasscode reserved, EOPNOTSUPP

At most one policy bit; flags_supported lists the ones the kernel accepts (0x3 now). The lists are the ones macOS writes (our own clean trace of public macOS APIs: cbio{pbioc, pbioh} for current-set, cbio{pbioc} for any, cpo "DeviceOwnerAuthentication" for user presence).

Why the password policies are reserved. On macOS the enclave checks the password itself, against the user's key bag, so root cannot fake the password path. On Linux the only passcode the enclave knows today is the Linux identity key bag's secret, which root holds. A Touch ID-or-password key built on it would be root-satisfiable, and a password checked in software would be too. Neither ships under the macOS name. They need per-user, password-keyed key bags (design in #921); how macOS binds the password credential is being researched. Self-test mode 6 measures what the enclave does with a .userPresence list on Linux today (bag-secret context vs unsatisfied context).

Limits, plainly

  • Lockout: after 5 failed matches Touch ID locks out, as on macOS; touch-gated keys then refuse (EACCES). macOS falls back to the password; on Linux there is no password path yet, so users keep a second way in.
  • No key can be exported, copied or recovered (no "one-time phrase" wrap, by design). Keep a backup key wherever one is trusted, e.g. a second key in authorized_keys, as with hardware security keys.
  • Root that holds a satisfied context could sign more than once until it is deleted (macOS credentials are not consumed per operation). Linux never gives one out: every context lives only inside one SIGN call.
  • Open research: whether the AP can replay a recorded sensor frame into a new match, and what exactly invalidates pbioc/pbioh on Linux.

ABI (include/uapi/linux/apple-sep.h, ioctl magic 0xB1, nr 0x20-0x24)

ioctl struct (bytes) does
SEP_KEY_GET_INFO sep_key_info (16) version 2, flags_supported 0x3, handle_max 2048
SEP_KEY_CREATE sep_key_create (2124) new key (with a policy): handle + public point
SEP_KEY_SIGN sep_key_sign (2164) DER signature; touch-gated keys wait about a minute for a finger
SEP_KEY_PUBLIC sep_key_public (2124) public point from a handle
SEP_KEY_SELFTEST sep_key_selftest (2104) test only (apple_sep.key_selftest=1, root): deliberately unsuitable contexts

Errors: EINVAL (malformed request/handle), EOPNOTSUPP (reserved policy), EKEYREJECTED (the enclave refused: another Mac or install, or the ACL is not satisfied), EACCES (no match or lockout), ETIMEDOUT (no finger), ENODEV (key store or Touch ID not up), EIO (other enclave failure).

Tests

  • KUnit apple_sep* 191/191 on 50ba8b0 (188/188 on ed3d647). W=1 clean on drivers/soc/apple.
  • Hardware, M1 Pro (7.3.0-aurora-sepkey2 = stage 1, 11-08):
    • no finger: enclave refuses with no context, an unsatisfied context, a key-bag-secret ("passcode") context, a deleted context; SIGN with the header flag cleared and with the ACL stripped from the record are refused; no touch -> ETIMEDOUT; plain keys still sign; OpenSSH provider hw 13/13.
    • Ryan's finger sheet 3/3: enrolled finger -> signed, signature verifies; not-enrolled finger -> EACCES; one touch signs once, the same (deleted) matched context is then refused by the enclave (status -8).
  • 7.3.0-aurora-sepkey3 (= 50ba8b0), no finger, on the M1 Pro (13.5 key store) and the Neo (variant 5): stage 1 11/11 and policies 15/15 on both (comments 6720791684, 6722423549).
  • Pending: the biometry-any signature with a finger (one touch, morning).

Userspace: OpenSSH security-key provider in aurora-silicon/aurora-sep-userspace#15 (AURORA_SEP_SK_POLICY=presence|any|current-set|passcode). Lane FIDO builds a CTAP2 authenticator on the same node.

Ready for review: rows passed on two Macs. Next: carried by the next fleet kernel (FLEET5), then merged with the merge train after that fleet passes on one Mac.

🤖 Generated with Claude Code

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
@RyanTheTide

Copy link
Copy Markdown
Contributor Author

Hardware results: one-shot test kernel 7.3.0-aurora-sepkey1. It is integration 36d9198 plus this PR's first revision, which differs from the head only by one rustfmt blank line. No finger was used and the boots were silent. Each Mac was returned to its default kernel with the test kernel removed. Afterwards each had Enforcing with 0 new AVCs, 0 xART guard refusals, and both enrolled Touch ID identities restored.

Neo (J700, variant-5 key store) M1 Pro (J316s, 13.5 key store)
raw ioctls 19/19 20/20
CREATE 270-byte handle, 62-66 ms 263-byte handle, 257 ms
SIGN 76 ms; signatures verify, two signatures differ, a wrong digest fails 143 ms; same
bad flags / length / magic / version / empty handle EINVAL EINVAL
3 tampered records EKEYREJECTED (enclave status -19, -19, -10) EKEYREJECTED (-19, -19, -10)
the Neo's handle on the M1 Pro n/a EKEYREJECTED (enclave status -7)
OpenSSH provider as a normal user (aurora-sep-userspace#15) 13/13 13/13
user without access to the node refused refused

The provider row covers: enrol ecdsa-sk, ssh-keygen -Y sign/verify, a tampered message, distinct keys, presence denied, ed25519-sk and resident keys refused, git commit -S + verify-commit, ssh login through a private sshd -i, and an unauthorised key refused.

Kernel log during the tests: ref-key create mailbox status 0 for every key, and the only warnings are the deliberate tamper and foreign refusals. Kit and logs: kits/test/20261108-sepkey1/results-{neo,m1pro}/.

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

Copy link
Copy Markdown
Contributor Author

Merged integration/aurora (af6d986) into the branch as c66c75c, no force push. The SEP KUnit run on the merge is now all-pass: 185/185. The 4 host_store failures are gone after #1193, and apple_sep_key_handle passes 5/5.

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
@RyanTheTide

Copy link
Copy Markdown
Contributor Author

Stage 1: Touch ID keys the enclave guards (ed3d647, ac6c191).

  • SEP_KEY_CREATE_BIOMETRY_CURRENT_SET (interface v2) puts the access-control list macOS uses for .biometryCurrentSet into the create request (ed). The list names this catacomb (SBIO 0x33) and enrolled-set hash (SBIO 0x36).
  • SEP_KEY_SIGN on such a key opens a fresh credential context and evaluates the signing constraint on it (ACM 0x20). It then runs one Touch ID match through the normal verify path, signs with the context (acmh), and deletes the context. All of this happens inside that one call. No handle to the context reaches userspace, so every signature needs its own touch.
  • SEP_KEY_SELFTEST is test only (apple_sep.key_selftest=1, root).
  • The layouts come from our own trace of macOS 26.6.2 doing the same through public APIs (traces/macos/20261108-sekey-capture).

Hardware, no finger, M1 Pro (13.5 key store): one-shot 7.3.0-aurora-sepkey2, Enforcing, 0 AVC, Touch ID identities restored.

case result
Touch ID key creation OK, 577-byte handle; the record carries the ACL
ACM 0x20 on the new context status 0: not satisfied, a biometric requirement outstanding (as on macOS)
sign, no context enclave refuses (-8)
sign, unsatisfied context enclave refuses (-8)
sign, passcode-only context (key-bag secret accepted, status 0) enclave refuses (-8)
sign, deleted context enclave refuses (-8)
SIGN with the handle's flag cleared enclave refuses (-8)
SIGN with the ACL stripped from the record enclave refuses (-19)
SIGN, nobody touches ETIMEDOUT
plain keys still sign; the OpenSSH provider passes 13/13

KUnit apple_sep* 188/188. Finger cases (a touch signs, a wrong finger is refused, the matched context is refused after delete) wait for Ryan.

…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

Finger results, M1 Pro, 7.3.0-aurora-sepkey2 (stage 1, same content as ed3d647), 11-08 21:33-21:37, silent, Ryan at the Touch ID button:

step result
re-arm, no finger 11/11: enclave refuses with no / unsatisfied / key-bag-secret / deleted context (status -8), flag cleared and ACL stripped refused, no touch -> ETIMEDOUT, plain keys still sign; provider hw 13/13; Enforcing, 0 AVC
1. enrolled finger SIGNED after 13.4 s (enclave status 0), signature verifies
2. not-enrolled finger EACCES after 17.5 s (enclave matched no finger), nothing signed
3. one touch, then the same deleted context first SIGNED (status 0); second refused by the enclave (EKEYREJECTED, status -8)

Two retries along the way, both correct refusals: a touch at the very end of a window that did not match (EACCES), and a window with no touch (ETIMEDOUT). The M1 Pro is back on fleet31, test kernel removed (0 files left). Logs: kits/test/20261108-sepkey2/results-m1pro (own Linux capture, not tainted).

@RyanTheTide

Copy link
Copy Markdown
Contributor Author

Policy rows on hardware, M1 Pro, 7.3.0-aurora-sepkey3 (= this branch at 50ba8b0), 11-08 22:09-22:13, silent, no finger:

  • Stage 1 regression: no-finger enclave refusals 11/11, OpenSSH provider hw unchanged, Enforcing, 0 AVC, 2 enrolled identities restored.
  • Policies 15/15:
    • GET_INFO: v2, flags_supported 0x3.
    • CREATE: 0x4 (userPresence) and 0x8 (devicePasscode) -> EOPNOTSUPP; 0x3 (two policies) and 0x10 -> EINVAL.
    • Biometry-any key: handle flags 0x03; the record names pbioc only, no pbioh. The current-set record still names both.
    • On the biometry-any key, the enclave refuses with no context, with an unsatisfied context, with a key-bag-secret context and with a deleted context (status -8). Header flags cleared -> refused. No touch -> ETIMEDOUT.
  • Presence probe (self-test 6): a throwaway key with macOS's .userPresence list is refused with an unsatisfied context (-8), but the enclave signs it with a context holding only the Linux identity bag's secret. So the enclave does enforce the cpo policy through a passcode credential, and today root's bag secret satisfies it. That confirms why USER_PRESENCE/DEVICE_PASSCODE stay reserved until per-user, password-keyed bags exist (apple_sep: password-keyed Touch ID user key bag #921).

Pending: the biometry-any signature with a finger (one touch, queued for the morning).

@RyanTheTide

Copy link
Copy Markdown
Contributor Author

Second Mac: the Neo (variant-5 key store), 7.3.0-aurora-sepkey3, 11-09 00:57-01:01, silent, no finger. Stage 1 no-finger 11/11, provider hw unchanged, Enforcing, 0 AVC, 2 enrolled identities restored. Policies 15/15, the same as on the M1 Pro: v2 with flags 0x3; 0x4/0x8 -> EOPNOTSUPP; the biometry-any record names pbioc only; the enclave refuses the any key with no, unsatisfied, key-bag-secret and deleted contexts; no touch -> ETIMEDOUT. Presence probe: the .userPresence key signs with the identity-bag secret and is refused unsatisfied, as on the M1 Pro. Neo back on fleet36, test kernel removed, pins on every entry. Still to come: the biometry-any touch (morning), and a fleet kernel carrying this before merge.

@RyanTheTide
RyanTheTide marked this pull request as ready for review November 8, 2026 14:28
…n/linux into sepkey/sep-key-chardev

# Conflicts:
#	include/uapi/linux/apple-sep.h
@RyanTheTide

Copy link
Copy Markdown
Contributor Author

Fleet qualification: test kernel 7.3.0-aurora-f4sk1 = integration bdb9994 + this head 01cb48c (tree == the PR head), one-shot on the M1 Pro by lane FLEET5. Suite vs fleet36: no hard regressions; 0 kernel errors, 0 AVCs, 0 failed units; sleeps 6/6; Touch ID default gate passed; host tests 196/196. /dev/sep-key is created 0600 root:root (SELinux label device_t; it gets its own type in the policy package, follow-up). CI is green on 01cb48c. Merging per the merge train; fleet37 carries it.

@RyanTheTide
RyanTheTide merged commit 1fadde6 into integration/aurora Nov 8, 2026
4 checks passed
@RyanTheTide
RyanTheTide deleted the sepkey/sep-key-chardev branch November 8, 2026 15:01
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