Add export command
This commit is contained in:
parent
ba79a12cd8
commit
a417c8860f
|
@ -34,6 +34,7 @@ struct CommandLineOptions {
|
||||||
private static let allCommands: [Command] = [
|
private static let allCommands: [Command] = [
|
||||||
ListDevicesCommand(),
|
ListDevicesCommand(),
|
||||||
InstallCACommand(),
|
InstallCACommand(),
|
||||||
|
ExportCACommand(),
|
||||||
RemoveCACommand()
|
RemoveCACommand()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
//
|
||||||
|
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Basic
|
||||||
|
import SPMUtility
|
||||||
|
|
||||||
|
class ExportCACommand: Command {
|
||||||
|
private struct Options {
|
||||||
|
}
|
||||||
|
let name = "export-ca"
|
||||||
|
let overview = "Export Certificate Authorities"
|
||||||
|
|
||||||
|
private let binder = ArgumentBinder<Options>()
|
||||||
|
private var options = Options()
|
||||||
|
|
||||||
|
private let filteringBinder = ArgumentBinder<FilteringOptions>()
|
||||||
|
private var filteringOptions = FilteringOptions()
|
||||||
|
|
||||||
|
private var exportedCertificates: Set<Data> = []
|
||||||
|
|
||||||
|
func addOptions(to parser: ArgumentParser) {
|
||||||
|
filteringBinder.bind(to: &filteringOptions, parser: parser)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fillParseResult(_ parseResult: ArgumentParser.Result) throws {
|
||||||
|
try binder.fill(parseResult: parseResult, into: &options)
|
||||||
|
try filteringBinder.fill(parseResult: parseResult, into: &filteringOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() throws {
|
||||||
|
exportedCertificates = []
|
||||||
|
for device in try Simctl.flatListDevices().filter(using: filteringOptions) {
|
||||||
|
exportCertificates(for: device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func exportCertificates(for device: Simctl.Device) {
|
||||||
|
do {
|
||||||
|
let store = try TrustStore(uuid: device.udid).open()
|
||||||
|
for row in try store.rows() where !exportedCertificates.contains(row.sha1) {
|
||||||
|
guard let data = row.data else { continue }
|
||||||
|
do {
|
||||||
|
let path = try exportCertificate(from: data)
|
||||||
|
print("Exported PEM to \(localFileSystem.printable(path))")
|
||||||
|
exportedCertificates.insert(row.sha1)
|
||||||
|
} catch {
|
||||||
|
print("Found invalid certificate: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func exportCertificate(from certificateData: Data) throws -> AbsolutePath {
|
||||||
|
let certificate = try Certificate(certificateData)
|
||||||
|
let fileName = certificate.subjectSummary?.clean() ?? "unknown-certificate"
|
||||||
|
let path = try localFileSystem.uniquePath(base: fileName, ext: "crt")
|
||||||
|
let data = try certificate.exportPEM()
|
||||||
|
try data.write(to: path.asURL)
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
func clean() -> String {
|
||||||
|
return String(map { $0.isWhitespace || $0 == "/" ? "-" : $0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FileSystemError: LocalizedError {
|
||||||
|
case couldNotGetCurrentyWorkingDirectory
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .couldNotGetCurrentyWorkingDirectory:
|
||||||
|
return "Unable to get current working directory"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension FileSystem {
|
||||||
|
/// Returns "base.ext" unless that already exists, in which case it appends a serial number until a unique path has
|
||||||
|
/// been found: "base.ext" -> "base-1.ext" -> "base-2.ext" etc.
|
||||||
|
func uniquePath(base: String, ext: String, count: Int? = nil) throws -> AbsolutePath {
|
||||||
|
guard let directory = currentWorkingDirectory else {
|
||||||
|
throw FileSystemError.couldNotGetCurrentyWorkingDirectory
|
||||||
|
}
|
||||||
|
let path = directory.appending(component: base + (count.map { "-\($0)" } ?? "") + "." + ext)
|
||||||
|
if exists(path) {
|
||||||
|
return try uniquePath(base: base, ext: ext, count: count.map { $0 + 1 } ?? 1)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func printable(_ path: AbsolutePath) -> String {
|
||||||
|
if let directory = currentWorkingDirectory {
|
||||||
|
return path.relative(to: directory).description
|
||||||
|
}
|
||||||
|
return path.description
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,8 @@ struct Certificate {
|
||||||
enum Error: LocalizedError {
|
enum Error: LocalizedError {
|
||||||
case invalidDERX509
|
case invalidDERX509
|
||||||
case importError(OSStatus)
|
case importError(OSStatus)
|
||||||
|
case exportError(OSStatus)
|
||||||
|
case exportNoData
|
||||||
case notACertficate
|
case notACertficate
|
||||||
case unknown
|
case unknown
|
||||||
|
|
||||||
|
@ -18,6 +20,10 @@ struct Certificate {
|
||||||
return "Given data was not a valid DER encoded X.509 certificate"
|
return "Given data was not a valid DER encoded X.509 certificate"
|
||||||
case .importError(let status):
|
case .importError(let status):
|
||||||
return "Error from SecItemImport: \(status)"
|
return "Error from SecItemImport: \(status)"
|
||||||
|
case .exportError(let status):
|
||||||
|
return "Error from SecItemExport: \(status)"
|
||||||
|
case .exportNoData:
|
||||||
|
return "No data returned from SecItemExport"
|
||||||
case .notACertficate:
|
case .notACertficate:
|
||||||
return "SecItemImport gave something else than a certificate"
|
return "SecItemImport gave something else than a certificate"
|
||||||
case .unknown:
|
case .unknown:
|
||||||
|
@ -93,4 +99,17 @@ struct Certificate {
|
||||||
return try Certificate(certData)
|
return try Certificate(certData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func exportPEM() throws -> Data {
|
||||||
|
var cfData: CFData?
|
||||||
|
let status = withUnsafeMutablePointer(to: &cfData) { pointer in
|
||||||
|
SecItemExport(certificate, .formatX509Cert, .pemArmour, nil, pointer)
|
||||||
|
}
|
||||||
|
guard status == errSecSuccess else {
|
||||||
|
throw Error.exportError(status)
|
||||||
|
}
|
||||||
|
guard let data = cfData else {
|
||||||
|
throw Error.exportNoData
|
||||||
|
}
|
||||||
|
return data as Data
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue