Skip to content
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

Support loading trust root CAs on Linux #136

Merged
merged 16 commits into from
Oct 18, 2023
Merged
2 changes: 1 addition & 1 deletion Benchmarks/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ let package = Package(
.package(path: "../"),
.package(url: "https://github.com/ordo-one/package-benchmark", from: "1.11.1"),
.package(url: "https://github.com/apple/swift-crypto.git", from: "2.5.0"),
.package(url: "https://github.com/apple/swift-asn1.git", from: "1.0.0-beta.1"),
.package(url: "https://github.com/apple/swift-asn1.git", from: "1.0.0-beta.2"),
],
targets: [
.executableTarget(
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ let package = Package(
.copy("OCSP Test Resources/www.apple.com.intermediate.ocsp-response.der"),
.copy("PEMTestRSACertificate.pem"),
.copy("CSR Vectors/"),
.copy("ca-certificates.crt"),
]),
.target(
name: "_CertificateInternals",
Expand All @@ -75,7 +76,7 @@ let package = Package(
if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil {
package.dependencies += [
.package(url: "https://github.com/apple/swift-crypto.git", from: "2.5.0"),
.package(url: "https://github.com/apple/swift-asn1.git", from: "1.0.0-beta.1"),
.package(url: "https://github.com/apple/swift-asn1.git", from: "1.0.0-beta.2"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
]
} else {
Expand Down
56 changes: 56 additions & 0 deletions Sources/X509/LockedValueBox.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftCertificates open source project
//
// Copyright (c) 2023 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
//
//===----------------------------------------------------------------------===//

import Foundation

final class LockedValueBox<Value> {
private let _lock: NSLock = .init()
private var _value: Value

var unlockedValue: Value {
dnadoba marked this conversation as resolved.
Show resolved Hide resolved
get { _value }
set { _value = newValue }
}

func lock() {
_lock.lock()
}

func unlock() {
_lock.unlock()
}

init(_ value: Value) {
self._value = value
}

func withLockedValue<Result>(
_ body: (inout Value) throws -> Result
) rethrows -> Result {
try _lock.withLock {
try body(&_value)
}
}
}

extension LockedValueBox: @unchecked Sendable where Value: Sendable {}

extension NSLock {
// this API doesn't exist on Linux and therefore we have a copy of it here
func withLock<Result>(_ body: () throws -> Result) rethrows -> Result {
self.lock()
defer { self.unlock() }
return try body()
}
}
123 changes: 123 additions & 0 deletions Sources/X509/PromiseAndFuture.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftCertificates open source project
//
// Copyright (c) 2023 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
//
//===----------------------------------------------------------------------===//

// MARK: - Promise
final class Promise<Value, Failure: Error> {
private enum State {
case unfulfilled(observers: [CheckedContinuation<Result<Value, Failure>, Never>])
case fulfilled(Result<Value, Failure>)
}

private let state = LockedValueBox(State.unfulfilled(observers: []))

init() {}

fileprivate var result: Result<Value, Failure> {
get async {
self.state.lock()

switch self.state.unlockedValue {
case .fulfilled(let result):
defer { self.state.unlock() }
return result

case .unfulfilled(var observers):
return await withCheckedContinuation {
(continuation: CheckedContinuation<Result<Value, Failure>, Never>) in
observers.append(continuation)
self.state.unlockedValue = .unfulfilled(observers: observers)
self.state.unlock()
}
}
}
}

func fulfil(with result: Result<Value, Failure>) {
self.state.withLockedValue { state in
switch state {
case .fulfilled(let oldResult):
fatalError("tried to fulfil Promise that is already fulfilled to \(oldResult). New result: \(result)")
case .unfulfilled(let observers):
for observer in observers {
observer.resume(returning: result)
}
state = .fulfilled(result)
}
}
}

deinit {
switch self.state.unlockedValue {
case .fulfilled:
break
case .unfulfilled:
fatalError("unfulfilled Promise leaked")
}
}
}

extension Promise: Sendable where Value: Sendable {}

extension Promise {
func succeed(with value: Value) {
self.fulfil(with: .success(value))
}

func fail(with error: Failure) {
self.fulfil(with: .failure(error))
}
}

// MARK: - Future

struct Future<Value, Failure: Error> {
private let promise: Promise<Value, Failure>

init(_ promise: Promise<Value, Failure>) {
self.promise = promise
}

var result: Result<Value, Failure> {
get async {
await promise.result
}
}
}

extension Future: Sendable where Value: Sendable {}

extension Future {
var value: Value {
get async throws {
try await result.get()
}
}
}

extension Future where Failure == Never {
var value: Value {
get async {
await result.get()
}
}
}

extension Result where Failure == Never {
func get() -> Success {
switch self {
case .success(let success):
return success
}
}
}
151 changes: 135 additions & 16 deletions Sources/X509/Verifier/CertificateStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,47 +12,166 @@
//
//===----------------------------------------------------------------------===//

import _CertificateInternals

/// A collection of ``Certificate`` objects for use in a verifier.
public struct CertificateStore: Sendable, Hashable {
/// Stores the certificates, indexed by subject name.
@usableFromInline
var _certificates: [DistinguishedName: [Certificate]]
enum Element: Sendable, Hashable {
#if os(Linux)
case trustRoots
dnadoba marked this conversation as resolved.
Show resolved Hide resolved
#endif
/// Stores the certificates, indexed by subject name.
case customCertificates([DistinguishedName: [Certificate]])
}

@usableFromInline
var _certificates: _TinyArray<Element>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there ever more than two elements here? If not, can we use an enum instead of a TinyArray?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought of allowing to combing certificate stores which would in theory allow up to 3 elements i.e. [.customCertificates(...), .trustRoots, .customCertificates(...)]. The current public API doesn't allow that though and I don't have a concrete use case in mind for that. Do you think something like that is useful or not? If not, we can go with a tuple. I think it makes the implementation a bit more complicated as we can't iterate over tuples but hopefully it will not be too bad.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think sticking with the tuple is better.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have now implemented _TinyArray2 which store up to two elements inline.


@inlinable
public init() {
self._certificates = [:]
self.init(elements: CollectionOfOne(.customCertificates([:])))
}

@inlinable
public init<Certificates: Sequence>(_ certificates: Certificates) where Certificates.Element == Certificate {
self._certificates = Dictionary(grouping: certificates, by: \.subject)
public init(_ certificates: some Sequence<Certificate>) {
self.init(elements: CollectionOfOne(.customCertificates(Dictionary(grouping: certificates, by: \.subject))))
}

@inlinable
internal init(elements: some Sequence<Element>) {
self._certificates = .init(elements)
}

@inlinable
mutating func insert(_ certificate: Certificate) {
self._certificates[certificate.subject, default: []].append(certificate)
public mutating func insert(_ certificate: Certificate) {
self.insert(contentsOf: CollectionOfOne(certificate))
}

@inlinable
mutating func insert<Certificates: Sequence>(contentsOf certificates: Certificates)
where Certificates.Element == Certificate {
for certificate in certificates {
self.insert(certificate)
public mutating func insert(contentsOf certificates: some Sequence<Certificate>) {
if self._certificates.isEmpty {
self = .init(certificates)
dnadoba marked this conversation as resolved.
Show resolved Hide resolved
}
let lastIndex = self._certificates.index(before: self._certificates.endIndex)
switch self._certificates[lastIndex] {
case .customCertificates(var certificatesIndexBySubjectName):
for certificate in certificates {
certificatesIndexBySubjectName[certificate.subject, default: []].append(certificate)
}
self._certificates[lastIndex] = .customCertificates(certificatesIndexBySubjectName)
#if os(Linux)
case .trustRoots:
self._certificates.append(.customCertificates(Dictionary(grouping: certificates, by: \.subject)))
#endif
}
}

#if swift(>=5.9)
@inlinable
public consuming func inserting(contentsOf certificates: some Sequence<Certificate>) -> Self {
dnadoba marked this conversation as resolved.
Show resolved Hide resolved
self.insert(contentsOf: certificates)
return self
}
#else
@inlinable
public func inserting(contentsOf certificates: some Sequence<Certificate>) -> Self {
var copy = self
copy.insert(contentsOf: certificates)
return copy
}
#endif

#if swift(>=5.9)
@inlinable
public consuming func inserting(_ certificate: Certificate) -> Self {
self.insert(certificate)
return self
}
#else
@inlinable
public func inserting(_ certificate: Certificate) -> Self {
var copy = self
copy.insert(certificate)
return self
}
#endif

func resolve(diagnosticsCallback: ((VerificationDiagnostic) -> Void)?) async -> Resolved {
await Resolved(self, diagnosticsCallback: diagnosticsCallback)
}
}

extension CertificateStore {
@usableFromInline
struct Resolved {
@usableFromInline
var _certificates: _TinyArray<[DistinguishedName: [Certificate]]> = .init()

init(_ store: CertificateStore, diagnosticsCallback: ((VerificationDiagnostic) -> Void)?) async {
for element in store._certificates {
switch element {
#if os(Linux)
case .trustRoots:
do {
_certificates.append(contentsOf: try await CertificateStore.cachedSystemTrustRootsFuture.value
.resolve(diagnosticsCallback: diagnosticsCallback)
._certificates
)
} catch {
diagnosticsCallback?(.loadingTrustRootsFailed(error))
}
#endif
case .customCertificates(let certificatesIndexedBySubject):
_certificates.append(certificatesIndexedBySubject)
}
}
}
}
}

extension CertificateStore.Resolved {
@inlinable
subscript(subject: DistinguishedName) -> [Certificate]? {
get {
self._certificates[subject]
}
set {
self._certificates[subject] = newValue
let matchingCertificates = _certificates.flatMap { $0[subject] ?? [] }
if matchingCertificates.isEmpty {
return nil
} else {
return matchingCertificates
}
}
}

@inlinable
func contains(_ certificate: Certificate) -> Bool {
return self[certificate.subject]?.contains(certificate) ?? false
for certificatesIndexedBySubject in _certificates {
if certificatesIndexedBySubject[certificate.subject]?.contains(certificate) == true {
return true
}
}
return false
}
}

extension Sequence {
/// Non-allocating version of `flatMap(_:)` if `transform` only returns a single `Array` with `count` > 0
@inlinable
func flatMap<ElementOfResult>(
_ transform: (Self.Element) throws -> [ElementOfResult]
) rethrows -> [ElementOfResult] {
var result = [ElementOfResult]()
for element in self {
let partialResult = try transform(element)
if partialResult.isEmpty {
continue
}
if result.isEmpty {
result = partialResult
} else {
result.append(contentsOf: partialResult)
}
}
return result
}
}
Loading