Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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 Documentation/devicetree/bindings/iommu/apple,dart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ properties:
- enum:
- apple,t6020-dart
- apple,t8122-dart
- apple,t8132-dart
- apple,t8140-dart
- apple,t8142-dart
- const: apple,t8110-dart
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ properties:
- apple,t8103-asc-mailbox
- apple,t8112-asc-mailbox
- apple,t8122-asc-mailbox
- apple,t8132-asc-mailbox
- apple,t8140-asc-mailbox
- apple,t8142-asc-mailbox
- apple,t6000-asc-mailbox
Expand Down
54 changes: 54 additions & 0 deletions Documentation/input/devices/j715-touchid.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
J715 Touch ID session inputs
===========================

The T8132/J715 profile uses nonce authentication and source-6 calibration.
It does not change the other boards' session policies, xART guards or
persistence rules. It still requires a successful sensor power cycle before
registration.

Root-key firmware
-----------------

``apple/msrk-j715.bin`` must contain this machine's own FDR MSRk data: the
complete, unmodified body supplied to SBIO SET_MSRK_DATA (opcode ``0x86``).
The qualified input is 10,848 bytes, as in the successful J715 candidate.
Do not substitute J700's signed-container file, strip or add a wrapper, pad
another input to this size, or use another machine's data.

The driver checks the length, not a private identifier or hash. It does not
parse or authenticate the FDR container; a matching length is not evidence
of validity. The enclave must accept the unmodified data. Acquisition and
container verification belong to provisioning, not the kernel driver.
No automatic extraction or alternate-file fallback is provided.

After SENSOR_NONCE, the driver first requests SESSION_SHARE normally.
Only an explicit status ``0x16`` on J715 triggers the firmware request and
upload. A successful upload permits exactly one further SESSION_SHARE
request within that session attempt. A missing file, wrong length, failed
upload, failed transfer or refused retry stops the attempt. A successful
initial share does not read the firmware file.

J700 retains ``apple/msrk-j700.bin``, the nonempty signed-container rule,
and its upload before SESSION_SHARE once per enclave lifetime. It does not
use J715's refusal recovery.

Calibration and validation
--------------------------

The existing sensor ``firmware-name`` property, or its existing default
``apple/mesa_calibration.bin``, supplies calibration. J715 requires its own
source-6 FSC2 data from the successful setup, not the older extracted FDR3
input. The existing calibration encoder prefixes the unchanged body with a
four-byte source word. This patch does not add a calibration fallback.

With ``CONFIG_APPLE_SEP_KUNIT_TEST=y``, the
``apple_sep_sensor_root_key`` suite tests board selection, input-length
boundaries and refusal classification. Also run ``apple_sep_profile``,
``apple_sep_dart_window`` and ``apple_sep_registration`` to cover the
existing profile and registration behavior.

The old candidate's successful enrollment and same-boot verification do not
qualify this upstream kernel. Hardware validation must establish sensor
readiness, enrollment, matching and persistence across a cold boot, with
the existing power-cycle and registration guards enabled. Login integration
is userspace work; disk unlock is outside this support.
44 changes: 44 additions & 0 deletions arch/arm64/boot/dts/apple/t8132.dtsi
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,50 @@
};
};

/*
* iBoot hands over a running SEP. Keep Linux off its DART at boot;
* apple_sep enables it at module initialization. The SEP only
* accepts IOVAs in this window, which carries the BIT(40) alias
* the DART translates (ADT vm-base-0 0x10000004000).
*/
sep_dart: iommu@3a8c80000 {
compatible = "apple,t8132-dart", "apple,t8110-dart";
reg = <0x3 0xa8c80000 0x0 0x4000>;
interrupt-parent = <&aic>;
interrupts = <AIC_IRQ 399 IRQ_TYPE_LEVEL_HIGH>;
#iommu-cells = <1>;
apple,dma-range = /bits/ 64 <0x10000004000 0xffff0000>;
apple,dma-offset = /bits/ 64 <0x0>;
status = "disabled";
};

sep: sep@3aa600000 {
compatible = "apple,sep";
reg = <0x3 0xaa600000 0x0 0x88000>;
mboxes = <&sep_mbox>;
mbox-names = "mbox";
iommus = <&sep_dart 0>;
status = "disabled";
};

/*
* The ADT lists these interrupts as 390 389 392 391. In that order
* the recv-not-empty slot holds the line that is asserted while the
* receive FIFO is empty, and starting the channel never returns.
*/
sep_mbox: mbox@3aa608000 {
compatible = "apple,t8132-asc-mailbox", "apple,asc-mailbox-v4";
reg = <0x3 0xaa608000 0x0 0x4000>;
interrupt-parent = <&aic>;
interrupts = <AIC_IRQ 389 IRQ_TYPE_LEVEL_HIGH>,
<AIC_IRQ 390 IRQ_TYPE_LEVEL_HIGH>,
<AIC_IRQ 391 IRQ_TYPE_LEVEL_HIGH>,
<AIC_IRQ 392 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "send-empty", "send-not-empty",
"recv-empty", "recv-not-empty";
#mbox-cells = <0>;
};

i2c0: i2c@3ad010000 {
compatible = "apple,t8132-i2c", "apple,t8103-i2c";
reg = <0x3 0xad010000 0x0 0x4000>;
Expand Down
166 changes: 166 additions & 0 deletions drivers/soc/apple/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,40 @@ pub(crate) enum IdentitySource {
HostPersisted,
}

/// The root-key input and loading point verified for each nonce-session board.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum SensorRootKey {
None,
/// Preserve J700's signed container upload before SESSION_SHARE.
J700,
/// J715's device-specific FDR MSRk body, uploaded only after status 0x16.
J715,
}

impl SensorRootKey {
pub(crate) const fn firmware(self) -> Option<&'static CStr> {
match self {
Self::None => None,
Self::J700 => Some(c"apple/msrk-j700.bin"),
Self::J715 => Some(c"apple/msrk-j715.bin"),
}
}

/// Only the observed J715 FDR body size is qualified. The enclave checks
/// its authenticity; a matching length alone does not authenticate it.
pub(crate) const fn accepts_length(self, length: usize) -> bool {
match self {
Self::None => false,
Self::J700 => length != 0,
Self::J715 => length == 10_848,
}
}

pub(crate) fn recover_share(self, status: Option<u32>) -> bool {
self == Self::J715 && status == Some(0x16)
}
}

/// SPI mode as the device tree spells it. Mode 1 is CPOL=0/CPHA=1 (`spi-cpha`);
/// mode 2 is CPOL=1/CPHA=0 (`spi-cpol`).
#[derive(Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -152,6 +186,17 @@ const DART_T8112: DartWindow = DartWindow {
range: [0x0, 0x4000, 0x0, 0xffff_0000],
};

/// The T8132 SEP DART input aperture is the BIT(40) alias itself,
/// 0x100_0000_4000 + 0xffff0000 (J715 ADT `dart-sep` vm-base-0 and
/// vm-size-0), given as a range with no offset. Unlike [`DART_HIGH_ALIAS`]
/// it reaches the SEP whether or not the DART driver applies
/// `apple,dma-offset`.
const DART_T8132: DartWindow = DartWindow {
name: "no offset, range 0x10000004000..+0xffff0000",
offset: [0x0, 0x0],
range: [0x100, 0x4000, 0x0, 0xffff_0000],
};

pub(crate) struct PlatformProfile {
pub(crate) name: &'static str,
/// Capacity of the boot shared-memory window. The layout is still checked
Expand All @@ -176,6 +221,7 @@ pub(crate) struct PlatformProfile {
pub(crate) keybag_create: KeybagCreate,
/// Require nonce authentication and signed sensor material before capture.
pub(crate) nonce_session: bool,
pub(crate) sensor_root_key: SensorRootKey,
/// Key-store protocol generation (see [`KeyStore`]).
pub(crate) key_store: KeyStore,
/// Platforms with reboot-tested owner export and user-before-master saves.
Expand Down Expand Up @@ -218,6 +264,7 @@ pub(crate) const T8103: PlatformProfile = PlatformProfile {
},
persistent_enrol: true,
nonce_session: false,
sensor_root_key: SensorRootKey::None,
// macOS shares this enclave: keep Linux to its own xART records.
xart_guard: 1,
};
Expand Down Expand Up @@ -258,6 +305,7 @@ pub(crate) const T6000: PlatformProfile = PlatformProfile {
},
persistent_enrol: true,
nonce_session: false,
sensor_root_key: SensorRootKey::None,
// As T8103. On J316s, creating the identity keybag also rewrites a
// session record that predates Linux. The guard refuses that, so the
// keybag can only be created there with xart_guard=0.
Expand Down Expand Up @@ -293,6 +341,7 @@ pub(crate) const T6020: PlatformProfile = PlatformProfile {
key_store: KeyStore::Variant5,
persistent_enrol: true,
nonce_session: false,
sensor_root_key: SensorRootKey::None,
// A warm-attach enclave shared with macOS, as on T8112.
xart_guard: 1,
};
Expand Down Expand Up @@ -348,6 +397,7 @@ pub(crate) const T8140: PlatformProfile = PlatformProfile {
arg: -1,
},
nonce_session: true,
sensor_root_key: SensorRootKey::J700,
// Linux records from before the guard's list exist here, and Linux has
// left the macOS records unchanged on this platform.
xart_guard: 0,
Expand Down Expand Up @@ -394,6 +444,7 @@ pub(crate) const T8112: PlatformProfile = PlatformProfile {
key_store: KeyStore::Variant5,
persistent_enrol: true,
nonce_session: false,
sensor_root_key: SensorRootKey::None,
// Guarded from its first Linux boot (2026-10-16): no record refused.
xart_guard: 1,
};
Expand Down Expand Up @@ -425,6 +476,27 @@ pub(crate) const T8122: PlatformProfile = PlatformProfile {
..T8112
};

/// MacBook Air 15-inch M4 (J715). The J715 ADT has `sepfw-booted`: iBoot
/// hands over a running SEP, so this is T8122's warm registration,
/// shared-memory geometry and variant-5 key store. The SEP DART window is the
/// ADT's own (see [`DART_T8132`]). The Mesa sensor sits on spi2 at
/// `0x3ad108000`; its identity and wiring are unchecked, so capture stays
/// unqualified. Like the M3, its enclave is shared with macOS.
/// The successful J715 session uses nonce authentication, FDR MSRk recovery
/// after SESSION_SHARE status 0x16, and source-6 FSC2 calibration.
pub(crate) const T8132: PlatformProfile = PlatformProfile {
name: "T8132/J715",
nonce_session: true,
sensor_root_key: SensorRootKey::J715,
sensor: SensorProfile {
controller_base: 0x3_ad10_8000,
capture_qualified: false,
..T8122.sensor
},
dart_window: &DART_T8132,
..T8122
};

static_assert!(T8103.shmem_capacity == 0x30000);
static_assert!(T6020.shmem_capacity == 0x40000);
static_assert!(T8140.shmem_capacity == 0x40000);
Expand All @@ -440,6 +512,8 @@ static_assert!(T8112.sensor.controller_base == T8103.sensor.controller_base);
static_assert!(T8122.shmem_capacity == T8112.shmem_capacity);
static_assert!(T8122.sensor.controller_base == 0x2a1108000);
static_assert!(T8122.wide_dma_mask);
static_assert!(T8132.wide_dma_mask);
static_assert!(T8132.shmem_capacity == T8122.shmem_capacity);

/// Whether the machine root declares `compatible`.
///
Expand Down Expand Up @@ -586,6 +660,10 @@ pub(crate) fn detect() -> Result<&'static PlatformProfile> {
if machine_has(b"apple,t8122") && (machine_has(b"apple,j613") || machine_has(b"apple,j615")) {
return Ok(&T8122);
}
// Only the board whose ADT was read.
if machine_has(b"apple,t8132") && machine_has(b"apple,j715") {
return Ok(&T8132);
}
Err(ENODEV)
}

Expand Down Expand Up @@ -630,6 +708,82 @@ mod tests {
assert!(T8122.wide_dma_mask);
assert!(T8122.shmem_capacity == T8112.shmem_capacity);
assert!(T8122.sensor.controller_base == 0x2_a110_8000);
assert!(T8132.nonce_session);
assert!(T8132.wide_dma_mask);
assert!(T8132.xart_guard == T8122.xart_guard);
assert!(!T8132.sensor.capture_qualified);
}
}

#[cfg(CONFIG_APPLE_SEP_KUNIT_TEST)]
#[kernel::macros::kunit_tests(apple_sep_sensor_root_key)]
mod root_key_tests {
use super::*;

#[test]
fn non_nonce_boards_never_load_or_recover_a_root_key() {
// Given the existing non-nonce profiles, including inherited ones.
for profile in [&T8103, &T6000, &T6020, &T6020_SEPOS13, &T8112, &T8122] {
// When selecting their root-key policy.
let policy = profile.sensor_root_key;
// Then no file, upload or recovery is selected.
assert!(!profile.nonce_session);
assert!(policy.firmware().is_none());
assert!(!policy.accepts_length(10_848));
assert!(!policy.recover_share(Some(0x16)));
}
}

#[test]
fn j700_preserves_its_nonempty_container_without_share_recovery() {
// Given J700's existing signed-container policy.
let policy = T8140.sensor_root_key;
// When selecting firmware and handling the observed refusal.
let name = policy.firmware();
// Then the existing name and nonempty input rule remain in force.
assert!(T8140.nonce_session);
assert!(policy == SensorRootKey::J700);
assert!(name == Some(c"apple/msrk-j700.bin"));
assert!(!policy.accepts_length(0));
assert!(policy.accepts_length(1));
assert!(policy.accepts_length(1726));
assert!(!policy.recover_share(Some(0x16)));
}

#[test]
fn j715_accepts_only_the_qualified_fdr_body_length() {
// Given J715's nonce-session profile.
let policy = T8132.sensor_root_key;
// When selecting firmware and checking input sizes.
let name = policy.firmware();
// Then the public board name selects only the qualified FDR size.
assert!(T8132.nonce_session);
assert!(policy == SensorRootKey::J715);
assert!(name == Some(c"apple/msrk-j715.bin"));
assert!(policy.accepts_length(10_848));
for length in [0, 1, 1726, 10_847, 10_849, usize::MAX] {
assert!(!policy.accepts_length(length));
}
}

#[test]
fn j715_recovers_only_an_explicit_share_status_16() {
// Given J715's recovery policy.
let policy = T8132.sensor_root_key;
// When classifying answered and missing statuses.
let recover = policy.recover_share(Some(0x16));
// Then only the observed refusal selects the single retry path.
assert!(recover);
for status in [
None,
Some(0),
Some(1),
Some(0x15),
Some(0x17),
Some(u32::MAX),
] {
assert!(!policy.recover_share(status));
}
}
}

Expand Down Expand Up @@ -665,4 +819,16 @@ mod dart_window_tests {
assert_eq!(range(profile.dart_window), (0, 1 << 32));
}
}

#[test]
fn t8132_uses_the_adt_window_without_an_offset() {
assert_eq!(offset(T8132.dart_window), 0);
assert_eq!(
range(T8132.dart_window),
(0x100_0000_4000, 0x100_0000_4000 + 0xffff_0000)
);
// The window sits at the BIT(40) alias, inside a 42-bit DMA mask.
assert!(range(T8132.dart_window).0 >= 1 << 40);
assert!(range(T8132.dart_window).1 <= 1 << 42);
}
}
Loading