| D-M4-10-0 |
2026-10-17 |
The ticket was built on an integration base and ported onto main with only its own commits; the English docs and spec of main win (chapters 08-identity.md and 15-client-architecture.md), and text this ticket adds is English. |
The tickets are built in parallel on integration bases; main takes each ticket as one pull request; merged names win (D-W-100). |
| D-M4-10-1 |
2026-10-15 |
device_key is one Flutter plugin package with a folder per platform and a Pigeon interface (DeviceKeyHostApi: ensureKey, keyInfo, sign (async), attestation, deleteKeys), not a set of federated packages. Errors cross the channel only as the codes user_cancelled, key_invalidated, not_available and internal with a fixed text; the Dart side keeps only the code, and a platform without the plugin (Pigeon's channel-error, or MissingPluginException) reports not_available. A missing key on sign or attestation is not_available; an enclave key the Secure Enclave no longer accepts is key_invalidated: the enclave loads any blob and rejects it only when it signs, with CryptoTokenKit corruptedData or objectNotFound (canceledByUser is user_cancelled), and a blob that no longer yields the registered public key counts the same (red team M4-10). Which code a real biometrics change produces is not measured on a device with Touch ID. |
15 3 names one member device_key; the Pigeon interface is the boundary federation asks for. Fixed texts keep key material and server addresses out of logs. |
| D-M4-10-2 |
2026-10-15 |
The public JWK (crv, kty, x, y, base64url without padding) and the RFC 7638 thumbprint are computed in Dart from the coordinates the native side returns; the native side returns signatures as R \|\| S (CryptoKit's rawRepresentation, so no DER conversion is needed), and the Dart side rejects any other length. |
One implementation for every platform (M4-11, M5-01 reuse it). |
| D-M4-10-3 |
2026-10-15 |
Access control: dpop with [.privateKeyUsage] and AfterFirstUnlockThisDeviceOnly; step_up with [.privateKeyUsage, .biometryCurrentSet] when biometrics are enrolled, otherwise [.privateKeyUsage, .userPresence], and WhenPasscodeSetThisDeviceOnly on iOS and macOS (the Data Protection keychain knows the same class). Without an enclave the software key's keychain item carries .userPresence for step_up, with the same accessibility classes, and hardwareBound is false. Biometrics count as enrolled unless the device has no sensor or reports biometryNotEnrolled, so a key created while the lid is closed or the sensor is locked out is still bound to them (code review M4-10). 15 9 is updated to this. |
08 6.1 requires invalidation on new biometrics, while a Mac without Touch ID must still confirm with the device password (brief note 3). |
| D-M4-10-4 |
2026-10-15 |
The core is the Swift package DeviceKeyCore (CryptoKit, Security and LocalAuthentication for the biometrics check) inside the plugin's Swift package folder, darwin/device_key/DeviceKeyCore, and the plugin package depends on it by the path DeviceKeyCore. The plugin uses Swift Package Manager only (enabled by default in Flutter 3.47.7), without a podspec. The shared access group of the dpop key comes from the Info.plist key DeviceKeyAccessGroup, read when the plugin registers; the extensions read the same key. |
Flutter links a plugin's Swift package through a symbolic link, so a path leaving its folder (../DeviceKeyCore) would resolve beside the link; the extensions (M5-04, M5-06) add the same core package (15 9). The pinned Flutter no longer needs CocoaPods for plugins (brief note 4). The plugin registrar takes no arguments, so the build configuration reaches the plugin through Info.plist. |
| D-M4-10-5 |
2026-10-15 |
testHardwarePathOnAppleChip runs only where SecureEnclave.isAvailable and otherwise reports the skip secure enclave not available on this host; it signs only with the dpop key. testKeychainRoundTrip skips when the test process cannot write to the Data Protection keychain (errSecMissingEntitlement for an unsigned test binary). darwin/run_ios_sim_tests.sh prints a SKIP line and exits 0 on a host without Xcode and its simulator; with Xcode a missing iPhone simulator fails. The enclave form of the step-up key is tested through the policy the manager passes to the enclave (KeyPolicy.policy), because creating an enclave key with WhenPasscodeSetThisDeviceOnly needs an unlocked user session, which a build host reached over ssh does not have (errSecInteractionNotAllowed). |
CI runners are virtual machines without an enclave (brief note 5); the hardware path counts as proven only on a host with one. |
| D-M4-10-6 |
2026-10-15 |
The Swift tests use Swift Testing instead of XCTest, and the first entry of platform-tests.json is ["sh", "darwin/run_core_tests.sh"] instead of a bare swift test: the script passes the Swift Testing paths of the command line tools when no Xcode is installed and runs plain swift test otherwise. |
The command line tools ship Swift Testing but no XCTest, so a Mac with an Apple chip and without Xcode can prove the Secure Enclave path; Xcode 16 on the macos-15 runner runs Swift Testing with swift test and xcodebuild test. |
| D-M4-10-7 |
2026-10-17 |
sonar-project.properties: **/Tests/** counts as test code (SwiftPM test targets), the Pigeon output *.g.swift, *.g.kt, *.g.cpp and *.g.h is excluded like the other generated code, and the native platform code under packages/*/darwin/**, packages/*/android/** and packages/*/windows/** is out of the coverage measure; it stays in the analysis for issues. |
SonarQube Cloud read DeviceKeyCoreTests.swift as product code (47 hard-coded URI findings and a keychain finding in the test probe) and counted the Swift sources as uncovered (5.6 % coverage on the pull request): the Swift suite runs on the macOS runner through platform-tests.json and writes no report the Linux scan can read. The Windows and Android members of device_key are the same case. |