diff --git a/Sources/NIOCertificateReloading/CertificateReloader.swift b/Sources/NIOCertificateReloading/CertificateReloader.swift index d3b3205..8c68f9e 100644 --- a/Sources/NIOCertificateReloading/CertificateReloader.swift +++ b/Sources/NIOCertificateReloading/CertificateReloader.swift @@ -20,7 +20,6 @@ import NIOSSL /// the form of a `NIOSSLContextConfigurationOverride`, which will be used when performing a TLS handshake in NIO. /// Each implementation can choose how to observe for changes, but they all require an ``sslContextConfigurationOverride`` /// to be exposed. -@available(macOS 11.0, iOS 14, tvOS 14, watchOS 7, *) public protocol CertificateReloader: Sendable { /// A `NIOSSLContextConfigurationOverride` that will be used as part of the NIO application's TLS configuration. /// Its certificate and private key will be kept up-to-date via whatever mechanism the specific ``CertificateReloader`` @@ -29,10 +28,53 @@ public protocol CertificateReloader: Sendable { } extension TLSConfiguration { + /// Errors thrown when creating a ``NIOSSL/TLSConfiguration`` with a ``CertificateReloader``. + public struct CertificateReloaderError: Error { + private enum _Backing { + case missingCertificateChain + case missingPrivateKey + } + + private let backing: _Backing + + private init(backing: _Backing) { + self.backing = backing + } + + /// The given ``CertificateReloader`` could not provide a certificate chain with which to create this config. + public static let 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) + } + + /// Create a ``NIOSSL/TLSConfiguration`` for use with server-side contexts, with certificate reloading enabled. + /// - Parameter certificateReloader: A ``CertificateReloader`` to watch for certificate and key pair updates. + /// - Returns: A ``NIOSSL/TLSConfiguration`` for use with server-side contexts, that reloads the certificate and key + /// used in its SSL handshake. + public static func makeServerConfiguration( + certificateReloader: some CertificateReloader + ) throws -> Self { + let override = certificateReloader.sslContextConfigurationOverride + + guard let certificateChain = override.certificateChain else { + throw CertificateReloaderError.missingCertificateChain + } + + guard let privateKey = override.privateKey else { + throw CertificateReloaderError.missingPrivateKey + } + + var configuration = Self.makeServerConfiguration( + certificateChain: certificateChain, + privateKey: privateKey + ) + return configuration.setCertificateReloader(certificateReloader) + } + /// 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 `TLSConfiguration` that reloads the certificate and key used in its SSL handshake. - @available(macOS 11.0, iOS 14, tvOS 14, watchOS 7, *) + /// - Returns: A ``NIOSSL/TLSConfiguration`` that reloads the certificate and key used in its SSL handshake. mutating public func setCertificateReloader(_ reloader: some CertificateReloader) -> Self { self.sslContextCallback = { _, promise in promise.succeed(reloader.sslContextConfigurationOverride) diff --git a/Sources/NIOCertificateReloading/TimedCertificateReloader.swift b/Sources/NIOCertificateReloading/TimedCertificateReloader.swift index 7807a90..25630f0 100644 --- a/Sources/NIOCertificateReloading/TimedCertificateReloader.swift +++ b/Sources/NIOCertificateReloading/TimedCertificateReloader.swift @@ -31,12 +31,56 @@ import Foundation /// key pair is updated at a fixed interval from the file path or memory location configured. /// /// You initialize a ``TimedCertificateReloader`` by providing a refresh interval, and locations for the certificate and the private -/// key. You must then call ``run()`` on this reloader for it to start observing changes. -/// Once the reloader is running, call ``sslContextConfigurationOverride`` to get a -/// `NIOSSLContextConfigurationOverride` which can be set on NIO's `TLSConfiguration`: this will keep the certificate -/// and private key pair up to date. -/// You may instead call ``NIOSSL/TLSConfiguration/setCertificateReloader(_:)`` to get a -/// ``NIOSSL/TLSConfiguration`` with a configured reloader. +/// key. You may then set it on your ``NIOSSL/TLSConfiguration`` using +/// ``NIOSSL/TLSConfiguration/setCertificateReloader(_:)``: +/// +/// ``` +/// var configuration = TLSConfiguration.makeServerConfiguration( +/// certificateChain: chain, +/// privateKey: key +/// ) +/// let reloader = TimedCertificateReloader( +/// refreshInterval: .seconds(500), +/// certificateDescription: TimedCertificateReloader.CertificateDescription(...), +/// privateKeyDescription: TimedCertificateReloader.PrivateKeyDescription(...) +/// ) +/// configuration.setCertificateReloader(reloader) +/// ``` +/// +/// If you're creating a server configuration, you can instead opt to use +/// ``NIOSSL/TLSConfiguration/makeServerConfiguration(certificateReloader:)``, which will set the initial +/// certificate chain and private key, as well as set the reloader: +/// +/// ``` +/// let reloader = TimedCertificateReloader( +/// refreshInterval: .seconds(500), +/// certificateDescription: TimedCertificateReloader.CertificateDescription(...), +/// privateKeyDescription: TimedCertificateReloader.PrivateKeyDescription(...) +/// ) +/// let configuration = TLSConfiguration.makeServerConfiguration( +/// certificateReloader: reloader +/// ) +/// ``` +/// +/// Finally, you must call ``run()`` on the reloader for it to start observing changes. +/// Once the reloader is running, you can also manually access its ``sslContextConfigurationOverride`` property to get a +/// `NIOSSLContextConfigurationOverride`, although this will typically not be necessary, as it's the NIO channel that will +/// handle the override when initiating TLS handshakes. +/// +/// ``` +/// try await withThrowingTaskGroup(of: Void.self) { group in +/// group.addTask { +/// reloader.run() +/// } +/// +/// // ... +/// let override = reloader.sslContextConfigurationOverride +/// // ... +/// } +/// ``` +/// +/// ``TimedCertificateReloader`` conforms to `ServiceLifecycle`'s `Service` protocol, meaning you can simply create +/// the reloader and add it to your `ServiceGroup` without having to manually run it. /// /// If any errors occur during a reload attempt (such as: being unable to find the file(s) containing the certificate or the key; the format /// not being recognized or not matching the configured one; not being able to verify a certificate's signature against the given diff --git a/Tests/NIOCertificateReloadingTests/TimedCertificateReloaderTests.swift b/Tests/NIOCertificateReloadingTests/TimedCertificateReloaderTests.swift index d2b2549..162a2c5 100644 --- a/Tests/NIOCertificateReloadingTests/TimedCertificateReloaderTests.swift +++ b/Tests/NIOCertificateReloadingTests/TimedCertificateReloaderTests.swift @@ -315,10 +315,7 @@ final class TimedCertificateReloaderTests: XCTestCase { group.addTask { try await reloader.run() } - group.addTask { - try await body(reloader) - } - try await group.next() + try await body(reloader) group.cancelAll() } }