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.lang.reflect.Method;
025import java.security.KeyPair;
026import java.security.cert.X509Certificate;
027import java.util.Optional;
028import org.apache.hadoop.conf.Configuration;
029import org.apache.hadoop.hbase.HBaseTestingUtil;
030import org.apache.hadoop.hbase.http.ssl.KeyStoreTestUtil;
031import org.apache.hadoop.hbase.rest.client.Client;
032import org.apache.hadoop.hbase.rest.client.Cluster;
033import org.apache.hadoop.hbase.rest.client.Response;
034import org.apache.hadoop.hbase.testclassification.MediumTests;
035import org.apache.hadoop.hbase.testclassification.RestTests;
036import org.apache.http.client.ClientProtocolException;
037import org.junit.jupiter.api.AfterAll;
038import org.junit.jupiter.api.AfterEach;
039import org.junit.jupiter.api.BeforeAll;
040import org.junit.jupiter.api.BeforeEach;
041import org.junit.jupiter.api.Tag;
042import org.junit.jupiter.api.Test;
043import org.slf4j.Logger;
044import org.slf4j.LoggerFactory;
045
046@Tag(RestTests.TAG)
047@Tag(MediumTests.TAG)
048public class TestRESTServerSSL {
049
050  private static final Logger LOG = LoggerFactory.getLogger(TestRESTServerSSL.class);
051
052  private static final String KEY_STORE_PASSWORD = "myKSPassword";
053  private static final String TRUST_STORE_PASSWORD = "myTSPassword";
054
055  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
056  private static final HBaseRESTTestingUtility REST_TEST_UTIL = new HBaseRESTTestingUtility();
057  private static Client sslClient;
058  private static File keyDir;
059  private Configuration conf;
060
061  // Workaround for jdk8 292 bug. See https://github.com/bcgit/bc-java/issues/941
062  // Below is a workaround described in above URL. Issue fingered first in comments in
063  // HBASE-25920 Support Hadoop 3.3.1
064  private static void initializeAlgorithmId() {
065    try {
066      Class<?> algoId = Class.forName("sun.security.x509.AlgorithmId");
067      Method method = algoId.getMethod("get", String.class);
068      method.setAccessible(true);
069      method.invoke(null, "PBEWithSHA1AndDESede");
070    } catch (Exception e) {
071      LOG.warn("failed to initialize AlgorithmId", e);
072    }
073  }
074
075  @BeforeAll
076  public static void beforeClass() throws Exception {
077    initializeAlgorithmId();
078    keyDir = initKeystoreDir();
079    KeyPair keyPair = KeyStoreTestUtil.generateKeyPair("RSA");
080    X509Certificate serverCertificate =
081      KeyStoreTestUtil.generateCertificate("CN=localhost, O=server", keyPair, 30, "SHA1withRSA");
082
083    generateTrustStore("jks", serverCertificate);
084    generateTrustStore("jceks", serverCertificate);
085    generateTrustStore("pkcs12", serverCertificate);
086
087    generateKeyStore("jks", keyPair, serverCertificate);
088    generateKeyStore("jceks", keyPair, serverCertificate);
089    generateKeyStore("pkcs12", keyPair, serverCertificate);
090
091    TEST_UTIL.startMiniCluster();
092  }
093
094  @AfterAll
095  public static void afterClass() throws Exception {
096    // this will also delete the generated test keystore / teststore files,
097    // as we were placing them under the dataTestDir used by the minicluster
098    TEST_UTIL.shutdownMiniCluster();
099  }
100
101  @BeforeEach
102  public void beforeEachTest() {
103    conf = new Configuration(TEST_UTIL.getConfiguration());
104    conf.set(Constants.REST_SSL_ENABLED, "true");
105    conf.set(Constants.REST_SSL_KEYSTORE_KEYPASSWORD, KEY_STORE_PASSWORD);
106    conf.set(Constants.REST_SSL_KEYSTORE_PASSWORD, KEY_STORE_PASSWORD);
107    conf.set(Constants.REST_SSL_TRUSTSTORE_PASSWORD, TRUST_STORE_PASSWORD);
108  }
109
110  @AfterEach
111  public void tearDownAfterTest() {
112    REST_TEST_UTIL.shutdownServletContainer();
113  }
114
115  @Test
116  public void testSslConnection() throws Exception {
117    startRESTServerWithDefaultKeystoreType();
118
119    Response response = sslClient.get("/version", Constants.MIMETYPE_TEXT);
120    assertEquals(200, response.getCode());
121
122    // Default security headers
123    assertEquals("max-age=63072000;includeSubDomains;preload",
124      response.getHeader("Strict-Transport-Security"));
125    assertEquals("default-src https: data: 'unsafe-inline' 'unsafe-eval'",
126      response.getHeader("Content-Security-Policy"));
127  }
128
129  @Test
130  public void testNonSslClientDenied() throws Exception {
131    startRESTServerWithDefaultKeystoreType();
132
133    Cluster localCluster = new Cluster().add("localhost", REST_TEST_UTIL.getServletPort());
134    Client nonSslClient = new Client(localCluster, false);
135
136    assertThrows(ClientProtocolException.class, () -> nonSslClient.get("/version"));
137  }
138
139  @Test
140  public void testSslConnectionUsingKeystoreFormatJKS() throws Exception {
141    startRESTServer("jks");
142
143    Response response = sslClient.get("/version", Constants.MIMETYPE_TEXT);
144    assertEquals(200, response.getCode());
145  }
146
147  @Test
148  public void testSslConnectionUsingKeystoreFormatJCEKS() throws Exception {
149    startRESTServer("jceks");
150
151    Response response = sslClient.get("/version", Constants.MIMETYPE_TEXT);
152    assertEquals(200, response.getCode());
153  }
154
155  @Test
156  public void testSslConnectionUsingKeystoreFormatPKCS12() throws Exception {
157    startRESTServer("pkcs12");
158
159    Response response = sslClient.get("/version", Constants.MIMETYPE_TEXT);
160    assertEquals(200, response.getCode());
161  }
162
163  private static File initKeystoreDir() {
164    String dataTestDir = TEST_UTIL.getDataTestDir().toString();
165    File keystoreDir = new File(dataTestDir, TestRESTServerSSL.class.getSimpleName() + "_keys");
166    keystoreDir.mkdirs();
167    return keystoreDir;
168  }
169
170  private static void generateKeyStore(String keyStoreType, KeyPair keyPair,
171    X509Certificate serverCertificate) throws Exception {
172    String keyStorePath = getKeystoreFilePath(keyStoreType);
173    KeyStoreTestUtil.createKeyStore(keyStorePath, KEY_STORE_PASSWORD, KEY_STORE_PASSWORD,
174      "serverKS", keyPair.getPrivate(), serverCertificate, keyStoreType);
175  }
176
177  private static void generateTrustStore(String trustStoreType, X509Certificate serverCertificate)
178    throws Exception {
179    String trustStorePath = getTruststoreFilePath(trustStoreType);
180    KeyStoreTestUtil.createTrustStore(trustStorePath, TRUST_STORE_PASSWORD, "serverTS",
181      serverCertificate, trustStoreType);
182  }
183
184  private static String getKeystoreFilePath(String keyStoreType) {
185    return String.format("%s/serverKS.%s", keyDir.getAbsolutePath(), keyStoreType);
186  }
187
188  private static String getTruststoreFilePath(String trustStoreType) {
189    return String.format("%s/serverTS.%s", keyDir.getAbsolutePath(), trustStoreType);
190  }
191
192  private void startRESTServerWithDefaultKeystoreType() throws Exception {
193    conf.set(Constants.REST_SSL_KEYSTORE_STORE, getKeystoreFilePath("jks"));
194    conf.set(Constants.REST_SSL_TRUSTSTORE_STORE, getTruststoreFilePath("jks"));
195
196    REST_TEST_UTIL.startServletContainer(conf);
197    Cluster localCluster = new Cluster().add("localhost", REST_TEST_UTIL.getServletPort());
198    sslClient = new Client(localCluster, getTruststoreFilePath("jks"),
199      Optional.of(TRUST_STORE_PASSWORD), Optional.empty());
200  }
201
202  private void startRESTServer(String storeType) throws Exception {
203    conf.set(Constants.REST_SSL_KEYSTORE_TYPE, storeType);
204    conf.set(Constants.REST_SSL_KEYSTORE_STORE, getKeystoreFilePath(storeType));
205
206    conf.set(Constants.REST_SSL_TRUSTSTORE_STORE, getTruststoreFilePath(storeType));
207    conf.set(Constants.REST_SSL_TRUSTSTORE_TYPE, storeType);
208
209    REST_TEST_UTIL.startServletContainer(conf);
210    Cluster localCluster = new Cluster().add("localhost", REST_TEST_UTIL.getServletPort());
211    sslClient = new Client(localCluster, getTruststoreFilePath(storeType),
212      Optional.of(TRUST_STORE_PASSWORD), Optional.of(storeType));
213  }
214}