Skip to content

Commit 5b2748a

Browse files
committed
Add E2E TLS tests
1 parent 21dbbb1 commit 5b2748a

File tree

1 file changed

+343
-0
lines changed

1 file changed

+343
-0
lines changed
Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
/*
2+
* Copyright 2024, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import GRPCNIOTransportHTTP2Posix
18+
import Testing
19+
import X509
20+
import Crypto
21+
import SwiftASN1
22+
import Foundation
23+
import NIOSSL
24+
25+
@Suite("HTTP/2 transport E2E tests with TLS enabled")
26+
struct HTTP2TransportTLSEnabledTests {
27+
// - MARK: Test Utilities
28+
29+
// A combination of client and server transport kinds.
30+
struct Transport: Sendable {
31+
var server: ServerKind
32+
var client: ClientKind
33+
34+
enum ClientKind: Sendable {
35+
case posix(HTTP2ClientTransport.Posix.Config.TransportSecurity)
36+
}
37+
38+
enum ServerKind: Sendable {
39+
case posix(HTTP2ServerTransport.Posix.Config.TransportSecurity)
40+
}
41+
}
42+
43+
func executeUnaryRPCForEachTransportPair(
44+
transportProvider: (TestSecurity) -> [Transport]
45+
) async throws {
46+
let security = try TestSecurity()
47+
for pair in transportProvider(security) {
48+
try await withThrowingTaskGroup(of: Void.self) { group in
49+
let (server, address) = try await self.runServer(
50+
in: &group,
51+
kind: pair.server
52+
)
53+
54+
let target: any ResolvableTarget
55+
if let ipv4 = address.ipv4 {
56+
target = .ipv4(host: ipv4.host, port: ipv4.port)
57+
} else if let ipv6 = address.ipv6 {
58+
target = .ipv6(host: ipv6.host, port: ipv6.port)
59+
} else if let uds = address.unixDomainSocket {
60+
target = .unixDomainSocket(path: uds.path)
61+
} else {
62+
Issue.record("Unexpected address to connect to")
63+
return
64+
}
65+
66+
let client = try self.makeClient(
67+
kind: pair.client,
68+
target: target
69+
)
70+
71+
group.addTask {
72+
try await client.run()
73+
}
74+
75+
let control = ControlClient(wrapping: client)
76+
try await self.executeUnaryRPC(control: control, pair: pair)
77+
78+
server.beginGracefulShutdown()
79+
client.beginGracefulShutdown()
80+
}
81+
}
82+
}
83+
84+
private func runServer(
85+
in group: inout ThrowingTaskGroup<Void, any Error>,
86+
kind: Transport.ServerKind
87+
) async throws -> (GRPCServer, GRPCNIOTransportHTTP2Posix.SocketAddress) {
88+
let services = [ControlService()]
89+
90+
switch kind {
91+
case .posix(let transportSecurity):
92+
let server = GRPCServer(
93+
transport: .http2NIOPosix(
94+
address: .ipv4(host: "127.0.0.1", port: 0),
95+
config: .defaults(transportSecurity: transportSecurity)
96+
),
97+
services: services
98+
)
99+
100+
group.addTask {
101+
try await server.serve()
102+
}
103+
104+
let address = try await server.listeningAddress!
105+
return (server, address)
106+
}
107+
}
108+
109+
private func makeClient(
110+
kind: Transport.ClientKind,
111+
target: any ResolvableTarget
112+
) throws -> GRPCClient {
113+
let transport: any ClientTransport
114+
115+
switch kind {
116+
case .posix(let transportSecurity):
117+
var serviceConfig = ServiceConfig()
118+
serviceConfig.loadBalancingConfig = [.roundRobin]
119+
transport = try HTTP2ClientTransport.Posix(
120+
target: target,
121+
config: .defaults(transportSecurity: transportSecurity) { config in
122+
config.backoff.initial = .milliseconds(100)
123+
config.backoff.multiplier = 1
124+
config.backoff.jitter = 0
125+
},
126+
serviceConfig: serviceConfig
127+
)
128+
}
129+
130+
return GRPCClient(transport: transport)
131+
}
132+
133+
private func executeUnaryRPC(control: ControlClient, pair: Transport) async throws {
134+
let input = ControlInput.with {
135+
$0.echoMetadataInHeaders = true
136+
$0.echoMetadataInTrailers = true
137+
$0.numberOfMessages = 1
138+
$0.payloadParameters = .with {
139+
$0.content = 0
140+
$0.size = 1024
141+
}
142+
}
143+
144+
let metadata: Metadata = ["test-key": "test-value"]
145+
let request = ClientRequest(message: input, metadata: metadata)
146+
147+
try await control.unary(request: request) { response in
148+
let message = try response.message
149+
#expect(message.payload == Data(repeating: 0, count: 1024))
150+
151+
let initial = response.metadata
152+
#expect(Array(initial["echo-test-key"]) == ["test-value"])
153+
154+
let trailing = response.trailingMetadata
155+
#expect(Array(trailing["echo-test-key"]) == ["test-value"])
156+
}
157+
}
158+
159+
// - MARK: Tests
160+
161+
@Test("When using defaults, server does not perform client verification")
162+
func testRPC_Defaults_OK() async throws {
163+
try await self.executeUnaryRPCForEachTransportPair { security in
164+
[
165+
HTTP2TransportTLSEnabledTests.Transport(
166+
server: .posix(.tls(.defaults(
167+
certificateChain: [.bytes(security.server.certificate, format: .der)],
168+
privateKey: .bytes(security.server.key, format: .der)
169+
))),
170+
client: .posix(.tls(.defaults {
171+
$0.trustRoots = .certificates([.bytes(security.server.certificate, format: .der)])
172+
$0.serverHostname = "localhost"
173+
}))
174+
)
175+
]
176+
}
177+
}
178+
179+
@Test("When using mTLS defaults, both client and server verify each others' certificates")
180+
func testRPC_mTLS_OK() async throws {
181+
try await self.executeUnaryRPCForEachTransportPair { security in
182+
[
183+
HTTP2TransportTLSEnabledTests.Transport(
184+
server: .posix(.tls(.mTLS(
185+
certificateChain: [.bytes(security.server.certificate, format: .der)],
186+
privateKey: .bytes(security.server.key, format: .der)) {
187+
$0.trustRoots = .certificates([.bytes(security.client.certificate, format: .der)])
188+
})),
189+
client: .posix(.tls(.mTLS(
190+
certificateChain: [.bytes(security.client.certificate, format: .der)],
191+
privateKey: .bytes(security.client.key, format: .der)) {
192+
$0.trustRoots = .certificates([.bytes(security.server.certificate, format: .der)])
193+
$0.serverHostname = "localhost"
194+
}))
195+
)
196+
]
197+
}
198+
}
199+
200+
@Test("Error is surfaced when client fails server verification")
201+
// Verification should fail because the custom hostname is missing on the client.
202+
func testClientFailsServerValidation() async throws {
203+
await #expect(performing: {
204+
try await self.executeUnaryRPCForEachTransportPair { security in
205+
[
206+
HTTP2TransportTLSEnabledTests.Transport(
207+
server: .posix(.tls(.mTLS(
208+
certificateChain: [.bytes(security.server.certificate, format: .der)],
209+
privateKey: .bytes(security.server.key, format: .der)) {
210+
$0.trustRoots = .certificates([.bytes(security.client.certificate, format: .der)])
211+
})),
212+
client: .posix(.tls(.mTLS(
213+
certificateChain: [.bytes(security.client.certificate, format: .der)],
214+
privateKey: .bytes(security.client.key, format: .der)) {
215+
$0.trustRoots = .certificates([.bytes(security.server.certificate, format: .der)])
216+
}))
217+
)
218+
]
219+
}
220+
}, throws: { error in
221+
guard let rootError = error as? RPCError else {
222+
Issue.record("Should be an RPC error")
223+
return false
224+
}
225+
#expect(rootError.code == .unavailable)
226+
#expect(rootError.message == "Channel isn't ready. The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface.")
227+
228+
guard
229+
let sslError = rootError.cause as? NIOSSLExtraError,
230+
case .failedToValidateHostname = sslError
231+
else {
232+
Issue.record("Should be a NIOSSLExtraError.failedToValidateHostname error, but was: \(String(describing: rootError.cause))")
233+
return false
234+
}
235+
236+
return true
237+
})
238+
}
239+
240+
@Test("Error is surfaced when server fails client verification")
241+
// Verification should fail because the server does not have trust roots containing the client cert.
242+
func testServerFailsClientValidation() async throws {
243+
await #expect(performing: {
244+
try await self.executeUnaryRPCForEachTransportPair { security in
245+
[
246+
HTTP2TransportTLSEnabledTests.Transport(
247+
server: .posix(.tls(.mTLS(
248+
certificateChain: [.bytes(security.server.certificate, format: .der)],
249+
privateKey: .bytes(security.server.key, format: .der)
250+
))),
251+
client: .posix(.tls(.mTLS(
252+
certificateChain: [.bytes(security.client.certificate, format: .der)],
253+
privateKey: .bytes(security.client.key, format: .der)) {
254+
$0.trustRoots = .certificates([.bytes(security.server.certificate, format: .der)])
255+
$0.serverHostname = "localhost"
256+
}))
257+
)
258+
]
259+
}
260+
}, throws: { error in
261+
guard let rootError = error as? RPCError else {
262+
Issue.record("Should be an RPC error")
263+
return false
264+
}
265+
#expect(rootError.code == .unavailable)
266+
#expect(rootError.message == "Channel isn't ready. The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface.")
267+
268+
guard
269+
let sslError = rootError.cause as? NIOSSL.BoringSSLError,
270+
case .sslError = sslError
271+
else {
272+
Issue.record("Should be a NIOSSL.sslError error, but was: \(String(describing: rootError.cause))")
273+
return false
274+
}
275+
276+
return true
277+
})
278+
}
279+
}
280+
281+
struct TestSecurity {
282+
struct Server {
283+
let certificate: [UInt8]
284+
let key: [UInt8]
285+
}
286+
287+
struct Client {
288+
let certificate: [UInt8]
289+
let key: [UInt8]
290+
}
291+
292+
let server: Server
293+
let client: Client
294+
295+
init() throws {
296+
let server = try Self.createSelfSignedDERCertificateAndPrivateKey(name: "Server Certificate")
297+
let client = try Self.createSelfSignedDERCertificateAndPrivateKey(name: "Client Certificate")
298+
299+
self.server = Server(certificate: server.cert, key: server.key)
300+
self.client = Client(certificate: client.cert, key: client.key)
301+
}
302+
303+
private static func createSelfSignedDERCertificateAndPrivateKey(
304+
name: String
305+
) throws -> (cert: [UInt8], key: [UInt8]) {
306+
let swiftCryptoKey = P256.Signing.PrivateKey()
307+
let key = Certificate.PrivateKey(swiftCryptoKey)
308+
let subjectName = try DistinguishedName { CommonName(name) }
309+
let issuerName = subjectName
310+
let now = Date()
311+
let extensions = try Certificate.Extensions {
312+
Critical(
313+
BasicConstraints.isCertificateAuthority(maxPathLength: nil)
314+
)
315+
Critical(
316+
KeyUsage(digitalSignature: true, keyCertSign: true)
317+
)
318+
Critical(
319+
try ExtendedKeyUsage([.serverAuth, .clientAuth])
320+
)
321+
SubjectAlternativeNames([.dnsName("localhost")])
322+
}
323+
let certificate = try Certificate(
324+
version: .v3,
325+
serialNumber: Certificate.SerialNumber(),
326+
publicKey: key.publicKey,
327+
notValidBefore: now.addingTimeInterval(-60 * 60),
328+
notValidAfter: now.addingTimeInterval(60 * 60 * 24 * 365),
329+
issuer: issuerName,
330+
subject: subjectName,
331+
signatureAlgorithm: .ecdsaWithSHA256,
332+
extensions: extensions,
333+
issuerPrivateKey: key
334+
)
335+
336+
var serializer = DER.Serializer()
337+
try serializer.serialize(certificate)
338+
339+
let certBytes = serializer.serializedBytes
340+
let keyBytes = try key.serializeAsPEM().derBytes
341+
return (certBytes, keyBytes)
342+
}
343+
}

0 commit comments

Comments
 (0)