PR changes

This commit is contained in:
Gus Cairo 2025-05-07 14:32:05 +01:00
parent 8fbba2dd52
commit 2f52c356af
4 changed files with 92 additions and 26 deletions

View File

@ -303,7 +303,7 @@ let package = Package(
.package(url: "https://github.com/apple/swift-http-structured-headers.git", from: "1.2.0"), .package(url: "https://github.com/apple/swift-http-structured-headers.git", from: "1.2.0"),
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.2.0"), .package(url: "https://github.com/apple/swift-atomics.git", from: "1.2.0"),
.package(url: "https://github.com/apple/swift-algorithms.git", from: "1.2.0"), .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.2.0"),
.package(url: "https://github.com/apple/swift-certificates.git", branch: "1.10.0"), .package(url: "https://github.com/apple/swift-certificates.git", from: "1.10.0"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.29.3"), .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.29.3"),
.package(url: "https://github.com/apple/swift-asn1.git", from: "1.3.1"), .package(url: "https://github.com/apple/swift-asn1.git", from: "1.3.1"),
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.8.0"), .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.8.0"),

View File

@ -30,23 +30,36 @@ public protocol CertificateReloader: Sendable {
extension TLSConfiguration { extension TLSConfiguration {
/// Errors thrown when creating a ``NIOSSL/TLSConfiguration`` with a ``CertificateReloader``. /// Errors thrown when creating a ``NIOSSL/TLSConfiguration`` with a ``CertificateReloader``.
public struct CertificateReloaderError: Error { public struct CertificateReloaderError: Error, Hashable, CustomStringConvertible {
private enum _Backing { private enum _Backing: CustomStringConvertible {
case missingCertificateChain case missingCertificateChain
case missingPrivateKey case missingPrivateKey
var description: String {
switch self {
case .missingCertificateChain:
return "Missing certificate chain"
case .missingPrivateKey:
return "Missing private key"
}
}
} }
private let backing: _Backing private let _backing: _Backing
private init(backing: _Backing) { private init(backing: _Backing) {
self.backing = backing self._backing = backing
}
public var description: String {
self._backing.description
} }
/// The given ``CertificateReloader`` could not provide a certificate chain with which to create this config. /// The given ``CertificateReloader`` could not provide a certificate chain with which to create this config.
public static let missingCertificateChain: Self = .init(backing: .missingCertificateChain) public static var missingCertificateChain: Self { .init(backing: .missingCertificateChain) }
/// The given ``CertificateReloader`` could not provide a private key with which to create this config. /// The given ``CertificateReloader`` could not provide a private key with which to create this config.
public static let missingPrivateKey: Self = .init(backing: .missingPrivateKey) public static var missingPrivateKey: Self { .init(backing: .missingPrivateKey) }
} }
/// Create a ``NIOSSL/TLSConfiguration`` for use with server-side contexts, with certificate reloading enabled. /// Create a ``NIOSSL/TLSConfiguration`` for use with server-side contexts, with certificate reloading enabled.
@ -70,17 +83,15 @@ extension TLSConfiguration {
certificateChain: certificateChain, certificateChain: certificateChain,
privateKey: privateKey privateKey: privateKey
) )
return configuration.setCertificateReloader(certificateReloader) configuration.setCertificateReloader(certificateReloader)
return configuration
} }
/// Configure a ``CertificateReloader`` to observe updates for the certificate and key pair used. /// Configure a ``CertificateReloader`` to observe updates for the certificate and key pair used.
/// - Parameter reloader: A ``CertificateReloader`` to watch for certificate and key pair updates. /// - Parameter reloader: A ``CertificateReloader`` to watch for certificate and key pair updates.
/// - Returns: A ``NIOSSL/TLSConfiguration`` that reloads the certificate and key used in its SSL handshake. mutating public func setCertificateReloader(_ reloader: some CertificateReloader) {
@discardableResult
mutating public func setCertificateReloader(_ reloader: some CertificateReloader) -> Self {
self.sslContextCallback = { _, promise in self.sslContextCallback = { _, promise in
promise.succeed(reloader.sslContextConfigurationOverride) promise.succeed(reloader.sslContextConfigurationOverride)
} }
return self
} }
} }

View File

@ -81,10 +81,14 @@ import Foundation
/// not being recognized or not matching the configured one; not being able to verify a certificate's signature against the given /// not being recognized or not matching the configured one; not being able to verify a certificate's signature against the given
/// private key; etc), then that attempt will be aborted but the service will keep on trying at the configured interval. /// private key; etc), then that attempt will be aborted but the service will keep on trying at the configured interval.
/// The last-valid certificate-key pair (if any) will be returned as the ``sslContextConfigurationOverride``. /// The last-valid certificate-key pair (if any) will be returned as the ``sslContextConfigurationOverride``.
#if compiler(>=6.0)
@available(macOS 13, iOS 16, watchOS 9, tvOS 16, macCatalyst 16, visionOS 1, *) @available(macOS 13, iOS 16, watchOS 9, tvOS 16, macCatalyst 16, visionOS 1, *)
#else
@available(macOS 13, iOS 16, watchOS 9, tvOS 16, macCatalyst 16, *)
#endif
public struct TimedCertificateReloader: CertificateReloader { public struct TimedCertificateReloader: CertificateReloader {
/// The encoding for the certificate or the key. /// The encoding for the certificate or the key.
public struct Encoding: Sendable, Equatable { public struct Encoding: Sendable, Hashable {
fileprivate enum _Backing { fileprivate enum _Backing {
case der case der
case pem case pem
@ -96,17 +100,26 @@ public struct TimedCertificateReloader: CertificateReloader {
} }
/// The encoding of this certificate/key is DER bytes. /// The encoding of this certificate/key is DER bytes.
public static let der = Encoding(.der) public static var der: Self { .init(.der) }
/// The encoding of this certificate/key is PEM. /// The encoding of this certificate/key is PEM.
public static let pem = Encoding(.pem) public static var pem: Self { .init(.pem) }
} }
/// A location specification for a certificate or key. /// A location specification for a certificate or key.
public struct Location: Sendable { public struct Location: Sendable, CustomStringConvertible {
fileprivate enum _Backing { fileprivate enum _Backing: CustomStringConvertible {
case file(path: String) case file(path: String)
case memory(provider: @Sendable () -> [UInt8]?) case memory(provider: @Sendable () -> [UInt8]?)
var description: String {
switch self {
case .file(let path):
return "Filepath: \(path)"
case .memory:
return "<in-memory location>"
}
}
} }
fileprivate let _backing: _Backing fileprivate let _backing: _Backing
@ -115,6 +128,10 @@ public struct TimedCertificateReloader: CertificateReloader {
self._backing = backing self._backing = backing
} }
public var description: String {
self._backing.description
}
/// This certificate/key can be found at the given filepath. /// This certificate/key can be found at the given filepath.
/// - Parameter path: The filepath where the certificate/key can be found. /// - Parameter path: The filepath where the certificate/key can be found.
/// - Returns: A `Location`. /// - Returns: A `Location`.
@ -170,10 +187,19 @@ public struct TimedCertificateReloader: CertificateReloader {
} }
/// Errors specific to the ``TimedCertificateReloader``. /// Errors specific to the ``TimedCertificateReloader``.
public struct Error: Swift.Error { public struct Error: Swift.Error, Hashable, CustomStringConvertible {
private enum _Backing { private enum _Backing: Hashable, CustomStringConvertible {
case certificatePathNotFound(String) case certificatePathNotFound(String)
case privateKeyPathNotFound(String) case privateKeyPathNotFound(String)
var description: String {
switch self {
case .certificatePathNotFound(let path):
return "Certificate path not found: \(path)"
case .privateKeyPathNotFound(let path):
return "Private key path not found: \(path)"
}
}
} }
private let _backing: _Backing private let _backing: _Backing
@ -195,6 +221,10 @@ public struct TimedCertificateReloader: CertificateReloader {
public static func privateKeyPathNotFound(_ path: String) -> Self { public static func privateKeyPathNotFound(_ path: String) -> Self {
Self(.privateKeyPathNotFound(path)) Self(.privateKeyPathNotFound(path))
} }
public var description: String {
self._backing.description
}
} }
private struct CertificateKeyPair { private struct CertificateKeyPair {
@ -259,7 +289,7 @@ public struct TimedCertificateReloader: CertificateReloader {
refreshInterval: Duration(refreshInterval), refreshInterval: Duration(refreshInterval),
validatingCertificateDescription: validatingCertificateDescription, validatingCertificateDescription: validatingCertificateDescription,
validatingPrivateKeyDescription: validatingPrivateKeyDescription, validatingPrivateKeyDescription: validatingPrivateKeyDescription,
logger: nil logger: logger
) )
} }
@ -319,13 +349,12 @@ public struct TimedCertificateReloader: CertificateReloader {
do { do {
try self.reloadPair() try self.reloadPair()
} catch { } catch {
self.logger?.error( self.logger?.debug(
""" "Failed to reload certificate and private key.",
An unexpected error was encountered while trying to reload the certificate and \
private key pair.
""",
metadata: [ metadata: [
"error": error "error": "\(error)",
"certificatePath": "\(self.certificateDescription.location)",
"privateKeyPath": "\(self.privateKeyDescription.location)",
] ]
) )
} }
@ -417,5 +446,9 @@ public struct TimedCertificateReloader: CertificateReloader {
} }
} }
#if compiler(>=6.0)
@available(macOS 13, iOS 16, watchOS 9, tvOS 16, macCatalyst 16, visionOS 1, *) @available(macOS 13, iOS 16, watchOS 9, tvOS 16, macCatalyst 16, visionOS 1, *)
#else
@available(macOS 13, iOS 16, watchOS 9, tvOS 16, macCatalyst 16, *)
#endif
extension TimedCertificateReloader: Service {} extension TimedCertificateReloader: Service {}

View File

@ -272,6 +272,28 @@ final class TimedCertificateReloaderTests: XCTestCase {
} }
} }
func testCertificateReloaderErrorDescription() {
XCTAssertEqual(
"\(TLSConfiguration.CertificateReloaderError.missingCertificateChain)",
"Missing certificate chain"
)
XCTAssertEqual(
"\(TLSConfiguration.CertificateReloaderError.missingPrivateKey)",
"Missing private key"
)
}
func testTimedCertificateReloaderErrorDescription() {
XCTAssertEqual(
"\(TimedCertificateReloader.Error.certificatePathNotFound("some/path"))",
"Certificate path not found: some/path"
)
XCTAssertEqual(
"\(TimedCertificateReloader.Error.privateKeyPathNotFound("some/path"))",
"Private key path not found: some/path"
)
}
static let startDate = Date() static let startDate = Date()
static let samplePrivateKey = P384.Signing.PrivateKey() static let samplePrivateKey = P384.Signing.PrivateKey()
static let sampleCertName = try! DistinguishedName { static let sampleCertName = try! DistinguishedName {