diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/TlsTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/TlsTest.java new file mode 100644 index 00000000000..7189437a645 --- /dev/null +++ b/interop-testing/src/test/java/io/grpc/testing/integration/TlsTest.java @@ -0,0 +1,256 @@ +/* + * Copyright 2015, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.grpc.testing.integration; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.EmptyProtos.Empty; + +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.NegotiationType; +import io.grpc.netty.NettyChannelBuilder; +import io.grpc.netty.NettyServerBuilder; +import io.grpc.testing.TestUtils; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslContext; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.File; +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + + +/** + * Integration tests for GRPC's TLS support. + */ +// TODO: Use @RunWith(Parameterized.class) to run these tests for all TLS providers, probably via +// GrpcSslContexts.configure(SslContextBuilder, SslProvider). +@RunWith(JUnit4.class) +public class TlsTest { + @Before + public void setUp() { + executor = Executors.newSingleThreadScheduledExecutor(); + } + + @After + public void tearDown() { + MoreExecutors.shutdownAndAwaitTermination(executor, 5, TimeUnit.SECONDS); + } + + + /** + * Tests that a client and a server configured using GrpcSslContexts can successfully + * communicate with each other. + */ + // TODO: Fix whatever causes this test to fail, then remove the @Ignore annotation. + @Ignore + @Test + public void basicClientServerIntegrationTest() throws Exception { + int port = TestUtils.pickUnusedPort(); + + // Create & start a server. + File serverCertFile = TestUtils.loadCert("server1.pem"); + File serverPrivateKeyFile = TestUtils.loadCert("server1.key"); + X509Certificate[] serverTrustedCaCerts = { + TestUtils.loadX509Cert("ca.pem") + }; + Server server = serverBuilder(port, serverCertFile, serverPrivateKeyFile, serverTrustedCaCerts) + .addService(TestServiceGrpc.bindService(new TestServiceImpl(executor))) + .build() + .start(); + + try { + // Create a client. + File clientCertFile = TestUtils.loadCert("client.pem"); + File clientPrivateKeyFile = TestUtils.loadCert("client.key"); + X509Certificate[] clientTrustedCaCerts = { + TestUtils.loadX509Cert("ca.pem") + }; + ManagedChannel channel = clientChannel("localhost", port, clientCertFile, + clientPrivateKeyFile, clientTrustedCaCerts); + TestServiceGrpc.TestServiceBlockingStub client = TestServiceGrpc.newBlockingStub(channel); + + // Send an actual request, via the full GRPC & network stack, and check that a proper + // response comes back. + Empty request = Empty.getDefaultInstance(); + client.emptyCall(request); + } finally { + server.shutdown(); + } + } + + + /** + * Tests that a server configured to require client authentication refuses to accept connections + * from a client that has an untrusted certificate. + */ + // TODO: Fix whatever causes this test to fail, then remove the @Ignore annotation. + @Ignore + @Test + public void serverRejectsUntrustedClientCert() throws Exception { + int port = TestUtils.pickUnusedPort(); + + // Create & start a server. It requires client authentication and trusts only the test CA. + File serverCertFile = TestUtils.loadCert("server1.pem"); + File serverPrivateKeyFile = TestUtils.loadCert("server1.key"); + X509Certificate[] serverTrustedCaCerts = { + TestUtils.loadX509Cert("ca.pem") + }; + Server server = serverBuilder(port, serverCertFile, serverPrivateKeyFile, serverTrustedCaCerts) + .addService(TestServiceGrpc.bindService(new TestServiceImpl(executor))) + .build() + .start(); + + try { + // Create a client. Its credentials come from a CA that the server does not trust. The client + // trusts both test CAs, so we can be sure that the handshake failure is due to the server + // rejecting the client's cert, not the client rejecting the server's cert. + File clientCertFile = TestUtils.loadCert("badclient.pem"); + File clientPrivateKeyFile = TestUtils.loadCert("badclient.key"); + X509Certificate[] clientTrustedCaCerts = { + TestUtils.loadX509Cert("ca.pem") + }; + ManagedChannel channel = clientChannel("localhost", port, clientCertFile, + clientPrivateKeyFile, clientTrustedCaCerts); + TestServiceGrpc.TestServiceBlockingStub client = TestServiceGrpc.newBlockingStub(channel); + + // Check that the TLS handshake fails. + Empty request = Empty.getDefaultInstance(); + try { + client.emptyCall(request); + fail("TLS handshake should have failed, but didn't; received RPC response"); + } catch (StatusRuntimeException e) { + // GRPC reports this situation by throwing a StatusRuntimeException that wraps either a + // javax.net.ssl.SSLHandshakeException or a java.nio.channels.ClosedChannelException. + // Thus, reliably detecting the underlying cause is not feasible. + assertEquals(Status.Code.UNAVAILABLE, e.getStatus().getCode()); + } + } finally { + server.shutdown(); + } + } + + + /** + * Tests that a server configured to require client authentication actually does require client + * authentication. + */ + @Test + public void noClientAuthFailure() throws Exception { + int port = TestUtils.pickUnusedPort(); + + // Create & start a server. + File serverCertFile = TestUtils.loadCert("server1.pem"); + File serverPrivateKeyFile = TestUtils.loadCert("server1.key"); + X509Certificate[] serverTrustedCaCerts = { + TestUtils.loadX509Cert("ca.pem") + }; + Server server = serverBuilder(port, serverCertFile, serverPrivateKeyFile, serverTrustedCaCerts) + .addService(TestServiceGrpc.bindService(new TestServiceImpl(executor))) + .build() + .start(); + + try { + // Create a client. It has no credentials. + ManagedChannel channel = NettyChannelBuilder.forAddress("localhost", port) + .overrideAuthority(TestUtils.TEST_SERVER_HOST) + .negotiationType(NegotiationType.TLS) + .build(); + TestServiceGrpc.TestServiceBlockingStub client = TestServiceGrpc.newBlockingStub(channel); + + // Check that the TLS handshake fails. + Empty request = Empty.getDefaultInstance(); + try { + client.emptyCall(request); + fail("TLS handshake should have failed, but didn't; received RPC response"); + } catch (StatusRuntimeException e) { + // GRPC reports this situation by throwing a StatusRuntimeException that wraps either a + // javax.net.ssl.SSLHandshakeException or a java.nio.channels.ClosedChannelException. + // Thus, reliably detecting the underlying cause is not feasible. + assertEquals(Status.Code.UNAVAILABLE, e.getStatus().getCode()); + } + } finally { + server.shutdown(); + } + } + + + private static ServerBuilder serverBuilder(int port, File serverCertChainFile, + File serverPrivateKeyFile, + X509Certificate[] serverTrustedCaCerts) + throws IOException { + SslContext sslContext = GrpcSslContexts.forServer(serverCertChainFile, serverPrivateKeyFile) + .trustManager(serverTrustedCaCerts) + .clientAuth(ClientAuth.REQUIRE) + .build(); + + return NettyServerBuilder.forPort(port) + .sslContext(sslContext); + } + + + private static ManagedChannel clientChannel(String serverHost, int serverPort, + File clientCertChainFile, + File clientPrivateKeyFile, + X509Certificate[] clientTrustedCaCerts) + throws IOException { + SslContext sslContext = GrpcSslContexts.forClient() + .keyManager(clientCertChainFile, clientPrivateKeyFile) + .trustManager(clientTrustedCaCerts) + .build(); + + return NettyChannelBuilder.forAddress(serverHost, serverPort) + .overrideAuthority(TestUtils.TEST_SERVER_HOST) + .negotiationType(NegotiationType.TLS) + .sslContext(sslContext) + .build(); + } + + + private ScheduledExecutorService executor; +} diff --git a/testing/src/main/java/io/grpc/testing/TestUtils.java b/testing/src/main/java/io/grpc/testing/TestUtils.java index f29baec2afd..dfff2ce987e 100644 --- a/testing/src/main/java/io/grpc/testing/TestUtils.java +++ b/testing/src/main/java/io/grpc/testing/TestUtils.java @@ -53,6 +53,7 @@ import java.net.UnknownHostException; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; @@ -191,7 +192,8 @@ public static List preferredTestCiphers() { } /** - * Load a file from the resources folder. + * Saves a file from the classpath resources in src/main/resources/certs as a file on the + * filesystem. * * @param name name of a file in src/main/resources/certs. */ @@ -213,6 +215,23 @@ public static File loadCert(String name) throws IOException { return tmpFile; } + /** + * Loads an X.509 certificate from the classpath resources in src/main/resources/certs. + * + * @param fileName name of a file in src/main/resources/certs. + */ + public static X509Certificate loadX509Cert(String fileName) + throws CertificateException, IOException { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + + InputStream in = TestUtils.class.getResourceAsStream("/certs/" + fileName); + try { + return (X509Certificate) cf.generateCertificate(in); + } finally { + in.close(); + } + } + /** * Creates an SSLSocketFactory which contains {@code certChainFile} as its only root certificate. */ diff --git a/testing/src/main/resources/certs/README b/testing/src/main/resources/certs/README index e6d411ad293..352b5fc3d88 100644 --- a/testing/src/main/resources/certs/README +++ b/testing/src/main/resources/certs/README @@ -10,7 +10,7 @@ $ openssl req -x509 -newkey rsa:1024 -keyout badserver.key -out badserver.pem \ -days 3650 -nodes When prompted for certificate information, everything is default except the -common name which is set to badserver.test.google.com. +common name, which is set to badserver.test.google.com. Valid test credentials: @@ -31,7 +31,7 @@ $ rm client.key.rsa $ openssl req -new -key client.key -out client.csr When prompted for certificate information, everything is default except the -common name which is set to testclient. +common name, which is set to testclient. $ openssl ca -in client.csr -out client.pem -keyfile ca.key -cert ca.pem -verbose -config openssl.cnf -days 3650 -updatedb $ openssl x509 -in client.pem -out client.pem -outform PEM @@ -45,7 +45,7 @@ $ rm server0.key.rsa $ openssl req -new -key server0.key -out server0.csr When prompted for certificate information, everything is default except the -common name which is set to *.test.google.com.au. +common name, which is set to *.test.google.com.au. $ openssl ca -in server0.csr -out server0.pem -keyfile ca.key -cert ca.pem -verbose -config openssl.cnf -days 3650 -updatedb $ openssl x509 -in server0.pem -out server0.pem -outform PEM @@ -59,7 +59,7 @@ $ rm server1.key.rsa $ openssl req -new -key server1.key -out server1.csr -config server1-openssl.cnf When prompted for certificate information, everything is default except the -common name which is set to *.test.google.com. +common name, which is set to *.test.google.com. $ 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 $ openssl x509 -in server1.pem -out server1.pem -outform PEM