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.http.ssl;
019
020import java.io.File;
021import java.io.FileOutputStream;
022import java.io.IOException;
023import java.io.Writer;
024import java.math.BigInteger;
025import java.net.URL;
026import java.nio.charset.StandardCharsets;
027import java.security.GeneralSecurityException;
028import java.security.InvalidKeyException;
029import java.security.Key;
030import java.security.KeyPair;
031import java.security.KeyPairGenerator;
032import java.security.KeyStore;
033import java.security.NoSuchAlgorithmException;
034import java.security.NoSuchProviderException;
035import java.security.SecureRandom;
036import java.security.SignatureException;
037import java.security.cert.Certificate;
038import java.security.cert.CertificateEncodingException;
039import java.security.cert.X509Certificate;
040import java.util.Date;
041import java.util.HashMap;
042import java.util.Map;
043import javax.security.auth.x500.X500Principal;
044import org.apache.hadoop.conf.Configuration;
045import org.apache.hadoop.hbase.HBaseCommonTestingUtil;
046import org.apache.hadoop.security.ssl.FileBasedKeyStoresFactory;
047import org.apache.hadoop.security.ssl.SSLFactory;
048import org.bouncycastle.x509.X509V1CertificateGenerator;
049
050import org.apache.hbase.thirdparty.com.google.common.io.Files;
051
052public final class KeyStoreTestUtil {
053  private KeyStoreTestUtil() {
054  }
055
056  public static String getClasspathDir(Class<?> klass) throws Exception {
057    String file = klass.getName();
058    file = file.replace('.', '/') + ".class";
059    URL url = Thread.currentThread().getContextClassLoader().getResource(file);
060    String baseDir = url.toURI().getPath();
061    baseDir = baseDir.substring(0, baseDir.length() - file.length() - 1);
062    return baseDir;
063  }
064
065  /**
066   * Create a self-signed X.509 Certificate.
067   * @param dn        the X.509 Distinguished Name, eg "CN=Test, L=London, C=GB"
068   * @param pair      the KeyPair
069   * @param days      how many days from now the Certificate is valid for
070   * @param algorithm the signing algorithm, eg "SHA1withRSA"
071   * @return the self-signed certificate
072   */
073  @SuppressWarnings("JavaUtilDate")
074  public static X509Certificate generateCertificate(String dn, KeyPair pair, int days,
075    String algorithm) throws CertificateEncodingException, InvalidKeyException,
076    IllegalStateException, NoSuchProviderException, NoSuchAlgorithmException, SignatureException {
077    Date from = new Date();
078    Date to = new Date(from.getTime() + days * 86400000L);
079    BigInteger sn = new BigInteger(64, new SecureRandom());
080    KeyPair keyPair = pair;
081    X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
082    X500Principal dnName = new X500Principal(dn);
083
084    certGen.setSerialNumber(sn);
085    certGen.setIssuerDN(dnName);
086    certGen.setNotBefore(from);
087    certGen.setNotAfter(to);
088    certGen.setSubjectDN(dnName);
089    certGen.setPublicKey(keyPair.getPublic());
090    certGen.setSignatureAlgorithm(algorithm);
091    X509Certificate cert = certGen.generate(pair.getPrivate());
092    return cert;
093  }
094
095  public static KeyPair generateKeyPair(String algorithm) throws NoSuchAlgorithmException {
096    KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm);
097    keyGen.initialize(1024);
098    return keyGen.genKeyPair();
099  }
100
101  private static KeyStore createEmptyKeyStore() throws GeneralSecurityException, IOException {
102    return createEmptyKeyStore("jks");
103  }
104
105  private static KeyStore createEmptyKeyStore(String keyStoreType)
106    throws GeneralSecurityException, IOException {
107    KeyStore ks = KeyStore.getInstance(keyStoreType);
108    ks.load(null, null); // initialize
109    return ks;
110  }
111
112  private static void saveKeyStore(KeyStore ks, String filename, String password)
113    throws GeneralSecurityException, IOException {
114    FileOutputStream out = new FileOutputStream(filename);
115    try {
116      ks.store(out, password.toCharArray());
117    } finally {
118      out.close();
119    }
120  }
121
122  /**
123   * Creates a keystore with a single key and saves it to a file. This method will use the same
124   * password for the keystore and for the key. This method will always generate a keystore file in
125   * JKS format.
126   * @param filename   String file to save
127   * @param password   String store password to set on keystore
128   * @param alias      String alias to use for the key
129   * @param privateKey Key to save in keystore
130   * @param cert       Certificate to use as certificate chain associated to key
131   * @throws GeneralSecurityException for any error with the security APIs
132   * @throws IOException              if there is an I/O error saving the file
133   */
134  public static void createKeyStore(String filename, String password, String alias, Key privateKey,
135    Certificate cert) throws GeneralSecurityException, IOException {
136    createKeyStore(filename, password, password, alias, privateKey, cert);
137  }
138
139  /**
140   * Creates a keystore with a single key and saves it to a file. This method will always generate a
141   * keystore file in JKS format.
142   * @param filename    String file to save
143   * @param password    String store password to set on keystore
144   * @param keyPassword String key password to set on key
145   * @param alias       String alias to use for the key
146   * @param privateKey  Key to save in keystore
147   * @param cert        Certificate to use as certificate chain associated to key
148   * @throws GeneralSecurityException for any error with the security APIs
149   * @throws IOException              if there is an I/O error saving the file
150   */
151  public static void createKeyStore(String filename, String password, String keyPassword,
152    String alias, Key privateKey, Certificate cert) throws GeneralSecurityException, IOException {
153    createKeyStore(filename, password, keyPassword, alias, privateKey, cert, "JKS");
154  }
155
156  /**
157   * Creates a keystore with a single key and saves it to a file.
158   * @param filename     String file to save
159   * @param password     String store password to set on keystore
160   * @param keyPassword  String key password to set on key
161   * @param alias        String alias to use for the key
162   * @param privateKey   Key to save in keystore
163   * @param cert         Certificate to use as certificate chain associated to key
164   * @param keystoreType String keystore file type (e.g. "JKS")
165   * @throws GeneralSecurityException for any error with the security APIs
166   * @throws IOException              if there is an I/O error saving the file
167   */
168  public static void createKeyStore(String filename, String password, String keyPassword,
169    String alias, Key privateKey, Certificate cert, String keystoreType)
170    throws GeneralSecurityException, IOException {
171    KeyStore ks = createEmptyKeyStore(keystoreType);
172    ks.setKeyEntry(alias, privateKey, keyPassword.toCharArray(), new Certificate[] { cert });
173    saveKeyStore(ks, filename, password);
174  }
175
176  /**
177   * Creates a truststore with a single certificate and saves it to a file. This method uses the
178   * default JKS truststore type.
179   * @param filename String file to save
180   * @param password String store password to set on truststore
181   * @param alias    String alias to use for the certificate
182   * @param cert     Certificate to add
183   * @throws GeneralSecurityException for any error with the security APIs
184   * @throws IOException              if there is an I/O error saving the file
185   */
186  public static void createTrustStore(String filename, String password, String alias,
187    Certificate cert) throws GeneralSecurityException, IOException {
188    createTrustStore(filename, password, alias, cert, "JKS");
189  }
190
191  /**
192   * Creates a truststore with a single certificate and saves it to a file.
193   * @param filename       String file to save
194   * @param password       String store password to set on truststore
195   * @param alias          String alias to use for the certificate
196   * @param cert           Certificate to add
197   * @param trustStoreType String keystore file type (e.g. "JKS")
198   * @throws GeneralSecurityException for any error with the security APIs
199   * @throws IOException              if there is an I/O error saving the file
200   */
201  public static void createTrustStore(String filename, String password, String alias,
202    Certificate cert, String trustStoreType) throws GeneralSecurityException, IOException {
203    KeyStore ks = createEmptyKeyStore(trustStoreType);
204    ks.setCertificateEntry(alias, cert);
205    saveKeyStore(ks, filename, password);
206  }
207
208  public static <T extends Certificate> void createTrustStore(String filename, String password,
209    Map<String, T> certs) throws GeneralSecurityException, IOException {
210    KeyStore ks = createEmptyKeyStore();
211    for (Map.Entry<String, T> cert : certs.entrySet()) {
212      ks.setCertificateEntry(cert.getKey(), cert.getValue());
213    }
214    saveKeyStore(ks, filename, password);
215  }
216
217  public static void cleanupSSLConfig(Configuration conf) throws Exception {
218    File f = new File(conf.get(FileBasedKeyStoresFactory.resolvePropertyName(SSLFactory.Mode.SERVER,
219      FileBasedKeyStoresFactory.SSL_TRUSTSTORE_LOCATION_TPL_KEY)));
220    f.delete();
221    f = new File(conf.get(FileBasedKeyStoresFactory.resolvePropertyName(SSLFactory.Mode.SERVER,
222      FileBasedKeyStoresFactory.SSL_KEYSTORE_LOCATION_TPL_KEY)));
223    f.delete();
224
225    String clientKeyStore =
226      conf.get(FileBasedKeyStoresFactory.resolvePropertyName(SSLFactory.Mode.CLIENT,
227        FileBasedKeyStoresFactory.SSL_KEYSTORE_LOCATION_TPL_KEY));
228    if (clientKeyStore != null) {
229      f = new File(clientKeyStore);
230      f.delete();
231    }
232    f = new File(KeyStoreTestUtil.getClasspathDir(KeyStoreTestUtil.class) + "/"
233      + conf.get(SSLFactory.SSL_CLIENT_CONF_KEY));
234    f.delete();
235    f = new File(KeyStoreTestUtil.getClasspathDir(KeyStoreTestUtil.class) + "/"
236      + conf.get(SSLFactory.SSL_SERVER_CONF_KEY));
237    f.delete();
238  }
239
240  /**
241   * Performs complete setup of SSL configuration in preparation for testing an SSLFactory. This
242   * includes keys, certs, keystores, truststores, the server SSL configuration file, the client SSL
243   * configuration file, and the master configuration file read by the SSLFactory.
244   * @param keystoresDir  String directory to save keystores
245   * @param sslConfDir    String directory to save SSL configuration files
246   * @param conf          Configuration master configuration to be used by an SSLFactory, which will
247   *                      be mutated by this method
248   * @param useClientCert boolean true to make the client present a cert in the SSL handshake
249   */
250  public static void setupSSLConfig(String keystoresDir, String sslConfDir, Configuration conf,
251    boolean useClientCert) throws Exception {
252    String clientKS = keystoresDir + "/clientKS.jks";
253    String clientPassword = "clientP";
254    String serverKS = keystoresDir + "/serverKS.jks";
255    String serverPassword = "serverP";
256    String trustKS = keystoresDir + "/trustKS.jks";
257    String trustPassword = "trustP";
258
259    File sslClientConfFile = new File(sslConfDir + "/ssl-client-" + System.nanoTime() + "-"
260      + HBaseCommonTestingUtil.getRandomUUID() + ".xml");
261    File sslServerConfFile = new File(sslConfDir + "/ssl-server-" + System.nanoTime() + "-"
262      + HBaseCommonTestingUtil.getRandomUUID() + ".xml");
263
264    Map<String, X509Certificate> certs = new HashMap<>();
265
266    if (useClientCert) {
267      KeyPair cKP = KeyStoreTestUtil.generateKeyPair("RSA");
268      X509Certificate cCert =
269        KeyStoreTestUtil.generateCertificate("CN=localhost, O=client", cKP, 30, "SHA1withRSA");
270      KeyStoreTestUtil.createKeyStore(clientKS, clientPassword, "client", cKP.getPrivate(), cCert);
271      certs.put("client", cCert);
272    }
273
274    KeyPair sKP = KeyStoreTestUtil.generateKeyPair("RSA");
275    X509Certificate sCert =
276      KeyStoreTestUtil.generateCertificate("CN=localhost, O=server", sKP, 30, "SHA1withRSA");
277    KeyStoreTestUtil.createKeyStore(serverKS, serverPassword, "server", sKP.getPrivate(), sCert);
278    certs.put("server", sCert);
279
280    KeyStoreTestUtil.createTrustStore(trustKS, trustPassword, certs);
281
282    Configuration clientSSLConf =
283      createClientSSLConfig(clientKS, clientPassword, clientPassword, trustKS);
284    Configuration serverSSLConf =
285      createServerSSLConfig(serverKS, serverPassword, serverPassword, trustKS);
286
287    saveConfig(sslClientConfFile, clientSSLConf);
288    saveConfig(sslServerConfFile, serverSSLConf);
289
290    conf.set(SSLFactory.SSL_HOSTNAME_VERIFIER_KEY, "ALLOW_ALL");
291    conf.set(SSLFactory.SSL_CLIENT_CONF_KEY, sslClientConfFile.getName());
292    conf.set(SSLFactory.SSL_SERVER_CONF_KEY, sslServerConfFile.getName());
293    conf.set("dfs.https.server.keystore.resource", sslServerConfFile.getName());
294
295    conf.setBoolean(SSLFactory.SSL_REQUIRE_CLIENT_CERT_KEY, useClientCert);
296  }
297
298  /**
299   * Creates SSL configuration for a client.
300   * @param clientKS    String client keystore file
301   * @param password    String store password, or null to avoid setting store password
302   * @param keyPassword String key password, or null to avoid setting key password
303   * @param trustKS     String truststore file
304   * @return Configuration for client SSL
305   */
306  public static Configuration createClientSSLConfig(String clientKS, String password,
307    String keyPassword, String trustKS) {
308    Configuration clientSSLConf =
309      createSSLConfig(SSLFactory.Mode.CLIENT, clientKS, password, keyPassword, trustKS);
310    return clientSSLConf;
311  }
312
313  /**
314   * Creates SSL configuration for a server.
315   * @param serverKS    String server keystore file
316   * @param password    String store password, or null to avoid setting store password
317   * @param keyPassword String key password, or null to avoid setting key password
318   * @param trustKS     String truststore file
319   * @return Configuration for server SSL
320   */
321  public static Configuration createServerSSLConfig(String serverKS, String password,
322    String keyPassword, String trustKS) throws IOException {
323    Configuration serverSSLConf =
324      createSSLConfig(SSLFactory.Mode.SERVER, serverKS, password, keyPassword, trustKS);
325    return serverSSLConf;
326  }
327
328  /**
329   * Creates SSL configuration.
330   * @param mode        SSLFactory.Mode mode to configure
331   * @param keystore    String keystore file
332   * @param password    String store password, or null to avoid setting store password
333   * @param keyPassword String key password, or null to avoid setting key password
334   * @param trustKS     String truststore file
335   * @return Configuration for SSL
336   */
337  private static Configuration createSSLConfig(SSLFactory.Mode mode, String keystore,
338    String password, String keyPassword, String trustKS) {
339    String trustPassword = "trustP";
340
341    Configuration sslConf = new Configuration(false);
342    if (keystore != null) {
343      sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(mode,
344        FileBasedKeyStoresFactory.SSL_KEYSTORE_LOCATION_TPL_KEY), keystore);
345    }
346    if (password != null) {
347      sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(mode,
348        FileBasedKeyStoresFactory.SSL_KEYSTORE_PASSWORD_TPL_KEY), password);
349    }
350    if (keyPassword != null) {
351      sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(mode,
352        FileBasedKeyStoresFactory.SSL_KEYSTORE_KEYPASSWORD_TPL_KEY), keyPassword);
353    }
354    if (trustKS != null) {
355      sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(mode,
356        FileBasedKeyStoresFactory.SSL_TRUSTSTORE_LOCATION_TPL_KEY), trustKS);
357    }
358    if (trustPassword != null) {
359      sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(mode,
360        FileBasedKeyStoresFactory.SSL_TRUSTSTORE_PASSWORD_TPL_KEY), trustPassword);
361    }
362    sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(mode,
363      FileBasedKeyStoresFactory.SSL_TRUSTSTORE_RELOAD_INTERVAL_TPL_KEY), "1000");
364
365    return sslConf;
366  }
367
368  /**
369   * Saves configuration to a file.
370   * @param file File to save
371   * @param conf Configuration contents to write to file
372   * @throws IOException if there is an I/O error saving the file
373   */
374  public static void saveConfig(File file, Configuration conf) throws IOException {
375    try (Writer writer = Files.newWriter(file, StandardCharsets.UTF_8)) {
376      conf.writeXml(writer);
377    }
378  }
379}