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