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