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: [
|
dependencies: [
|
||||||
// There is no tagged release of Swift 5's SwiftPM yet, so let's use this for now.
|
// 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/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: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
//
|
||||||
|
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||||
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Basic
|
import Basic
|
||||||
import SPMUtility
|
import SPMUtility
|
||||||
|
@ -63,7 +67,7 @@ struct CommandLineOptions {
|
||||||
|
|
||||||
binder.bind(
|
binder.bind(
|
||||||
parser: parser,
|
parser: parser,
|
||||||
to: { _, subcommand in
|
to: { _, _ in
|
||||||
// print("Parsed subcommand: \(subcommand)")
|
// print("Parsed subcommand: \(subcommand)")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
//
|
||||||
|
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||||
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SPMUtility
|
import SPMUtility
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
//
|
||||||
|
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||||
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SPMUtility
|
import SPMUtility
|
||||||
|
|
||||||
|
@ -47,12 +51,17 @@ struct ListDevicesCommand: Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
var didPrintHeader = false
|
var didPrintHeader = false
|
||||||
for certificate in try store.certificates() {
|
for row in try store.rows() {
|
||||||
if !didPrintHeader {
|
if !didPrintHeader {
|
||||||
print(" Certificates:")
|
print(" Certificates:")
|
||||||
didPrintHeader = true
|
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 Foundation
|
||||||
import Basic
|
import Basic
|
||||||
import SPMUtility
|
import SPMUtility
|
||||||
|
|
|
@ -44,6 +44,17 @@ struct Certificate {
|
||||||
return data as Data
|
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() {
|
func printInfo() {
|
||||||
if let summary = subjectSummary {
|
if let summary = subjectSummary {
|
||||||
print(summary)
|
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 Foundation
|
||||||
import Basic
|
import Basic
|
||||||
import SQLite
|
import SQLite
|
||||||
|
@ -23,6 +28,10 @@ struct TrustStore {
|
||||||
private let connection: SQLite.Connection
|
private let connection: SQLite.Connection
|
||||||
private let sqliteMaster = Table("sqlite_master")
|
private let sqliteMaster = Table("sqlite_master")
|
||||||
private let tsettings = Table("tsettings")
|
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")
|
private let dataColumn = Expression<Blob?>("data")
|
||||||
|
|
||||||
fileprivate init(openingPath path: String) throws {
|
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] {
|
func certificates() throws -> [Certificate] {
|
||||||
return try connection.prepare(tsettings).compactMap { row in
|
return try connection.prepare(tsettings).compactMap { row in
|
||||||
try row[dataColumn].map { blob 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
|
import Basic
|
||||||
|
|
||||||
class XcodeSimulator {
|
class XcodeSimulator {
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
//
|
||||||
|
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||||
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Basic
|
import Basic
|
||||||
import SPMUtility
|
import SPMUtility
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
//
|
||||||
|
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
|
||||||
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import XcodeSimulatorKit
|
import XcodeSimulatorKit
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue