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