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-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-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-asn1.git", from: "1.3.1"),
.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 {
/// Errors thrown when creating a ``NIOSSL/TLSConfiguration`` with a ``CertificateReloader``.
public struct CertificateReloaderError: Error {
private enum _Backing {
public struct CertificateReloaderError: Error, Hashable, CustomStringConvertible {
private enum _Backing: CustomStringConvertible {
case missingCertificateChain
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) {
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.
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.
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.
@ -70,17 +83,15 @@ extension TLSConfiguration {
certificateChain: certificateChain,
privateKey: privateKey
)
return configuration.setCertificateReloader(certificateReloader)
configuration.setCertificateReloader(certificateReloader)
return configuration
}
/// 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.
/// - Returns: A ``NIOSSL/TLSConfiguration`` that reloads the certificate and key used in its SSL handshake.
@discardableResult
mutating public func setCertificateReloader(_ reloader: some CertificateReloader) -> Self {
mutating public func setCertificateReloader(_ reloader: some CertificateReloader) {
self.sslContextCallback = { _, promise in
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
/// 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``.
#if compiler(>=6.0)
@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 {
/// The encoding for the certificate or the key.
public struct Encoding: Sendable, Equatable {
public struct Encoding: Sendable, Hashable {
fileprivate enum _Backing {
case der
case pem
@ -96,17 +100,26 @@ public struct TimedCertificateReloader: CertificateReloader {
}
/// 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.
public static let pem = Encoding(.pem)
public static var pem: Self { .init(.pem) }
}
/// A location specification for a certificate or key.
public struct Location: Sendable {
fileprivate enum _Backing {
public struct Location: Sendable, CustomStringConvertible {
fileprivate enum _Backing: CustomStringConvertible {
case file(path: String)
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
@ -115,6 +128,10 @@ public struct TimedCertificateReloader: CertificateReloader {
self._backing = backing
}
public var description: String {
self._backing.description
}
/// This certificate/key can be found at the given filepath.
/// - Parameter path: The filepath where the certificate/key can be found.
/// - Returns: A `Location`.
@ -170,10 +187,19 @@ public struct TimedCertificateReloader: CertificateReloader {
}
/// Errors specific to the ``TimedCertificateReloader``.
public struct Error: Swift.Error {
private enum _Backing {
public struct Error: Swift.Error, Hashable, CustomStringConvertible {
private enum _Backing: Hashable, CustomStringConvertible {
case certificatePathNotFound(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
@ -195,6 +221,10 @@ public struct TimedCertificateReloader: CertificateReloader {
public static func privateKeyPathNotFound(_ path: String) -> Self {
Self(.privateKeyPathNotFound(path))
}
public var description: String {
self._backing.description
}
}
private struct CertificateKeyPair {
@ -259,7 +289,7 @@ public struct TimedCertificateReloader: CertificateReloader {
refreshInterval: Duration(refreshInterval),
validatingCertificateDescription: validatingCertificateDescription,
validatingPrivateKeyDescription: validatingPrivateKeyDescription,
logger: nil
logger: logger
)
}
@ -319,13 +349,12 @@ public struct TimedCertificateReloader: CertificateReloader {
do {
try self.reloadPair()
} catch {
self.logger?.error(
"""
An unexpected error was encountered while trying to reload the certificate and \
private key pair.
""",
self.logger?.debug(
"Failed to reload certificate and private key.",
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, *)
#else
@available(macOS 13, iOS 16, watchOS 9, tvOS 16, macCatalyst 16, *)
#endif
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 samplePrivateKey = P384.Signing.PrivateKey()
static let sampleCertName = try! DistinguishedName {