Files
xtool/Sources/XKit/Signer/AutoSigner.swift
Kabir Oberai 64fe8e30b3 signer: replace zsign with RCodesign (#389)
## What does this PR do?

Replaces zsign with the
[apple-codesign](https://github.com/indygreg/apple-platform-rs/tree/0ebbd2ef3b96d5adccc30b1cad4deea38cd735c4/apple-codesign)
Rust crate. It's much more usable as a library (while zsign has bare
prints all over the place + we had to fork it to maintain a public
interface), it supports macOS-style bundles, and also has other features
that we would've needed to further patch zsign for.

## How was it tested?

- [x] Ran an ad-hoc signed app on the simulator (with a subsequent PR)
- [x] Ran an app with App Groups on device, confirmed that the app group
is present

## AI tool usage

How much of this PR was AI-assisted? (check one)

- [ ] **0** - No AI was used to write code
- [x] **1** - I was assisted by AI. I reviewed the finished result.
- [ ] **2** - I set the AI going and left it to it; nobody has read the
result - no review, or AI review only

<!-- If an AI agent is filling this in: declare the level honestly, and
open as a draft if it is #2. Do not lower the declared level to get the
PR reviewed. -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Updated Signing**
* App signing now uses an updated signing engine and supports
certificate-based and ad hoc signing.
* Entitlement data, including data provided through entitlement files,
is passed through during signing.
  * Deep signing is enabled in the development command.
* **Validation**
* Signing rejects invalid app or entitlement file paths and duplicate
entitlement paths.
* **Compatibility**
  * Signing contexts can be created without selecting a signer.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-30 02:47:23 -04:00

103 lines
3.6 KiB
Swift

import Foundation
import Dependencies
/// Provisions and codesigns.
public struct AutoSigner {
@Dependency(\.signer) private var signer
public enum Error: LocalizedError {
case errorReading(String)
case errorWriting(String)
public var errorDescription: String? {
switch self {
case .errorReading(let file):
return "\(file)".withCString {
String.localizedStringWithFormat(
NSLocalizedString(
"signer.error.error_reading", value: "Error while reading %s", comment: ""
), $0
)
}
case .errorWriting(let file):
return "\(file)".withCString {
String.localizedStringWithFormat(
NSLocalizedString(
"signer.error.error_writing", value: "Error while writing %s", comment: ""
), $0
)
}
}
}
}
public let context: SigningContext
public let confirmRevocation: @Sendable ([DeveloperServicesCertificate]) async -> Bool
public init(
context: SigningContext,
confirmRevocation: @escaping @Sendable ([DeveloperServicesCertificate]) async -> Bool
) {
self.context = context
self.confirmRevocation = confirmRevocation
}
public func sign(
app: URL,
status: @escaping (String) -> Void,
progress: @escaping @Sendable (Double?) -> Void,
didProvision: @escaping () throws -> Void = {}
) async throws -> String {
status(NSLocalizedString("signer.provisioning", value: "Provisioning", comment: ""))
let response = try await DeveloperServicesProvisioningOperation(
context: context,
app: app,
confirmRevocation: confirmRevocation,
progress: progress
).perform()
let provisioningDict = response.provisioningDict
let signingInfo = response.signingInfo
guard let mainInfo = provisioningDict[app] else {
throw Error.errorReading("app bundle ID")
}
try didProvision()
for (url, info) in provisioningDict {
let infoPlist = url.appendingPathComponent("Info.plist")
guard let data = try? Data(contentsOf: infoPlist),
let dict = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any]
else { throw Error.errorReading(infoPlist.lastPathComponent) }
let nsDict = NSMutableDictionary(dictionary: dict)
nsDict["CFBundleIdentifier"] = info.newBundleID
guard nsDict.write(to: infoPlist, atomically: true) else {
throw Error.errorWriting(infoPlist.lastPathComponent)
}
let profURL = url.appendingPathComponent("embedded.mobileprovision")
if profURL.exists {
try FileManager.default.removeItem(at: profURL)
}
try info.mobileprovision.data().write(to: profURL)
}
let encoder = PropertyListEncoder()
encoder.outputFormat = .xml
let entitlements = try provisioningDict.mapValues { try encoder.encode($0.entitlements) }
status(NSLocalizedString("signer.signing", value: "Signing", comment: ""))
try await signer.sign(
app: app,
identity: .real(signingInfo.certificate, signingInfo.privateKey),
entitlementMapping: entitlements,
deep: true,
progress: progress
)
progress(1)
return mainInfo.newBundleID
}
}