Skip to content
Open
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 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