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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions Sources/GRPCNIOTransportHTTP2Posix/NIOSSL+GRPC.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024, gRPC Authors All rights reserved.
* Copyright 2025, 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.
Expand Down Expand Up @@ -141,19 +141,24 @@ extension NIOSSLTrustRoots {
fileprivate init(_ trustRoots: TLSConfig.TrustRootsSource) throws {
switch trustRoots.wrapped {
case .certificates(let certificateSources):
let certificates = try certificateSources.map { source in
var certificates: [NIOSSLCertificate] = []
for source in certificateSources {
switch source.wrapped {
case .bytes(let bytes, let serializationFormat):
return try NIOSSLCertificate(
bytes: bytes,
format: NIOSSLSerializationFormats(serializationFormat)
)
switch serializationFormat.wrapped {
case .pem:
certificates.append(contentsOf: try NIOSSLCertificate.fromPEMBytes(bytes))
case .der:
certificates.append(try NIOSSLCertificate(bytes: bytes, format: .der))
}

case .file(let path, let serializationFormat):
return try NIOSSLCertificate(
file: path,
format: NIOSSLSerializationFormats(serializationFormat)
)
switch serializationFormat.wrapped {
case .pem:
certificates.append(contentsOf: try NIOSSLCertificate.fromPEMFile(path))
case .der:
certificates.append(try NIOSSLCertificate(file: path, format: .der))
}

case .transportSpecific(let specific):
guard let source = specific.wrapped as? NIOSSLCertificateSource else {
Expand All @@ -162,14 +167,14 @@ extension NIOSSLTrustRoots {

switch source {
case .certificate(let certificate):
return certificate
certificates.append(certificate)

case .file(let path):
switch path.split(separator: ".").last {
case "pem":
return try NIOSSLCertificate(file: path, format: .pem)
certificates.append(contentsOf: try NIOSSLCertificate.fromPEMFile(path))
case "der":
return try NIOSSLCertificate(file: path, format: .der)
certificates.append(try NIOSSLCertificate(file: path, format: .der))
default:
throw RPCError(
code: .invalidArgument,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024, gRPC Authors All rights reserved.
* Copyright 2025, 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.
Expand Down Expand Up @@ -69,7 +69,8 @@ struct HTTP2TransportTLSEnabledTests {
func intercept<Input, Output>(
request: GRPCCore.StreamingServerRequest<Input>,
context: GRPCCore.ServerContext,
next: @Sendable (GRPCCore.StreamingServerRequest<Input>, GRPCCore.ServerContext) async throws
next:
@Sendable (GRPCCore.StreamingServerRequest<Input>, GRPCCore.ServerContext) async throws
-> GRPCCore.StreamingServerResponse<Output>
) async throws -> GRPCCore.StreamingServerResponse<Output>
where Input: Sendable, Output: Sendable {
Expand Down Expand Up @@ -146,6 +147,44 @@ struct HTTP2TransportTLSEnabledTests {
}
}

@Test(
"When using mTLS with PEM files, both client and server verify each others' certificates"
)
@available(gRPCSwiftNIOTransport 2.0, *)
func testRPC_mTLS_posixFileBasedCertificates_OK() async throws {
// Create a new certificate chain that has 4 certificate/key pairs: root, intermediate, client, server
let certificateChain = try CertificateChain()
// Tag our certificate files with the function name
let filePaths = try certificateChain.writeToTemp()
// Check that the files
#expect(FileManager.default.fileExists(atPath: filePaths.clientCert))
#expect(FileManager.default.fileExists(atPath: filePaths.clientKey))
#expect(FileManager.default.fileExists(atPath: filePaths.serverCert))
#expect(FileManager.default.fileExists(atPath: filePaths.serverKey))
#expect(FileManager.default.fileExists(atPath: filePaths.trustRoots))
// Create configurations
let clientConfig = self.makeMTLSClientConfig(
certificatePath: filePaths.clientCert,
keyPath: filePaths.clientKey,
trustRootsPath: filePaths.trustRoots,
serverHostname: CertificateChain.serverName
)
let serverConfig = self.makeMTLSServerConfig(
certificatePath: filePaths.serverCert,
keyPath: filePaths.serverKey,
trustRootsPath: filePaths.trustRoots
)
// Run the test
try await self.withClientAndServer(
clientConfig: clientConfig,
serverConfig: serverConfig
) { control in
await #expect(throws: Never.self) {
try await self.executeUnaryRPC(control: control)
}
}
}

@Test(
"Error is surfaced when client fails server verification",
arguments: TransportKind.clientsWithTLS,
Expand Down Expand Up @@ -471,6 +510,26 @@ struct HTTP2TransportTLSEnabledTests {
}
}

@available(gRPCSwiftNIOTransport 2.0, *)
private func makeMTLSClientConfig(
certificatePath: String,
keyPath: String,
trustRootsPath: String,
serverHostname: String?
) -> ClientConfig {
var config = self.makeDefaultPlaintextPosixClientConfig()
config.security = .mTLS(
certificateChain: [.file(path: certificatePath, format: .pem)],
privateKey: .file(path: keyPath, format: .pem)
) {
$0.trustRoots = .certificates([
.file(path: trustRootsPath, format: .pem)
])
}
config.transport.http2.authority = serverHostname
return .posix(config)
}

@available(gRPCSwiftNIOTransport 2.0, *)
private func makeDefaultPlaintextPosixServerConfig() -> ServerConfig.Posix {
ServerConfig.Posix(security: .plaintext, transport: .defaults)
Expand Down Expand Up @@ -558,6 +617,24 @@ struct HTTP2TransportTLSEnabledTests {
}
}

@available(gRPCSwiftNIOTransport 2.0, *)
private func makeMTLSServerConfig(
certificatePath: String,
keyPath: String,
trustRootsPath: String
) -> ServerConfig {
var config = self.makeDefaultPlaintextPosixServerConfig()
config.security = .mTLS(
certificateChain: [.file(path: certificatePath, format: .pem)],
privateKey: .file(path: keyPath, format: .pem)
) {
$0.trustRoots = .certificates([
.file(path: trustRootsPath, format: .pem)
])
}
return .posix(config)
}

@available(gRPCSwiftNIOTransport 2.0, *)
func withClientAndServer(
clientConfig: ClientConfig,
Expand Down
Loading