001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase.security; 019 020import static org.apache.hadoop.hbase.ipc.TestProtobufRpcServiceImpl.SERVICE; 021import static org.junit.jupiter.api.Assertions.assertThrows; 022import static org.junit.jupiter.api.Assertions.fail; 023 024import java.io.File; 025import java.io.IOException; 026import java.net.InetSocketAddress; 027import java.security.Security; 028import java.security.cert.X509Certificate; 029import javax.net.ssl.SSLHandshakeException; 030import org.apache.commons.io.FileUtils; 031import org.apache.hadoop.conf.Configuration; 032import org.apache.hadoop.hbase.HBaseCommonTestingUtil; 033import org.apache.hadoop.hbase.io.crypto.tls.KeyStoreFileType; 034import org.apache.hadoop.hbase.io.crypto.tls.X509KeyType; 035import org.apache.hadoop.hbase.io.crypto.tls.X509TestContext; 036import org.apache.hadoop.hbase.io.crypto.tls.X509TestContextProvider; 037import org.apache.hadoop.hbase.io.crypto.tls.X509Util; 038import org.apache.hadoop.hbase.ipc.FifoRpcScheduler; 039import org.apache.hadoop.hbase.ipc.NettyRpcClient; 040import org.apache.hadoop.hbase.ipc.NettyRpcServer; 041import org.apache.hadoop.hbase.ipc.RpcClient; 042import org.apache.hadoop.hbase.ipc.RpcClientFactory; 043import org.apache.hadoop.hbase.ipc.RpcServer; 044import org.apache.hadoop.hbase.ipc.RpcServerFactory; 045import org.apache.hadoop.hbase.ipc.TestProtobufRpcServiceImpl; 046import org.bouncycastle.asn1.x500.X500NameBuilder; 047import org.bouncycastle.asn1.x500.style.BCStyle; 048import org.bouncycastle.jce.provider.BouncyCastleProvider; 049import org.junit.jupiter.api.AfterAll; 050import org.junit.jupiter.api.AfterEach; 051import org.junit.jupiter.api.BeforeAll; 052import org.junit.jupiter.api.BeforeEach; 053import org.junit.jupiter.api.TestTemplate; 054 055import org.apache.hbase.thirdparty.com.google.common.base.Throwables; 056import org.apache.hbase.thirdparty.com.google.common.collect.Lists; 057import org.apache.hbase.thirdparty.com.google.common.io.Closeables; 058import org.apache.hbase.thirdparty.com.google.protobuf.ServiceException; 059 060import org.apache.hadoop.hbase.shaded.ipc.protobuf.generated.TestProtos; 061import org.apache.hadoop.hbase.shaded.ipc.protobuf.generated.TestRpcServiceProtos; 062 063public abstract class AbstractTestMutualTls { 064 protected static HBaseCommonTestingUtil UTIL; 065 066 protected static File DIR; 067 068 protected static X509TestContextProvider PROVIDER; 069 070 private X509TestContext x509TestContext; 071 072 protected RpcServer rpcServer; 073 074 protected RpcClient rpcClient; 075 076 private TestRpcServiceProtos.TestProtobufRpcProto.BlockingInterface stub; 077 078 protected X509KeyType caKeyType; 079 080 protected X509KeyType certKeyType; 081 082 protected String keyPassword; 083 084 protected boolean expectSuccess; 085 086 protected boolean validateHostnames; 087 088 protected CertConfig certConfig; 089 090 public enum CertConfig { 091 // For no cert, we literally pass no certificate to the server. It's possible (assuming server 092 // allows it based on ClientAuth mode) to use SSL without a KeyStore which will still do all 093 // the handshaking but without a client cert. This is what we do here. 094 // This mode only makes sense for client side, as server side must return a cert. 095 NO_CLIENT_CERT, 096 // For non-verifiable cert, we create a new certificate which is signed by a different 097 // CA. So we're passing a cert, but the client/server can't verify it. 098 NON_VERIFIABLE_CERT, 099 // Good cert is the default mode, which uses a cert signed by the same CA both sides 100 // and the hostname should match (localhost) 101 GOOD_CERT, 102 // For good cert/bad host, we create a new certificate signed by the same CA. But 103 // this cert has a SANS that will not match the localhost peer. 104 VERIFIABLE_CERT_WITH_BAD_HOST 105 } 106 107 protected AbstractTestMutualTls(X509KeyType caKeyType, X509KeyType certKeyType, 108 String keyPassword, boolean expectSuccess, boolean validateHostnames, CertConfig certConfig) { 109 this.caKeyType = caKeyType; 110 this.certKeyType = certKeyType; 111 this.keyPassword = keyPassword; 112 this.expectSuccess = expectSuccess; 113 this.validateHostnames = validateHostnames; 114 this.certConfig = certConfig; 115 } 116 117 @BeforeAll 118 public static void setUpBeforeClass() throws IOException { 119 UTIL = new HBaseCommonTestingUtil(); 120 Security.addProvider(new BouncyCastleProvider()); 121 DIR = 122 new File(UTIL.getDataTestDir(AbstractTestTlsRejectPlainText.class.getSimpleName()).toString()) 123 .getCanonicalFile(); 124 FileUtils.forceMkdir(DIR); 125 Configuration conf = UTIL.getConfiguration(); 126 conf.setClass(RpcClientFactory.CUSTOM_RPC_CLIENT_IMPL_CONF_KEY, NettyRpcClient.class, 127 RpcClient.class); 128 conf.setClass(RpcServerFactory.CUSTOM_RPC_SERVER_IMPL_CONF_KEY, NettyRpcServer.class, 129 RpcServer.class); 130 conf.setBoolean(X509Util.HBASE_SERVER_NETTY_TLS_ENABLED, true); 131 conf.setBoolean(X509Util.HBASE_SERVER_NETTY_TLS_SUPPORTPLAINTEXT, false); 132 conf.setBoolean(X509Util.HBASE_CLIENT_NETTY_TLS_ENABLED, true); 133 PROVIDER = new X509TestContextProvider(conf, DIR); 134 } 135 136 @AfterAll 137 public static void cleanUp() { 138 Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); 139 UTIL.cleanupTestDir(); 140 } 141 142 protected abstract void initialize(Configuration serverConf, Configuration clientConf) 143 throws Exception; 144 145 @BeforeEach 146 public void setUp() throws Exception { 147 x509TestContext = PROVIDER.get(caKeyType, certKeyType, keyPassword.toCharArray()); 148 x509TestContext.setConfigurations(KeyStoreFileType.JKS, KeyStoreFileType.JKS); 149 150 Configuration serverConf = new Configuration(UTIL.getConfiguration()); 151 Configuration clientConf = new Configuration(UTIL.getConfiguration()); 152 153 initialize(serverConf, clientConf); 154 155 rpcServer = new NettyRpcServer(null, "testRpcServer", 156 Lists.newArrayList(new RpcServer.BlockingServiceAndInterface(SERVICE, null)), 157 new InetSocketAddress("localhost", 0), serverConf, new FifoRpcScheduler(serverConf, 1), true); 158 rpcServer.start(); 159 160 rpcClient = new NettyRpcClient(clientConf); 161 stub = TestProtobufRpcServiceImpl.newBlockingStub(rpcClient, rpcServer.getListenerAddress()); 162 } 163 164 protected void handleCertConfig(Configuration confToSet) throws Exception { 165 switch (certConfig) { 166 case NO_CLIENT_CERT: 167 // clearing out the keystore location will cause no cert to be sent. 168 confToSet.set(X509Util.TLS_CONFIG_KEYSTORE_LOCATION, ""); 169 break; 170 case NON_VERIFIABLE_CERT: 171 // to simulate a bad cert, we inject a new keystore into the client side. 172 // the same truststore exists, so it will still successfully verify the server cert 173 // but since the new client keystore cert is created from a new CA (which the server doesn't 174 // have), 175 // the server will not be able to verify it. 176 X509TestContext context = 177 PROVIDER.get(caKeyType, certKeyType, "random value".toCharArray()); 178 context.setKeystoreConfigurations(KeyStoreFileType.JKS, confToSet); 179 break; 180 case VERIFIABLE_CERT_WITH_BAD_HOST: 181 // to simulate a good cert with a bad host, we need to create a new cert using the existing 182 // context's CA/truststore. Here we can pass any random SANS, as long as it won't match 183 // localhost or any reasonable name that this test might run on. 184 X509Certificate cert = x509TestContext.newCert( 185 new X500NameBuilder(BCStyle.INSTANCE) 186 .addRDN(BCStyle.CN, getClass().getSimpleName() + " With Bad Host Test").build(), 187 "www.example.com"); 188 x509TestContext.cloneWithNewKeystoreCert(cert) 189 .setKeystoreConfigurations(KeyStoreFileType.JKS, confToSet); 190 break; 191 default: 192 break; 193 } 194 } 195 196 @AfterEach 197 public void tearDown() throws IOException { 198 if (rpcServer != null) { 199 rpcServer.stop(); 200 } 201 Closeables.close(rpcClient, true); 202 x509TestContext.clearConfigurations(); 203 x509TestContext.getConf().unset(X509Util.TLS_CONFIG_OCSP); 204 x509TestContext.getConf().unset(X509Util.TLS_CONFIG_CLR); 205 x509TestContext.getConf().unset(X509Util.TLS_CONFIG_PROTOCOL); 206 System.clearProperty("com.sun.net.ssl.checkRevocation"); 207 System.clearProperty("com.sun.security.enableCRLDP"); 208 Security.setProperty("ocsp.enable", Boolean.FALSE.toString()); 209 Security.setProperty("com.sun.security.enableCRLDP", Boolean.FALSE.toString()); 210 } 211 212 @TestTemplate 213 public void testClientAuth() throws Exception { 214 if (expectSuccess) { 215 // we expect no exception, so if one is thrown the test will fail 216 submitRequest(); 217 } else { 218 ServiceException se = assertThrows(ServiceException.class, this::submitRequest); 219 // The SSLHandshakeException is encapsulated differently depending on the TLS version 220 Throwable current = se; 221 do { 222 if (current instanceof SSLHandshakeException) { 223 return; 224 } 225 current = current.getCause(); 226 } while (current != null); 227 fail("Exception chain does not include SSLHandshakeException: " 228 + Throwables.getStackTraceAsString(se)); 229 } 230 } 231 232 private void submitRequest() throws ServiceException { 233 stub.echo(null, TestProtos.EchoRequestProto.newBuilder().setMessage("hello world").build()); 234 } 235}