diff --git a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/ssl/KeyStoreTestUtil.java b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/ssl/KeyStoreTestUtil.java index c8abd9c6ceb..c201c7a5232 100644 --- a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/ssl/KeyStoreTestUtil.java +++ b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/ssl/KeyStoreTestUtil.java @@ -101,7 +101,12 @@ public final class KeyStoreTestUtil { private static KeyStore createEmptyKeyStore() throws GeneralSecurityException, IOException { - KeyStore ks = KeyStore.getInstance("JKS"); + return createEmptyKeyStore("jks"); + } + + private static KeyStore createEmptyKeyStore(String keyStoreType) + throws GeneralSecurityException, IOException { + KeyStore ks = KeyStore.getInstance(keyStoreType); ks.load(null, null); // initialize return ks; } @@ -117,18 +122,29 @@ public final class KeyStoreTestUtil { } } + /** + * Creates a keystore with a single key and saves it to a file. + * This method will use the same password for the keystore and for the key. + * This method will always generate a keystore file in JKS format. + * + * @param filename String file to save + * @param password String store password to set on keystore + * @param alias String alias to use for the key + * @param privateKey Key to save in keystore + * @param cert Certificate to use as certificate chain associated to key + * @throws GeneralSecurityException for any error with the security APIs + * @throws IOException if there is an I/O error saving the file + */ public static void createKeyStore(String filename, String password, String alias, Key privateKey, Certificate cert) throws GeneralSecurityException, IOException { - KeyStore ks = createEmptyKeyStore(); - ks.setKeyEntry(alias, privateKey, password.toCharArray(), - new Certificate[]{cert}); - saveKeyStore(ks, filename, password); + createKeyStore(filename, password, password, alias, privateKey, cert); } /** * Creates a keystore with a single key and saves it to a file. + * This method will always generate a keystore file in JKS format. * * @param filename String file to save * @param password String store password to set on keystore @@ -143,17 +159,66 @@ public final class KeyStoreTestUtil { String password, String keyPassword, String alias, Key privateKey, Certificate cert) throws GeneralSecurityException, IOException { - KeyStore ks = createEmptyKeyStore(); + createKeyStore(filename, password, keyPassword, alias, privateKey, cert, "JKS"); + } + + + /** + * Creates a keystore with a single key and saves it to a file. + * + * @param filename String file to save + * @param password String store password to set on keystore + * @param keyPassword String key password to set on key + * @param alias String alias to use for the key + * @param privateKey Key to save in keystore + * @param cert Certificate to use as certificate chain associated to key + * @param keystoreType String keystore file type (e.g. "JKS") + * @throws GeneralSecurityException for any error with the security APIs + * @throws IOException if there is an I/O error saving the file + */ + public static void createKeyStore(String filename, String password, String keyPassword, + String alias, Key privateKey, Certificate cert, + String keystoreType) + throws GeneralSecurityException, IOException { + KeyStore ks = createEmptyKeyStore(keystoreType); ks.setKeyEntry(alias, privateKey, keyPassword.toCharArray(), new Certificate[]{cert}); saveKeyStore(ks, filename, password); } + /** + * Creates a truststore with a single certificate and saves it to a file. + * This method uses the default JKS truststore type. + * + * @param filename String file to save + * @param password String store password to set on truststore + * @param alias String alias to use for the certificate + * @param cert Certificate to add + * @throws GeneralSecurityException for any error with the security APIs + * @throws IOException if there is an I/O error saving the file + */ public static void createTrustStore(String filename, String password, String alias, Certificate cert) throws GeneralSecurityException, IOException { - KeyStore ks = createEmptyKeyStore(); + createTrustStore(filename, password, alias, cert, "JKS"); + } + + /** + * Creates a truststore with a single certificate and saves it to a file. + * + * @param filename String file to save + * @param password String store password to set on truststore + * @param alias String alias to use for the certificate + * @param cert Certificate to add + * @param trustStoreType String keystore file type (e.g. "JKS") + * @throws GeneralSecurityException for any error with the security APIs + * @throws IOException if there is an I/O error saving the file + */ + public static void createTrustStore(String filename, String password, String alias, + Certificate cert, String trustStoreType) + throws GeneralSecurityException, IOException { + KeyStore ks = createEmptyKeyStore(trustStoreType); ks.setCertificateEntry(alias, cert); saveKeyStore(ks, filename, password); } diff --git a/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/Constants.java b/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/Constants.java index 4cf8a93ed5b..704eac78db5 100644 --- a/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/Constants.java +++ b/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/Constants.java @@ -51,6 +51,10 @@ public interface Constants { String REST_SSL_ENABLED = "hbase.rest.ssl.enabled"; String REST_SSL_KEYSTORE_STORE = "hbase.rest.ssl.keystore.store"; String REST_SSL_KEYSTORE_PASSWORD = "hbase.rest.ssl.keystore.password"; + String REST_SSL_KEYSTORE_TYPE = "hbase.rest.ssl.keystore.type"; + String REST_SSL_TRUSTSTORE_STORE = "hbase.rest.ssl.truststore.store"; + String REST_SSL_TRUSTSTORE_PASSWORD = "hbase.rest.ssl.truststore.password"; + String REST_SSL_TRUSTSTORE_TYPE = "hbase.rest.ssl.truststore.type"; String REST_SSL_KEYSTORE_KEYPASSWORD = "hbase.rest.ssl.keystore.keypassword"; String REST_SSL_EXCLUDE_CIPHER_SUITES = "hbase.rest.ssl.exclude.cipher.suites"; String REST_SSL_INCLUDE_CIPHER_SUITES = "hbase.rest.ssl.include.cipher.suites"; diff --git a/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/RESTServer.java b/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/RESTServer.java index c6f769ee605..4e6adfb6d7c 100644 --- a/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/RESTServer.java +++ b/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/RESTServer.java @@ -27,6 +27,7 @@ import java.util.Map; import java.util.concurrent.ArrayBlockingQueue; import javax.servlet.DispatcherType; import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.hadoop.hbase.HBaseInterfaceAudience; @@ -305,14 +306,32 @@ public class RESTServer implements Constants { SslContextFactory sslCtxFactory = new SslContextFactory(); String keystore = conf.get(REST_SSL_KEYSTORE_STORE); + String keystoreType = conf.get(REST_SSL_KEYSTORE_TYPE); String password = HBaseConfiguration.getPassword(conf, REST_SSL_KEYSTORE_PASSWORD, null); String keyPassword = HBaseConfiguration.getPassword(conf, REST_SSL_KEYSTORE_KEYPASSWORD, password); sslCtxFactory.setKeyStorePath(keystore); + if(StringUtils.isNotBlank(keystoreType)) { + sslCtxFactory.setKeyStoreType(keystoreType); + } sslCtxFactory.setKeyStorePassword(password); sslCtxFactory.setKeyManagerPassword(keyPassword); + String trustStore = conf.get(REST_SSL_TRUSTSTORE_STORE); + if(StringUtils.isNotBlank(trustStore)) { + sslCtxFactory.setTrustStorePath(trustStore); + } + String trustStorePassword = + HBaseConfiguration.getPassword(conf, REST_SSL_TRUSTSTORE_PASSWORD, null); + if(StringUtils.isNotBlank(trustStorePassword)) { + sslCtxFactory.setTrustStorePassword(trustStorePassword); + } + String trustStoreType = conf.get(REST_SSL_TRUSTSTORE_TYPE); + if(StringUtils.isNotBlank(trustStoreType)) { + sslCtxFactory.setTrustStoreType(trustStoreType); + } + String[] excludeCiphers = servlet.getConfiguration().getStrings( REST_SSL_EXCLUDE_CIPHER_SUITES, ArrayUtils.EMPTY_STRING_ARRAY); if (excludeCiphers.length != 0) { diff --git a/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/client/Client.java b/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/client/Client.java index 9e6661bd2aa..47700aa9e4f 100644 --- a/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/client/Client.java +++ b/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/client/Client.java @@ -21,15 +21,23 @@ package org.apache.hadoop.hbase.rest.client; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; import java.util.Collections; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; - +import javax.net.ssl.SSLContext; import org.apache.hadoop.security.authentication.client.AuthenticatedURL; import org.apache.hadoop.security.authentication.client.AuthenticationException; import org.apache.hadoop.security.authentication.client.KerberosAuthenticator; @@ -37,6 +45,7 @@ import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpHead; @@ -44,9 +53,10 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.InputStreamEntity; -import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicHeader; -import org.apache.http.params.CoreConnectionPNames; +import org.apache.http.ssl.SSLContexts; import org.apache.http.util.EntityUtils; import org.apache.yetus.audience.InterfaceAudience; import org.slf4j.Logger; @@ -81,14 +91,35 @@ public class Client { this(null); } - private void initialize(Cluster cluster, boolean sslEnabled) { + private void initialize(Cluster cluster, boolean sslEnabled, Optional trustStore) { this.cluster = cluster; this.sslEnabled = sslEnabled; extraHeaders = new ConcurrentHashMap<>(); String clspath = System.getProperty("java.class.path"); LOG.debug("classpath " + clspath); - this.httpClient = new DefaultHttpClient(); - this.httpClient.getParams().setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 2000); + HttpClientBuilder httpClientBuilder = HttpClients.custom(); + + RequestConfig requestConfig = RequestConfig.custom(). + setConnectTimeout(2000).build(); + httpClientBuilder.setDefaultRequestConfig(requestConfig); + + // Since HBASE-25267 we don't use the deprecated DefaultHttpClient anymore. + // The new http client would decompress the gzip content automatically. + // In order to keep the original behaviour of this public class, we disable + // automatic content compression. + httpClientBuilder.disableContentCompression(); + + if(sslEnabled && trustStore.isPresent()) { + try { + SSLContext sslcontext = + SSLContexts.custom().loadTrustMaterial(trustStore.get(), null).build(); + httpClientBuilder.setSSLContext(sslcontext); + } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { + throw new ClientTrustStoreInitializationException("Error while processing truststore", e); + } + } + + this.httpClient = httpClientBuilder.build(); } /** @@ -96,7 +127,7 @@ public class Client { * @param cluster the cluster definition */ public Client(Cluster cluster) { - initialize(cluster, false); + this(cluster, false); } /** @@ -105,7 +136,38 @@ public class Client { * @param sslEnabled enable SSL or not */ public Client(Cluster cluster, boolean sslEnabled) { - initialize(cluster, sslEnabled); + initialize(cluster, sslEnabled, Optional.empty()); + } + + /** + * Constructor, allowing to define custom trust store (only for SSL connections) + * + * @param cluster the cluster definition + * @param trustStorePath custom trust store to use for SSL connections + * @param trustStorePassword password to use for custom trust store + * @param trustStoreType type of custom trust store + * + * @throws ClientTrustStoreInitializationException if the trust store file can not be loaded + */ + public Client(Cluster cluster, String trustStorePath, + Optional trustStorePassword, Optional trustStoreType) { + + char[] password = trustStorePassword.map(String::toCharArray).orElse(null); + String type = trustStoreType.orElse(KeyStore.getDefaultType()); + + KeyStore trustStore; + try(FileInputStream inputStream = new FileInputStream(new File(trustStorePath))) { + trustStore = KeyStore.getInstance(type); + trustStore.load(inputStream, password); + } catch (KeyStoreException e) { + throw new ClientTrustStoreInitializationException( + "Invalid trust store type: " + type, e); + } catch (CertificateException | NoSuchAlgorithmException | IOException e) { + throw new ClientTrustStoreInitializationException( + "Trust store load error: " + trustStorePath, e); + } + + initialize(cluster, true, Optional.of(trustStore)); } /** @@ -724,4 +786,12 @@ public class Client { method.releaseConnection(); } } + + + public static class ClientTrustStoreInitializationException extends RuntimeException { + + public ClientTrustStoreInitializationException(String message, Throwable cause) { + super(message, cause); + } + } } diff --git a/hbase-rest/src/test/java/org/apache/hadoop/hbase/rest/TestRESTServerSSL.java b/hbase-rest/src/test/java/org/apache/hadoop/hbase/rest/TestRESTServerSSL.java new file mode 100644 index 00000000000..a1fe2f010fd --- /dev/null +++ b/hbase-rest/src/test/java/org/apache/hadoop/hbase/rest/TestRESTServerSSL.java @@ -0,0 +1,195 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.rest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.util.Optional; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.HBaseTestingUtility; +import org.apache.hadoop.hbase.http.ssl.KeyStoreTestUtil; +import org.apache.hadoop.hbase.rest.client.Client; +import org.apache.hadoop.hbase.rest.client.Cluster; +import org.apache.hadoop.hbase.rest.client.Response; +import org.apache.hadoop.hbase.testclassification.MediumTests; +import org.apache.hadoop.hbase.testclassification.RestTests; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +@Category({ RestTests.class, MediumTests.class}) +public class TestRESTServerSSL { + + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestRESTServerSSL.class); + + private static final String KEY_STORE_PASSWORD = "myKSPassword"; + private static final String TRUST_STORE_PASSWORD = "myTSPassword"; + + private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility(); + private static final HBaseRESTTestingUtility REST_TEST_UTIL = new HBaseRESTTestingUtility(); + private static Client sslClient; + private static File keyDir; + private Configuration conf; + + @BeforeClass + public static void beforeClass() throws Exception { + keyDir = initKeystoreDir(); + KeyPair keyPair = KeyStoreTestUtil.generateKeyPair("RSA"); + X509Certificate serverCertificate = KeyStoreTestUtil.generateCertificate( + "CN=localhost, O=server", keyPair, 30, "SHA1withRSA"); + + generateTrustStore("jks", serverCertificate); + generateTrustStore("jceks", serverCertificate); + generateTrustStore("pkcs12", serverCertificate); + + generateKeyStore("jks", keyPair, serverCertificate); + generateKeyStore("jceks", keyPair, serverCertificate); + generateKeyStore("pkcs12", keyPair, serverCertificate); + + TEST_UTIL.startMiniCluster(); + } + + @AfterClass + public static void afterClass() throws Exception { + // this will also delete the generated test keystore / teststore files, + // as we were placing them under the dataTestDir used by the minicluster + TEST_UTIL.shutdownMiniCluster(); + } + + @Before + public void beforeEachTest() { + conf = new Configuration(TEST_UTIL.getConfiguration()); + conf.set(Constants.REST_SSL_ENABLED, "true"); + conf.set(Constants.REST_SSL_KEYSTORE_KEYPASSWORD, KEY_STORE_PASSWORD); + conf.set(Constants.REST_SSL_KEYSTORE_PASSWORD, KEY_STORE_PASSWORD); + conf.set(Constants.REST_SSL_TRUSTSTORE_PASSWORD, TRUST_STORE_PASSWORD); + } + + @After + public void tearDownAfterTest() { + REST_TEST_UTIL.shutdownServletContainer(); + } + + @Test + public void testSslConnection() throws Exception { + startRESTServerWithDefaultKeystoreType(); + + Response response = sslClient.get("/version", Constants.MIMETYPE_TEXT); + assertEquals(200, response.getCode()); + } + + @Test(expected = org.apache.http.client.ClientProtocolException.class) + public void testNonSslClientDenied() throws Exception { + startRESTServerWithDefaultKeystoreType(); + + Cluster localCluster = new Cluster().add("localhost", REST_TEST_UTIL.getServletPort()); + Client nonSslClient = new Client(localCluster, false); + + nonSslClient.get("/version"); + } + + @Test + public void testSslConnectionUsingKeystoreFormatJKS() throws Exception { + startRESTServer("jks"); + + Response response = sslClient.get("/version", Constants.MIMETYPE_TEXT); + assertEquals(200, response.getCode()); + } + + @Test + public void testSslConnectionUsingKeystoreFormatJCEKS() throws Exception { + startRESTServer("jceks"); + + Response response = sslClient.get("/version", Constants.MIMETYPE_TEXT); + assertEquals(200, response.getCode()); + } + + @Test + public void testSslConnectionUsingKeystoreFormatPKCS12() throws Exception { + startRESTServer("pkcs12"); + + Response response = sslClient.get("/version", Constants.MIMETYPE_TEXT); + assertEquals(200, response.getCode()); + } + + + + private static File initKeystoreDir() { + String dataTestDir = TEST_UTIL.getDataTestDir().toString(); + File keystoreDir = new File(dataTestDir, TestRESTServerSSL.class.getSimpleName() + "_keys"); + keystoreDir.mkdirs(); + return keystoreDir; + } + + private static void generateKeyStore(String keyStoreType, KeyPair keyPair, + X509Certificate serverCertificate) throws Exception { + String keyStorePath = getKeystoreFilePath(keyStoreType); + KeyStoreTestUtil.createKeyStore(keyStorePath, KEY_STORE_PASSWORD, KEY_STORE_PASSWORD, + "serverKS", keyPair.getPrivate(), serverCertificate, keyStoreType); + } + + private static void generateTrustStore(String trustStoreType, X509Certificate serverCertificate) + throws Exception { + String trustStorePath = getTruststoreFilePath(trustStoreType); + KeyStoreTestUtil.createTrustStore(trustStorePath, TRUST_STORE_PASSWORD, "serverTS", + serverCertificate, trustStoreType); + } + + private static String getKeystoreFilePath(String keyStoreType) { + return String.format("%s/serverKS.%s", keyDir.getAbsolutePath(), keyStoreType); + } + + private static String getTruststoreFilePath(String trustStoreType) { + return String.format("%s/serverTS.%s", keyDir.getAbsolutePath(), trustStoreType); + } + + private void startRESTServerWithDefaultKeystoreType() throws Exception { + conf.set(Constants.REST_SSL_KEYSTORE_STORE, getKeystoreFilePath("jks")); + conf.set(Constants.REST_SSL_TRUSTSTORE_STORE, getTruststoreFilePath("jks")); + + REST_TEST_UTIL.startServletContainer(conf); + Cluster localCluster = new Cluster().add("localhost", REST_TEST_UTIL.getServletPort()); + sslClient = new Client(localCluster, getTruststoreFilePath("jks"), + Optional.of(TRUST_STORE_PASSWORD), Optional.empty()); + } + + private void startRESTServer(String storeType) throws Exception { + conf.set(Constants.REST_SSL_KEYSTORE_TYPE, storeType); + conf.set(Constants.REST_SSL_KEYSTORE_STORE, getKeystoreFilePath(storeType)); + + conf.set(Constants.REST_SSL_TRUSTSTORE_STORE, getTruststoreFilePath(storeType)); + conf.set(Constants.REST_SSL_TRUSTSTORE_TYPE, storeType); + + REST_TEST_UTIL.startServletContainer(conf); + Cluster localCluster = new Cluster().add("localhost", REST_TEST_UTIL.getServletPort()); + sslClient = new Client(localCluster, getTruststoreFilePath(storeType), + Optional.of(TRUST_STORE_PASSWORD), Optional.of(storeType)); + } + +}