Skip to content
Closed
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
256 changes: 256 additions & 0 deletions interop-testing/src/test/java/io/grpc/testing/integration/TlsTest.java
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just filed #1330 for what I expect to be the same problem as here.

// 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;
}
21 changes: 20 additions & 1 deletion testing/src/main/java/io/grpc/testing/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -191,7 +192,8 @@ public static List<String> 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.
*/
Expand All @@ -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.
*/
Expand Down
8 changes: 4 additions & 4 deletions testing/src/main/resources/certs/README
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down