Add export command

This commit is contained in:
Simon Kågedal Reimer 2019-06-02 19:04:03 +02:00
parent ba79a12cd8
commit a417c8860f
3 changed files with 124 additions and 1 deletions

View File

@ -34,6 +34,7 @@ struct CommandLineOptions {
private static let allCommands: [Command] = [
ListDevicesCommand(),
InstallCACommand(),
ExportCACommand(),
RemoveCACommand()
]

View File

@ -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
}
}

View File

@ -9,6 +9,8 @@ struct Certificate {
enum Error: LocalizedError {
case invalidDERX509
case importError(OSStatus)
case exportError(OSStatus)
case exportNoData
case notACertficate
case unknown
@ -18,6 +20,10 @@ struct Certificate {
return "Given data was not a valid DER encoded X.509 certificate"
case .importError(let 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:
return "SecItemImport gave something else than a certificate"
case .unknown:
@ -44,7 +50,7 @@ struct Certificate {
var sha1: Data {
return Digest.sha1(data)
}
func normalizedSubjectSequence() throws -> Data {
var error: Unmanaged<CFError>?
guard let data = SecCertificateCopyNormalizedSubjectContent(certificate, &error) else {
@ -93,4 +99,17 @@ struct Certificate {
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
}
}