diff --git a/interop-testing/build.gradle b/interop-testing/build.gradle index fdc524ac12c..edeff2335cf 100644 --- a/interop-testing/build.gradle +++ b/interop-testing/build.gradle @@ -23,6 +23,7 @@ dependencies { project(':grpc-testing'), libraries.junit, libraries.mockito, + libraries.netty_tcnative, libraries.oauth_client } diff --git a/interop-testing/src/main/proto/io/grpc/testing/integration/echo_service.proto b/interop-testing/src/main/proto/io/grpc/testing/integration/echo_service.proto new file mode 100644 index 00000000000..4d62ebf0841 --- /dev/null +++ b/interop-testing/src/main/proto/io/grpc/testing/integration/echo_service.proto @@ -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); +} 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..4ffe698b49b --- /dev/null +++ b/interop-testing/src/test/java/io/grpc/testing/integration/TlsTest.java @@ -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 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(); + } +} 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..c8f86d6e71e 100644 --- a/testing/src/main/resources/certs/README +++ b/testing/src/main/resources/certs/README @@ -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 ======= diff --git a/testing/src/main/resources/certs/localhost_server.key b/testing/src/main/resources/certs/localhost_server.key new file mode 100644 index 00000000000..6a2822990c9 --- /dev/null +++ b/testing/src/main/resources/certs/localhost_server.key @@ -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----- diff --git a/testing/src/main/resources/certs/localhost_server.pem b/testing/src/main/resources/certs/localhost_server.pem new file mode 100644 index 00000000000..5062935df19 --- /dev/null +++ b/testing/src/main/resources/certs/localhost_server.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC8DCCAlmgAwIBAgIJALkk95OtCy9MMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNTEyMjkyMTM5Mjha +Fw0yNTEyMjYyMTM5MjhaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0 +YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMM +CWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA7H3DfeKZjha4 +NIPj5IatSUIM+JFeW0nhBbcpCoD769pS3eawTdjqmZqBLuFwQNToikkgV9fnTrED +85zIoHFQj+ib99xMRcear+E/I6tVwQPyKKLmTnmYaey4HG0Zlz3Im7ShydI1+5b+ +kNaggDRkXKnHDUhnWj/U9hvVylwCsa0CAwEAAaOBwjCBvzAJBgNVHRMEAjAAMAsG +A1UdDwQEAwIF4DAdBgNVHQ4EFgQUBXX49FvGG7KQfqtWcr0N7wffn28wcAYDVR0j +BGkwZ6FapFgwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAf +BgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAxMGdGVzdGNh +ggkAkcYZHh0aKgcwCQYDVR0RBAIwADAJBgNVHRIEAjAAMA0GCSqGSIb3DQEBCwUA +A4GBAEZyKEh3IqPxUP46GfIaJRjIVuFSI5iRO/7jqnoHDNbv+nRX/vxfrHldCgv2 +EQ4+ip8X5fzEz2TOeTj5MDwnO29r5DZi7/SoedU8hfREhszQyF9pjfV7dLq1vvtl +DobLdaex1IxUoy6Yt8Am0mUfMb31gIxakl0UIQokfLJ83he1 +-----END CERTIFICATE-----