Skip to content

Commit 95d7bfd

Browse files
matthildejona86
authored andcommitted
Add some initial integration tests for GRPC's TLS support.
Tests run using Jetty ALPN only, not using OpenSSL.
1 parent 509c7e7 commit 95d7bfd

File tree

3 files changed

+280
-5
lines changed

3 files changed

+280
-5
lines changed
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
/*
2+
* Copyright 2015, Google Inc. All rights reserved.
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
*
15+
* * Neither the name of Google Inc. nor the names of its
16+
* contributors may be used to endorse or promote products derived from
17+
* this software without specific prior written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
*/
31+
32+
package io.grpc.testing.integration;
33+
34+
import static org.junit.Assert.assertEquals;
35+
import static org.junit.Assert.fail;
36+
37+
import com.google.common.util.concurrent.MoreExecutors;
38+
import com.google.protobuf.EmptyProtos.Empty;
39+
40+
import io.grpc.ManagedChannel;
41+
import io.grpc.Server;
42+
import io.grpc.ServerBuilder;
43+
import io.grpc.Status;
44+
import io.grpc.StatusRuntimeException;
45+
import io.grpc.netty.GrpcSslContexts;
46+
import io.grpc.netty.NegotiationType;
47+
import io.grpc.netty.NettyChannelBuilder;
48+
import io.grpc.netty.NettyServerBuilder;
49+
import io.grpc.testing.TestUtils;
50+
import io.netty.handler.ssl.ClientAuth;
51+
import io.netty.handler.ssl.SslContext;
52+
53+
import org.junit.After;
54+
import org.junit.Before;
55+
import org.junit.Ignore;
56+
import org.junit.Test;
57+
import org.junit.runner.RunWith;
58+
import org.junit.runners.JUnit4;
59+
60+
import java.io.File;
61+
import java.io.IOException;
62+
import java.security.cert.X509Certificate;
63+
import java.util.concurrent.Executors;
64+
import java.util.concurrent.ScheduledExecutorService;
65+
import java.util.concurrent.TimeUnit;
66+
67+
68+
/**
69+
* Integration tests for GRPC's TLS support.
70+
*/
71+
// TODO: Use @RunWith(Parameterized.class) to run these tests for all TLS providers, probably via
72+
// GrpcSslContexts.configure(SslContextBuilder, SslProvider).
73+
@RunWith(JUnit4.class)
74+
public class TlsTest {
75+
@Before
76+
public void setUp() {
77+
executor = Executors.newSingleThreadScheduledExecutor();
78+
}
79+
80+
@After
81+
public void tearDown() {
82+
MoreExecutors.shutdownAndAwaitTermination(executor, 5, TimeUnit.SECONDS);
83+
}
84+
85+
86+
/**
87+
* Tests that a client and a server configured using GrpcSslContexts can successfully
88+
* communicate with each other.
89+
*/
90+
// TODO: Fix whatever causes this test to fail, then remove the @Ignore annotation.
91+
@Ignore
92+
@Test
93+
public void basicClientServerIntegrationTest() throws Exception {
94+
int port = TestUtils.pickUnusedPort();
95+
96+
// Create & start a server.
97+
File serverCertFile = TestUtils.loadCert("server1.pem");
98+
File serverPrivateKeyFile = TestUtils.loadCert("server1.key");
99+
X509Certificate[] serverTrustedCaCerts = {
100+
TestUtils.loadX509Cert("ca.pem")
101+
};
102+
Server server = serverBuilder(port, serverCertFile, serverPrivateKeyFile, serverTrustedCaCerts)
103+
.addService(TestServiceGrpc.bindService(new TestServiceImpl(executor)))
104+
.build()
105+
.start();
106+
107+
try {
108+
// Create a client.
109+
File clientCertFile = TestUtils.loadCert("client.pem");
110+
File clientPrivateKeyFile = TestUtils.loadCert("client.key");
111+
X509Certificate[] clientTrustedCaCerts = {
112+
TestUtils.loadX509Cert("ca.pem")
113+
};
114+
ManagedChannel channel = clientChannel("localhost", port, clientCertFile,
115+
clientPrivateKeyFile, clientTrustedCaCerts);
116+
TestServiceGrpc.TestServiceBlockingStub client = TestServiceGrpc.newBlockingStub(channel);
117+
118+
// Send an actual request, via the full GRPC & network stack, and check that a proper
119+
// response comes back.
120+
Empty request = Empty.getDefaultInstance();
121+
client.emptyCall(request);
122+
} finally {
123+
server.shutdown();
124+
}
125+
}
126+
127+
128+
/**
129+
* Tests that a server configured to require client authentication refuses to accept connections
130+
* from a client that has an untrusted certificate.
131+
*/
132+
// TODO: Fix whatever causes this test to fail, then remove the @Ignore annotation.
133+
@Ignore
134+
@Test
135+
public void serverRejectsUntrustedClientCert() throws Exception {
136+
int port = TestUtils.pickUnusedPort();
137+
138+
// Create & start a server. It requires client authentication and trusts only the test CA.
139+
File serverCertFile = TestUtils.loadCert("server1.pem");
140+
File serverPrivateKeyFile = TestUtils.loadCert("server1.key");
141+
X509Certificate[] serverTrustedCaCerts = {
142+
TestUtils.loadX509Cert("ca.pem")
143+
};
144+
Server server = serverBuilder(port, serverCertFile, serverPrivateKeyFile, serverTrustedCaCerts)
145+
.addService(TestServiceGrpc.bindService(new TestServiceImpl(executor)))
146+
.build()
147+
.start();
148+
149+
try {
150+
// Create a client. Its credentials come from a CA that the server does not trust. The client
151+
// trusts both test CAs, so we can be sure that the handshake failure is due to the server
152+
// rejecting the client's cert, not the client rejecting the server's cert.
153+
File clientCertFile = TestUtils.loadCert("badclient.pem");
154+
File clientPrivateKeyFile = TestUtils.loadCert("badclient.key");
155+
X509Certificate[] clientTrustedCaCerts = {
156+
TestUtils.loadX509Cert("ca.pem")
157+
};
158+
ManagedChannel channel = clientChannel("localhost", port, clientCertFile,
159+
clientPrivateKeyFile, clientTrustedCaCerts);
160+
TestServiceGrpc.TestServiceBlockingStub client = TestServiceGrpc.newBlockingStub(channel);
161+
162+
// Check that the TLS handshake fails.
163+
Empty request = Empty.getDefaultInstance();
164+
try {
165+
client.emptyCall(request);
166+
fail("TLS handshake should have failed, but didn't; received RPC response");
167+
} catch (StatusRuntimeException e) {
168+
// GRPC reports this situation by throwing a StatusRuntimeException that wraps either a
169+
// javax.net.ssl.SSLHandshakeException or a java.nio.channels.ClosedChannelException.
170+
// Thus, reliably detecting the underlying cause is not feasible.
171+
assertEquals(Status.Code.UNAVAILABLE, e.getStatus().getCode());
172+
}
173+
} finally {
174+
server.shutdown();
175+
}
176+
}
177+
178+
179+
/**
180+
* Tests that a server configured to require client authentication actually does require client
181+
* authentication.
182+
*/
183+
@Test
184+
public void noClientAuthFailure() throws Exception {
185+
int port = TestUtils.pickUnusedPort();
186+
187+
// Create & start a server.
188+
File serverCertFile = TestUtils.loadCert("server1.pem");
189+
File serverPrivateKeyFile = TestUtils.loadCert("server1.key");
190+
X509Certificate[] serverTrustedCaCerts = {
191+
TestUtils.loadX509Cert("ca.pem")
192+
};
193+
Server server = serverBuilder(port, serverCertFile, serverPrivateKeyFile, serverTrustedCaCerts)
194+
.addService(TestServiceGrpc.bindService(new TestServiceImpl(executor)))
195+
.build()
196+
.start();
197+
198+
try {
199+
// Create a client. It has no credentials.
200+
ManagedChannel channel = NettyChannelBuilder.forAddress("localhost", port)
201+
.overrideAuthority(TestUtils.TEST_SERVER_HOST)
202+
.negotiationType(NegotiationType.TLS)
203+
.build();
204+
TestServiceGrpc.TestServiceBlockingStub client = TestServiceGrpc.newBlockingStub(channel);
205+
206+
// Check that the TLS handshake fails.
207+
Empty request = Empty.getDefaultInstance();
208+
try {
209+
client.emptyCall(request);
210+
fail("TLS handshake should have failed, but didn't; received RPC response");
211+
} catch (StatusRuntimeException e) {
212+
// GRPC reports this situation by throwing a StatusRuntimeException that wraps either a
213+
// javax.net.ssl.SSLHandshakeException or a java.nio.channels.ClosedChannelException.
214+
// Thus, reliably detecting the underlying cause is not feasible.
215+
assertEquals(Status.Code.UNAVAILABLE, e.getStatus().getCode());
216+
}
217+
} finally {
218+
server.shutdown();
219+
}
220+
}
221+
222+
223+
private static ServerBuilder<?> serverBuilder(int port, File serverCertChainFile,
224+
File serverPrivateKeyFile,
225+
X509Certificate[] serverTrustedCaCerts)
226+
throws IOException {
227+
SslContext sslContext = GrpcSslContexts.forServer(serverCertChainFile, serverPrivateKeyFile)
228+
.trustManager(serverTrustedCaCerts)
229+
.clientAuth(ClientAuth.REQUIRE)
230+
.build();
231+
232+
return NettyServerBuilder.forPort(port)
233+
.sslContext(sslContext);
234+
}
235+
236+
237+
private static ManagedChannel clientChannel(String serverHost, int serverPort,
238+
File clientCertChainFile,
239+
File clientPrivateKeyFile,
240+
X509Certificate[] clientTrustedCaCerts)
241+
throws IOException {
242+
SslContext sslContext = GrpcSslContexts.forClient()
243+
.keyManager(clientCertChainFile, clientPrivateKeyFile)
244+
.trustManager(clientTrustedCaCerts)
245+
.build();
246+
247+
return NettyChannelBuilder.forAddress(serverHost, serverPort)
248+
.overrideAuthority(TestUtils.TEST_SERVER_HOST)
249+
.negotiationType(NegotiationType.TLS)
250+
.sslContext(sslContext)
251+
.build();
252+
}
253+
254+
255+
private ScheduledExecutorService executor;
256+
}

testing/src/main/java/io/grpc/testing/TestUtils.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import java.net.UnknownHostException;
5454
import java.security.KeyStore;
5555
import java.security.NoSuchAlgorithmException;
56+
import java.security.cert.CertificateException;
5657
import java.security.cert.CertificateFactory;
5758
import java.security.cert.X509Certificate;
5859
import java.util.ArrayList;
@@ -191,7 +192,8 @@ public static List<String> preferredTestCiphers() {
191192
}
192193

193194
/**
194-
* Load a file from the resources folder.
195+
* Saves a file from the classpath resources in src/main/resources/certs as a file on the
196+
* filesystem.
195197
*
196198
* @param name name of a file in src/main/resources/certs.
197199
*/
@@ -213,6 +215,23 @@ public static File loadCert(String name) throws IOException {
213215
return tmpFile;
214216
}
215217

218+
/**
219+
* Loads an X.509 certificate from the classpath resources in src/main/resources/certs.
220+
*
221+
* @param fileName name of a file in src/main/resources/certs.
222+
*/
223+
public static X509Certificate loadX509Cert(String fileName)
224+
throws CertificateException, IOException {
225+
CertificateFactory cf = CertificateFactory.getInstance("X.509");
226+
227+
InputStream in = TestUtils.class.getResourceAsStream("/certs/" + fileName);
228+
try {
229+
return (X509Certificate) cf.generateCertificate(in);
230+
} finally {
231+
in.close();
232+
}
233+
}
234+
216235
/**
217236
* Creates an SSLSocketFactory which contains {@code certChainFile} as its only root certificate.
218237
*/

testing/src/main/resources/certs/README

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ $ openssl req -x509 -newkey rsa:1024 -keyout badserver.key -out badserver.pem \
1010
-days 3650 -nodes
1111

1212
When prompted for certificate information, everything is default except the
13-
common name which is set to badserver.test.google.com.
13+
common name, which is set to badserver.test.google.com.
1414

1515

1616
Valid test credentials:
@@ -31,7 +31,7 @@ $ rm client.key.rsa
3131
$ openssl req -new -key client.key -out client.csr
3232

3333
When prompted for certificate information, everything is default except the
34-
common name which is set to testclient.
34+
common name, which is set to testclient.
3535

3636
$ openssl ca -in client.csr -out client.pem -keyfile ca.key -cert ca.pem -verbose -config openssl.cnf -days 3650 -updatedb
3737
$ openssl x509 -in client.pem -out client.pem -outform PEM
@@ -45,7 +45,7 @@ $ rm server0.key.rsa
4545
$ openssl req -new -key server0.key -out server0.csr
4646

4747
When prompted for certificate information, everything is default except the
48-
common name which is set to *.test.google.com.au.
48+
common name, which is set to *.test.google.com.au.
4949

5050
$ openssl ca -in server0.csr -out server0.pem -keyfile ca.key -cert ca.pem -verbose -config openssl.cnf -days 3650 -updatedb
5151
$ openssl x509 -in server0.pem -out server0.pem -outform PEM
@@ -59,7 +59,7 @@ $ rm server1.key.rsa
5959
$ openssl req -new -key server1.key -out server1.csr -config server1-openssl.cnf
6060

6161
When prompted for certificate information, everything is default except the
62-
common name which is set to *.test.google.com.
62+
common name, which is set to *.test.google.com.
6363

6464
$ openssl ca -in server1.csr -out server1.pem -keyfile ca.key -cert ca.pem -verbose -config server1-openssl.cnf -days 3650 -extensions v3_req -updatedb
6565
$ openssl x509 -in server1.pem -out server1.pem -outform PEM

0 commit comments

Comments
 (0)