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