Skip to content
Open
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
33 changes: 18 additions & 15 deletions Sources/Example/Example.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,25 @@ struct Example {
logger: logger,
configuration: .init(
bindTarget: .hostAndPort(host: "127.0.0.1", port: 12345),
supportedHTTPVersions: [.http1_1, .http2(config: .init())],
transportSecurity: .tls(
certificateChain: [
try Certificate(
version: .v3,
serialNumber: .init(bytes: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
publicKey: .init(privateKey.publicKey),
notValidBefore: Date.now.addingTimeInterval(-60),
notValidAfter: Date.now.addingTimeInterval(60 * 60),
issuer: DistinguishedName(),
subject: DistinguishedName(),
signatureAlgorithm: .ecdsaWithSHA256,
extensions: .init(),
issuerPrivateKey: Certificate.PrivateKey(privateKey)
)
],
privateKey: Certificate.PrivateKey(privateKey)
credentials: .inMemory(
certificateChain: [
try Certificate(
version: .v3,
serialNumber: .init(bytes: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
publicKey: .init(privateKey.publicKey),
notValidBefore: Date.now.addingTimeInterval(-60),
notValidAfter: Date.now.addingTimeInterval(60 * 60),
issuer: DistinguishedName(),
subject: DistinguishedName(),
signatureAlgorithm: .ecdsaWithSHA256,
extensions: .init(),
issuerPrivateKey: Certificate.PrivateKey(privateKey)
)
],
privateKey: Certificate.PrivateKey(privateKey)
)
)
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ import SwiftASN1
public import X509

enum NIOHTTPServerConfigurationError: Error, CustomStringConvertible {
case customVerificationCallbackAndTrustRootsProvided
case customVerificationCallbackProvidedWhenNotUsingMTLS

var description: String {
switch self {
case .customVerificationCallbackAndTrustRootsProvided:
"Invalid configuration. Both a custom certificate verification callback and a set of trust roots were provided. When a custom verification callback is provided, trust must be established directly within the callback."

case .customVerificationCallbackProvidedWhenNotUsingMTLS:
"Invalid configuration. A custom certificate verification callback was provided despite the server not being configured for mTLS."
}
Expand All @@ -40,11 +44,15 @@ extension NIOHTTPServerConfiguration {
/// ``NIOHTTPServerConfiguration`` is comprised of four types. Provide configuration for each type under the
/// specified key:
/// - ``BindTarget`` - Provide under key `"bindTarget"` (keys listed in ``BindTarget/init(config:)``).
///
/// - ``SupportedHTTPVersions`` - Provide under key `"supportedHTTPVersions"` (keys listed in
/// ``SupportedHTTPVersions/init(config:)``).
///
/// - ``TransportSecurity`` - Provide under key `"transportSecurity"` (keys listed in
/// ``TransportSecurity/init(config:customCertificateVerificationCallback:)``).
///
/// - ``BackPressureStrategy`` - Provide under key `"backpressureStrategy"` (keys listed in
/// ``BackPressureStrategy/init(config:)``).
/// - ``HTTP2`` - Provide under key `"http2"` (keys listed in ``HTTP2/init(config:)``).
///
/// - Parameters:
/// - config: The configuration reader to read configuration values from.
Expand All @@ -64,12 +72,12 @@ extension NIOHTTPServerConfiguration {

self.init(
bindTarget: try .init(config: snapshot.scoped(to: "bindTarget")),
supportedHTTPVersions: try .init(config: snapshot),
transportSecurity: try .init(
config: snapshot.scoped(to: "transportSecurity"),
customCertificateVerificationCallback: customCertificateVerificationCallback
),
backpressureStrategy: .init(config: snapshot.scoped(to: "backpressureStrategy")),
http2: .init(config: snapshot.scoped(to: "http2"))
backpressureStrategy: .init(config: snapshot.scoped(to: "backpressureStrategy"))
)
}
}
Expand All @@ -93,6 +101,49 @@ extension NIOHTTPServerConfiguration.BindTarget {
}
}

private enum HTTPVersionKind: String {
case http1_1
case http2
}

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension Set where Element == NIOHTTPServerConfiguration.HTTPVersion {
/// Initialize a supported HTTP versions configuration from a config reader.
///
/// ## Configuration keys:
/// - `supportedHTTPVersions` (string array, required): A set of HTTP versions the server should support (permitted
/// values: `"http1_1"`, `"http2"`). If `"http2"` is contained in this array, then HTTP/2 configuration can be
/// specified under the `"http2"` key. See ``NIOHTTPServerConfiguration/HTTP2/init(config:)`` for the supported
/// keys under `"http2"`.
///
/// - Parameter config: The configuration reader.
public init(config: ConfigSnapshotReader) throws {
self = .init()

let versions = Set<HTTPVersionKind>(
try config.requiredStringArray(
forKey: "supportedHTTPVersions",
as: HTTPVersionKind.self
)
)

if versions.isEmpty {
fatalError("Invalid configuration: at least one supported HTTP version must be specified.")
}

for version in versions {
switch version {
case .http1_1:
self.insert(.http1_1)

case .http2:
let h2Config = NIOHTTPServerConfiguration.HTTP2(config: config.scoped(to: "http2"))
self.insert(.http2(config: h2Config))
}
}
}
}

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension NIOHTTPServerConfiguration.TransportSecurity {
/// Initialize a transport security configuration from a config reader.
Expand All @@ -114,7 +165,7 @@ extension NIOHTTPServerConfiguration.TransportSecurity {
/// ### Configuration keys for `"mTLS"`:
/// - `certificateChainPEMString` (string, required): PEM-formatted certificate chain content.
/// - `privateKeyPEMString` (string, required, secret): PEM-formatted private key content.
/// - `trustRoots` (string array, optional, default: system trust roots): The root certificates to trust when
/// - `trustRootsPEMString` (string, optional, default: system trust roots): The root certificates to trust when
/// verifying client certificates.
/// - `certificateVerificationMode` (string, required): The client certificate validation behavior (permitted
/// values: "optionalVerification" or "noHostnameVerification").
Expand All @@ -124,7 +175,7 @@ extension NIOHTTPServerConfiguration.TransportSecurity {
/// private key will be reloaded.
/// - `certificateChainPEMPath` (string, required): Path to the certificate chain PEM file.
/// - `privateKeyPEMPath` (string, required): Path to the private key PEM file.
/// - `trustRoots` (string array, optional, default: system trust roots): The root certificates to trust when
/// - `trustRootsPEMString` (string, optional, default: system trust roots): The root certificates to trust when
/// verifying client certificates.
/// - `certificateVerificationMode` (string, required): The client certificate validation behavior (permitted
/// values: "optionalVerification" or "noHostnameVerification").
Expand Down Expand Up @@ -179,9 +230,11 @@ extension NIOHTTPServerConfiguration.TransportSecurity {
let privateKeyPEMString = try config.requiredString(forKey: "privateKeyPEMString", isSecret: true)

return Self.tls(
certificateChain: try PEMDocument.parseMultiple(pemString: certificateChainPEMString)
.map { try Certificate(pemEncoded: $0.pemString) },
privateKey: try .init(pemEncoded: privateKeyPEMString)
credentials: .inMemory(
certificateChain: try PEMDocument.parseMultiple(pemString: certificateChainPEMString)
.map { try Certificate(pemEncoded: $0.pemString) },
privateKey: try .init(pemEncoded: privateKeyPEMString)
)
)
}

Expand All @@ -190,11 +243,13 @@ extension NIOHTTPServerConfiguration.TransportSecurity {
let certificateChainPEMPath = try config.requiredString(forKey: "certificateChainPEMPath")
let privateKeyPEMPath = try config.requiredString(forKey: "privateKeyPEMPath")

return try Self.tls(
certificateReloader: TimedCertificateReloader(
refreshInterval: .seconds(refreshInterval),
certificateSource: .init(location: .file(path: certificateChainPEMPath), format: .pem),
privateKeySource: .init(location: .file(path: privateKeyPEMPath), format: .pem)
return Self.tls(
credentials: .reloading(
certificateReloader: TimedCertificateReloader(
refreshInterval: .seconds(refreshInterval),
certificateSource: .init(location: .file(path: certificateChainPEMPath), format: .pem),
privateKeySource: .init(location: .file(path: privateKeyPEMPath), format: .pem)
)
)
)
}
Expand All @@ -207,19 +262,17 @@ extension NIOHTTPServerConfiguration.TransportSecurity {
) throws -> Self {
let certificateChainPEMString = try config.requiredString(forKey: "certificateChainPEMString")
let privateKeyPEMString = try config.requiredString(forKey: "privateKeyPEMString", isSecret: true)
let trustRoots = config.stringArray(forKey: "trustRoots")
let verificationMode = try config.requiredString(
forKey: "certificateVerificationMode",
as: VerificationMode.self
)

return Self.mTLS(
certificateChain: try PEMDocument.parseMultiple(pemString: certificateChainPEMString)
.map { try Certificate(pemEncoded: $0.pemString) },
privateKey: try .init(pemEncoded: privateKeyPEMString),
trustRoots: try trustRoots?.map { try Certificate(pemEncoded: $0) },
certificateVerification: .init(verificationMode),
customCertificateVerificationCallback: customCertificateVerificationCallback
credentials: .inMemory(
certificateChain: try PEMDocument.parseMultiple(pemString: certificateChainPEMString)
.map { try Certificate(pemEncoded: $0.pemString) },
privateKey: try .init(pemEncoded: privateKeyPEMString)
),
trustConfiguration: try .init(
config: config,
customCertificateVerificationCallback: customCertificateVerificationCallback
)
)
}

Expand All @@ -232,25 +285,76 @@ extension NIOHTTPServerConfiguration.TransportSecurity {
let refreshInterval = config.int(forKey: "refreshInterval", default: 30)
let certificateChainPEMPath = try config.requiredString(forKey: "certificateChainPEMPath")
let privateKeyPEMPath = try config.requiredString(forKey: "privateKeyPEMPath")
let trustRoots = config.stringArray(forKey: "trustRoots")
let verificationMode = try config.requiredString(
forKey: "certificateVerificationMode",
as: VerificationMode.self
)

return try Self.mTLS(
certificateReloader: TimedCertificateReloader(
refreshInterval: .seconds(refreshInterval),
certificateSource: .init(location: .file(path: certificateChainPEMPath), format: .pem),
privateKeySource: .init(location: .file(path: privateKeyPEMPath), format: .pem)
credentials: .reloading(
certificateReloader: TimedCertificateReloader(
refreshInterval: .seconds(refreshInterval),
certificateSource: .init(location: .file(path: certificateChainPEMPath), format: .pem),
privateKeySource: .init(location: .file(path: privateKeyPEMPath), format: .pem)
)
),
trustRoots: try trustRoots?.map { try Certificate(pemEncoded: $0) },
certificateVerification: .init(verificationMode),
customCertificateVerificationCallback: customCertificateVerificationCallback
trustConfiguration: .init(
config: config,
customCertificateVerificationCallback: customCertificateVerificationCallback
)
)
}
}

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension NIOHTTPServerConfiguration.TransportSecurity.MTLSTrustConfiguration {
/// Initialize an mTLS trust configuration from a config reader.
///
/// ## Configuration keys:
/// - `trustRootsPEMString` (string, optional, default: system trust roots): The root certificates to trust when
/// verifying client certificates.
/// - `certificateVerificationMode` (string, required): The client certificate validation behavior (permitted
/// values: "optionalVerification" or "noHostnameVerification")
///
/// - Parameters:
/// - config: The configuration reader.
/// - customCertificateVerificationCallback: An optional client certificate verification callback to use when
/// mTLS is configured (i.e., when `"transportSecurity.security"` is `"mTLS"` or `"reloadingMTLS"`). If set to
/// `nil`, the default client certificate verification logic of the underlying SSL implementation is used.
///
/// - Note: It is invalid to pass both a custom verification callback and a set of trust roots. If using a custom
/// verification callback, trust must be established within the callback itself. Providing both will result in a
/// `NIOHTTPServerConfigurationError.customVerificationCallbackAndTrustRootsProvided` error.
public init(
config: ConfigSnapshotReader,
customCertificateVerificationCallback: (
@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult
)?
) throws {
let trustRootsPEMString = config.string(forKey: "trustRootsPEMString")
let certificateVerificationMode = try config.requiredString(
forKey: "certificateVerificationMode",
as: VerificationMode.self
)

if trustRootsPEMString != nil, customCertificateVerificationCallback != nil {
// Throw if both `trustRoots` and `customCertificateVerificationCallback` are provided.
throw NIOHTTPServerConfigurationError.customVerificationCallbackAndTrustRootsProvided
}

if let trustRootsPEMString {
self = .inMemory(
trustRoots: try PEMDocument.parseMultiple(pemString: trustRootsPEMString)
.map { try Certificate(pemEncoded: $0.pemString) },
certificateVerification: .init(certificateVerificationMode)
)
} else if let customCertificateVerificationCallback {
self = .customCertificateVerificationCallback(
customCertificateVerificationCallback,
certificateVerification: .init(certificateVerificationMode)
)
} else {
self = .systemDefaults(certificateVerification: .init(certificateVerificationMode))
}
}
}

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension NIOHTTPServerConfiguration.BackPressureStrategy {
/// Initialize the backpressure strategy configuration from a config reader.
Expand Down Expand Up @@ -343,7 +447,10 @@ extension NIOHTTPServerConfiguration.TransportSecurity {
}
}
}
}

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension NIOHTTPServerConfiguration.TransportSecurity.MTLSTrustConfiguration {
/// A wrapper over ``CertificateVerificationMode``.
fileprivate enum VerificationMode: String {
case optionalVerification
Expand All @@ -353,7 +460,7 @@ extension NIOHTTPServerConfiguration.TransportSecurity {

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension CertificateVerificationMode {
fileprivate init(_ mode: NIOHTTPServerConfiguration.TransportSecurity.VerificationMode) {
fileprivate init(_ mode: NIOHTTPServerConfiguration.TransportSecurity.MTLSTrustConfiguration.VerificationMode) {
switch mode {
case .optionalVerification:
self.init(mode: .optionalVerification)
Expand Down
Loading
Loading