1 /* 2 * Copyright 2015 The gRPC Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.grpc.netty; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.fail; 21 22 import com.google.common.base.Throwables; 23 import com.google.common.util.concurrent.MoreExecutors; 24 import io.grpc.ManagedChannel; 25 import io.grpc.Server; 26 import io.grpc.ServerBuilder; 27 import io.grpc.Status; 28 import io.grpc.StatusRuntimeException; 29 import io.grpc.internal.testing.TestUtils; 30 import io.grpc.stub.StreamObserver; 31 import io.grpc.testing.protobuf.SimpleRequest; 32 import io.grpc.testing.protobuf.SimpleResponse; 33 import io.grpc.testing.protobuf.SimpleServiceGrpc; 34 import io.netty.handler.ssl.ClientAuth; 35 import io.netty.handler.ssl.OpenSsl; 36 import io.netty.handler.ssl.SslContext; 37 import io.netty.handler.ssl.SslContextBuilder; 38 import io.netty.handler.ssl.SslProvider; 39 import java.io.File; 40 import java.io.IOException; 41 import java.security.NoSuchAlgorithmException; 42 import java.security.Provider; 43 import java.security.Security; 44 import java.security.cert.X509Certificate; 45 import java.util.Arrays; 46 import java.util.concurrent.Executors; 47 import java.util.concurrent.ScheduledExecutorService; 48 import java.util.concurrent.TimeUnit; 49 import javax.net.ssl.SSLContext; 50 import org.junit.After; 51 import org.junit.Assume; 52 import org.junit.Before; 53 import org.junit.BeforeClass; 54 import org.junit.Test; 55 import org.junit.runner.RunWith; 56 import org.junit.runners.Parameterized; 57 import org.junit.runners.Parameterized.Parameter; 58 import org.junit.runners.Parameterized.Parameters; 59 60 61 /** 62 * Integration tests for Netty's TLS support. 63 */ 64 @RunWith(Parameterized.class) 65 public class TlsTest { 66 67 public static enum TlsImpl { 68 TCNATIVE, JDK, CONSCRYPT; 69 } 70 71 /** 72 * Iterable of various configurations to use for tests. 73 */ 74 @Parameters(name = "{0}") data()75 public static Iterable<Object[]> data() { 76 return Arrays.asList(new Object[][] { 77 {TlsImpl.TCNATIVE}, {TlsImpl.JDK}, {TlsImpl.CONSCRYPT}, 78 }); 79 } 80 81 @Parameter(value = 0) 82 public TlsImpl tlsImpl; 83 84 private ScheduledExecutorService executor; 85 private Server server; 86 private ManagedChannel channel; 87 private SslProvider sslProvider; 88 private Provider jdkProvider; 89 private SslContextBuilder clientContextBuilder; 90 91 @BeforeClass loadConscrypt()92 public static void loadConscrypt() { 93 TestUtils.installConscryptIfAvailable(); 94 } 95 96 @Before setUp()97 public void setUp() throws NoSuchAlgorithmException { 98 executor = Executors.newSingleThreadScheduledExecutor(); 99 switch (tlsImpl) { 100 case TCNATIVE: 101 Assume.assumeTrue(OpenSsl.isAvailable()); 102 sslProvider = SslProvider.OPENSSL; 103 break; 104 case JDK: 105 Assume.assumeTrue(Arrays.asList( 106 SSLContext.getDefault().getSupportedSSLParameters().getCipherSuites()) 107 .contains("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256")); 108 sslProvider = SslProvider.JDK; 109 jdkProvider = Security.getProvider("SunJSSE"); 110 Assume.assumeNotNull(jdkProvider); 111 try { 112 // Check for presence of an (ironic) class added in Java 9 113 Class.forName("java.lang.Runtime$Version"); 114 // Java 9+ 115 } catch (ClassNotFoundException ignored) { 116 // Before Java 9 117 try { 118 GrpcSslContexts.configure(SslContextBuilder.forClient(), jdkProvider); 119 } catch (IllegalArgumentException ex) { 120 Assume.assumeNoException("Not Java 9+ and Jetty ALPN does not seem available", ex); 121 } 122 } 123 break; 124 case CONSCRYPT: 125 sslProvider = SslProvider.JDK; 126 jdkProvider = Security.getProvider("Conscrypt"); 127 Assume.assumeNotNull(jdkProvider); 128 break; 129 default: 130 throw new AssertionError(); 131 } 132 clientContextBuilder = SslContextBuilder.forClient(); 133 if (sslProvider == SslProvider.JDK) { 134 GrpcSslContexts.configure(clientContextBuilder, jdkProvider); 135 } else { 136 GrpcSslContexts.configure(clientContextBuilder, sslProvider); 137 } 138 } 139 140 @After tearDown()141 public void tearDown() { 142 if (server != null) { 143 server.shutdown(); 144 } 145 if (channel != null) { 146 channel.shutdown(); 147 } 148 MoreExecutors.shutdownAndAwaitTermination(executor, 5, TimeUnit.SECONDS); 149 } 150 151 152 /** 153 * Tests that a client and a server configured using GrpcSslContexts can successfully 154 * communicate with each other. 155 */ 156 @Test basicClientServerIntegrationTest()157 public void basicClientServerIntegrationTest() throws Exception { 158 // Create & start a server. 159 File serverCertFile = TestUtils.loadCert("server1.pem"); 160 File serverPrivateKeyFile = TestUtils.loadCert("server1.key"); 161 X509Certificate[] serverTrustedCaCerts = { 162 TestUtils.loadX509Cert("ca.pem") 163 }; 164 server = serverBuilder(0, serverCertFile, serverPrivateKeyFile, serverTrustedCaCerts) 165 .addService(new SimpleServiceImpl()) 166 .build() 167 .start(); 168 169 // Create a client. 170 File clientCertChainFile = TestUtils.loadCert("client.pem"); 171 File clientPrivateKeyFile = TestUtils.loadCert("client.key"); 172 X509Certificate[] clientTrustedCaCerts = { 173 TestUtils.loadX509Cert("ca.pem") 174 }; 175 channel = clientChannel(server.getPort(), clientContextBuilder 176 .keyManager(clientCertChainFile, clientPrivateKeyFile) 177 .trustManager(clientTrustedCaCerts) 178 .build()); 179 SimpleServiceGrpc.SimpleServiceBlockingStub client = SimpleServiceGrpc.newBlockingStub(channel); 180 181 // Send an actual request, via the full GRPC & network stack, and check that a proper 182 // response comes back. 183 client.unaryRpc(SimpleRequest.getDefaultInstance()); 184 } 185 186 /** 187 * Tests that a server configured to require client authentication refuses to accept connections 188 * from a client that has an untrusted certificate. 189 */ 190 @Test serverRejectsUntrustedClientCert()191 public void serverRejectsUntrustedClientCert() throws Exception { 192 // Create & start a server. It requires client authentication and trusts only the test CA. 193 File serverCertFile = TestUtils.loadCert("server1.pem"); 194 File serverPrivateKeyFile = TestUtils.loadCert("server1.key"); 195 X509Certificate[] serverTrustedCaCerts = { 196 TestUtils.loadX509Cert("ca.pem") 197 }; 198 server = serverBuilder(0, serverCertFile, serverPrivateKeyFile, serverTrustedCaCerts) 199 .addService(new SimpleServiceImpl()) 200 .build() 201 .start(); 202 203 // Create a client. Its credentials come from a CA that the server does not trust. The client 204 // trusts both test CAs, so we can be sure that the handshake failure is due to the server 205 // rejecting the client's cert, not the client rejecting the server's cert. 206 File clientCertChainFile = TestUtils.loadCert("badclient.pem"); 207 File clientPrivateKeyFile = TestUtils.loadCert("badclient.key"); 208 X509Certificate[] clientTrustedCaCerts = { 209 TestUtils.loadX509Cert("ca.pem") 210 }; 211 channel = clientChannel(server.getPort(), clientContextBuilder 212 .keyManager(clientCertChainFile, clientPrivateKeyFile) 213 .trustManager(clientTrustedCaCerts) 214 .build()); 215 SimpleServiceGrpc.SimpleServiceBlockingStub client = SimpleServiceGrpc.newBlockingStub(channel); 216 217 // Check that the TLS handshake fails. 218 try { 219 client.unaryRpc(SimpleRequest.getDefaultInstance()); 220 fail("TLS handshake should have failed, but didn't; received RPC response"); 221 } catch (StatusRuntimeException e) { 222 // GRPC reports this situation by throwing a StatusRuntimeException that wraps either a 223 // javax.net.ssl.SSLHandshakeException or a java.nio.channels.ClosedChannelException. 224 // Thus, reliably detecting the underlying cause is not feasible. 225 assertEquals( 226 Throwables.getStackTraceAsString(e), 227 Status.Code.UNAVAILABLE, e.getStatus().getCode()); 228 } 229 } 230 231 232 /** 233 * Tests that a server configured to require client authentication actually does require client 234 * authentication. 235 */ 236 @Test noClientAuthFailure()237 public void noClientAuthFailure() throws Exception { 238 // Create & start a server. 239 File serverCertFile = TestUtils.loadCert("server1.pem"); 240 File serverPrivateKeyFile = TestUtils.loadCert("server1.key"); 241 X509Certificate[] serverTrustedCaCerts = { 242 TestUtils.loadX509Cert("ca.pem") 243 }; 244 server = serverBuilder(0, serverCertFile, serverPrivateKeyFile, serverTrustedCaCerts) 245 .addService(new SimpleServiceImpl()) 246 .build() 247 .start(); 248 249 // Create a client. It has no credentials. 250 X509Certificate[] clientTrustedCaCerts = { 251 TestUtils.loadX509Cert("ca.pem") 252 }; 253 channel = clientChannel(server.getPort(), clientContextBuilder 254 .trustManager(clientTrustedCaCerts) 255 .build()); 256 SimpleServiceGrpc.SimpleServiceBlockingStub client = SimpleServiceGrpc.newBlockingStub(channel); 257 258 // Check that the TLS handshake fails. 259 try { 260 client.unaryRpc(SimpleRequest.getDefaultInstance()); 261 fail("TLS handshake should have failed, but didn't; received RPC response"); 262 } catch (StatusRuntimeException e) { 263 // GRPC reports this situation by throwing a StatusRuntimeException that wraps either a 264 // javax.net.ssl.SSLHandshakeException or a java.nio.channels.ClosedChannelException. 265 // Thus, reliably detecting the underlying cause is not feasible. 266 assertEquals( 267 Throwables.getStackTraceAsString(e), 268 Status.Code.UNAVAILABLE, e.getStatus().getCode()); 269 } 270 } 271 272 273 /** 274 * Tests that a client configured using GrpcSslContexts refuses to talk to a server that has an 275 * an untrusted certificate. 276 */ 277 @Test clientRejectsUntrustedServerCert()278 public void clientRejectsUntrustedServerCert() throws Exception { 279 // Create & start a server. 280 File serverCertFile = TestUtils.loadCert("badserver.pem"); 281 File serverPrivateKeyFile = TestUtils.loadCert("badserver.key"); 282 X509Certificate[] serverTrustedCaCerts = { 283 TestUtils.loadX509Cert("ca.pem") 284 }; 285 server = serverBuilder(0, serverCertFile, serverPrivateKeyFile, serverTrustedCaCerts) 286 .addService(new SimpleServiceImpl()) 287 .build() 288 .start(); 289 290 // Create a client. 291 File clientCertChainFile = TestUtils.loadCert("client.pem"); 292 File clientPrivateKeyFile = TestUtils.loadCert("client.key"); 293 X509Certificate[] clientTrustedCaCerts = { 294 TestUtils.loadX509Cert("ca.pem") 295 }; 296 channel = clientChannel(server.getPort(), clientContextBuilder 297 .keyManager(clientCertChainFile, clientPrivateKeyFile) 298 .trustManager(clientTrustedCaCerts) 299 .build()); 300 SimpleServiceGrpc.SimpleServiceBlockingStub client = SimpleServiceGrpc.newBlockingStub(channel); 301 302 // Check that the TLS handshake fails. 303 try { 304 client.unaryRpc(SimpleRequest.getDefaultInstance()); 305 fail("TLS handshake should have failed, but didn't; received RPC response"); 306 } catch (StatusRuntimeException e) { 307 // GRPC reports this situation by throwing a StatusRuntimeException that wraps either a 308 // javax.net.ssl.SSLHandshakeException or a java.nio.channels.ClosedChannelException. 309 // Thus, reliably detecting the underlying cause is not feasible. 310 // TODO(carl-mastrangelo): eventually replace this with a hamcrest matcher. 311 assertEquals( 312 Throwables.getStackTraceAsString(e), 313 Status.Code.UNAVAILABLE, e.getStatus().getCode()); 314 } 315 } 316 317 serverBuilder(int port, File serverCertChainFile, File serverPrivateKeyFile, X509Certificate[] serverTrustedCaCerts)318 private ServerBuilder<?> serverBuilder(int port, File serverCertChainFile, 319 File serverPrivateKeyFile, X509Certificate[] serverTrustedCaCerts) throws IOException { 320 SslContextBuilder sslContextBuilder 321 = SslContextBuilder.forServer(serverCertChainFile, serverPrivateKeyFile); 322 if (sslProvider == SslProvider.JDK) { 323 GrpcSslContexts.configure(sslContextBuilder, jdkProvider); 324 } else { 325 GrpcSslContexts.configure(sslContextBuilder, sslProvider); 326 } 327 sslContextBuilder.trustManager(serverTrustedCaCerts) 328 .clientAuth(ClientAuth.REQUIRE); 329 330 return NettyServerBuilder.forPort(port) 331 .sslContext(sslContextBuilder.build()); 332 } 333 334 clientChannel(int port, SslContext sslContext)335 private static ManagedChannel clientChannel(int port, SslContext sslContext) throws IOException { 336 return NettyChannelBuilder.forAddress("localhost", port) 337 .overrideAuthority(TestUtils.TEST_SERVER_HOST) 338 .negotiationType(NegotiationType.TLS) 339 .sslContext(sslContext) 340 .build(); 341 } 342 343 private static class SimpleServiceImpl extends SimpleServiceGrpc.SimpleServiceImplBase { 344 @Override unaryRpc(SimpleRequest req, StreamObserver<SimpleResponse> respOb)345 public void unaryRpc(SimpleRequest req, StreamObserver<SimpleResponse> respOb) { 346 respOb.onNext(SimpleResponse.getDefaultInstance()); 347 respOb.onCompleted(); 348 } 349 } 350 } 351