mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 20:10:30 +00:00
fix(ddi): mount verified generic Cryptex images on iOS 27
This commit is contained in:
parent
265ceb1d99
commit
a4722251f2
4 changed files with 155 additions and 0 deletions
12
Package.swift
Normal file
12
Package.swift
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// swift-tools-version: 5.9
|
||||
import PackageDescription
|
||||
|
||||
// Exercise DDI storage and update behavior without linking the device-only idevice library.
|
||||
let package = Package(
|
||||
name: "StikDebugDDI",
|
||||
platforms: [.macOS(.v14)],
|
||||
targets: [
|
||||
.target(name: "DDIKit", path: "StikDebug/Services/DeveloperDiskImages"),
|
||||
.testTarget(name: "DDIKitTests", dependencies: ["DDIKit"], path: "Tests/DDI")
|
||||
]
|
||||
)
|
||||
|
|
@ -292,6 +292,11 @@ extension JITEnableContext {
|
|||
}
|
||||
|
||||
func mountPersonalDDI(withImagePath imagePath: String, trustcachePath: String, manifestPath: String) throws {
|
||||
if #available(iOS 27, *),
|
||||
let directory = Bundle.main.url(forResource: "BundledCryptexDDI", withExtension: nil),
|
||||
try mountCryptexDDIIfAvailable(in: directory) {
|
||||
return
|
||||
}
|
||||
let imageData = try IdeviceBridge.mappedFileData(atPath: imagePath, description: "developer disk image")
|
||||
let trustcacheData = try IdeviceBridge.mappedFileData(atPath: trustcachePath, description: "developer disk image trust cache")
|
||||
let manifestData = try IdeviceBridge.mappedFileData(atPath: manifestPath, description: "developer disk image manifest")
|
||||
|
|
@ -352,6 +357,37 @@ extension JITEnableContext {
|
|||
}
|
||||
}
|
||||
|
||||
/// Use Apple's generic Cryptex identity on iOS 27. New hardware can lack a
|
||||
/// legacy personalized identity even in the newest Xcode developer image.
|
||||
private func mountCryptexDDIIfAvailable(in directory: URL) throws -> Bool {
|
||||
try CryptexDDI.validate(in: directory)
|
||||
return try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
||||
var available = false
|
||||
if let error = rsd_service_available(handshake, "com.apple.security.cryptexd.remote", &available) {
|
||||
throw IdeviceBridge.consumeFFIError(error, fallback: "Failed to query Cryptex mounting support")
|
||||
}
|
||||
guard available else { return false }
|
||||
|
||||
var assets: OpaquePointer?
|
||||
if let error = cryptex1_assets_load(directory.path, &assets) {
|
||||
throw IdeviceBridge.consumeFFIError(error, fallback: "Failed to load the verified Cryptex DDI")
|
||||
}
|
||||
guard let assets else { throw IdeviceBridge.makeError(message: "Cryptex assets were not created") }
|
||||
defer { cryptex1_assets_free(assets) }
|
||||
|
||||
var installed: UnsafeMutablePointer<InstalledCryptexC>?
|
||||
defer { cryptexd_free_installed_cryptex(installed) }
|
||||
if let error = cryptexd_install_ddi(adapter, handshake, assets, &installed) {
|
||||
throw IdeviceBridge.consumeFFIError(error, fallback: "Failed to mount the Cryptex DDI")
|
||||
}
|
||||
guard let installed, let identifier = installed.pointee.identifier,
|
||||
String(cString: identifier) == "com.apple.MobileAsset.DDI" else {
|
||||
throw IdeviceBridge.makeError(message: "Cryptex install did not confirm a mounted developer image")
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func fetchAllProfiles() throws -> [Data] {
|
||||
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
||||
try IdeviceBridge.withConnectedClient(
|
||||
|
|
|
|||
54
StikDebug/Services/DeveloperDiskImages/CryptexDDI.swift
Normal file
54
StikDebug/Services/DeveloperDiskImages/CryptexDDI.swift
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
/// Cryptex DDIs use a generic identity and four signed payloads, rather than an
|
||||
/// ApChipID/ApBoardID identity. Never mix these with the personalized image files.
|
||||
enum CryptexDDI {
|
||||
static let payloads = [
|
||||
"Cryptex1,GenericDmg": "Image.dmg",
|
||||
"Cryptex1,GenericTrustCache": "Image.dmg.trustcache",
|
||||
"Cryptex1,CryptexInfoPlist": "Image.dmg.cryptex_info",
|
||||
"Cryptex1,GenericVolume": "Image.dmg.root_hash"
|
||||
]
|
||||
|
||||
static func validate(in directory: URL) throws {
|
||||
let data = try Data(contentsOf: directory.appendingPathComponent("BuildManifest.plist"))
|
||||
guard let manifest = try PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any],
|
||||
let identities = manifest["BuildIdentities"] as? [[String: Any]],
|
||||
let identity = identities.first(where: {
|
||||
(($0["Info"] as? [String: Any])?["Variant"] as? String)?
|
||||
.hasSuffix("Developer Disk Image Cryptex") == true
|
||||
}),
|
||||
let entries = identity["Manifest"] as? [String: [String: Any]] else {
|
||||
throw ValidationError.invalidManifest
|
||||
}
|
||||
// The native loader follows manifest paths. Require the normalized names
|
||||
// we verified so it cannot read an unverified file or follow a symlink out.
|
||||
let root = directory.resolvingSymlinksInPath().standardizedFileURL.path + "/"
|
||||
for (key, name) in payloads {
|
||||
guard let entry = entries[key],
|
||||
(entry["Info"] as? [String: Any])?["Path"] as? String == name,
|
||||
let digest = entry["Digest"] as? Data else {
|
||||
throw ValidationError.invalidManifest
|
||||
}
|
||||
let url = directory.appendingPathComponent(name).resolvingSymlinksInPath().standardizedFileURL
|
||||
guard url.path.hasPrefix(root) else { throw ValidationError.invalidManifest }
|
||||
let bytes = try Data(contentsOf: url, options: .mappedIfSafe)
|
||||
guard !bytes.isEmpty, Data(SHA384.hash(data: bytes)) == digest else {
|
||||
throw ValidationError.invalidPayload(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ValidationError: LocalizedError {
|
||||
case invalidManifest
|
||||
case invalidPayload(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidManifest: return "The bundled Cryptex DDI manifest is invalid. Reinstall a complete build of this fork."
|
||||
case .invalidPayload(let name): return "The bundled Cryptex DDI file \(name) failed verification. Reinstall a complete build of this fork."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
Tests/DDI/CryptexDDITests.swift
Normal file
53
Tests/DDI/CryptexDDITests.swift
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import CryptoKit
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import DDIKit
|
||||
|
||||
struct CryptexDDITests {
|
||||
private func fixture(_ body: (URL) throws -> Void) throws {
|
||||
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
var entries: [String: Any] = [:]
|
||||
for (key, name) in CryptexDDI.payloads {
|
||||
let data = Data(name.utf8)
|
||||
try data.write(to: directory.appendingPathComponent(name))
|
||||
entries[key] = ["Digest": Data(SHA384.hash(data: data)), "Info": ["Path": name]]
|
||||
}
|
||||
let manifest: [String: Any] = ["BuildIdentities": [[
|
||||
"Info": ["Variant": "iOS Developer Disk Image Cryptex"], "Manifest": entries
|
||||
]]]
|
||||
try PropertyListSerialization.data(fromPropertyList: manifest, format: .xml, options: 0)
|
||||
.write(to: directory.appendingPathComponent("BuildManifest.plist"))
|
||||
try body(directory)
|
||||
}
|
||||
|
||||
@Test func genericIdentityDoesNotRequireLegacyHardwareIDs() throws {
|
||||
try fixture { try CryptexDDI.validate(in: $0) }
|
||||
}
|
||||
|
||||
@Test(arguments: Array(CryptexDDI.payloads.values))
|
||||
func everyPayloadMustMatchItsSignedDigest(_ name: String) throws {
|
||||
try fixture { directory in
|
||||
try Data("corrupt".utf8).write(to: directory.appendingPathComponent(name))
|
||||
#expect(throws: (any Error).self) { try CryptexDDI.validate(in: directory) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test func missingRootHashIsRejected() throws {
|
||||
try fixture { directory in
|
||||
try FileManager.default.removeItem(at: directory.appendingPathComponent("Image.dmg.root_hash"))
|
||||
#expect(throws: (any Error).self) { try CryptexDDI.validate(in: directory) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test func nativeLoaderCannotFollowUnverifiedManifestPaths() throws {
|
||||
try fixture { directory in
|
||||
let url = directory.appendingPathComponent("BuildManifest.plist")
|
||||
let xml = try String(contentsOf: url, encoding: .utf8)
|
||||
.replacingOccurrences(of: "<string>Image.dmg</string>", with: "<string>../Image.dmg</string>")
|
||||
try Data(xml.utf8).write(to: url)
|
||||
#expect(throws: (any Error).self) { try CryptexDDI.validate(in: directory) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue