From 7e500cb37d3a4690e047a26f87285a244c5115fd Mon Sep 17 00:00:00 2001 From: Selim Mustafaev Date: Mon, 15 May 2023 22:57:11 +0300 Subject: [PATCH] Receiving signing identity --- reSign.xcodeproj/project.pbxproj | 10 ++ reSign/ProcessTask.swift | 193 ++++++++++++++++++++++++++ reSign/Views/FormPickerItem.swift | 28 ++++ reSign/Views/Sign/SignView.swift | 9 +- reSign/Views/Sign/SignViewModel.swift | 30 +++- 5 files changed, 263 insertions(+), 7 deletions(-) create mode 100644 reSign/ProcessTask.swift create mode 100644 reSign/Views/FormPickerItem.swift diff --git a/reSign.xcodeproj/project.pbxproj b/reSign.xcodeproj/project.pbxproj index 96407a8..1af9445 100644 --- a/reSign.xcodeproj/project.pbxproj +++ b/reSign.xcodeproj/project.pbxproj @@ -15,6 +15,8 @@ 7A72231529DCABE400503F78 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A72231429DCABE400503F78 /* ContentView.swift */; }; 7A72231729DCABE500503F78 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7A72231629DCABE500503F78 /* Assets.xcassets */; }; 7A72231A29DCABE500503F78 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7A72231929DCABE500503F78 /* Preview Assets.xcassets */; }; + 7A9478012A0974BA00EC7329 /* ProcessTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9478002A0974BA00EC7329 /* ProcessTask.swift */; }; + 7A9478032A0984D200EC7329 /* FormPickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9478022A0984D200EC7329 /* FormPickerItem.swift */; }; 7AF0C51829EDCF59008D4084 /* URL+ExtendedAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF0C51729EDCF59008D4084 /* URL+ExtendedAttributes.swift */; }; 7AF0C51A29EF43C2008D4084 /* OutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF0C51929EF43C2008D4084 /* OutlineView.swift */; }; 7AF0C51C29EF43CD008D4084 /* TreeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF0C51B29EF43CD008D4084 /* TreeItem.swift */; }; @@ -37,6 +39,8 @@ 7A72231629DCABE500503F78 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 7A72231929DCABE500503F78 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 7A72231B29DCABE500503F78 /* reSign.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = reSign.entitlements; sourceTree = ""; }; + 7A9478002A0974BA00EC7329 /* ProcessTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessTask.swift; sourceTree = ""; }; + 7A9478022A0984D200EC7329 /* FormPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormPickerItem.swift; sourceTree = ""; }; 7AF0C51729EDCF59008D4084 /* URL+ExtendedAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+ExtendedAttributes.swift"; sourceTree = ""; }; 7AF0C51929EF43C2008D4084 /* OutlineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineView.swift; sourceTree = ""; }; 7AF0C51B29EF43CD008D4084 /* TreeItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TreeItem.swift; sourceTree = ""; }; @@ -75,6 +79,7 @@ 7AF0C51929EF43C2008D4084 /* OutlineView.swift */, 7A064BE829DE18C700C5D978 /* SignInfoView.swift */, 7AF0C53A29F72151008D4084 /* PlistView.swift */, + 7A9478022A0984D200EC7329 /* FormPickerItem.swift */, ); path = Views; sourceTree = ""; @@ -107,6 +112,7 @@ 7A72231629DCABE500503F78 /* Assets.xcassets */, 7A72231B29DCABE500503F78 /* reSign.entitlements */, 7A72231829DCABE500503F78 /* Preview Content */, + 7A9478002A0974BA00EC7329 /* ProcessTask.swift */, ); path = reSign; sourceTree = ""; @@ -226,11 +232,13 @@ 7A064BE929DE18C700C5D978 /* SignInfoView.swift in Sources */, 7AF0C54229FFBDB0008D4084 /* SignViewModel.swift in Sources */, 7AF0C54729FFCC66008D4084 /* Identity.swift in Sources */, + 7A9478012A0974BA00EC7329 /* ProcessTask.swift in Sources */, 7A064BED29E2C91D00C5D978 /* Certificate.swift in Sources */, 7A72231329DCABE400503F78 /* reSignApp.swift in Sources */, 7AF0C53D29F9B7FE008D4084 /* Quarantine.swift in Sources */, 7AF0C54529FFCAD8008D4084 /* SecError.swift in Sources */, 7AF0C53F29FFB765008D4084 /* SignView.swift in Sources */, + 7A9478032A0984D200EC7329 /* FormPickerItem.swift in Sources */, 7A064BE429DE107000C5D978 /* Category.swift in Sources */, 7AF0C51A29EF43C2008D4084 /* OutlineView.swift in Sources */, ); @@ -371,6 +379,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.reSign; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -398,6 +407,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.reSign; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/reSign/ProcessTask.swift b/reSign/ProcessTask.swift new file mode 100644 index 0000000..dac66c2 --- /dev/null +++ b/reSign/ProcessTask.swift @@ -0,0 +1,193 @@ +// +// Task.swift +// reSign +// +// Created by Selim Mustafaev on 08.05.2023. +// + +import Foundation + +class ProcessTask { + + enum TaskError: LocalizedError { + + case unreadableOutput + + var errorDescription: String? { + switch self { + case .unreadableOutput: return "Process output is not a string" + } + } + } + + static func run(url: URL, arguments: [String] = []) async throws -> String { + + try await withCheckedThrowingContinuation { continuation in + + DispatchQueue.main.async { + launch(tool: url, arguments: arguments) { result, output in + switch result { + case .success(let code): + print("Process return code: \(code)") + if let str = String(data: output, encoding: .utf8) { + continuation.resume(returning: str) + } else { + continuation.resume(throwing: TaskError.unreadableOutput) + } + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + } + + /// Runs the specified tool as a child process, supplying `stdin` and capturing `stdout`. + /// + /// - important: Must be run on the main queue. + /// + /// - Parameters: + /// - tool: The tool to run. + /// - arguments: The command-line arguments to pass to that tool; defaults to the empty array. + /// - input: Data to pass to the tool’s `stdin`; defaults to empty. + /// - completionHandler: Called on the main queue when the tool has terminated. + + static func launch(tool: URL, arguments: [String] = [], input: Data = Data(), completionHandler: @escaping CompletionHandler) { + // This precondition is important; read the comment near the `run()` call to + // understand why. + dispatchPrecondition(condition: .onQueue(.main)) + + let group = DispatchGroup() + let inputPipe = Pipe() + let outputPipe = Pipe() + + var errorQ: Error? = nil + var output = Data() + + let proc = Process() + proc.executableURL = tool + proc.arguments = arguments + proc.standardInput = inputPipe + proc.standardOutput = outputPipe + group.enter() + proc.terminationHandler = { _ in + // This bounce to the main queue is important; read the comment near the + // `run()` call to understand why. + DispatchQueue.main.async { + group.leave() + } + } + + // This runs the supplied block when all three events have completed (task + // termination and the end of both I/O channels). + // + // - important: If the process was never launched, requesting its + // termination status raises an Objective-C exception (ouch!). So, we only + // read `terminationStatus` if `errorQ` is `nil`. + + group.notify(queue: .main) { + if let error = errorQ { + completionHandler(.failure(error), output) + } else { + completionHandler(.success(proc.terminationStatus), output) + } + } + + do { + func posixErr(_ error: Int32) -> Error { NSError(domain: NSPOSIXErrorDomain, code: Int(error), userInfo: nil) } + + // If you write to a pipe whose remote end has closed, the OS raises a + // `SIGPIPE` signal whose default disposition is to terminate your + // process. Helpful! `F_SETNOSIGPIPE` disables that feature, causing + // the write to fail with `EPIPE` instead. + + let fcntlResult = fcntl(inputPipe.fileHandleForWriting.fileDescriptor, F_SETNOSIGPIPE, 1) + guard fcntlResult >= 0 else { throw posixErr(errno) } + + // Actually run the process. + + try proc.run() + + // At this point the termination handler could run and leave the group + // before we have a chance to enter the group for each of the I/O + // handlers. I avoid this problem by having the termination handler + // dispatch to the main thread. We are running on the main thread, so + // the termination handler can’t run until we return, at which point we + // have already entered the group for each of the I/O handlers. + // + // An alternative design would be to enter the group at the top of this + // block and then leave it in the error hander. I decided on this + // design because it has the added benefit of all my code running on the + // main queue and thus I can access shared mutable state, like `errorQ`, + // without worrying about thread safety. + + // Enter the group and then set up a Dispatch I/O channel to write our + // data to the child’s `stdin`. When that’s done, record any error and + // leave the group. + // + // Note that we ignore the residual value passed to the + // `write(offset:data:queue:ioHandler:)` completion handler. Earlier + // versions of this code passed it along to our completion handler but + // the reality is that it’s not very useful. The pipe buffer is big + // enough that it usually soaks up all our data, so the residual is a + // very poor indication of how much data was actually read by the + // client. + + group.enter() + let writeIO = DispatchIO(type: .stream, fileDescriptor: inputPipe.fileHandleForWriting.fileDescriptor, queue: .main) { _ in + // `FileHandle` will automatically close the underlying file + // descriptor when you release the last reference to it. By holidng + // on to `inputPipe` until here, we ensure that doesn’t happen. And + // as we have to hold a reference anyway, we might as well close it + // explicitly. + // + // We apply the same logic to `readIO` below. + try! inputPipe.fileHandleForWriting.close() + } + let inputDD = input.withUnsafeBytes { DispatchData(bytes: $0) } + writeIO.write(offset: 0, data: inputDD, queue: .main) { isDone, _, error in + if isDone || error != 0 { + writeIO.close() + if errorQ == nil && error != 0 { errorQ = posixErr(error) } + group.leave() + } + } + + // Enter the group and then set up a Dispatch I/O channel to read data + // from the child’s `stdin`. When that’s done, record any error and + // leave the group. + + group.enter() + let readIO = DispatchIO(type: .stream, fileDescriptor: outputPipe.fileHandleForReading.fileDescriptor, queue: .main) { _ in + try! outputPipe.fileHandleForReading.close() + } + readIO.read(offset: 0, length: .max, queue: .main) { isDone, chunkQ, error in + output.append(contentsOf: chunkQ ?? .empty) + if isDone || error != 0 { + readIO.close() + if errorQ == nil && error != 0 { errorQ = posixErr(error) } + group.leave() + } + } + } catch { + // If either the `fcntl` or the `run()` call threw, we set the error + // and manually call the termination handler. Note that we’ve only + // entered the group once at this point, so the single leave done by the + // termination handler is enough to run the notify block and call the + // client’s completion handler. + errorQ = error + proc.terminationHandler!(proc) + } + } + + /// Called when the tool has terminated. + /// + /// This must be run on the main queue. + /// + /// - Parameters: + /// - result: Either the tool’s termination status or, if something went + /// wrong, an error indicating what that was. + /// - output: Data captured from the tool’s `stdout`. + + typealias CompletionHandler = (_ result: Result, _ output: Data) -> Void +} diff --git a/reSign/Views/FormPickerItem.swift b/reSign/Views/FormPickerItem.swift new file mode 100644 index 0000000..aeec347 --- /dev/null +++ b/reSign/Views/FormPickerItem.swift @@ -0,0 +1,28 @@ +// +// FormPickerItem.swift +// reSign +// +// Created by Selim Mustafaev on 08.05.2023. +// + +import SwiftUI + +struct FormPickerItem: View { + + let name: String + let options: [String] + @Binding var selectedItem: String + + var body: some View { + HStack { + Text(name) + Spacer() + Picker("", selection: $selectedItem) { + ForEach(options, id: \.self) { option in + Text(option) + } + } + .frame(minWidth: 100) + } + } +} diff --git a/reSign/Views/Sign/SignView.swift b/reSign/Views/Sign/SignView.swift index 93a709d..aea1030 100644 --- a/reSign/Views/Sign/SignView.swift +++ b/reSign/Views/Sign/SignView.swift @@ -14,7 +14,14 @@ struct SignView: View { @StateObject var viewModel = SignViewModel() var body: some View { - Text("Hello, World!") + Form { + Section { + FormPickerItem(name: "Signing Identity", + options: viewModel.signingIdentities, + selectedItem: $viewModel.selectedIdentity) + } + } + .formStyle(.grouped) } } diff --git a/reSign/Views/Sign/SignViewModel.swift b/reSign/Views/Sign/SignViewModel.swift index 9702475..6634153 100644 --- a/reSign/Views/Sign/SignViewModel.swift +++ b/reSign/Views/Sign/SignViewModel.swift @@ -10,15 +10,22 @@ import SwiftUI class SignViewModel: ObservableObject { @Published var signingIdentities: [String] = [] + @Published var selectedIdentity: String = "" init() { - do { - self.signingIdentities = try readSigningIdentities() - for identity in signingIdentities { - print(identity) + updateSigningIdentities() + } + + func updateSigningIdentities() { + Task { @MainActor in + do { + signingIdentities = try await readSigningIdentitiesFromCli() + selectedIdentity = signingIdentities.first ?? "" + } catch { + print("Error getting signing identity from cli (security command): ", error) + print("Trying read from keychain...") + signingIdentities = (try? readSigningIdentities()) ?? [] } - } catch { - print("SignViewModel init error: ", error.localizedDescription) } } @@ -37,4 +44,15 @@ class SignViewModel: ObservableObject { return [] } + + func readSigningIdentitiesFromCli() async throws -> [String] { + let output = try await ProcessTask.run(url: URL(filePath: "/usr/bin/security"), + arguments: ["find-identity", "-v", "-p", "codesigning"]) + + return output.split(separator: "\n").map { line in + let parts = line.split(separator: "\"") + return parts.count >= 2 ? String(parts[1]) : nil + } + .compactMap { $0 } + } }