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 support for TLS on H2 NIOTS server transport #2040

Merged
merged 7 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ public import GRPCCore
public import NIOTransportServices // has to be public because of default argument value in init
public import GRPCHTTP2Core

internal import NIOCore
internal import NIOExtras
internal import NIOHTTP2
private import NIOCore
private import NIOExtras
private import NIOHTTP2
private import Network

private import Synchronization

Expand Down Expand Up @@ -171,7 +172,25 @@ extension HTTP2ServerTransport {
}
}

let serverChannel = try await NIOTSListenerBootstrap(group: self.eventLoopGroup)
let bootstrap: NIOTSListenerBootstrap

let requireALPN: Bool
let scheme: Scheme
switch self.config.transportSecurity.wrapped {
case .plaintext:
requireALPN = false
scheme = .http
bootstrap = NIOTSListenerBootstrap(group: self.eventLoopGroup)

case .tls(let tlsConfig):
requireALPN = tlsConfig.requireALPN
scheme = .https
bootstrap = NIOTSListenerBootstrap(group: self.eventLoopGroup)
.tlsOptions(try NWProtocolTLS.Options(tlsConfig))
}

let serverChannel =
try await bootstrap
.serverChannelOption(
ChannelOptions.socketOption(.so_reuseaddr),
value: 1
Expand All @@ -190,8 +209,8 @@ extension HTTP2ServerTransport {
connectionConfig: self.config.connection,
http2Config: self.config.http2,
rpcConfig: self.config.rpc,
requireALPN: false,
scheme: .http
requireALPN: requireALPN,
scheme: scheme
)
}
}
Expand Down Expand Up @@ -292,41 +311,55 @@ extension HTTP2ServerTransport.TransportServices {
public struct Config: Sendable {
/// Compression configuration.
public var compression: HTTP2ServerTransport.Config.Compression

/// Connection configuration.
public var connection: HTTP2ServerTransport.Config.Connection

/// HTTP2 configuration.
public var http2: HTTP2ServerTransport.Config.HTTP2

/// RPC configuration.
public var rpc: HTTP2ServerTransport.Config.RPC

/// The transport's security.
public var transportSecurity: TransportSecurity

/// Construct a new `Config`.
/// - Parameters:
/// - compression: Compression configuration.
/// - connection: Connection configuration.
/// - http2: HTTP2 configuration.
/// - rpc: RPC configuration.
/// - transportSecurity: The transport's security configuration.
public init(
compression: HTTP2ServerTransport.Config.Compression,
connection: HTTP2ServerTransport.Config.Connection,
http2: HTTP2ServerTransport.Config.HTTP2,
rpc: HTTP2ServerTransport.Config.RPC
rpc: HTTP2ServerTransport.Config.RPC,
transportSecurity: TransportSecurity
) {
self.compression = compression
self.connection = connection
self.http2 = http2
self.rpc = rpc
self.transportSecurity = transportSecurity
}

/// Default values for the different configurations.
///
/// - Parameter configure: A closure which allows you to modify the defaults before
/// returning them.
public static func defaults(configure: (_ config: inout Self) -> Void = { _ in }) -> Self {
/// - Parameters:
/// - transportSecurity: The transport's security configuration.
/// - configure: A closure which allows you to modify the defaults before returning them.
public static func defaults(
transportSecurity: TransportSecurity,
configure: (_ config: inout Self) -> Void = { _ in }
) -> Self {
var config = Self(
compression: .defaults,
connection: .defaults,
http2: .defaults,
rpc: .defaults
rpc: .defaults,
transportSecurity: transportSecurity
)
configure(&config)
return config
Expand Down Expand Up @@ -396,4 +429,38 @@ extension ServerTransport where Self == HTTP2ServerTransport.TransportServices {
)
}
}

@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
extension NWProtocolTLS.Options {
convenience init(_ tlsConfig: HTTP2ServerTransport.TransportServices.Config.TLS) throws {
self.init()

guard let sec_identity = sec_identity_create(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.
"""
)
}

sec_protocol_options_set_local_identity(
self.securityProtocolOptions,
sec_identity
)

sec_protocol_options_set_min_tls_protocol_version(
self.securityProtocolOptions,
.TLSv12
)

for `protocol` in ["grpc-exp", "h2"] {
sec_protocol_options_add_tls_application_protocol(
self.securityProtocolOptions,
`protocol`
)
}
}
}
#endif
63 changes: 63 additions & 0 deletions Sources/GRPCHTTP2TransportNIOTransportServices/TLSConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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

@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
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 {
/// The `SecIdentity` to be used when setting up TLS.
public var identityProvider: @Sendable () -> SecIdentity
gjcairo marked this conversation as resolved.
Show resolved Hide resolved

/// 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, with some values defaulted:
/// - `requireALPN` equals `false`
///
/// - Returns: A new HTTP2 NIO Transport Services transport TLS config.
public static func defaults(
identityProvider: @Sendable @escaping () -> SecIdentity
) -> Self {
Self(
identityProvider: identityProvider,
requireALPN: false
)
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,59 @@ import XCTest

@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
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() -> 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 = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices(
address: .ipv4(host: "0.0.0.0", port: 0),
config: .defaults()
config: .defaults(transportSecurity: .plaintext)
)

try await withThrowingDiscardingTaskGroup { group in
Expand All @@ -45,7 +94,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase {
func testGetListeningAddress_IPv6() async throws {
let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices(
address: .ipv6(host: "::1", port: 0),
config: .defaults()
config: .defaults(transportSecurity: .plaintext)
)

try await withThrowingDiscardingTaskGroup { group in
Expand All @@ -65,7 +114,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase {
func testGetListeningAddress_UnixDomainSocket() async throws {
let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices(
address: .unixDomainSocket(path: "/tmp/niots-uds-test"),
config: .defaults()
config: .defaults(transportSecurity: .plaintext)
)
defer {
// NIOTS does not unlink the UDS on close.
Expand All @@ -91,7 +140,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase {
func testGetListeningAddress_InvalidAddress() async {
let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices(
address: .unixDomainSocket(path: "/this/should/be/an/invalid/path"),
config: .defaults()
config: .defaults(transportSecurity: .plaintext)
)

try? await withThrowingDiscardingTaskGroup { group in
Expand Down Expand Up @@ -120,7 +169,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase {
func testGetListeningAddress_StoppedListening() async throws {
let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices(
address: .ipv4(host: "0.0.0.0", port: 0),
config: .defaults()
config: .defaults(transportSecurity: .plaintext)
)

try? await withThrowingDiscardingTaskGroup { group in
Expand Down Expand Up @@ -149,5 +198,14 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase {
}
}
}

func testTLSConfig_Defaults() throws {
let identityProvider = Self.loadIdentity
let grpcTLSConfig = HTTP2ServerTransport.TransportServices.Config.TLS.defaults(
identityProvider: identityProvider
)
XCTAssertEqual(grpcTLSConfig.identityProvider(), identityProvider())
XCTAssertEqual(grpcTLSConfig.requireALPN, false)
}
}
#endif
2 changes: 1 addition & 1 deletion Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ final class HTTP2TransportTests: XCTestCase {
let server = GRPCServer(
transport: .http2NIOTS(
address: .ipv4(host: "127.0.0.1", port: 0),
config: .defaults {
config: .defaults(transportSecurity: .plaintext) {
$0.compression.enabledAlgorithms = compression
}
),
Expand Down
Loading