diff --git a/Package.swift b/Package.swift index e22ecd2..485bcd0 100644 --- a/Package.swift +++ b/Package.swift @@ -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", diff --git a/Sources/GRPCNIOTransportCore/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCNIOTransportCore/Client/Connection/ClientConnectionHandler.swift index 8886597..e9a613f 100644 --- a/Sources/GRPCNIOTransportCore/Client/Connection/ClientConnectionHandler.swift +++ b/Sources/GRPCNIOTransportCore/Client/Connection/ClientConnectionHandler.swift @@ -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: diff --git a/Sources/GRPCNIOTransportCore/TLSConfig.swift b/Sources/GRPCNIOTransportCore/TLSConfig.swift new file mode 100644 index 0000000..50bf0b2 --- /dev/null +++ b/Sources/GRPCNIOTransportCore/TLSConfig.swift @@ -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) + } +} diff --git a/Sources/GRPCNIOTransportHTTP2Posix/TLSConfig.swift b/Sources/GRPCNIOTransportHTTP2Posix/Config+TLS.swift similarity index 67% rename from Sources/GRPCNIOTransportHTTP2Posix/TLSConfig.swift rename to Sources/GRPCNIOTransportHTTP2Posix/Config+TLS.swift index 1cbf291..4a082d3 100644 --- a/Sources/GRPCNIOTransportHTTP2Posix/TLSConfig.swift +++ b/Sources/GRPCNIOTransportHTTP2Posix/Config+TLS.swift @@ -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 { @@ -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. diff --git a/Sources/GRPCNIOTransportHTTP2Posix/HTTP2ClientTransport+Posix.swift b/Sources/GRPCNIOTransportHTTP2Posix/HTTP2ClientTransport+Posix.swift index 1e3e22d..eda075e 100644 --- a/Sources/GRPCNIOTransportHTTP2Posix/HTTP2ClientTransport+Posix.swift +++ b/Sources/GRPCNIOTransportHTTP2Posix/HTTP2ClientTransport+Posix.swift @@ -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 { @@ -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, @@ -183,7 +186,11 @@ extension HTTP2ClientTransport.Posix { } } - return HTTP2Connection(channel: channel, multiplexer: multiplexer, isPlaintext: true) + return HTTP2Connection( + channel: channel, + multiplexer: multiplexer, + isPlaintext: self.isPlainText + ) } } } diff --git a/Sources/GRPCNIOTransportHTTP2TransportServices/Config+TLS.swift b/Sources/GRPCNIOTransportHTTP2TransportServices/Config+TLS.swift new file mode 100644 index 0000000..508bbb9 --- /dev/null +++ b/Sources/GRPCNIOTransportHTTP2TransportServices/Config+TLS.swift @@ -0,0 +1,225 @@ +/* + * 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. + */ + +#if canImport(Network) +public import GRPCNIOTransportCore +public import Network + +extension HTTP2ServerTransport.TransportServices.Config { + /// The security configuration for this connection. + public struct TransportSecurity: Sendable { + package enum Wrapped: Sendable { + case plaintext + case tls(TLS) + } + + package let wrapped: Wrapped + + /// This connection is plaintext: no encryption will take place. + public static let plaintext = Self(wrapped: .plaintext) + + /// This connection will use TLS. + public static func tls(_ tls: TLS) -> Self { + Self(wrapped: .tls(tls)) + } + } + + public struct TLS: Sendable { + /// How to verify the client certificate, if one is presented. + public var clientCertificateVerification: TLSConfig.CertificateVerification + + /// The trust roots to be used when verifying client certificates. + public var trustRoots: TLSConfig.TrustRootsSource + + /// Whether ALPN is required. + /// + /// If this is set to `true` but the client does not support ALPN, then the connection will be rejected. + public var requireALPN: Bool + + /// A provider for the `SecIdentity` to be used when setting up TLS. + public var identityProvider: @Sendable () throws -> SecIdentity + + /// Create a new HTTP2 NIO Transport Services transport TLS config. + /// - Parameters: + /// - clientCertificateVerification: How to verify the client certificate, if one is presented. + /// - trustRoots: The trust roots to be used when verifying client certificates. + /// - requireALPN: Whether ALPN is required. + /// - identityProvider: A provider for the `SecIdentity` to be used when setting up TLS. + public init( + clientCertificateVerification: TLSConfig.CertificateVerification, + trustRoots: TLSConfig.TrustRootsSource, + requireALPN: Bool, + identityProvider: @Sendable @escaping () throws -> SecIdentity + ) { + self.clientCertificateVerification = clientCertificateVerification + self.trustRoots = trustRoots + self.requireALPN = requireALPN + self.identityProvider = identityProvider + } + + /// Create a new HTTP2 NIO Transport Services transport TLS config, with some values defaulted: + /// - `clientCertificateVerificationMode` equals `doNotVerify` + /// - `trustRoots` equals `systemDefault` + /// - `requireALPN` equals `false` + /// + /// - Parameters: + /// - identityProvider: A provider for the `SecIdentity` to be used when setting up TLS. + /// - Returns: A new HTTP2 NIO Transport Services transport TLS config. + public static func defaults( + identityProvider: @Sendable @escaping () throws -> SecIdentity, + configure: (_ config: inout Self) -> Void = { _ in } + ) -> Self { + var config = Self( + clientCertificateVerification: .noVerification, + trustRoots: .systemDefault, + requireALPN: false, + identityProvider: identityProvider + ) + configure(&config) + return config + } + + /// Create a new HTTP2 NIO Transport Services transport TLS config, with some values defaulted to match + /// the requirements of mTLS: + /// - `clientCertificateVerificationMode` equals `noHostnameVerification` + /// - `trustRoots` equals `systemDefault` + /// - `requireALPN` equals `false` + /// + /// - Parameters: + /// - identityProvider: A provider for the `SecIdentity` to be used when setting up TLS. + /// - configure: A closure which allows you to modify the defaults before returning them. + /// - Returns: A new HTTP2 NIO Transport Services transport TLS config. + public static func mTLS( + identityProvider: @Sendable @escaping () throws -> SecIdentity, + configure: (_ config: inout Self) -> Void = { _ in } + ) -> Self { + var config = Self( + clientCertificateVerification: .noHostnameVerification, + trustRoots: .systemDefault, + requireALPN: false, + identityProvider: identityProvider + ) + configure(&config) + return config + } + } +} + +extension HTTP2ClientTransport.TransportServices.Config { + /// The security configuration for this connection. + public struct TransportSecurity: Sendable { + package enum Wrapped: Sendable { + case plaintext + case tls(TLS) + } + + package let wrapped: Wrapped + + /// This connection is plaintext: no encryption will take place. + public static let plaintext = Self(wrapped: .plaintext) + + /// This connection will use TLS. + public static func tls(_ tls: TLS) -> Self { + Self(wrapped: .tls(tls)) + } + } + + public struct TLS: Sendable { + /// How to verify the server certificate, if one is presented. + public var serverCertificateVerification: TLSConfig.CertificateVerification + + /// The trust roots to be used when verifying server certificates. + public var trustRoots: TLSConfig.TrustRootsSource + + /// An optional server hostname to use when verifying certificates. + public var serverHostname: String? + + /// An optional provider for the `SecIdentity` to be used when setting up TLS. + public var identityProvider: (@Sendable () throws -> SecIdentity)? + + /// Create a new HTTP2 NIO Transport Services transport TLS config. + /// - Parameters: + /// - serverCertificateVerification: How to verify the server certificate, if one is presented. + /// - trustRoots: The trust roots to be used when verifying server certificates. + /// - serverHostname: An optional server hostname to use when verifying certificates. + /// - identityProvider: A provider for the `SecIdentity` to be used when setting up TLS. + public init( + serverCertificateVerification: TLSConfig.CertificateVerification, + trustRoots: TLSConfig.TrustRootsSource, + serverHostname: String?, + identityProvider: (@Sendable () throws -> SecIdentity)? + ) { + self.serverCertificateVerification = serverCertificateVerification + self.serverHostname = serverHostname + self.trustRoots = trustRoots + self.identityProvider = identityProvider + } + + /// Create a new HTTP2 NIO Transport Services transport TLS config, with some values defaulted: + /// - `serverCertificateVerification` equals `fullVerification` + /// - `trustRoots` equals `systemDefault` + /// - `serverHostname` equals `nil` + /// - `identityProvider` equals `nil` + /// + /// - Parameters: + /// - configure: A closure which allows you to modify the defaults before returning them. + /// - Returns: A new HTTP2 NIO Posix transport TLS config. + public static func defaults( + configure: (_ config: inout Self) -> Void = { _ in } + ) -> Self { + var config = Self( + serverCertificateVerification: .fullVerification, + trustRoots: .systemDefault, + serverHostname: nil, + identityProvider: nil + ) + configure(&config) + return config + } + + /// Create a new HTTP2 NIO Transport Services transport TLS config, with some values defaulted: + /// - `serverCertificateVerification` equals `fullVerification` + /// - `trustRoots` equals `systemDefault` + /// - `serverHostname` equals `nil` + /// - `identityProvider` equals `nil` + public static var defaults: Self { .defaults() } + + /// Create a new HTTP2 NIO Transport Services transport TLS config, with some values defaulted to match + /// the requirements of mTLS: + /// - `serverCertificateVerification` equals `fullVerification` + /// - `trustRoots` equals `systemDefault` + /// - `serverHostname` equals `nil` + /// + /// - Parameters: + /// - identityProvider: A provider for the `SecIdentity` to be used when setting up TLS. + /// - configure: A closure which allows you to modify the defaults before returning them. + /// - Returns: A new HTTP2 NIO Posix transport TLS config. + public static func mTLS( + identityProvider: @Sendable @escaping () throws -> SecIdentity, + configure: (_ config: inout Self) -> Void = { _ in } + ) -> Self { + var config = Self( + serverCertificateVerification: .fullVerification, + trustRoots: .systemDefault, + serverHostname: nil, + identityProvider: identityProvider + ) + configure(&config) + return config + } + } +} +#endif diff --git a/Sources/GRPCNIOTransportHTTP2TransportServices/HTTP2ClientTransport+TransportServices.swift b/Sources/GRPCNIOTransportHTTP2TransportServices/HTTP2ClientTransport+TransportServices.swift index 3f1b6d3..ed9d523 100644 --- a/Sources/GRPCNIOTransportHTTP2TransportServices/HTTP2ClientTransport+TransportServices.swift +++ b/Sources/GRPCNIOTransportHTTP2TransportServices/HTTP2ClientTransport+TransportServices.swift @@ -21,6 +21,8 @@ public import NIOTransportServices // has to be public because of default argum public import NIOCore // has to be public because of EventLoopGroup param in init private import Network +private import struct Foundation.Data +private import struct Foundation.URL extension HTTP2ClientTransport { /// A `ClientTransport` using HTTP/2 built on top of `NIOTransportServices`. @@ -145,11 +147,22 @@ extension HTTP2ClientTransport.TransportServices { case .plaintext: isPlainText = true bootstrap = NIOTSConnectionBootstrap(group: self.eventLoopGroup) + .channelOption(NIOTSChannelOptions.waitForActivity, value: false) case .tls(let tlsConfig): isPlainText = false - bootstrap = NIOTSConnectionBootstrap(group: self.eventLoopGroup) - .tlsOptions(try NWProtocolTLS.Options(tlsConfig)) + do { + let options = try NWProtocolTLS.Options(tlsConfig) + bootstrap = NIOTSConnectionBootstrap(group: self.eventLoopGroup) + .channelOption(NIOTSChannelOptions.waitForActivity, value: false) + .tlsOptions(options) + } catch { + throw RuntimeError( + code: .transportError, + message: "Couldn't create NWProtocolTLS.Options, check your TLS configuration.", + cause: error + ) + } } let (channel, multiplexer) = try await bootstrap.connect(to: address) { channel in @@ -301,20 +314,48 @@ extension NWProtocolTLS.Options { convenience init(_ tlsConfig: HTTP2ClientTransport.TransportServices.Config.TLS) throws { self.init() - guard let sec_identity = sec_identity_create(try tlsConfig.identityProvider()) else { - throw RuntimeError( - code: .transportError, - message: """ - There was an issue creating the SecIdentity required to set up TLS. \ - Please check your TLS configuration. - """ + if let identityProvider = tlsConfig.identityProvider { + guard let sec_identity = sec_identity_create(try identityProvider()) else { + throw RuntimeError( + code: .transportError, + message: """ + There was an issue creating the SecIdentity required to set up TLS. \ + Please check your TLS configuration. + """ + ) + } + + sec_protocol_options_set_local_identity( + self.securityProtocolOptions, + sec_identity ) } - sec_protocol_options_set_local_identity( - self.securityProtocolOptions, - sec_identity - ) + switch tlsConfig.serverCertificateVerification.wrapped { + case .doNotVerify: + sec_protocol_options_set_peer_authentication_required( + self.securityProtocolOptions, + false + ) + + case .fullVerification: + sec_protocol_options_set_peer_authentication_required( + self.securityProtocolOptions, + true + ) + tlsConfig.serverHostname?.withCString { serverName in + sec_protocol_options_set_tls_server_name( + self.securityProtocolOptions, + serverName + ) + } + + case .noHostnameVerification: + sec_protocol_options_set_peer_authentication_required( + self.securityProtocolOptions, + true + ) + } sec_protocol_options_set_min_tls_protocol_version( self.securityProtocolOptions, @@ -327,6 +368,54 @@ extension NWProtocolTLS.Options { `protocol` ) } + + switch tlsConfig.trustRoots.wrapped { + case .certificates(let certificates): + let verifyQueue = DispatchQueue(label: "certificateVerificationQueue") + let verifyBlock: sec_protocol_verify_t = { (metadata, trust, verifyCompleteCallback) in + let actualTrust = sec_trust_copy_ref(trust).takeRetainedValue() + + let customAnchors: [SecCertificate] + do { + customAnchors = try certificates.map { certificateSource in + let certificateBytes: Data + switch certificateSource.wrapped { + case .file(let path, .der): + certificateBytes = try Data(contentsOf: URL(filePath: path)) + + case .bytes(let bytes, .der): + certificateBytes = Data(bytes) + + case .file(_, let format), .bytes(_, let format): + fatalError("Certificate format must be DER, but was \(format).") + } + + guard let certificate = SecCertificateCreateWithData(nil, certificateBytes as CFData) + else { + fatalError("Certificate was not a valid DER-encoded X509 certificate.") + } + return certificate + } + } catch { + verifyCompleteCallback(false) + return + } + + SecTrustSetAnchorCertificates(actualTrust, customAnchors as CFArray) + SecTrustEvaluateAsyncWithError(actualTrust, verifyQueue) { _, trusted, _ in + verifyCompleteCallback(trusted) + } + } + + sec_protocol_options_set_verify_block( + self.securityProtocolOptions, + verifyBlock, + verifyQueue + ) + + case .systemDefault: + () + } } } #endif diff --git a/Sources/GRPCNIOTransportHTTP2TransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCNIOTransportHTTP2TransportServices/HTTP2ServerTransport+TransportServices.swift index 3b5bf62..e035a5b 100644 --- a/Sources/GRPCNIOTransportHTTP2TransportServices/HTTP2ServerTransport+TransportServices.swift +++ b/Sources/GRPCNIOTransportHTTP2TransportServices/HTTP2ServerTransport+TransportServices.swift @@ -25,6 +25,8 @@ private import NIOHTTP2 private import Network private import Synchronization +private import struct Foundation.Data +private import struct Foundation.URL extension HTTP2ServerTransport { /// A NIO Transport Services-backed implementation of a server transport. @@ -253,6 +255,20 @@ extension NWProtocolTLS.Options { sec_identity ) + switch tlsConfig.clientCertificateVerification.wrapped { + case .doNotVerify: + sec_protocol_options_set_peer_authentication_required( + self.securityProtocolOptions, + false + ) + + case .fullVerification, .noHostnameVerification: + sec_protocol_options_set_peer_authentication_required( + self.securityProtocolOptions, + true + ) + } + sec_protocol_options_set_min_tls_protocol_version( self.securityProtocolOptions, .TLSv12 @@ -264,6 +280,54 @@ extension NWProtocolTLS.Options { `protocol` ) } + + switch tlsConfig.trustRoots.wrapped { + case .certificates(let certificates): + let verifyQueue = DispatchQueue(label: "certificateVerificationQueue") + let verifyBlock: sec_protocol_verify_t = { (metadata, trust, verifyCompleteCallback) in + let actualTrust = sec_trust_copy_ref(trust).takeRetainedValue() + + let customAnchors: [SecCertificate] + do { + customAnchors = try certificates.map { certificateSource in + let certificateBytes: Data + switch certificateSource.wrapped { + case .file(let path, .der): + certificateBytes = try Data(contentsOf: URL(filePath: path)) + + case .bytes(let bytes, .der): + certificateBytes = Data(bytes) + + case .file(_, let format), .bytes(_, let format): + fatalError("Certificate format must be DER, but was \(format).") + } + + guard let certificate = SecCertificateCreateWithData(nil, certificateBytes as CFData) + else { + fatalError("Certificate was not a valid DER-encoded X509 certificate.") + } + return certificate + } + } catch { + verifyCompleteCallback(false) + return + } + + SecTrustSetAnchorCertificates(actualTrust, customAnchors as CFArray) + SecTrustEvaluateAsyncWithError(actualTrust, verifyQueue) { _, trusted, _ in + verifyCompleteCallback(trusted) + } + } + + sec_protocol_options_set_verify_block( + self.securityProtocolOptions, + verifyBlock, + verifyQueue + ) + + case .systemDefault: + () + } } } #endif diff --git a/Sources/GRPCNIOTransportHTTP2TransportServices/TLSConfig.swift b/Sources/GRPCNIOTransportHTTP2TransportServices/TLSConfig.swift deleted file mode 100644 index 1784748..0000000 --- a/Sources/GRPCNIOTransportHTTP2TransportServices/TLSConfig.swift +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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. - */ - -#if canImport(Network) -public import Network - -extension HTTP2ServerTransport.TransportServices.Config { - /// The security configuration for this connection. - public struct TransportSecurity: Sendable { - package enum Wrapped: Sendable { - case plaintext - case tls(TLS) - } - - package let wrapped: Wrapped - - /// This connection is plaintext: no encryption will take place. - public static let plaintext = Self(wrapped: .plaintext) - - /// This connection will use TLS. - public static func tls(_ tls: TLS) -> Self { - Self(wrapped: .tls(tls)) - } - } - - public struct TLS: Sendable { - /// A provider for the `SecIdentity` to be used when setting up TLS. - public var identityProvider: @Sendable () throws -> SecIdentity - - /// Whether ALPN is required. - /// - /// If this is set to `true` but the client does not support ALPN, then the connection will be rejected. - public var requireALPN: Bool - - /// Create a new HTTP2 NIO Transport Services transport TLS config. - /// - Parameters: - /// - requireALPN: Whether ALPN is required. - /// - identityProvider: A provider for the `SecIdentity` to be used when setting up TLS. - public init( - requireALPN: Bool, - identityProvider: @Sendable @escaping () throws -> SecIdentity - ) { - self.requireALPN = requireALPN - self.identityProvider = identityProvider - } - - /// Create a new HTTP2 NIO Transport Services transport TLS config, with some values defaulted: - /// - `requireALPN` equals `false` - /// - /// - Returns: A new HTTP2 NIO Transport Services transport TLS config. - public static func defaults( - identityProvider: @Sendable @escaping () throws -> SecIdentity - ) -> Self { - Self(requireALPN: false, identityProvider: identityProvider) - } - } -} - -extension HTTP2ClientTransport.TransportServices.Config { - /// The security configuration for this connection. - public struct TransportSecurity: Sendable { - package enum Wrapped: Sendable { - case plaintext - case tls(TLS) - } - - package let wrapped: Wrapped - - /// This connection is plaintext: no encryption will take place. - public static let plaintext = Self(wrapped: .plaintext) - - /// This connection will use TLS. - public static func tls(_ tls: TLS) -> Self { - Self(wrapped: .tls(tls)) - } - } - - public struct TLS: Sendable { - /// A provider for the `SecIdentity` to be used when setting up TLS. - public var identityProvider: @Sendable () throws -> SecIdentity - - /// Create a new HTTP2 NIO Transport Services transport TLS config. - public init(identityProvider: @Sendable @escaping () throws -> SecIdentity) { - self.identityProvider = identityProvider - } - } -} -#endif diff --git a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportNIOTransportServicesTests.swift b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportNIOTransportServicesTests.swift index eb9fbab..e14a54e 100644 --- a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportNIOTransportServicesTests.swift +++ b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportNIOTransportServicesTests.swift @@ -19,57 +19,9 @@ import GRPCCore import GRPCNIOTransportCore import GRPCNIOTransportHTTP2TransportServices import XCTest +import NIOSSL final class HTTP2TransportNIOTransportServicesTests: XCTestCase { - private static let p12bundleURL = URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() // (this file) - .deletingLastPathComponent() // GRPCHTTP2TransportTests - .deletingLastPathComponent() // Tests - .appendingPathComponent("Sources") - .appendingPathComponent("GRPCSampleData") - .appendingPathComponent("bundle") - .appendingPathExtension("p12") - - @Sendable private static func loadIdentity() throws -> SecIdentity { - let data = try Data(contentsOf: Self.p12bundleURL) - - var externalFormat = SecExternalFormat.formatUnknown - var externalItemType = SecExternalItemType.itemTypeUnknown - let passphrase = "password" as CFTypeRef - var exportKeyParams = SecItemImportExportKeyParameters() - exportKeyParams.passphrase = Unmanaged.passUnretained(passphrase) - var items: CFArray? - - let status = SecItemImport( - data as CFData, - "bundle.p12" as CFString, - &externalFormat, - &externalItemType, - SecItemImportExportFlags(rawValue: 0), - &exportKeyParams, - nil, - &items - ) - - if status != errSecSuccess { - XCTFail( - """ - Unable to load identity from '\(Self.p12bundleURL)'. \ - SecItemImport failed with status \(status) - """ - ) - } else if items == nil { - XCTFail( - """ - Unable to load identity from '\(Self.p12bundleURL)'. \ - SecItemImport failed. - """ - ) - } - - return ((items! as NSArray)[0] as! SecIdentity) - } - func testGetListeningAddress_IPv4() async throws { let transport = GRPCNIOTransportCore.HTTP2ServerTransport.TransportServices( address: .ipv4(host: "0.0.0.0", port: 0), @@ -199,11 +151,8 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { } func testServerConfig_Defaults() throws { - try XCTSkipIf(true) - - let identityProvider = Self.loadIdentity let grpcTLSConfig = HTTP2ServerTransport.TransportServices.Config.TLS.defaults( - identityProvider: identityProvider + identityProvider: Self.loadIdentity ) let grpcConfig = HTTP2ServerTransport.TransportServices.Config.defaults( transportSecurity: .tls(grpcTLSConfig) @@ -213,17 +162,36 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { XCTAssertEqual(grpcConfig.connection, HTTP2ServerTransport.Config.Connection.defaults) XCTAssertEqual(grpcConfig.http2, HTTP2ServerTransport.Config.HTTP2.defaults) XCTAssertEqual(grpcConfig.rpc, HTTP2ServerTransport.Config.RPC.defaults) - XCTAssertEqual(try grpcTLSConfig.identityProvider(), try identityProvider()) - XCTAssertEqual(grpcTLSConfig.requireALPN, false) + XCTAssertNotNil(grpcTLSConfig.identityProvider) } - func testClientConfig_Defaults() throws { - try XCTSkipIf(true) - - let identityProvider = Self.loadIdentity - let grpcTLSConfig = HTTP2ClientTransport.TransportServices.Config.TLS( - identityProvider: identityProvider + @Sendable private static func loadIdentity() throws -> SecIdentity { + let certificateKeyPairs = try SelfSignedCertificateKeyPairs() + let password = "somepassword" + let bundle = NIOSSLPKCS12Bundle( + certificateChain: [try NIOSSLCertificate(bytes: certificateKeyPairs.server.certificate, format: .der)], + privateKey: try NIOSSLPrivateKey(bytes: certificateKeyPairs.server.key, format: .der) + ) + let pkcs12Bytes = try bundle.serialize(passphrase: password.utf8) + let options = [kSecImportExportPassphrase as String: password] + var rawItems: CFArray? + let status = SecPKCS12Import( + Data(pkcs12Bytes) as CFData, + options as CFDictionary, + &rawItems ) + guard status == errSecSuccess else { + XCTFail("Failed to import PKCS12 bundle: status \(status).") + throw HTTP2TransportNIOTransportServicesTestsError.failedToImportPKCS12 + } + let items = rawItems! as! [[String: Any]] + let firstItem = items[0] + let identity = firstItem[kSecImportItemIdentity as String] as! SecIdentity + return identity + } + + func testClientConfig_Defaults() throws { + let grpcTLSConfig = HTTP2ClientTransport.TransportServices.Config.TLS.defaults let grpcConfig = HTTP2ClientTransport.TransportServices.Config.defaults( transportSecurity: .tls(grpcTLSConfig) ) @@ -232,7 +200,11 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { XCTAssertEqual(grpcConfig.connection, HTTP2ClientTransport.Config.Connection.defaults) XCTAssertEqual(grpcConfig.http2, HTTP2ClientTransport.Config.HTTP2.defaults) XCTAssertEqual(grpcConfig.backoff, HTTP2ClientTransport.Config.Backoff.defaults) - XCTAssertEqual(try grpcTLSConfig.identityProvider(), try identityProvider()) + XCTAssertNil(grpcTLSConfig.identityProvider) } } + +enum HTTP2TransportNIOTransportServicesTestsError: Error { + case failedToImportPKCS12 +} #endif diff --git a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift index 7992826..6cd3a61 100644 --- a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift +++ b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift @@ -14,13 +14,12 @@ * limitations under the License. */ -import Crypto import Foundation import GRPCNIOTransportHTTP2Posix +import GRPCNIOTransportHTTP2TransportServices import NIOSSL -import SwiftASN1 +import Network import Testing -import X509 @Suite("HTTP/2 transport E2E tests with TLS enabled") struct HTTP2TransportTLSEnabledTests { @@ -28,8 +27,8 @@ struct HTTP2TransportTLSEnabledTests { @Test( "When using defaults, server does not perform client verification", - arguments: [TransportKind.posix], - [TransportKind.posix] + arguments: [TransportKind/*.posix, */.transportServices], + [TransportKind/*.posix, */.transportServices] ) func testRPC_Defaults_OK( clientTransport: TransportKind, @@ -38,7 +37,8 @@ struct HTTP2TransportTLSEnabledTests { let certificateKeyPairs = try SelfSignedCertificateKeyPairs() let clientTransportConfig = self.makeDefaultClientTLSConfig( for: clientTransport, - certificateKeyPairs: certificateKeyPairs + certificateKeyPairs: certificateKeyPairs, + serverHostname: "localhost" ) let serverTransportConfig = self.makeDefaultServerTLSConfig( for: serverTransport, @@ -57,8 +57,8 @@ struct HTTP2TransportTLSEnabledTests { @Test( "When using mTLS defaults, both client and server verify each others' certificates", - arguments: [TransportKind.posix], - [TransportKind.posix] + arguments: [TransportKind.posix, .transportServices], + [TransportKind.posix, .transportServices] ) func testRPC_mTLS_OK( clientTransport: TransportKind, @@ -72,8 +72,7 @@ struct HTTP2TransportTLSEnabledTests { ) let serverTransportConfig = self.makeMTLSServerTLSConfig( for: serverTransport, - certificateKeyPairs: certificateKeyPairs, - includeClientCertificateInTrustRoots: true + certificateKeyPairs: certificateKeyPairs ) try await self.withClientAndServer( @@ -88,8 +87,8 @@ struct HTTP2TransportTLSEnabledTests { @Test( "Error is surfaced when client fails server verification", - arguments: [TransportKind.posix], - [TransportKind.posix] + arguments: [TransportKind.posix, .transportServices], + [TransportKind.posix, .transportServices] ) // Verification should fail because the custom hostname is missing on the client. func testClientFailsServerValidation( @@ -97,15 +96,14 @@ struct HTTP2TransportTLSEnabledTests { serverTransport: TransportKind ) async throws { let certificateKeyPairs = try SelfSignedCertificateKeyPairs() - let clientTransportConfig = self.makeMTLSClientTLSConfig( + let clientTransportConfig = self.makeDefaultClientTLSConfig( for: clientTransport, certificateKeyPairs: certificateKeyPairs, serverHostname: nil ) - let serverTransportConfig = self.makeMTLSServerTLSConfig( + let serverTransportConfig = self.makeDefaultServerTLSConfig( for: serverTransport, - certificateKeyPairs: certificateKeyPairs, - includeClientCertificateInTrustRoots: true + certificateKeyPairs: certificateKeyPairs ) try await self.withClientAndServer( @@ -115,24 +113,32 @@ struct HTTP2TransportTLSEnabledTests { await #expect { try await self.executeUnaryRPC(control: control) } throws: { error in - guard let rootError = error as? RPCError else { - Issue.record("Should be an RPC error") - return false - } + let rootError = try #require(error as? RPCError) #expect(rootError.code == .unavailable) - #expect( - rootError.message - == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." - ) - guard - let sslError = rootError.cause as? NIOSSLExtraError, - case .failedToValidateHostname = sslError - else { - Issue.record( - "Should be a NIOSSLExtraError.failedToValidateHostname error, but was: \(String(describing: rootError.cause))" + switch clientTransport { + case .posix: + #expect( + rootError.message + == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." ) - return false + let sslError = try #require(rootError.cause as? NIOSSLExtraError) + guard sslError == .failedToValidateHostname else { + Issue.record( + "Should be a NIOSSLExtraError.failedToValidateHostname error, but was: \(String(describing: rootError.cause))" + ) + return false + } + + case .transportServices: + #expect(rootError.message.starts(with: "Could not establish a connection to")) + let nwError = try #require(rootError.cause as? NWError) + guard case .tls(-9808) /* bad certificate format */ = nwError else { + Issue.record( + "Should be a NWError.tls(-9808) error, but was: \(String(describing: rootError.cause))" + ) + return false + } } return true @@ -142,24 +148,24 @@ struct HTTP2TransportTLSEnabledTests { @Test( "Error is surfaced when server fails client verification", - arguments: [TransportKind.posix], - [TransportKind.posix] + arguments: [TransportKind/*.posix, */.transportServices], + [TransportKind/*.posix, */.transportServices] ) - // Verification should fail because the server does not have trust roots containing the client cert. + // Verification should fail because the client does not offer a cert that + // the server can use for mutual verification. func testServerFailsClientValidation( clientTransport: TransportKind, serverTransport: TransportKind ) async throws { let certificateKeyPairs = try SelfSignedCertificateKeyPairs() - let clientTransportConfig = self.makeMTLSClientTLSConfig( + let clientTransportConfig = self.makeDefaultClientTLSConfig( for: clientTransport, certificateKeyPairs: certificateKeyPairs, serverHostname: "localhost" ) let serverTransportConfig = self.makeMTLSServerTLSConfig( for: serverTransport, - certificateKeyPairs: certificateKeyPairs, - includeClientCertificateInTrustRoots: false + certificateKeyPairs: certificateKeyPairs ) try await self.withClientAndServer( @@ -169,24 +175,31 @@ struct HTTP2TransportTLSEnabledTests { await #expect { try await self.executeUnaryRPC(control: control) } throws: { error in - guard let rootError = error as? RPCError else { - Issue.record("Should be an RPC error") - return false - } + let rootError = try #require(error as? RPCError) #expect(rootError.code == .unavailable) #expect( rootError.message == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." ) - guard - let sslError = rootError.cause as? NIOSSL.BoringSSLError, - case .sslError = sslError - else { - Issue.record( - "Should be a NIOSSL.sslError error, but was: \(String(describing: rootError.cause))" - ) - return false + switch clientTransport { + case .posix: + let sslError = try #require(rootError.cause as? NIOSSL.BoringSSLError) + guard case .sslError = sslError else { + Issue.record( + "Should be a NIOSSL.sslError error, but was: \(String(describing: rootError.cause))" + ) + return false + } + + case .transportServices: + let nwError = try #require(rootError.cause as? NWError) + guard case .tls(-9829) /* unknown peer certificate */ = nwError else { + Issue.record( + "Should be a NWError.tls(-9829) error, but was: \(String(describing: rootError.cause))" + ) + return false + } } return true @@ -196,23 +209,31 @@ struct HTTP2TransportTLSEnabledTests { // - MARK: Test Utilities + enum TLSEnabledTestsError: Error { + case failedToImportPKCS12 + } + enum TransportKind: Sendable { case posix + case transportServices } enum TLSConfig { enum Client { case posix(HTTP2ClientTransport.Posix.Config.TransportSecurity) + case transportServices(HTTP2ClientTransport.TransportServices.Config.TransportSecurity) } enum Server { case posix(HTTP2ServerTransport.Posix.Config.TransportSecurity) + case transportServices(HTTP2ServerTransport.TransportServices.Config.TransportSecurity) } } func makeDefaultClientTLSConfig( for transportSecurity: TransportKind, - certificateKeyPairs: SelfSignedCertificateKeyPairs + certificateKeyPairs: SelfSignedCertificateKeyPairs, + serverHostname: String? ) -> TLSConfig.Client { switch transportSecurity { case .posix: @@ -222,7 +243,19 @@ struct HTTP2TransportTLSEnabledTests { $0.trustRoots = .certificates([ .bytes(certificateKeyPairs.server.certificate, format: .der) ]) - $0.serverHostname = "localhost" + $0.serverHostname = serverHostname + } + ) + ) + + case .transportServices: + return .transportServices( + .tls( + .defaults { + $0.trustRoots = .certificates([ + .bytes(certificateKeyPairs.server.certificate, format: .der) + ]) + $0.serverHostname = serverHostname } ) ) @@ -249,6 +282,38 @@ struct HTTP2TransportTLSEnabledTests { } ) ) + + case .transportServices: + return .transportServices( + .tls( + .mTLS { + let password = "somepassword" + let bundle = NIOSSLPKCS12Bundle( + certificateChain: [try NIOSSLCertificate(bytes: certificateKeyPairs.client.certificate, format: .der)], + privateKey: try NIOSSLPrivateKey(bytes: certificateKeyPairs.client.key, format: .der) + ) + let pkcs12Bytes = try bundle.serialize(passphrase: password.utf8) + let options = [kSecImportExportPassphrase as String: password] + var rawItems: CFArray? + let status = SecPKCS12Import( + Data(pkcs12Bytes) as CFData, + options as CFDictionary, + &rawItems + ) + guard status == errSecSuccess else { + Issue.record("Failed to import PKCS12 bundle: status \(status).") + throw TLSEnabledTestsError.failedToImportPKCS12 + } + let items = rawItems! as! [[String: Any]] + let firstItem = items[0] + let identity = firstItem[kSecImportItemIdentity as String] as! SecIdentity + return identity + } configure: { + $0.serverHostname = serverHostname + $0.trustRoots = .certificates([.bytes(certificateKeyPairs.server.certificate, format: .der)]) + } + ) + ) } } @@ -266,13 +331,41 @@ struct HTTP2TransportTLSEnabledTests { ) ) ) + + case .transportServices: + return .transportServices( + .tls( + .defaults { + let password = "somepassword" + let bundle = NIOSSLPKCS12Bundle( + certificateChain: [try NIOSSLCertificate(bytes: certificateKeyPairs.server.certificate, format: .der)], + privateKey: try NIOSSLPrivateKey(bytes: certificateKeyPairs.server.key, format: .der) + ) + let pkcs12Bytes = try bundle.serialize(passphrase: password.utf8) + let options = [kSecImportExportPassphrase as String: password] + var rawItems: CFArray? + let status = SecPKCS12Import( + Data(pkcs12Bytes) as CFData, + options as CFDictionary, + &rawItems + ) + guard status == errSecSuccess else { + Issue.record("Failed to import PKCS12 bundle: status \(status).") + throw TLSEnabledTestsError.failedToImportPKCS12 + } + let items = rawItems! as! [[String: Any]] + let firstItem = items[0] + let identity = firstItem[kSecImportItemIdentity as String] as! SecIdentity + return identity + } + ) + ) } } func makeMTLSServerTLSConfig( for transportKind: TransportKind, - certificateKeyPairs: SelfSignedCertificateKeyPairs, - includeClientCertificateInTrustRoots: Bool + certificateKeyPairs: SelfSignedCertificateKeyPairs ) -> TLSConfig.Server { switch transportKind { case .posix: @@ -282,11 +375,42 @@ struct HTTP2TransportTLSEnabledTests { certificateChain: [.bytes(certificateKeyPairs.server.certificate, format: .der)], privateKey: .bytes(certificateKeyPairs.server.key, format: .der) ) { - if includeClientCertificateInTrustRoots { - $0.trustRoots = .certificates([ - .bytes(certificateKeyPairs.client.certificate, format: .der) - ]) + $0.trustRoots = .certificates([ + .bytes(certificateKeyPairs.client.certificate, format: .der) + ]) + } + ) + ) + + case .transportServices: + return .transportServices( + .tls( + .mTLS { + let password = "somepassword" + let bundle = NIOSSLPKCS12Bundle( + certificateChain: [try NIOSSLCertificate(bytes: certificateKeyPairs.server.certificate, format: .der)], + privateKey: try NIOSSLPrivateKey(bytes: certificateKeyPairs.server.key, format: .der) + ) + let pkcs12Bytes = try bundle.serialize(passphrase: password.utf8) + let options = [kSecImportExportPassphrase as String: password] + var rawItems: CFArray? + let status = SecPKCS12Import( + Data(pkcs12Bytes) as CFData, + options as CFDictionary, + &rawItems + ) + guard status == errSecSuccess else { + Issue.record("Failed to import PKCS12 bundle: status \(status).") + throw TLSEnabledTestsError.failedToImportPKCS12 } + let items = rawItems! as! [[String: Any]] + let firstItem = items[0] + let identity = firstItem[kSecImportItemIdentity as String] as! SecIdentity + return identity + } configure: { + $0.trustRoots = .certificates([ + .bytes(certificateKeyPairs.client.certificate, format: .der) + ]) } ) ) @@ -302,43 +426,59 @@ struct HTTP2TransportTLSEnabledTests { let server = self.makeServer(tlsConfig: serverTLSConfig) group.addTask { - try await server.serve() + do { + try await server.serve() + } catch { + Issue.record(error, "Something went wrong when running the server") + } + print("Server finished serving") } guard let address = try await server.listeningAddress?.ipv4 else { Issue.record("Unexpected address to connect to") return } + let target: any ResolvableTarget = .ipv4(host: address.host, port: address.port) let client = try self.makeClient(tlsConfig: clientTLSConfig, target: target) group.addTask { - try await client.run() + do { + try await client.run() + } catch { + Issue.record(error, "Something went wrong when running client") + } + print("Client finished running") } let control = ControlClient(wrapping: client) try await test(control) - server.beginGracefulShutdown() client.beginGracefulShutdown() + server.beginGracefulShutdown() } } private func makeServer(tlsConfig: TLSConfig.Server) -> GRPCServer { - let services = [ControlService()] - + let serverTransport: any ServerTransport switch tlsConfig { case .posix(let transportSecurity): - let server = GRPCServer( - transport: .http2NIOPosix( - address: .ipv4(host: "127.0.0.1", port: 0), - config: .defaults(transportSecurity: transportSecurity) - ), - services: services + serverTransport = .http2NIOPosix( + address: .ipv4(host: "127.0.0.1", port: 0), + config: .defaults(transportSecurity: transportSecurity) ) - return server + case .transportServices(let transportSecurity): + serverTransport = .http2NIOTS( + address: .ipv4(host: "127.0.0.1", port: 0), + config: .defaults(transportSecurity: transportSecurity) + ) } + + return GRPCServer( + transport: serverTransport, + services: [ControlService()] + ) } private func makeClient( @@ -346,17 +486,27 @@ struct HTTP2TransportTLSEnabledTests { target: any ResolvableTarget ) throws -> GRPCClient { let transport: any ClientTransport - switch tlsConfig { case .posix(let transportSecurity): - transport = try HTTP2ClientTransport.Posix( + transport = try .http2NIOPosix( target: target, config: .defaults(transportSecurity: transportSecurity) { config in config.backoff.initial = .milliseconds(100) + config.backoff.max = .milliseconds(200) config.backoff.multiplier = 1 config.backoff.jitter = 0 - }, - serviceConfig: ServiceConfig() + } + ) + + case .transportServices(let transportSecurity): + transport = try .http2NIOTS( + target: target, + config: .defaults(transportSecurity: transportSecurity) { config in + config.backoff.initial = .milliseconds(100) + config.backoff.max = .milliseconds(200) + config.backoff.multiplier = 1 + config.backoff.jitter = 0 + } ) } @@ -371,62 +521,3 @@ struct HTTP2TransportTLSEnabledTests { } } } - -struct SelfSignedCertificateKeyPairs { - struct CertificateKeyPair { - let certificate: [UInt8] - let key: [UInt8] - } - - let server: CertificateKeyPair - let client: CertificateKeyPair - - init() throws { - let server = try Self.makeSelfSignedDERCertificateAndPrivateKey(name: "Server Certificate") - let client = try Self.makeSelfSignedDERCertificateAndPrivateKey(name: "Client Certificate") - - self.server = CertificateKeyPair(certificate: server.cert, key: server.key) - self.client = CertificateKeyPair(certificate: client.cert, key: client.key) - } - - private static func makeSelfSignedDERCertificateAndPrivateKey( - name: String - ) throws -> (cert: [UInt8], key: [UInt8]) { - let swiftCryptoKey = P256.Signing.PrivateKey() - let key = Certificate.PrivateKey(swiftCryptoKey) - let subjectName = try DistinguishedName { CommonName(name) } - let issuerName = subjectName - let now = Date() - let extensions = try Certificate.Extensions { - Critical( - BasicConstraints.isCertificateAuthority(maxPathLength: nil) - ) - Critical( - KeyUsage(digitalSignature: true, keyCertSign: true) - ) - Critical( - try ExtendedKeyUsage([.serverAuth, .clientAuth]) - ) - SubjectAlternativeNames([.dnsName("localhost")]) - } - let certificate = try Certificate( - version: .v3, - serialNumber: Certificate.SerialNumber(), - publicKey: key.publicKey, - notValidBefore: now.addingTimeInterval(-60 * 60), - notValidAfter: now.addingTimeInterval(60 * 60 * 24 * 365), - issuer: issuerName, - subject: subjectName, - signatureAlgorithm: .ecdsaWithSHA256, - extensions: extensions, - issuerPrivateKey: key - ) - - var serializer = DER.Serializer() - try serializer.serialize(certificate) - - let certBytes = serializer.serializedBytes - let keyBytes = try key.serializeAsPEM().derBytes - return (certBytes, keyBytes) - } -} diff --git a/Tests/GRPCNIOTransportHTTP2Tests/Test Utilities/SelfSignedCertificateKeyPairs.swift b/Tests/GRPCNIOTransportHTTP2Tests/Test Utilities/SelfSignedCertificateKeyPairs.swift new file mode 100644 index 0000000..6c5cb4c --- /dev/null +++ b/Tests/GRPCNIOTransportHTTP2Tests/Test Utilities/SelfSignedCertificateKeyPairs.swift @@ -0,0 +1,79 @@ +/* + * 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. + */ + +import SwiftASN1 +import X509 +import Crypto +import Foundation + +struct SelfSignedCertificateKeyPairs { + struct CertificateKeyPair { + let certificate: [UInt8] + let key: [UInt8] + } + + let server: CertificateKeyPair + let client: CertificateKeyPair + + init() throws { + let server = try Self.makeSelfSignedDERCertificateAndPrivateKey(name: "Server Certificate") + let client = try Self.makeSelfSignedDERCertificateAndPrivateKey(name: "Client Certificate") + + self.server = CertificateKeyPair(certificate: server.cert, key: server.key) + self.client = CertificateKeyPair(certificate: client.cert, key: client.key) + } + + private static func makeSelfSignedDERCertificateAndPrivateKey( + name: String + ) throws -> (cert: [UInt8], key: [UInt8]) { + let swiftCryptoKey = P256.Signing.PrivateKey() + let key = Certificate.PrivateKey(swiftCryptoKey) + let subjectName = try DistinguishedName { CommonName(name) } + let issuerName = subjectName + let now = Date() + let extensions = try Certificate.Extensions { + Critical( + BasicConstraints.isCertificateAuthority(maxPathLength: nil) + ) + Critical( + KeyUsage(digitalSignature: true, keyCertSign: true) + ) + Critical( + try ExtendedKeyUsage([.serverAuth, .clientAuth]) + ) + SubjectAlternativeNames([.dnsName("localhost")]) + } + let certificate = try Certificate( + version: .v3, + serialNumber: Certificate.SerialNumber(), + publicKey: key.publicKey, + notValidBefore: now.addingTimeInterval(-60 * 60), + notValidAfter: now.addingTimeInterval(60 * 60 * 24 * 365), + issuer: issuerName, + subject: subjectName, + signatureAlgorithm: .ecdsaWithSHA256, + extensions: extensions, + issuerPrivateKey: key + ) + + var serializer = DER.Serializer() + try serializer.serialize(certificate) + + let certBytes = serializer.serializedBytes + let keyBytes = try key.serializeAsPEM().derBytes + return (certBytes, keyBytes) + } +}