Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ let package = Package(
.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", 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.34.0"),
.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/apple/swift-async-algorithms.git", from: "1.0.0"),
Expand Down
37 changes: 37 additions & 0 deletions Sources/NIOCertificateReloading/CertificateReloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,43 @@ extension TLSConfiguration {
return configuration
}

/// Create a ``NIOSSL/TLSConfiguration`` for use with server-side contexts that expect to validate a client, with
/// certificate reloading enabled. For servers that don't need mTLS, try ``TLSConfiguration/makeServerConfiguration(certificateReloader:)``.
/// This configuration is very similar to ``TLSConfiguration/makeServerConfiguration(certificateReloader:)`` but
/// adds a `trustRoots` requirement. These roots will be used to validate the certificate presented by the peer. It
/// also sets the `certificateVerification` field to `noHostnameVerification`, which enables verification but
/// disables any hostname checking, which cannot succeed in a server context.
///
/// - Parameters:
/// - certificateReloader: A ``CertificateReloader`` to watch for certificate and key pair updates.
/// - trustRoots: The roots used to validate the client certificate.
/// - Returns: A ``NIOSSL/TLSConfiguration`` for use with server-side contexts, that reloads the certificate and key
/// used in its SSL handshake.
/// - Throws: This method will throw if an override isn't present. This may happen if a certificate or private key
/// could not be loaded from the given paths.
public static func makeServerConfigurationWithMTLS(
certificateReloader: some CertificateReloader,
trustRoots: NIOSSLTrustRoots
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to default this to the system defaults?

Choose a reason for hiding this comment

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

Not sure about this, but I suspect most mTLS applications would require clients to present a certificate that is anchored to some custom trust root, which is not likely to be a system default trust root. If this is the case then we shouldn't default the argument to system default trust roots.

The underlying NIOSSL function that this function mirrors also doesn't have a default argument.

cc @Lukasa

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, we should not default this to the system trust roots, that will tend to be surprising.

) 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.makeServerConfigurationWithMTLS(
certificateChain: certificateChain,
privateKey: privateKey,
trustRoots: trustRoots
)
configuration.setCertificateReloader(certificateReloader)
return configuration
}

/// Create a ``NIOSSL/TLSConfiguration`` for use with client-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 client-side contexts, that reloads the certificate and key
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,56 @@ final class TimedCertificateReloaderTests: XCTestCase {
XCTAssertThrowsError(try TimedCertificateReloader.makeReloaderValidatingSources(configuration: config))
}

/// This tests that `makeServerConfigurationWithMTLS(certificateReloader:trustRoots:)` correctly extracts the
/// certificate chain and private key from `certificateReloader` and sets those in the returned `TLSConfiguration`
/// (along with `trustRoots` and setting `.certificateVerification` to `.noHostnameVerification`)
func testCreateServerConfigWithMTLS() async throws {
let certificateReloader = try TimedCertificateReloader.makeReloaderValidatingSources(
refreshInterval: .seconds(10),
certificateSource: .init(
location: .memory(provider: {
.init(
try Self.sampleCertChain.map { try $0.serializeAsPEM().pemString }.joined(separator: "\n").utf8
)
}),
format: .pem
),
privateKeySource: .init(
location: .memory(provider: { .init(Self.samplePrivateKey1.derRepresentation) }),
format: .der
)
)

let trustRoots = NIOSSLTrustRoots.certificates(
try Self.sampleCertChain.map {
try NIOSSLCertificate(bytes: .init($0.serializeAsPEM().pemString.utf8), format: .pem)
}
)

let tlsConfiguration = try TLSConfiguration.makeServerConfigurationWithMTLS(
certificateReloader: certificateReloader,
trustRoots: trustRoots
)

// Check whether the configuration is set up with the same certificate chain, private key, and trust roots
// that were used to initialize the reloader
XCTAssertEqual(
tlsConfiguration.certificateChain,
try Self.sampleCertChain.map {
.certificate(try NIOSSLCertificate(bytes: $0.serializeAsPEM().derBytes, format: .der))
}
)
XCTAssertEqual(
tlsConfiguration.privateKey,
NIOSSLPrivateKeySource.privateKey(
try NIOSSLPrivateKey(bytes: .init(Self.samplePrivateKey1.derRepresentation), format: .der)
)
)
XCTAssertEqual(tlsConfiguration.trustRoots, trustRoots)

XCTAssertEqual(tlsConfiguration.certificateVerification, .noHostnameVerification)
}

static let startDate = Date()
static let samplePrivateKey1 = P384.Signing.PrivateKey()
static let samplePrivateKey2 = P384.Signing.PrivateKey()
Expand Down
Loading