Skip to content

Custom certificate store #256

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/X509/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ add_library(X509
"Verifier/AllOfPolicies.swift"
"Verifier/AnyPolicy.swift"
"Verifier/CertificateStore.swift"
"Verifier/CustomCertificateStore.swift"
"Verifier/OneOfPolicies.swift"
"Verifier/PolicyBuilder.swift"
"Verifier/RFC5280/BasicConstraintsPolicy.swift"
Expand Down
113 changes: 99 additions & 14 deletions Sources/X509/Verifier/CertificateStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,30 @@ import _CertificateInternals
public struct CertificateStore: Sendable, Hashable {

@usableFromInline
var systemTrustStore: Bool
@usableFromInline
var additionalTrustRoots: [DistinguishedName: [Certificate]]
var backing: Backing

@inlinable
public init() {
self.init([])
}

/// Wrap a ``CustomCertificateStore`` in a ``CertificateStore`` so the custom
/// implementation it can be used interchangeably. For details on why one
/// may decide to implement a ``CustomCertificateStore``, please see the
/// documentation on that protocol.
@inlinable
public init(custom: some CustomCertificateStore) {
backing = .custom(AnyCustomCertificateStore(custom))
}

/// Initialize a certificate store from a sequence of certificates.
@inlinable
public init(_ certificates: some Sequence<Certificate>) {
self.systemTrustStore = false
self.additionalTrustRoots = Dictionary(grouping: certificates, by: \.subject)
backing = .concrete(.init(certificates))
}

init(systemTrustStore: Bool) {
self.systemTrustStore = systemTrustStore
self.additionalTrustRoots = [:]
backing = .concrete(.init(systemTrustStore: systemTrustStore))
}

@inlinable
Expand All @@ -46,9 +52,7 @@ public struct CertificateStore: Sendable, Hashable {

@inlinable
public mutating func append(contentsOf certificates: some Sequence<Certificate>) {
for certificate in certificates {
additionalTrustRoots[certificate.subject, default: []].append(certificate)
}
backing.append(contentsOf: certificates)
}

@inlinable
Expand All @@ -66,22 +70,103 @@ public struct CertificateStore: Sendable, Hashable {
}

func resolve(diagnosticsCallback: ((VerificationDiagnostic) -> Void)?) async -> Resolved {
await Resolved(self, diagnosticsCallback: diagnosticsCallback)
switch self.backing {
case .custom(let inner): .custom(inner)
case .concrete(let inner): .concrete(await ConcreteResolved(inner, diagnosticsCallback: diagnosticsCallback))
}
}
}

@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
extension CertificateStore {
@usableFromInline
struct Resolved {
struct ConcreteBacking: Sendable, Hashable {
@usableFromInline
var systemTrustStore: Bool
@usableFromInline
var additionalTrustRoots: [DistinguishedName: [Certificate]]

@inlinable
public init(_ certificates: some Sequence<Certificate>) {
self.systemTrustStore = false
self.additionalTrustRoots = Dictionary(grouping: certificates, by: \.subject)
}

@inlinable
init(systemTrustStore: Bool) {
self.systemTrustStore = systemTrustStore
self.additionalTrustRoots = [:]
}

@inlinable
mutating func append(contentsOf certificates: some Sequence<Certificate>) {
for certificate in certificates {
self.additionalTrustRoots[certificate.subject, default: []].append(certificate)
}
}
}

@usableFromInline
enum Backing: Sendable, Hashable {
case custom(AnyCustomCertificateStore)
case concrete(ConcreteBacking)

@inlinable
mutating func append(contentsOf certificates: some Sequence<Certificate>) {
switch self {
case .custom(var inner):
inner.append(contentsOf: certificates)
self = .custom(inner)
case .concrete(var inner):
inner.append(contentsOf: certificates)
self = .concrete(inner)
}
}
}
}

@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
extension CertificateStore {
@usableFromInline
enum Resolved {
case custom(AnyCustomCertificateStore)
case concrete(ConcreteResolved)
}
}

@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
extension CertificateStore.Resolved {
@inlinable
subscript(subject: DistinguishedName) -> [Certificate]? {
get async {
switch self {
case .custom(let inner): await inner[subject]
case .concrete(let inner): inner[subject]
}
}
}

@inlinable
func contains(_ certificate: Certificate) async -> Bool {
switch self {
case .custom(let inner): await inner.contains(certificate)
case .concrete(let inner): inner.contains(certificate)
}
}
}

@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
extension CertificateStore {
@usableFromInline
struct ConcreteResolved {

@usableFromInline
var systemTrustRoots: [DistinguishedName: [Certificate]]

@usableFromInline
var additionalTrustRoots: [DistinguishedName: [Certificate]]

init(_ store: CertificateStore, diagnosticsCallback: ((VerificationDiagnostic) -> Void)?) async {
init(_ store: ConcreteBacking, diagnosticsCallback: ((VerificationDiagnostic) -> Void)?) async {
if store.systemTrustStore {
do {
systemTrustRoots = try await CertificateStore.cachedSystemTrustRootsFuture.value
Expand All @@ -99,7 +184,7 @@ extension CertificateStore {
}

@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
extension CertificateStore.Resolved {
extension CertificateStore.ConcreteResolved {
@inlinable
subscript(subject: DistinguishedName) -> [Certificate]? {
get {
Expand Down
124 changes: 124 additions & 0 deletions Sources/X509/Verifier/CustomCertificateStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftCertificates open source project
//
// Copyright (c) 2022 Apple Inc. and the SwiftCertificates project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftCertificates project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// Implement the ``CustomCertificateStore`` if you want to perform dynamic
/// certificate lookup, or if you need custom logic when matching the
/// ``DistinguishedName`` of an Issuer with the Subject of the issuer
/// certificate, then implement a custom certificate store used by the
/// ```Verifier```.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
public protocol CustomCertificateStore: Sendable, Hashable {
/// Obtain a list of certificates which has a given subject. Note that this
/// is an async method so that database lookups can be performed
/// asynchronously.
subscript(subject: DistinguishedName) -> [Certificate]? {
get async
}

/// Validate if a given certificate is known to exist in this certificate
/// store. Note that this is an async method so that the existence check
/// can be performed against a database.
func contains(_ certificate: Certificate) async -> Bool

/// Add a certificate to this certificate store.
mutating func append(contentsOf certificates: some Sequence<Certificate>)
}

@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
@usableFromInline
struct AnyCustomCertificateStore: CustomCertificateStore {
@usableFromInline
var value: any DynCustomCertificateStore

@usableFromInline
init<T: CustomCertificateStore>(_ value: T) {
self.value = Backing(value)
}

@inlinable
subscript(subject: DistinguishedName) -> [Certificate]? {
get async {
await value[subject]
}
}

@inlinable
func contains(_ certificate: Certificate) async -> Bool {
await value.contains(certificate)
}

@inlinable
mutating func append(contentsOf certificates: some Sequence<Certificate>) {
value.append(contentsOf: certificates)
}
}

@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
extension AnyCustomCertificateStore: Hashable {
public static func == (lhs: AnyCustomCertificateStore, rhs: AnyCustomCertificateStore) -> Bool {
return lhs.value.isEqual(rhs.value, recurse: true)
}

public var hashValue: Int {
return value.hashValue
}

public func hash(into hasher: inout Hasher) {
value.hash(into: &hasher)
}
}

@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
extension AnyCustomCertificateStore {
@usableFromInline
protocol DynCustomCertificateStore: CustomCertificateStore {
func isEqual(_ rhs: any DynCustomCertificateStore, recurse: Bool) -> Bool
}
}

@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *)
extension AnyCustomCertificateStore {
struct Backing<T: CustomCertificateStore>: DynCustomCertificateStore {
var value: T

init(_ value: T) {
self.value = value
}

subscript(subject: DistinguishedName) -> [Certificate]? {
get async {
await value[subject]
}
}

func contains(_ certificate: Certificate) async -> Bool {
await value.contains(certificate)
}

@inlinable
mutating func append(contentsOf certificates: some Sequence<Certificate>) {
value.append(contentsOf: certificates)
}

func isEqual(_ rhs: any DynCustomCertificateStore, recurse: Bool) -> Bool {
guard let rhs = rhs as? Self else {
guard recurse else {
return false
}
return rhs.isEqual(self, recurse: false)
}
return self == rhs
}
}
}
6 changes: 3 additions & 3 deletions Sources/X509/Verifier/Verifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public struct Verifier<Policy: VerifierPolicy> {
// which may let us chain through another variant of this certificate and build a valid chain. This is a very
// deliberate choice: certificates that assert the same combination of (subject, public key, SAN) but different
// extensions or policies should not be tolerated by this check, and will be ignored.
if rootCertificates.contains(leafCertificate) {
if await rootCertificates.contains(leafCertificate) {
let unverifiedChain = UnverifiedCertificateChain([leafCertificate])

switch await self.policy.chainMeetsPolicyRequirements(chain: unverifiedChain) {
Expand All @@ -81,7 +81,7 @@ public struct Verifier<Policy: VerifierPolicy> {
diagnosticCallback?(.searchingForIssuerOfPartialChain(nextPartialCandidate))
// We want to search for parents. Our preferred parent comes from the root store, as this will potentially
// produce smaller chains.
if var rootParents = rootCertificates[nextPartialCandidate.currentTip.issuer] {
if var rootParents = await rootCertificates[nextPartialCandidate.currentTip.issuer] {
// We then want to sort by suitability.
rootParents.sortBySuitabilityForIssuing(certificate: nextPartialCandidate.currentTip)
diagnosticCallback?(
Expand Down Expand Up @@ -115,7 +115,7 @@ public struct Verifier<Policy: VerifierPolicy> {
}
}

if var intermediateParents = intermediates[nextPartialCandidate.currentTip.issuer] {
if var intermediateParents = await intermediates[nextPartialCandidate.currentTip.issuer] {
// We then want to sort by suitability.
intermediateParents.sortBySuitabilityForIssuing(certificate: nextPartialCandidate.currentTip)
diagnosticCallback?(
Expand Down
Loading
Loading