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
1 change: 1 addition & 0 deletions interop-testing/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ dependencies {
project(':grpc-testing'),
libraries.junit,
libraries.mockito,
libraries.netty_tcnative,
libraries.oauth_client
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@

// 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.

// A dummy GRPC service for use in tests.
syntax = "proto3";

package grpc.testing;

option java_package = "io.grpc.testing.integration";


message EchoRequest {
string text = 1;
}

message EchoResponse {
string text = 1;
}


service EchoService {
rpc Echo (EchoRequest) returns (EchoResponse);
}
253 changes: 253 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,253 @@
/*
* Copyright 2014, 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 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.stub.StreamObserver;
import io.grpc.testing.TestUtils;
import io.grpc.testing.integration.EchoServiceGrpc.EchoServiceBlockingStub;
import io.grpc.testing.integration.EchoServiceOuterClass.EchoRequest;
import io.grpc.testing.integration.EchoServiceOuterClass.EchoResponse;
import io.netty.handler.ssl.ClientAuth;
import io.netty.handler.ssl.SslContext;

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;

/**
* Integration tests for GRPC's TLS support.
*/
// TODO: Use @RunWith(Parameterized.class) to run these tests for all TLS providers. Doing so will
// require changes to allow programmatically choosing which TLS provider to use.
@RunWith(JUnit4.class)
public class TlsTest {
private static class DummyEchoRpcService implements EchoServiceGrpc.EchoService {
@Override
public void echo(EchoRequest request, StreamObserver<EchoResponse> responseObserver) {
EchoResponse response = EchoResponse.newBuilder()
.setText("Request said: " + request.getText())
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}


private static final File TESTDATA_ROOT = new File("src/test/resources/pki");


/**
* Tests that a client and a server configured using GrpcSslContexts can successfully
* communicate with each other.
*/
@Test
public void basicClientServerIntegrationTest() throws Exception {
int port = TestUtils.pickUnusedPort();

// Create & start a server.
File serverCertFile = TestUtils.loadCert("localhost_server.pem");
File serverPrivateKeyFile = TestUtils.loadCert("localhost_server.key");
X509Certificate[] serverTrustedCaCerts = {
TestUtils.loadX509Cert("ca.pem")
};
Server server = serverBuilder(port, serverCertFile, serverPrivateKeyFile, serverTrustedCaCerts)
.addService(EchoServiceGrpc.bindService(new DummyEchoRpcService()))
.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);
EchoServiceBlockingStub client = EchoServiceGrpc.newBlockingStub(channel);

// Send an actual request, via the full GRPC & network stack, and check that a proper
// response comes back.
EchoRequest request = EchoRequest.newBuilder()
.setText("dummy text")
.build();
EchoResponse response = client.echo(request);
assertEquals("Request said: dummy text", response.getText());
} finally {
server.shutdown();
}
}


/**
* 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 {
int port = TestUtils.pickUnusedPort();

// Create & start a server. It requires client authentication and trusts only the test CA.
File serverCertFile = TestUtils.loadCert("localhost_server.pem");
File serverPrivateKeyFile = TestUtils.loadCert("localhost_server.key");
X509Certificate[] serverTrustedCaCerts = {
TestUtils.loadX509Cert("ca.pem")
};
Server server = serverBuilder(port, serverCertFile, serverPrivateKeyFile, serverTrustedCaCerts)
.addService(EchoServiceGrpc.bindService(new DummyEchoRpcService()))
.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"),
TestUtils.loadX509Cert("badclient.pem") // Cert is self-signed, and so is its own issuer.
};
ManagedChannel channel = clientChannel("localhost", port, clientCertFile,
clientPrivateKeyFile, clientTrustedCaCerts);
EchoServiceBlockingStub client = EchoServiceGrpc.newBlockingStub(channel);

// Check that the TLS handshake fails.
EchoRequest request = EchoRequest.newBuilder()
.setText("dummy text")
.build();
try {
EchoResponse response = client.echo(request);
fail("TLS handshake should have failed, but didn't; received RPC response: " + 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("localhost_server.pem");
File serverPrivateKeyFile = TestUtils.loadCert("localhost_server.key");
X509Certificate[] serverTrustedCaCerts = {
TestUtils.loadX509Cert("ca.pem")
};
Server server = serverBuilder(port, serverCertFile, serverPrivateKeyFile, serverTrustedCaCerts)
.addService(EchoServiceGrpc.bindService(new DummyEchoRpcService()))
.build()
.start();

try {
// Create a client. It has no credentials.
ManagedChannel channel = NettyChannelBuilder.forAddress("localhost", port)
.negotiationType(NegotiationType.TLS)
.build();
EchoServiceBlockingStub client = EchoServiceGrpc.newBlockingStub(channel);

// Check that the TLS handshake fails.
EchoRequest request = EchoRequest.newBuilder()
.setText("dummy text")
.build();
try {
EchoResponse response = client.echo(request);
fail("TLS handshake should have failed, but didn't; received RPC response: " + 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)
.negotiationType(NegotiationType.TLS)
.sslContext(sslContext)
.build();
}
}
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
14 changes: 14 additions & 0 deletions testing/src/main/resources/certs/README
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ 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

localhost_server is issued by CA with the common name "localhost":
------------------------------------------------------------------

$ openssl genrsa -out localhost_server.key.rsa 1024
$ openssl pkcs8 -topk8 -in localhost_server.key.rsa -out localhost_server.key -nocrypt
$ rm localhost_server.key.rsa
$ openssl req -new -key localhost_server.key -out localhost_server.csr

When prompted for certificate information, everything is default except the
common name which is set to "localhost".

$ openssl ca -in localhost_server.csr -out localhost_server.pem -keyfile ca.key -cert ca.pem -verbose -config openssl.cnf -days 3650 -updatedb -create_serial
$ openssl x509 -in localhost_server.pem -out localhost_server.pem -outform PEM

Gotchas
=======

Expand Down
16 changes: 16 additions & 0 deletions testing/src/main/resources/certs/localhost_server.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-----BEGIN PRIVATE KEY-----
MIICeQIBADANBgkqhkiG9w0BAQEFAASCAmMwggJfAgEAAoGBAOx9w33imY4WuDSD
4+SGrUlCDPiRXltJ4QW3KQqA++vaUt3msE3Y6pmagS7hcEDU6IpJIFfX506xA/Oc
yKBxUI/om/fcTEXHmq/hPyOrVcED8iii5k55mGnsuBxtGZc9yJu0ocnSNfuW/pDW
oIA0ZFypxw1IZ1o/1PYb1cpcArGtAgMBAAECgYEAj73ZRvimUMDqcbEAoXRiezaU
X7kr2tzK0wiC/4lqle57k7iVzJtd7MMGZhJMgntmZDcSW5I1W5UoS7guEacOSV7h
Lw37Ni3i4w5iccPLfjSey7ChYB1PHx/4LaxEgP3NQXxlIbKoSSP9FoDCInqx4C6b
CkEo26T9/qVrWYenXIECQQD9cbPHjHmg3jt5dT6Sv8KPswpC/YAy6RNcS6Tg/CNN
CGVlbd2dhxaUBNfLwwE14x6tK/vNvmF8H+NpAp5leyUxAkEA7uBLnlRk14o0dvi+
j2gvRTWx9dzLw+uAeM3Bl7Hcweliz1V02dyQVQEIgLah6U6yeTYFy06/vDQKaXH3
+kblPQJBAOPgzhLH/bxk1Pj6ME7mWFu4Uau2HwSniJ7d7NvWGS90Myclx7OR+P0R
9a3iIj5/fd+awodVfHWMfn62uhDozqECQQCCGetVioV51y4H9iZjmMzWFw6b5+ub
A3LvWLEt25NukZxdbB++YKDDi1KEN/QrS89ssP2q43MOIBHjqEz1JRPJAkEAyIZQ
+5y/mg7MuAR5zguzhr+eqQ1FhHyhhYfMD73DTEjFWbfdq9/bdWW/+yNNU4f2GZ7A
lknwMAXWAJu2njYQYw==
-----END PRIVATE KEY-----
Loading