diff --git a/Package.resolved b/Package.resolved index c3116c94..fd48bf56 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "729c46eb45d30de963087f3210e336d813bf25e00d9d7ccb7ee51a14927478f7", + "originHash" : "2ccd077fc130f0f1b1279d87aa61d1fc243964d7ad49489a12021577c54df9de", "pins" : [ { "identity" : "aexml", @@ -343,6 +343,15 @@ "version" : "1.4.0" } }, + { + "identity" : "swift-rcodesign", + "kind" : "remoteSourceControl", + "location" : "https://github.com/xtool-org/swift-rcodesign", + "state" : { + "revision" : "08650ab9d3bf95a0c585581b8bd63eb7b8315264", + "version" : "0.2.0" + } + }, { "identity" : "swift-service-context", "kind" : "remoteSourceControl", @@ -450,15 +459,6 @@ "revision" : "a27b21e0c81c5bf42049b897a62aaf387e80f279", "version" : "6.2.2" } - }, - { - "identity" : "zsign", - "kind" : "remoteSourceControl", - "location" : "https://github.com/xtool-org/zsign", - "state" : { - "revision" : "f6158be9e62a5ce2a53d72b1a6d880880da956c8", - "version" : "1.9.0" - } } ], "version" : 3 diff --git a/Package.swift b/Package.swift index 9d316751..a15419bf 100644 --- a/Package.swift +++ b/Package.swift @@ -52,7 +52,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/xtool-org/xtool-core", .upToNextMinor(from: "1.5.1")), .package(url: "https://github.com/xtool-org/SwiftyMobileDevice", .upToNextMinor(from: "1.7.0")), - .package(url: "https://github.com/xtool-org/zsign", .upToNextMinor(from: "1.9.0")), + .package(url: "https://github.com/xtool-org/swift-rcodesign", .upToNextMinor(from: "0.2.0")), .package(url: "https://github.com/apple/swift-system", from: "1.4.0"), .package(url: "https://github.com/apple/swift-http-types", from: "1.3.1"), @@ -132,8 +132,7 @@ let package = Package( .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "SwiftyMobileDevice", package: "SwiftyMobileDevice"), - .product(name: "Zupersign", package: "zsign"), - .product(name: "SignerSupport", package: "xtool-core"), + .product(name: "RCodesign", package: "swift-rcodesign"), .product(name: "ProtoCodable", package: "xtool-core"), .product(name: "Superutils", package: "xtool-core"), .product(name: "Crypto", package: "swift-crypto"), diff --git a/Sources/XKit/DeveloperServices/App IDs/DeveloperServicesAddAppOperation.swift b/Sources/XKit/DeveloperServices/App IDs/DeveloperServicesAddAppOperation.swift index d726e91e..85503cad 100644 --- a/Sources/XKit/DeveloperServices/App IDs/DeveloperServicesAddAppOperation.swift +++ b/Sources/XKit/DeveloperServices/App IDs/DeveloperServicesAddAppOperation.swift @@ -8,9 +8,12 @@ import Foundation import DeveloperAPI +import Dependencies public struct DeveloperServicesAddAppOperation: DeveloperServicesOperation { + @Dependency(\.signer) private var signer + public enum Error: LocalizedError { case invalidApp(URL) @@ -172,7 +175,7 @@ public struct DeveloperServicesAddAppOperation: DeveloperServicesOperation { var entitlements: Entitlements // if the executable doesn't already have entitlements, that's // okay. We don't have to throw an error. - if let entitlementsData = try? await context.signer.analyze(executable: executableURL), + if let entitlementsData = try? await signer.analyze(executable: executableURL), let decodedEntitlements = try? PropertyListDecoder().decode(Entitlements.self, from: entitlementsData) { entitlements = decodedEntitlements diff --git a/Sources/XKit/Integration/IntegratedInstaller.swift b/Sources/XKit/Integration/IntegratedInstaller.swift index 0ecbd193..ad070539 100644 --- a/Sources/XKit/Integration/IntegratedInstaller.swift +++ b/Sources/XKit/Integration/IntegratedInstaller.swift @@ -294,7 +294,7 @@ public actor IntegratedInstaller { app: URL, targetDevice: SigningContext.TargetDevice? = nil, ) async throws -> String { - let context = try SigningContext( + let context = SigningContext( auth: auth, targetDevice: targetDevice, ) diff --git a/Sources/XKit/Signer/AutoSigner.swift b/Sources/XKit/Signer/AutoSigner.swift index b19dde8d..01048448 100644 --- a/Sources/XKit/Signer/AutoSigner.swift +++ b/Sources/XKit/Signer/AutoSigner.swift @@ -1,17 +1,17 @@ import Foundation +import Dependencies /// Provisions and codesigns. public struct AutoSigner { + @Dependency(\.signer) private var signer + public enum Error: LocalizedError { - case noSigners case errorReading(String) case errorWriting(String) public var errorDescription: String? { switch self { - case .noSigners: - return NSLocalizedString("signer.error.no_signers", value: "No signers found", comment: "") case .errorReading(let file): return "\(file)".withCString { String.localizedStringWithFormat( @@ -82,13 +82,16 @@ public struct AutoSigner { try info.mobileprovision.data().write(to: profURL) } - let entitlements = provisioningDict.mapValues(\.entitlements) + 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 context.signer.sign( + try await signer.sign( app: app, identity: .real(signingInfo.certificate, signingInfo.privateKey), entitlementMapping: entitlements, + deep: true, progress: progress ) progress(1) diff --git a/Sources/XKit/Signer/RCodesignSigner.swift b/Sources/XKit/Signer/RCodesignSigner.swift new file mode 100644 index 00000000..3e08f7eb --- /dev/null +++ b/Sources/XKit/Signer/RCodesignSigner.swift @@ -0,0 +1,57 @@ +import Foundation +import RCodesign + +public actor RCodesignSigner: Signer { + + public enum Error: LocalizedError { + case badFilePath + + public var errorDescription: String? { + switch self { + case .badFilePath: + return NSLocalizedString( + "signer_impl.error.bad_file_path", value: "A bad file path was provided", comment: "" + ) + } + } + } + + public init() {} + + public func sign( + app: URL, + identity: SignerIdentity, + entitlementMapping: [URL: Data], + deep: Bool, + progress: @escaping @Sendable (Double?) -> Void + ) throws { + progress(nil) + guard app.isFileURL else { throw Error.badFilePath } + let root = app.standardizedFileURL.pathComponents + var entitlements: [String: Data] = [:] + for (bundle, value) in entitlementMapping { + let components = bundle.standardizedFileURL.pathComponents + guard bundle.isFileURL, components.starts(with: root) else { throw Error.badFilePath } + // RCodesign scopes entitlements by bundle-relative path: "" for the + // app itself and e.g. "PlugIns/Widget.appex" for an extension. Compare + // path components so a sibling such as App.app-copy cannot match App.app. + let path = components.dropFirst(root.count).joined(separator: "/") + guard entitlements[path] == nil else { throw Error.badFilePath } + entitlements[path] = value + } + + let signingIdentity: RCodesign.Identity + switch identity { + case .real(let certificate, let privateKey): + signingIdentity = .certificate(certificateDER: try certificate.data(), privateKeyPEM: privateKey.data) + case .adhoc: + signingIdentity = .adHoc + } + try RCodesign.sign(at: app, identity: signingIdentity, options: .init(entitlements: entitlements, shallow: !deep)) + } + + public func analyze(executable: URL) throws -> Data? { + try RCodesign.entitlements(at: executable) + } + +} diff --git a/Sources/XKit/Signer/Signer.swift b/Sources/XKit/Signer/Signer.swift new file mode 100644 index 00000000..4408d67e --- /dev/null +++ b/Sources/XKit/Signer/Signer.swift @@ -0,0 +1,50 @@ +import Foundation +import Dependencies + +public enum SignerIdentity: Sendable { + case real(Certificate, PrivateKey) + case adhoc +} + +public protocol Signer: Sendable { + // entitlementMapping is [bundle url] -> [plist data] + func sign( + app: URL, + identity: SignerIdentity, + entitlementMapping: [URL: Data], + deep: Bool, + progress: @escaping @Sendable (Double?) -> Void + ) async throws + + func analyze(executable: URL) async throws -> Data? +} + +private enum SignerDependencyKey: DependencyKey { + static var liveValue: any Signer { RCodesignSigner() } + static var testValue: any Signer { UnimplementedSigner() } +} + +extension DependencyValues { + public var signer: any Signer { + get { self[SignerDependencyKey.self] } + set { self[SignerDependencyKey.self] = newValue } + } +} + +private struct UnimplementedSigner: Signer { + func sign( + app: URL, + identity: SignerIdentity, + entitlementMapping: [URL: Data], + deep: Bool, + progress: @escaping @Sendable (Double?) -> Void + ) async throws { + let operation: () throws -> Void = unimplemented() + try operation() + } + + func analyze(executable: URL) async throws -> Data? { + let operation: () throws -> Data? = unimplemented() + return try operation() + } +} diff --git a/Sources/XKit/Signer/SignerImpl.swift b/Sources/XKit/Signer/SignerImpl.swift deleted file mode 100644 index 3217a956..00000000 --- a/Sources/XKit/Signer/SignerImpl.swift +++ /dev/null @@ -1,178 +0,0 @@ -import Foundation -import CXKit -import SignerSupport -import ConcurrencyExtras - -/// a wrapper around `signer_t` -public actor Signer { - - public enum Error: LocalizedError { - case notFound - case badFilePath - case signer(String?) - - public var errorDescription: String? { - switch self { - case .notFound: - return NSLocalizedString( - "signer_impl.error.not_found", value: "No signer implementation found", comment: "" - ) - case .badFilePath: - return NSLocalizedString( - "signer_impl.error.bad_file_path", value: "A bad file path was provided", comment: "" - ) - case .signer(let error?): - return error - case .signer(nil): - return NSLocalizedString( - "signer_impl.error.unknown", value: "An unknown signing error occurred", comment: "" - ) - } - } - } - - public enum Identity { - case real(Certificate, PrivateKey) - case adhoc - } - - private let signer: UnsafePointer - public var name: String { - String(cString: signer.pointee.name) - } - - private init(signer: UncheckedSendable>) { - self.signer = signer.value - } - - public static func all() -> some Collection { - var list = get_signers() - var arr: [Signer] = [] - while let curr = list { - arr.append(Signer(signer: UncheckedSendable(curr))) - list = curr.pointee.next - } - return arr - } - - public static func first() throws -> Signer { - try all().first.orThrow(Error.notFound) - } - - private func _sign( - app: URL, - identity: Identity, - entitlementMapping: [URL: Entitlements], - progress: @escaping (Double?) -> Void - ) throws { - let encoder = PropertyListEncoder() - encoder.outputFormat = .xml - let entsArray: [entitlements_data_t] = try entitlementMapping.map { url, ents in - try url.withUnsafeFileSystemRepresentation { bundlePath in - guard let bundlePath = bundlePath else { throw Error.badFilePath } - return try encoder.encode(ents).withUnsafeBytes { bytes in - let copy = UnsafeMutableBufferPointer.allocate(capacity: bytes.count) - bytes.copyBytes(to: copy) - // an XML plist should always be non-empty so force unwrapping baseAddress is safe - return entitlements_data_t( - bundle_path: strdup(bundlePath), - data: copy.baseAddress!, - len: copy.count - ) - } - } - } - defer { - entsArray.forEach { - free(UnsafeMutablePointer(mutating: $0.bundle_path)) - $0.data.deallocate() - } - } - let sendableProgress = UncheckedSendable(progress) - let sendableEntsArray = UncheckedSendable(entsArray) - - let certificateData: Data - let privateKeyData: Data - let lengthOverride: Int? - switch identity { - case .real(let certificate, let privateKey): - certificateData = try certificate.data() - privateKeyData = privateKey.data - lengthOverride = nil - case .adhoc: - // we can't use empty data here because withUnsafeBytes - // might return a bufferpointer with a nil base address, - // but signer.h expects non-null values. The real fix is - // updating xtool-core/SignerSupport/signer.h to annotate - // the pointers as nullable, but alas that requires effort. - certificateData = Data([0]) - privateKeyData = Data([0]) - lengthOverride = 0 - } - - try certificateData.withUnsafeBytes { cert in - try privateKeyData.withUnsafeBytes { priv in - var exception: UnsafeMutablePointer? - defer { exception.map { free($0) } } - - let progress = sendableProgress.value - let entsArray = sendableEntsArray.value - - guard let certBase = cert.baseAddress, - let privBase = priv.baseAddress - else { throw Error.signer(nil) } - - let box = Unmanaged.passRetained(progress as AnyObject) - defer { box.release() } - guard signer.pointee.sign( - app.path, - certBase, lengthOverride ?? cert.count, - privBase, lengthOverride ?? priv.count, - entsArray, entsArray.count, - { - (Unmanaged - .fromOpaque($0) - .takeUnretainedValue() - // swiftlint:disable:next force_cast - as! (Double?) -> Void - )($1 == -1 ? nil : $1) - }, - box.toOpaque(), - &exception - ) == 0 else { - throw Error.signer(exception.map { String(cString: $0) }) - } - } - } - } - - public func sign( - app: URL, - identity: Identity, - entitlementMapping: [URL: Entitlements], - progress: @escaping (Double?) -> Void - ) async throws { - // TODO: #131: update Zupersign to handle multiple entitlements files - // this is a temp hack to just pass along the root app entitlements - let entitlementMapping = entitlementMapping.filter { $0.key.path == app.path } - try self._sign( - app: app, - identity: identity, - entitlementMapping: entitlementMapping, - progress: progress - ) - } - - public func analyze(executable: URL) throws -> Data { - try executable.withUnsafeFileSystemRepresentation { (path: UnsafePointer?) -> Data in - guard let path = path else { throw Error.badFilePath } - var exception: UnsafeMutablePointer? - defer { exception.map { free($0) } } - var count = 0 - guard let bytes = signer.pointee.analyze(path, &count, &exception) - else { throw Error.signer(exception.map { String(cString: $0) })} - return Data(bytesNoCopy: bytes, count: count, deallocator: .free) - } - } - -} diff --git a/Sources/XKit/Signer/SigningContext.swift b/Sources/XKit/Signer/SigningContext.swift index 644f176f..4400eccd 100644 --- a/Sources/XKit/Signer/SigningContext.swift +++ b/Sources/XKit/Signer/SigningContext.swift @@ -18,7 +18,6 @@ public struct SigningContext: Sendable { public var auth: DeveloperAPIAuthData public var targetDevice: TargetDevice? - public var signer: Signer public var developerAPIClient: DeveloperAPIClient { DeveloperAPIClient(auth: auth) @@ -26,12 +25,10 @@ public struct SigningContext: Sendable { public init( auth: DeveloperAPIAuthData, - targetDevice: TargetDevice? = nil, - signer: Signer? = nil - ) throws { + targetDevice: TargetDevice? = nil + ) { self.auth = auth self.targetDevice = targetDevice - self.signer = try signer ?? .first() } } diff --git a/Sources/XToolSupport/DevCommand.swift b/Sources/XToolSupport/DevCommand.swift index 801f341d..12852b97 100644 --- a/Sources/XToolSupport/DevCommand.swift +++ b/Sources/XToolSupport/DevCommand.swift @@ -81,22 +81,21 @@ struct PackOperation { .allProducts .compactMap { p in p.entitlementsPath.map { (p, $0) } } if !productsWithEntitlements.isEmpty { - let mapping = try await withThrowingTaskGroup(of: (URL, Entitlements).self) { group in + let mapping = try await withThrowingTaskGroup(of: (URL, Data).self) { group in for (product, path) in productsWithEntitlements { group.addTask { - let data = try await Data(reading: URL(fileURLWithPath: path)) - let decoder = PropertyListDecoder() - let entitlements = try decoder.decode(Entitlements.self, from: data) + let entitlements = try await Data(reading: URL(fileURLWithPath: path)) return (product.directory(inApp: bundle), entitlements) } } return try await group.reduce(into: [:]) { $0[$1.0] = $1.1 } } d.out.print("Applying entitlements...") - try await Signer.first().sign( + try await d.signer.sign( app: bundle, identity: .adhoc, entitlementMapping: mapping, + deep: true, progress: { _ in } ) }