Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NIOTS transport to E2E TLS-enabled tests #20

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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 @@ -51,7 +51,7 @@ let dependencies: [Package.Dependency] = [
),
.package(
url: "https://github.com/apple/swift-nio-ssl.git",
from: "2.27.2"
from: "2.29.0"
),
.package(
url: "https://github.com/apple/swift-nio-extras.git",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -522,9 +522,14 @@ extension ClientConnectionHandler {
mutating func receivedError(_ error: any Error) {
switch self.state {
case .active(var active):
self.state = ._modifying
active.error = error
self.state = .active(active)
// Do not overwrite the first error that caused the closure:
// Sometimes, multiple errors can be triggered before the channel fully
// closes, but latter errors can mask the original issue.
if active.error == nil {
self.state = ._modifying
active.error = error
self.state = .active(active)
}
case .closing, .closed:
()
case ._modifying:
Expand Down
134 changes: 134 additions & 0 deletions Sources/GRPCNIOTransportCore/TLSConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright 2024, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

public enum TLSConfig: Sendable {
/// The serialization format of the provided certificates and private keys.
public struct SerializationFormat: Sendable, Equatable {
package enum Wrapped {
case pem
case der
}

package let wrapped: Wrapped

public static let pem = Self(wrapped: .pem)
public static let der = Self(wrapped: .der)
}

/// A description of where a certificate is coming from: either a byte array or a file.
/// The serialization format is specified by ``TLSConfig/SerializationFormat``.
public struct CertificateSource: Sendable {
package enum Wrapped {
case file(path: String, format: SerializationFormat)
case bytes(bytes: [UInt8], format: SerializationFormat)
}

package let wrapped: Wrapped

/// The certificate's source is a file.
/// - Parameters:
/// - path: The file path containing the certificate.
/// - format: The certificate's format, as a ``TLSConfig/SerializationFormat``.
/// - Returns: A source describing the certificate source is the given file.
public static func file(path: String, format: SerializationFormat) -> Self {
Self(wrapped: .file(path: path, format: format))
}

/// The certificate's source is an array of bytes.
/// - Parameters:
/// - bytes: The array of bytes making up the certificate.
/// - format: The certificate's format, as a ``TLSConfig/SerializationFormat``.
/// - Returns: A source describing the certificate source is the given bytes.
public static func bytes(_ bytes: [UInt8], format: SerializationFormat) -> Self {
Self(wrapped: .bytes(bytes: bytes, format: format))
}
}

/// A description of where the private key is coming from: either a byte array or a file.
/// The serialization format is specified by ``TLSConfig/SerializationFormat``.
public struct PrivateKeySource: Sendable {
package enum Wrapped {
case file(path: String, format: SerializationFormat)
case bytes(bytes: [UInt8], format: SerializationFormat)
}

package let wrapped: Wrapped

/// The private key's source is a file.
/// - Parameters:
/// - path: The file path containing the private key.
/// - format: The private key's format, as a ``TLSConfig/SerializationFormat``.
/// - Returns: A source describing the private key source is the given file.
public static func file(path: String, format: SerializationFormat) -> Self {
Self(wrapped: .file(path: path, format: format))
}

/// The private key's source is an array of bytes.
/// - Parameters:
/// - bytes: The array of bytes making up the private key.
/// - format: The private key's format, as a ``TLSConfig/SerializationFormat``.
/// - Returns: A source describing the private key source is the given bytes.
public static func bytes(
_ bytes: [UInt8],
format: SerializationFormat
) -> Self {
Self(wrapped: .bytes(bytes: bytes, format: format))
}
}

/// A description of where the trust roots are coming from: either a custom certificate chain, or the system default trust store.
public struct TrustRootsSource: Sendable {
package enum Wrapped {
case certificates([CertificateSource])
case systemDefault
}

package let wrapped: Wrapped

/// A list of ``TLSConfig/CertificateSource``s making up the
/// chain of trust.
/// - Parameter certificateSources: The sources for the certificates that make up the chain of trust.
/// - Returns: A trust root for the given chain of trust.
public static func certificates(
_ certificateSources: [CertificateSource]
) -> Self {
Self(wrapped: .certificates(certificateSources))
}

/// The system default trust store.
public static let systemDefault: Self = Self(wrapped: .systemDefault)
}

/// How to verify certificates.
public struct CertificateVerification: Sendable {
package enum Wrapped {
case doNotVerify
case fullVerification
case noHostnameVerification
}

package let wrapped: Wrapped

/// All certificate verification disabled.
public static let noVerification: Self = Self(wrapped: .doNotVerify)

/// Certificates will be validated against the trust store, but will not be checked to see if they are valid for the given hostname.
public static let noHostnameVerification: Self = Self(wrapped: .noHostnameVerification)

/// Certificates will be validated against the trust store and checked against the hostname of the service we are contacting.
public static let fullVerification: Self = Self(wrapped: .fullVerification)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,125 +14,6 @@
* limitations under the License.
*/

public enum TLSConfig: Sendable {
/// The serialization format of the provided certificates and private keys.
public struct SerializationFormat: Sendable, Equatable {
package enum Wrapped {
case pem
case der
}

package let wrapped: Wrapped

public static let pem = Self(wrapped: .pem)
public static let der = Self(wrapped: .der)
}

/// A description of where a certificate is coming from: either a byte array or a file.
/// The serialization format is specified by ``TLSConfig/SerializationFormat``.
public struct CertificateSource: Sendable {
package enum Wrapped {
case file(path: String, format: SerializationFormat)
case bytes(bytes: [UInt8], format: SerializationFormat)
}

package let wrapped: Wrapped

/// The certificate's source is a file.
/// - Parameters:
/// - path: The file path containing the certificate.
/// - format: The certificate's format, as a ``TLSConfig/SerializationFormat``.
/// - Returns: A source describing the certificate source is the given file.
public static func file(path: String, format: SerializationFormat) -> Self {
Self(wrapped: .file(path: path, format: format))
}

/// The certificate's source is an array of bytes.
/// - Parameters:
/// - bytes: The array of bytes making up the certificate.
/// - format: The certificate's format, as a ``TLSConfig/SerializationFormat``.
/// - Returns: A source describing the certificate source is the given bytes.
public static func bytes(_ bytes: [UInt8], format: SerializationFormat) -> Self {
Self(wrapped: .bytes(bytes: bytes, format: format))
}
}

/// A description of where the private key is coming from: either a byte array or a file.
/// The serialization format is specified by ``TLSConfig/SerializationFormat``.
public struct PrivateKeySource: Sendable {
package enum Wrapped {
case file(path: String, format: SerializationFormat)
case bytes(bytes: [UInt8], format: SerializationFormat)
}

package let wrapped: Wrapped

/// The private key's source is a file.
/// - Parameters:
/// - path: The file path containing the private key.
/// - format: The private key's format, as a ``TLSConfig/SerializationFormat``.
/// - Returns: A source describing the private key source is the given file.
public static func file(path: String, format: SerializationFormat) -> Self {
Self(wrapped: .file(path: path, format: format))
}

/// The private key's source is an array of bytes.
/// - Parameters:
/// - bytes: The array of bytes making up the private key.
/// - format: The private key's format, as a ``TLSConfig/SerializationFormat``.
/// - Returns: A source describing the private key source is the given bytes.
public static func bytes(
_ bytes: [UInt8],
format: SerializationFormat
) -> Self {
Self(wrapped: .bytes(bytes: bytes, format: format))
}
}

/// A description of where the trust roots are coming from: either a custom certificate chain, or the system default trust store.
public struct TrustRootsSource: Sendable {
package enum Wrapped {
case certificates([CertificateSource])
case systemDefault
}

package let wrapped: Wrapped

/// A list of ``TLSConfig/CertificateSource``s making up the
/// chain of trust.
/// - Parameter certificateSources: The sources for the certificates that make up the chain of trust.
/// - Returns: A trust root for the given chain of trust.
public static func certificates(
_ certificateSources: [CertificateSource]
) -> Self {
Self(wrapped: .certificates(certificateSources))
}

/// The system default trust store.
public static let systemDefault: Self = Self(wrapped: .systemDefault)
}

/// How to verify client certificates.
public struct CertificateVerification: Sendable {
package enum Wrapped {
case doNotVerify
case fullVerification
case noHostnameVerification
}

package let wrapped: Wrapped

/// All certificate verification disabled.
public static let noVerification: Self = Self(wrapped: .doNotVerify)

/// Certificates will be validated against the trust store, but will not be checked to see if they are valid for the given hostname.
public static let noHostnameVerification: Self = Self(wrapped: .noHostnameVerification)

/// Certificates will be validated against the trust store and checked against the hostname of the service we are contacting.
public static let fullVerification: Self = Self(wrapped: .fullVerification)
}
}

extension HTTP2ServerTransport.Posix.Config {
/// The security configuration for this connection.
public struct TransportSecurity: Sendable {
Expand Down Expand Up @@ -336,14 +217,13 @@ extension HTTP2ClientTransport.Posix.Config {
/// - `serverCertificateVerification` equals `fullVerification`
/// - `trustRoots` equals `systemDefault`
/// - `serverHostname` equals `nil`
public static var defaults: Self {
Self.defaults()
}
public static var defaults: Self { .defaults() }

/// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted to match
/// the requirements of mTLS:
/// - `trustRoots` equals `systemDefault`
/// - `serverCertificateVerification` equals `fullVerification`
/// - `serverHostname` equals `nil`
///
/// - Parameters:
/// - certificateChain: The certificates the client will offer during negotiation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ extension HTTP2ClientTransport.Posix {
#if canImport(NIOSSL)
private let nioSSLContext: NIOSSLContext?
private let serverHostname: String?
private let isPlainText: Bool
#endif

init(eventLoopGroup: any EventLoopGroup, config: HTTP2ClientTransport.Posix.Config) throws {
Expand All @@ -143,10 +144,12 @@ extension HTTP2ClientTransport.Posix {
case .plaintext:
self.nioSSLContext = nil
self.serverHostname = nil
self.isPlainText = true
case .tls(let tlsConfig):
do {
self.nioSSLContext = try NIOSSLContext(configuration: TLSConfiguration(tlsConfig))
self.serverHostname = tlsConfig.serverHostname
self.isPlainText = false
} catch {
throw RuntimeError(
code: .transportError,
Expand Down Expand Up @@ -183,7 +186,11 @@ extension HTTP2ClientTransport.Posix {
}
}

return HTTP2Connection(channel: channel, multiplexer: multiplexer, isPlaintext: true)
return HTTP2Connection(
channel: channel,
multiplexer: multiplexer,
isPlaintext: self.isPlainText
)
}
}
}
Expand Down
Loading
Loading