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