Add docs and a new TLSConfig init

This commit is contained in:
Gus Cairo 2025-05-06 16:43:37 +01:00
parent 2fd8dca7b1
commit ed45e53610
3 changed files with 96 additions and 13 deletions

View File

@ -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)

View File

@ -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

View File

@ -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()
}
}