Verify validity of CAs
This commit is contained in:
parent
af72a890da
commit
7f4575fdba
|
@ -0,0 +1,21 @@
|
|||
The MIT License
|
||||
|
||||
Copyright (c) 2019 Simon Kågedal Reimer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
|
@ -13,7 +13,7 @@ let package = Package(
|
|||
dependencies: [
|
||||
// There is no tagged release of Swift 5's SwiftPM yet, so let's use this for now.
|
||||
.package(url: "https://github.com/apple/swift-package-manager.git", .branch("swift-5.0-branch")),
|
||||
.package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.12.0"),
|
||||
.package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.12.0")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
//
|
||||
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Basic
|
||||
import SPMUtility
|
||||
|
@ -63,7 +67,7 @@ struct CommandLineOptions {
|
|||
|
||||
binder.bind(
|
||||
parser: parser,
|
||||
to: { _, subcommand in
|
||||
to: { _, _ in
|
||||
// print("Parsed subcommand: \(subcommand)")
|
||||
}
|
||||
)
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
//
|
||||
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
//
|
||||
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
|
||||
|
@ -47,12 +51,17 @@ struct ListDevicesCommand: Command {
|
|||
}
|
||||
|
||||
var didPrintHeader = false
|
||||
for certificate in try store.certificates() {
|
||||
for row in try store.rows() {
|
||||
if !didPrintHeader {
|
||||
print(" Certificates:")
|
||||
didPrintHeader = true
|
||||
}
|
||||
print(" - \(certificate.subjectSummary ?? "<unknown certificate>")")
|
||||
do {
|
||||
let certificate = try row.validatedCertificate()
|
||||
print(" - \(certificate.subjectSummary ?? "<unknown certificate>")")
|
||||
} catch {
|
||||
print(" - Invalid row: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||
//
|
||||
// swiftlint:disable line_length
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Data {
|
||||
func dumpAsSwift(_ name: String = "data") {
|
||||
if count > 48 {
|
||||
print(#"""
|
||||
let \#(name) = Data(base64Encoded: """
|
||||
\#(base64EncodedString(options: .lineLength64Characters).replacingOccurrences(of: "\n", with: "\n "))
|
||||
""", options: .ignoreUnknownCharacters)
|
||||
"""#)
|
||||
} else {
|
||||
print(#"let \#(name) = Data(base64Encoded: "\#(base64EncodedString())")"#)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
//
|
||||
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Basic
|
||||
import SPMUtility
|
||||
|
|
|
@ -44,6 +44,17 @@ struct Certificate {
|
|||
return data as Data
|
||||
}
|
||||
|
||||
func normalizedSubjectContent() throws -> Data {
|
||||
var error: Unmanaged<CFError>?
|
||||
guard let data = SecCertificateCopyNormalizedSubjectContent(certificate, &error) else {
|
||||
guard let error = error else {
|
||||
throw Error.unknown
|
||||
}
|
||||
throw error.takeRetainedValue()
|
||||
}
|
||||
return data as Data
|
||||
}
|
||||
|
||||
func printInfo() {
|
||||
if let summary = subjectSummary {
|
||||
print(summary)
|
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A very minimal DER parser
|
||||
struct DERParser {
|
||||
enum TagClass: UInt8 {
|
||||
case universal = 0
|
||||
case application = 1
|
||||
case contentSpecific = 2
|
||||
case `private` = 3
|
||||
}
|
||||
struct TLV {
|
||||
let tagClass: TagClass
|
||||
let isConstructed: Bool
|
||||
let tagNumber: Int
|
||||
let data: Data
|
||||
}
|
||||
|
||||
enum Error: Swift.Error {
|
||||
case parseError(String)
|
||||
}
|
||||
|
||||
func parse(data: Data) throws -> TLV {
|
||||
var iterator = data.makeIterator()
|
||||
guard let tlv = try parse(iterator: &iterator) else {
|
||||
throw Error.parseError("Expected DER contents")
|
||||
}
|
||||
return tlv
|
||||
}
|
||||
|
||||
func parse(iterator: inout Data.Iterator) throws -> TLV? {
|
||||
guard let identifier = iterator.next() else {
|
||||
return nil
|
||||
}
|
||||
let tagClass = TagClass(rawValue: identifier >> 6)!
|
||||
let isConstructed = (identifier & 0b0010_0000) == 0b0010_0000
|
||||
let tagNumber = Int(identifier & 0b0001_1111)
|
||||
let length = try parseLength(&iterator)
|
||||
var data = Data(count: length)
|
||||
for index in data.indices {
|
||||
guard let byte = iterator.next() else {
|
||||
throw Error.parseError("Expected \(length) bytes of data, got \(index)")
|
||||
}
|
||||
data[index] = byte
|
||||
}
|
||||
return TLV(
|
||||
tagClass: tagClass,
|
||||
isConstructed: isConstructed,
|
||||
tagNumber: tagNumber,
|
||||
data: data
|
||||
)
|
||||
}
|
||||
|
||||
private func parseLength(_ iterator: inout Data.Iterator) throws -> Int {
|
||||
guard let firstLengthOctet = iterator.next() else {
|
||||
throw Error.parseError("Expected initial length octet")
|
||||
}
|
||||
|
||||
if firstLengthOctet < 0b1000_0000 {
|
||||
return Int(firstLengthOctet)
|
||||
}
|
||||
|
||||
var length: Int = 0
|
||||
let comingOctets = firstLengthOctet & 0b0111_1111
|
||||
for i in 0..<comingOctets {
|
||||
guard let x = iterator.next() else {
|
||||
throw Error.parseError("Expected \(comingOctets) length octets, got \(i)")
|
||||
}
|
||||
length = length << 8 + Int(x)
|
||||
}
|
||||
return length
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||
//
|
||||
|
||||
import CommonCrypto
|
||||
import Foundation
|
||||
|
||||
enum Digest {
|
||||
static func sha1(_ data: Data) -> Data {
|
||||
var digest = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
|
||||
data.withUnsafeBytes { input in
|
||||
_ = CC_SHA1(input.baseAddress, CC_LONG(input.count), &digest)
|
||||
}
|
||||
return Data(digest)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
extension Blob {
|
||||
var data: Data {
|
||||
return Data(bytes)
|
||||
}
|
||||
}
|
|
@ -1,3 +1,8 @@
|
|||
//
|
||||
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||
//
|
||||
|
||||
import CommonCrypto
|
||||
import Foundation
|
||||
import Basic
|
||||
import SQLite
|
||||
|
@ -23,6 +28,10 @@ struct TrustStore {
|
|||
private let connection: SQLite.Connection
|
||||
private let sqliteMaster = Table("sqlite_master")
|
||||
private let tsettings = Table("tsettings")
|
||||
|
||||
private let sha1Column = Expression<Blob>("sha1")
|
||||
private let subjColumn = Expression<Blob>("subj")
|
||||
private let tsetColumn = Expression<Blob?>("tset")
|
||||
private let dataColumn = Expression<Blob?>("data")
|
||||
|
||||
fileprivate init(openingPath path: String) throws {
|
||||
|
@ -42,6 +51,17 @@ struct TrustStore {
|
|||
}
|
||||
}
|
||||
|
||||
func rows() throws -> [TrustStoreRow] {
|
||||
return try connection.prepare(tsettings).compactMap { row in
|
||||
TrustStoreRow(
|
||||
subj: row[subjColumn].data,
|
||||
sha1: row[sha1Column].data,
|
||||
tset: row[tsetColumn]?.data,
|
||||
data: row[dataColumn]?.data
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func certificates() throws -> [Certificate] {
|
||||
return try connection.prepare(tsettings).compactMap { row in
|
||||
try row[dataColumn].map { blob in
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
//
|
||||
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TrustStoreRow {
|
||||
let subj: Data
|
||||
let sha1: Data
|
||||
let tset: Data?
|
||||
let data: Data?
|
||||
}
|
||||
|
||||
extension TrustStoreRow {
|
||||
enum Error: LocalizedError {
|
||||
case noTsetValue
|
||||
case noData
|
||||
case subjectContentDoesNotMatch
|
||||
case sha1DoesNotMatch
|
||||
case tsetNotValid
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .noTsetValue:
|
||||
return "No tset data was found"
|
||||
case .tsetNotValid:
|
||||
return "Format of tset data was not valid"
|
||||
case .noData:
|
||||
return "No certificate data was found"
|
||||
case .subjectContentDoesNotMatch:
|
||||
return "The content of the subject column did not match expectations"
|
||||
case .sha1DoesNotMatch:
|
||||
return "SHA-1 hash did not match"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validatedCertificate() throws -> Certificate {
|
||||
guard let tset = tset else {
|
||||
throw Error.noTsetValue
|
||||
}
|
||||
guard let data = data else {
|
||||
throw Error.noData
|
||||
}
|
||||
let certificate = try Certificate(data)
|
||||
let subjectContent = try certificate.normalizedSubjectContent()
|
||||
let tlv = try DERParser().parse(data: subjectContent)
|
||||
|
||||
guard tlv.data == subj else {
|
||||
throw Error.subjectContentDoesNotMatch
|
||||
}
|
||||
|
||||
guard sha1 == Digest.sha1(data) else {
|
||||
throw Error.sha1DoesNotMatch
|
||||
}
|
||||
|
||||
guard isValidTset(tset) else {
|
||||
throw Error.tsetNotValid
|
||||
}
|
||||
|
||||
return certificate
|
||||
}
|
||||
|
||||
private func isValidTset(_ data: Data) -> Bool {
|
||||
do {
|
||||
let plist = try PropertyListSerialization.propertyList(from: data, options: [], format: nil)
|
||||
return plist is [Any]
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
//
|
||||
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||
//
|
||||
|
||||
import Basic
|
||||
|
||||
class XcodeSimulator {
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
//
|
||||
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Basic
|
||||
import SPMUtility
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
//
|
||||
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XcodeSimulatorKit
|
||||
|
||||
|
|
Loading…
Reference in New Issue