/*
* 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.base.Throwables;
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.OpenSsl;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import java.io.File;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import org.junit.After;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
/**
* Integration tests for GRPC's TLS support.
*/
@RunWith(Parameterized.class)
public class TlsTest {
/**
* Iterable of various configurations to use for tests.
*/
@Parameters(name = "{0}")
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][] {
{SslProvider.JDK}, {SslProvider.OPENSSL},
});
}
@Parameter(value = 0)
public SslProvider sslProvider;
private ScheduledExecutorService executor;
private Server server;
private ManagedChannel channel;
private SslContextBuilder clientContextBuilder;
@Before
public void setUp() throws NoSuchAlgorithmException {
executor = Executors.newSingleThreadScheduledExecutor();
if (sslProvider == SslProvider.OPENSSL) {
Assume.assumeTrue(OpenSsl.isAvailable());
}
if (sslProvider == SslProvider.JDK) {
Assume.assumeTrue(Arrays.asList(
SSLContext.getDefault().getSupportedSSLParameters().getCipherSuites())
.contains("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"));
}
clientContextBuilder = GrpcSslContexts.configure(SslContextBuilder.forClient(), sslProvider);
}
@After
public void tearDown() {
if (server != null) {
server.shutdown();
}
if (channel != null) {
channel.shutdown();
}
MoreExecutors.shutdownAndAwaitTermination(executor, 5, TimeUnit.SECONDS);
}
/**
* Tests that a client and a server configured using GrpcSslContexts can successfully
* communicate with each other.
*/
@Test
public void basicClientServerIntegrationTest() throws Exception {
// Create & start a server.
File serverCertFile = TestUtils.loadCert("server1.pem");
File serverPrivateKeyFile = TestUtils.loadCert("server1.key");
X509Certificate[] serverTrustedCaCerts = {
TestUtils.loadX509Cert("ca.pem")
};
server = serverBuilder(0, serverCertFile, serverPrivateKeyFile, serverTrustedCaCerts)
.addService(new TestServiceImpl(executor))
.build()
.start();
// Create a client.
File clientCertChainFile = TestUtils.loadCert("client.pem");
File clientPrivateKeyFile = TestUtils.loadCert("client.key");
X509Certificate[] clientTrustedCaCerts = {
TestUtils.loadX509Cert("ca.pem")
};
channel = clientChannel(server.getPort(), clientContextBuilder
.keyManager(clientCertChainFile, clientPrivateKeyFile)
.trustManager(clientTrustedCaCerts)
.build());
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);
}
/**
* Tests that a server configured to require client authentication refuses to accept connections
* from a client that has an untrusted certificate.
*/
@Test
public void serverRejectsUntrustedClientCert() throws Exception {
// 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 = serverBuilder(0, serverCertFile, serverPrivateKeyFile, serverTrustedCaCerts)
.addService(new TestServiceImpl(executor))
.build()
.start();
// 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 clientCertChainFile = TestUtils.loadCert("badclient.pem");
File clientPrivateKeyFile = TestUtils.loadCert("badclient.key");
X509Certificate[] clientTrustedCaCerts = {
TestUtils.loadX509Cert("ca.pem")
};
channel = clientChannel(server.getPort(), clientContextBuilder
.keyManager(clientCertChainFile, clientPrivateKeyFile)
.trustManager(clientTrustedCaCerts)
.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(
Throwables.getStackTraceAsString(e),
Status.Code.UNAVAILABLE, e.getStatus().getCode());
}
}
/**
* Tests that a server configured to require client authentication actually does require client
* authentication.
*/
@Test
public void noClientAuthFailure() throws Exception {
// Create & start a server.
File serverCertFile = TestUtils.loadCert("server1.pem");
File serverPrivateKeyFile = TestUtils.loadCert("server1.key");
X509Certificate[] serverTrustedCaCerts = {
TestUtils.loadX509Cert("ca.pem")
};
server = serverBuilder(0, serverCertFile, serverPrivateKeyFile, serverTrustedCaCerts)
.addService(new TestServiceImpl(executor))
.build()
.start();
// Create a client. It has no credentials.
X509Certificate[] clientTrustedCaCerts = {
TestUtils.loadX509Cert("ca.pem")
};
channel = clientChannel(server.getPort(), clientContextBuilder
.trustManager(clientTrustedCaCerts)
.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(
Throwables.getStackTraceAsString(e),
Status.Code.UNAVAILABLE, e.getStatus().getCode());
}
}
/**
* Tests that a client configured using GrpcSslContexts refuses to talk to a server that has an
* an untrusted certificate.
*/
@Test
public void clientRejectsUntrustedServerCert() throws Exception {
// Create & start a server.
File serverCertFile = TestUtils.loadCert("badserver.pem");
File serverPrivateKeyFile = TestUtils.loadCert("badserver.key");
X509Certificate[] serverTrustedCaCerts = {
TestUtils.loadX509Cert("ca.pem")
};
server = serverBuilder(0, serverCertFile, serverPrivateKeyFile, serverTrustedCaCerts)
.addService(new TestServiceImpl(executor))
.build()
.start();
// Create a client.
File clientCertChainFile = TestUtils.loadCert("client.pem");
File clientPrivateKeyFile = TestUtils.loadCert("client.key");
X509Certificate[] clientTrustedCaCerts = {
TestUtils.loadX509Cert("ca.pem")
};
channel = clientChannel(server.getPort(), clientContextBuilder
.keyManager(clientCertChainFile, clientPrivateKeyFile)
.trustManager(clientTrustedCaCerts)
.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.
// TODO(carl-mastrangelo): eventually replace this with a hamcrest matcher.
assertEquals(
Throwables.getStackTraceAsString(e),
Status.Code.UNAVAILABLE, e.getStatus().getCode());
}
}
private ServerBuilder<?> serverBuilder(int port, File serverCertChainFile,
File serverPrivateKeyFile, X509Certificate[] serverTrustedCaCerts) throws IOException {
SslContextBuilder sslContextBuilder
= SslContextBuilder.forServer(serverCertChainFile, serverPrivateKeyFile);
GrpcSslContexts.configure(sslContextBuilder, sslProvider);
sslContextBuilder.trustManager(serverTrustedCaCerts)
.clientAuth(ClientAuth.REQUIRE);
return NettyServerBuilder.forPort(port)
.sslContext(sslContextBuilder.build());
}
private static ManagedChannel clientChannel(int port, SslContext sslContext) throws IOException {
return NettyChannelBuilder.forAddress("localhost", port)
.overrideAuthority(TestUtils.TEST_SERVER_HOST)
.negotiationType(NegotiationType.TLS)
.sslContext(sslContext)
.build();
}
}