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.rest; 019 020import static org.junit.jupiter.api.Assertions.assertEquals; 021import static org.junit.jupiter.api.Assertions.assertThrows; 022 023import java.io.File; 024import java.io.IOException; 025import java.lang.reflect.Method; 026import java.security.KeyPair; 027import java.security.cert.X509Certificate; 028import java.util.Optional; 029import org.apache.hadoop.conf.Configuration; 030import org.apache.hadoop.hbase.HBaseTestingUtil; 031import org.apache.hadoop.hbase.http.ssl.KeyStoreTestUtil; 032import org.apache.hadoop.hbase.rest.client.Client; 033import org.apache.hadoop.hbase.rest.client.Cluster; 034import org.apache.hadoop.hbase.rest.client.Response; 035import org.apache.hadoop.hbase.testclassification.MediumTests; 036import org.apache.hadoop.hbase.testclassification.RestTests; 037import org.apache.http.client.ClientProtocolException; 038import org.junit.jupiter.api.AfterAll; 039import org.junit.jupiter.api.AfterEach; 040import org.junit.jupiter.api.BeforeAll; 041import org.junit.jupiter.api.BeforeEach; 042import org.junit.jupiter.api.Tag; 043import org.junit.jupiter.api.Test; 044import org.slf4j.Logger; 045import org.slf4j.LoggerFactory; 046 047@Tag(RestTests.TAG) 048@Tag(MediumTests.TAG) 049public class TestRESTServerSSL { 050 051 private static final Logger LOG = LoggerFactory.getLogger(TestRESTServerSSL.class); 052 053 private static final String KEY_STORE_PASSWORD = "myKSPassword"; 054 private static final String TRUST_STORE_PASSWORD = "myTSPassword"; 055 056 private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil(); 057 private static final HBaseRESTTestingUtility REST_TEST_UTIL = new HBaseRESTTestingUtility(); 058 private static Client sslClient; 059 private static File keyDir; 060 private Configuration conf; 061 062 // Workaround for jdk8 292 bug. See https://github.com/bcgit/bc-java/issues/941 063 // Below is a workaround described in above URL. Issue fingered first in comments in 064 // HBASE-25920 Support Hadoop 3.3.1 065 private static void initializeAlgorithmId() { 066 try { 067 Class<?> algoId = Class.forName("sun.security.x509.AlgorithmId"); 068 Method method = algoId.getMethod("get", String.class); 069 method.setAccessible(true); 070 method.invoke(null, "PBEWithSHA1AndDESede"); 071 } catch (Exception e) { 072 LOG.warn("failed to initialize AlgorithmId", e); 073 } 074 } 075 076 @BeforeAll 077 public static void beforeClass() throws Exception { 078 initializeAlgorithmId(); 079 keyDir = initKeystoreDir(); 080 KeyPair keyPair = KeyStoreTestUtil.generateKeyPair("RSA"); 081 X509Certificate serverCertificate = 082 KeyStoreTestUtil.generateCertificate("CN=localhost, O=server", keyPair, 30, "SHA1withRSA"); 083 084 generateTrustStore("jks", serverCertificate); 085 generateTrustStore("jceks", serverCertificate); 086 generateTrustStore("pkcs12", serverCertificate); 087 088 generateKeyStore("jks", keyPair, serverCertificate); 089 generateKeyStore("jceks", keyPair, serverCertificate); 090 generateKeyStore("pkcs12", keyPair, serverCertificate); 091 092 TEST_UTIL.startMiniCluster(); 093 } 094 095 @AfterAll 096 public static void afterClass() throws Exception { 097 // this will also delete the generated test keystore / teststore files, 098 // as we were placing them under the dataTestDir used by the minicluster 099 TEST_UTIL.shutdownMiniCluster(); 100 } 101 102 @BeforeEach 103 public void beforeEachTest() { 104 conf = new Configuration(TEST_UTIL.getConfiguration()); 105 conf.set(Constants.REST_SSL_ENABLED, "true"); 106 conf.set(Constants.REST_SSL_KEYSTORE_KEYPASSWORD, KEY_STORE_PASSWORD); 107 conf.set(Constants.REST_SSL_KEYSTORE_PASSWORD, KEY_STORE_PASSWORD); 108 conf.set(Constants.REST_SSL_TRUSTSTORE_PASSWORD, TRUST_STORE_PASSWORD); 109 } 110 111 @AfterEach 112 public void tearDownAfterTest() { 113 REST_TEST_UTIL.shutdownServletContainer(); 114 } 115 116 @Test 117 public void testSslConnection() throws Exception { 118 startRESTServerWithDefaultKeystoreType(); 119 120 Response response = sslClient.get("/version", Constants.MIMETYPE_TEXT); 121 assertEquals(200, response.getCode()); 122 123 // Default security headers 124 assertEquals("max-age=63072000;includeSubDomains;preload", 125 response.getHeader("Strict-Transport-Security")); 126 assertEquals("default-src https: data: 'unsafe-inline' 'unsafe-eval'", 127 response.getHeader("Content-Security-Policy")); 128 } 129 130 @Test 131 public void testNonSslClientDenied() throws Exception { 132 startRESTServerWithDefaultKeystoreType(); 133 134 Cluster localCluster = new Cluster().add("localhost", REST_TEST_UTIL.getServletPort()); 135 Client nonSslClient = new Client(localCluster, false); 136 137 assertThrows(ClientProtocolException.class, () -> nonSslClient.get("/version")); 138 } 139 140 @Test 141 public void testSslConnectionUsingKeystoreFormatJKS() throws Exception { 142 startRESTServer("jks"); 143 144 Response response = sslClient.get("/version", Constants.MIMETYPE_TEXT); 145 assertEquals(200, response.getCode()); 146 } 147 148 @Test 149 public void testSslConnectionUsingKeystoreFormatJCEKS() throws Exception { 150 startRESTServer("jceks"); 151 152 Response response = sslClient.get("/version", Constants.MIMETYPE_TEXT); 153 assertEquals(200, response.getCode()); 154 } 155 156 @Test 157 public void testSslConnectionUsingKeystoreFormatPKCS12() throws Exception { 158 startRESTServer("pkcs12"); 159 160 Response response = sslClient.get("/version", Constants.MIMETYPE_TEXT); 161 assertEquals(200, response.getCode()); 162 } 163 164 // --------------------------------------------------------------------------- 165 // Role-scoped configuration + mTLS (single-EKU certificate support). 166 // --------------------------------------------------------------------------- 167 168 /** 169 * Server started with only the role-scoped hbase.rest.ssl.server.* keys (and legacy keys unset). 170 * A successful SSL connection proves the server-scoped keys were consulted. 171 */ 172 @Test 173 public void testSslConnectionUsingRoleScopedServerKeys() throws Exception { 174 // Move the legacy passwords set in beforeEachTest onto the role-scoped keys and clear the 175 // legacy passwords so a successful start proves the server-scoped keys are what the code 176 // actually picked up. 177 conf.unset(Constants.REST_SSL_KEYSTORE_PASSWORD); 178 conf.unset(Constants.REST_SSL_KEYSTORE_KEYPASSWORD); 179 conf.unset(Constants.REST_SSL_TRUSTSTORE_PASSWORD); 180 conf.set(Constants.REST_SSL_SERVER_KEYSTORE_PASSWORD, KEY_STORE_PASSWORD); 181 conf.set(Constants.REST_SSL_SERVER_KEYSTORE_KEYPASSWORD, KEY_STORE_PASSWORD); 182 conf.set(Constants.REST_SSL_SERVER_TRUSTSTORE_PASSWORD, TRUST_STORE_PASSWORD); 183 conf.set(Constants.REST_SSL_SERVER_KEYSTORE_STORE, getKeystoreFilePath("jks")); 184 conf.set(Constants.REST_SSL_SERVER_TRUSTSTORE_STORE, getTruststoreFilePath("jks")); 185 186 REST_TEST_UTIL.startServletContainer(conf); 187 Cluster localCluster = new Cluster().add("localhost", REST_TEST_UTIL.getServletPort()); 188 sslClient = new Client(localCluster, getTruststoreFilePath("jks"), 189 Optional.of(TRUST_STORE_PASSWORD), Optional.empty()); 190 191 Response response = sslClient.get("/version", Constants.MIMETYPE_TEXT); 192 assertEquals(200, response.getCode()); 193 } 194 195 /** 196 * Backward-compatibility regression: existing deployments that know only about the legacy 197 * unscoped keys must continue to work exactly as before. This mirrors {@link #testSslConnection} 198 * but names the intent explicitly. 199 */ 200 @Test 201 public void testSslConnectionFallsBackToLegacyKeystoreKeys() throws Exception { 202 // Make sure no role-scoped key is set — the beforeEachTest configures only legacy passwords, 203 // so this is a fresh state check. 204 conf.unset(Constants.REST_SSL_SERVER_KEYSTORE_STORE); 205 conf.unset(Constants.REST_SSL_SERVER_KEYSTORE_PASSWORD); 206 conf.unset(Constants.REST_SSL_SERVER_KEYSTORE_KEYPASSWORD); 207 conf.unset(Constants.REST_SSL_SERVER_KEYSTORE_TYPE); 208 conf.unset(Constants.REST_SSL_SERVER_TRUSTSTORE_STORE); 209 conf.unset(Constants.REST_SSL_SERVER_TRUSTSTORE_PASSWORD); 210 conf.unset(Constants.REST_SSL_SERVER_TRUSTSTORE_TYPE); 211 212 startRESTServerWithDefaultKeystoreType(); 213 214 Response response = sslClient.get("/version", Constants.MIMETYPE_TEXT); 215 assertEquals(200, response.getCode()); 216 } 217 218 /** 219 * With {@code client.auth.mode=NONE} (the default), a client that presents no client certificate 220 * is accepted — matching today's behavior. 221 */ 222 @Test 223 public void testClientAuthNoneAcceptsClientWithoutCert() throws Exception { 224 conf.set(Constants.REST_SSL_CLIENT_AUTH_MODE, "NONE"); 225 startRESTServerWithDefaultKeystoreType(); 226 227 Response response = sslClient.get("/version", Constants.MIMETYPE_TEXT); 228 assertEquals(200, response.getCode()); 229 } 230 231 /** 232 * With {@code client.auth.mode=WANT}, an anonymous client (no client cert) is still accepted; the 233 * server requests a cert but does not require it. 234 */ 235 @Test 236 public void testClientAuthWantAllowsAnonymousClient() throws Exception { 237 conf.set(Constants.REST_SSL_CLIENT_AUTH_MODE, "WANT"); 238 startRESTServerWithDefaultKeystoreType(); 239 240 Response response = sslClient.get("/version", Constants.MIMETYPE_TEXT); 241 assertEquals(200, response.getCode()); 242 } 243 244 /** 245 * With {@code client.auth.mode=NEED}, an anonymous client (no client cert) is rejected by the 246 * server. The base {@link Client} configures truststore-only, so it presents no key material to 247 * the server. 248 */ 249 @Test 250 public void testClientAuthNeedRejectsClientWithoutCert() throws Exception { 251 conf.set(Constants.REST_SSL_CLIENT_AUTH_MODE, "NEED"); 252 startRESTServerWithDefaultKeystoreType(); 253 assertThrows(IOException.class, () -> sslClient.get("/version")); 254 } 255 256 private static File initKeystoreDir() { 257 String dataTestDir = TEST_UTIL.getDataTestDir().toString(); 258 File keystoreDir = new File(dataTestDir, TestRESTServerSSL.class.getSimpleName() + "_keys"); 259 keystoreDir.mkdirs(); 260 return keystoreDir; 261 } 262 263 private static void generateKeyStore(String keyStoreType, KeyPair keyPair, 264 X509Certificate serverCertificate) throws Exception { 265 String keyStorePath = getKeystoreFilePath(keyStoreType); 266 KeyStoreTestUtil.createKeyStore(keyStorePath, KEY_STORE_PASSWORD, KEY_STORE_PASSWORD, 267 "serverKS", keyPair.getPrivate(), serverCertificate, keyStoreType); 268 } 269 270 private static void generateTrustStore(String trustStoreType, X509Certificate serverCertificate) 271 throws Exception { 272 String trustStorePath = getTruststoreFilePath(trustStoreType); 273 KeyStoreTestUtil.createTrustStore(trustStorePath, TRUST_STORE_PASSWORD, "serverTS", 274 serverCertificate, trustStoreType); 275 } 276 277 private static String getKeystoreFilePath(String keyStoreType) { 278 return String.format("%s/serverKS.%s", keyDir.getAbsolutePath(), keyStoreType); 279 } 280 281 private static String getTruststoreFilePath(String trustStoreType) { 282 return String.format("%s/serverTS.%s", keyDir.getAbsolutePath(), trustStoreType); 283 } 284 285 private void startRESTServerWithDefaultKeystoreType() throws Exception { 286 conf.set(Constants.REST_SSL_KEYSTORE_STORE, getKeystoreFilePath("jks")); 287 conf.set(Constants.REST_SSL_TRUSTSTORE_STORE, getTruststoreFilePath("jks")); 288 289 REST_TEST_UTIL.startServletContainer(conf); 290 Cluster localCluster = new Cluster().add("localhost", REST_TEST_UTIL.getServletPort()); 291 sslClient = new Client(localCluster, getTruststoreFilePath("jks"), 292 Optional.of(TRUST_STORE_PASSWORD), Optional.empty()); 293 } 294 295 private void startRESTServer(String storeType) throws Exception { 296 conf.set(Constants.REST_SSL_KEYSTORE_TYPE, storeType); 297 conf.set(Constants.REST_SSL_KEYSTORE_STORE, getKeystoreFilePath(storeType)); 298 299 conf.set(Constants.REST_SSL_TRUSTSTORE_STORE, getTruststoreFilePath(storeType)); 300 conf.set(Constants.REST_SSL_TRUSTSTORE_TYPE, storeType); 301 302 REST_TEST_UTIL.startServletContainer(conf); 303 Cluster localCluster = new Cluster().add("localhost", REST_TEST_UTIL.getServletPort()); 304 sslClient = new Client(localCluster, getTruststoreFilePath(storeType), 305 Optional.of(TRUST_STORE_PASSWORD), Optional.of(storeType)); 306 } 307}