-
Notifications
You must be signed in to change notification settings - Fork 22
Load multiple certificates from a single trust root file #115
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
Changes from 4 commits
dfc6fba
67d2130
3dd21e8
7007625
c359917
4d2dc3b
4943d52
7a4dd68
3a4650d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||
|
|
@@ -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) | ||
| certificates.append( | ||
| try NIOSSLCertificate( | ||
| bytes: bytes, | ||
| format: NIOSSLSerializationFormats(serializationFormat) | ||
| ) | ||
| ) | ||
|
|
||
| case .file(let path, let serializationFormat): | ||
| return try NIOSSLCertificate( | ||
| file: path, | ||
| format: NIOSSLSerializationFormats(serializationFormat) | ||
| ) | ||
| switch NIOSSLSerializationFormats(serializationFormat) { | ||
|
||
| 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 { | ||
|
|
@@ -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, | ||
|
|
||
| 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. | ||
|
|
@@ -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 { | ||
|
|
@@ -146,6 +147,49 @@ 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 fileNames = try certificateChain.writeToTemp(fileTag: #function) | ||
| // Check that the files | ||
| let clientCertPath = fileNames[.clientCert]! | ||
| #expect(FileManager.default.fileExists(atPath: clientCertPath)) | ||
| let clientKeyPath = fileNames[.clientKey]! | ||
| #expect(FileManager.default.fileExists(atPath: clientKeyPath)) | ||
| let serverCertPath = fileNames[.serverCert]! | ||
| #expect(FileManager.default.fileExists(atPath: serverCertPath)) | ||
| let serverKeyPath = fileNames[.serverKey]! | ||
| #expect(FileManager.default.fileExists(atPath: serverKeyPath)) | ||
| let trustRootsPath = fileNames[.trustRoots]! | ||
| #expect(FileManager.default.fileExists(atPath: trustRootsPath)) | ||
| // Create configurations | ||
| let clientConfig = self.makeMTLSClientConfig( | ||
| certificate: clientCertPath, | ||
| key: clientKeyPath, | ||
| trustRoots: trustRootsPath, | ||
| serverHostname: certificateChain.serverName | ||
| ) | ||
| let serverConfig = self.makeMTLSServerConfig( | ||
| certificate: serverCertPath, | ||
| key: serverKeyPath, | ||
| trustRoots: trustRootsPath, | ||
| ) | ||
| // 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, | ||
|
|
@@ -471,6 +515,26 @@ struct HTTP2TransportTLSEnabledTests { | |
| } | ||
| } | ||
|
|
||
| @available(gRPCSwiftNIOTransport 2.0, *) | ||
| private func makeMTLSClientConfig( | ||
| certificate: String, | ||
| key: String, | ||
| trustRoots: String, | ||
|
||
| serverHostname: String? | ||
| ) -> ClientConfig { | ||
| var config = self.makeDefaultPlaintextPosixClientConfig() | ||
| config.security = .mTLS( | ||
| certificateChain: [.file(path: certificate, format: .pem)], | ||
| privateKey: .file(path: key, format: .pem) | ||
| ) { | ||
| $0.trustRoots = .certificates([ | ||
| .file(path: trustRoots, 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) | ||
|
|
@@ -558,6 +622,24 @@ struct HTTP2TransportTLSEnabledTests { | |
| } | ||
| } | ||
|
|
||
| @available(gRPCSwiftNIOTransport 2.0, *) | ||
| private func makeMTLSServerConfig( | ||
| certificate: String, | ||
| key: String, | ||
| trustRoots: String | ||
|
||
| ) -> ServerConfig { | ||
| var config = self.makeDefaultPlaintextPosixServerConfig() | ||
| config.security = .mTLS( | ||
| certificateChain: [.file(path: certificate, format: .pem)], | ||
| privateKey: .file(path: key, format: .pem) | ||
| ) { | ||
| $0.trustRoots = .certificates([ | ||
| .file(path: trustRoots, format: .pem) | ||
| ]) | ||
| } | ||
| return .posix(config) | ||
| } | ||
|
|
||
| @available(gRPCSwiftNIOTransport 2.0, *) | ||
| func withClientAndServer( | ||
| clientConfig: ClientConfig, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably use
fromPEMByteshere as well