Verify validity of CAs

This commit is contained in:
Simon Kågedal Reimer 2019-05-20 06:33:03 +02:00
parent af72a890da
commit 7f4575fdba
16 changed files with 285 additions and 4 deletions

21
LICENSE Normal file
View File

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

View File

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

View File

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

View File

@ -1,3 +1,7 @@
//
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
//
import Foundation
import SPMUtility

View File

@ -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
}
do {
let certificate = try row.validatedCertificate()
print(" - \(certificate.subjectSummary ?? "<unknown certificate>")")
} catch {
print(" - Invalid row: \(error.localizedDescription)")
}
}
}
}

View File

@ -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())")"#)
}
}
}

View File

@ -1,3 +1,7 @@
//
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
//
import Foundation
import Basic
import SPMUtility

View File

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

View File

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

View File

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

View File

@ -0,0 +1,12 @@
//
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
//
import Foundation
import SQLite
extension Blob {
var data: Data {
return Data(bytes)
}
}

View File

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

View File

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

View File

@ -1,3 +1,7 @@
//
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
//
import Basic
class XcodeSimulator {

View File

@ -1,3 +1,7 @@
//
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
//
import Foundation
import Basic
import SPMUtility

View File

@ -1,3 +1,7 @@
//
// Copyright © 2019 Simon Kågedal Reimer. See LICENSE.
//
import Foundation
import XcodeSimulatorKit