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
1 change: 1 addition & 0 deletions 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 Down
3 changes: 3 additions & 0 deletions Sources/X509/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ add_library(X509
"Extensions.swift"
"ExtensionsBuilder.swift"
"GeneralName.swift"
"LockedValueBox.swift"
"OCSP/BasicOCSPResponse.swift"
"OCSP/DirectoryString.swift"
"OCSP/OCSPCertID.swift"
Expand All @@ -75,6 +76,7 @@ add_library(X509
"OCSP/OCSPTBSRequest.swift"
"OCSP/OCSPVersion.swift"
"PKCS8PrivateKey.swift"
"PromiseAndFuture.swift"
"RDNAttribute.swift"
"RandomNumberGenerator+bytes.swift"
"RelativeDistinguishedName.swift"
Expand All @@ -93,6 +95,7 @@ add_library(X509
"Verifier/RFC5280/RFC5280Policy.swift"
"Verifier/RFC5280/URIConstraints.swift"
"Verifier/RFC5280/VersionPolicy.swift"
"Verifier/TrustRootLoading.swift"
"Verifier/UnverifiedChain.swift"
"Verifier/VerificationDiagnostic.swift"
"Verifier/Verifier.swift"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ public enum CMS {
// Ok, the signature was signed by the private key associated with this cert. Now we need to validate the certificate.
// This force-unwrap is safe: we know there are certificates because we've located at least one certificate from this set!
var untrustedIntermediates = CertificateStore(signedData.certificates!)
untrustedIntermediates.insert(contentsOf: additionalIntermediateCertificates)
untrustedIntermediates.append(contentsOf: additionalIntermediateCertificates)

var verifier = try Verifier(rootCertificates: trustRoots, policy: policy)
let result = await verifier.validate(
Expand Down
23 changes: 23 additions & 0 deletions Sources/X509/Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,25 @@ public struct CertificateError: Error, Hashable, CustomStringConvertible {
)
)
}

/// The system trust store could not be found or failed to load from disk.
/// - Parameter reason: A detailed reason included which locations were tried and which error got thrown.
/// - Returns: A ``CertificateError`` with ``code`` set to ``ErrorCode/failedToLoadSystemTrustStore``.
@inline(never)
public static func failedToLoadSystemTrustStore(
reason: String,
file: String = #fileID,
line: UInt = #line
) -> CertificateError {
return CertificateError(
backing: .init(
code: .failedToLoadSystemTrustStore,
reason: reason,
file: file,
line: line
)
)
}
}

extension CertificateError {
Expand All @@ -247,6 +266,7 @@ extension CertificateError {
case incorrectOIDForAttribute
case invalidCSRAttribute
case duplicateOID
case failedToLoadSystemTrustStore
}

fileprivate var backingCode: BackingCode
Expand Down Expand Up @@ -282,6 +302,9 @@ extension CertificateError {
/// An OID is present twice.
public static let duplicateOID = ErrorCode(.duplicateOID)

/// The system trust store could not be located or failed to load from disk.
public static let failedToLoadSystemTrustStore = ErrorCode(.failedToLoadSystemTrustStore)

public var description: String {
return String(describing: self.backingCode)
}
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 unsafeUnlockedValue: Value {
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.unsafeUnlockedValue {
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.unsafeUnlockedValue = .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.unsafeUnlockedValue {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think this optimization is appropriate: we can just take the lock, it'll be uncontended.

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