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    KeyStore ks = KeyStore.getInstance("JKS");
105    ks.load(null, null); // initialize
106    return ks;
107  }
108
109  private static void saveKeyStore(KeyStore ks, String filename,
110                                   String password)
111    throws GeneralSecurityException, IOException {
112    FileOutputStream out = new FileOutputStream(filename);
113    try {
114      ks.store(out, password.toCharArray());
115    } finally {
116      out.close();
117    }
118  }
119
120  public static void createKeyStore(String filename,
121                                    String password, String alias,
122                                    Key privateKey, Certificate cert)
123    throws GeneralSecurityException, IOException {
124    KeyStore ks = createEmptyKeyStore();
125    ks.setKeyEntry(alias, privateKey, password.toCharArray(),
126                   new Certificate[]{cert});
127    saveKeyStore(ks, filename, password);
128  }
129
130  /**
131   * Creates a keystore with a single key and saves it to a file.
132   *
133   * @param filename String file to save
134   * @param password String store password to set on keystore
135   * @param keyPassword String key password to set on key
136   * @param alias String alias to use for the key
137   * @param privateKey Key to save in keystore
138   * @param cert Certificate to use as certificate chain associated to key
139   * @throws GeneralSecurityException for any error with the security APIs
140   * @throws IOException if there is an I/O error saving the file
141   */
142  public static void createKeyStore(String filename,
143                                    String password, String keyPassword, String alias,
144                                    Key privateKey, Certificate cert)
145    throws GeneralSecurityException, IOException {
146    KeyStore ks = createEmptyKeyStore();
147    ks.setKeyEntry(alias, privateKey, keyPassword.toCharArray(),
148                   new Certificate[]{cert});
149    saveKeyStore(ks, filename, password);
150  }
151
152  public static void createTrustStore(String filename,
153                                      String password, String alias,
154                                      Certificate cert)
155    throws GeneralSecurityException, IOException {
156    KeyStore ks = createEmptyKeyStore();
157    ks.setCertificateEntry(alias, cert);
158    saveKeyStore(ks, filename, password);
159  }
160
161  public static <T extends Certificate> void createTrustStore(
162    String filename, String password, Map<String, T> certs)
163    throws GeneralSecurityException, IOException {
164    KeyStore ks = createEmptyKeyStore();
165    for (Map.Entry<String, T> cert : certs.entrySet()) {
166      ks.setCertificateEntry(cert.getKey(), cert.getValue());
167    }
168    saveKeyStore(ks, filename, password);
169  }
170
171  public static void cleanupSSLConfig(Configuration conf)
172    throws Exception {
173    File f = new File(conf.get(FileBasedKeyStoresFactory.resolvePropertyName(SSLFactory.Mode.SERVER,
174        FileBasedKeyStoresFactory.SSL_TRUSTSTORE_LOCATION_TPL_KEY)));
175    f.delete();
176    f = new File(conf.get(FileBasedKeyStoresFactory.resolvePropertyName(SSLFactory.Mode.SERVER,
177        FileBasedKeyStoresFactory.SSL_KEYSTORE_LOCATION_TPL_KEY)));
178    f.delete();
179
180    String clientKeyStore = conf.get(FileBasedKeyStoresFactory
181        .resolvePropertyName(SSLFactory.Mode.CLIENT,
182            FileBasedKeyStoresFactory.SSL_KEYSTORE_LOCATION_TPL_KEY));
183    if (clientKeyStore != null) {
184      f = new File(clientKeyStore);
185      f.delete();
186    }
187    f = new File(KeyStoreTestUtil.getClasspathDir(KeyStoreTestUtil.class) + "/" + conf
188        .get(SSLFactory.SSL_CLIENT_CONF_KEY));
189    f.delete();
190    f = new File(KeyStoreTestUtil.getClasspathDir(KeyStoreTestUtil.class) + "/" + conf
191        .get(SSLFactory.SSL_SERVER_CONF_KEY));
192    f.delete();
193  }
194
195  /**
196   * Performs complete setup of SSL configuration in preparation for testing an
197   * SSLFactory.  This includes keys, certs, keystores, truststores, the server
198   * SSL configuration file, the client SSL configuration file, and the master
199   * configuration file read by the SSLFactory.
200   *
201   * @param keystoresDir String directory to save keystores
202   * @param sslConfDir String directory to save SSL configuration files
203   * @param conf Configuration master configuration to be used by an SSLFactory,
204   *   which will be mutated by this method
205   * @param useClientCert boolean true to make the client present a cert in the
206   *   SSL handshake
207   */
208  public static void setupSSLConfig(String keystoresDir, String sslConfDir,
209                                    Configuration conf, boolean useClientCert)
210    throws Exception {
211    String clientKS = keystoresDir + "/clientKS.jks";
212    String clientPassword = "clientP";
213    String serverKS = keystoresDir + "/serverKS.jks";
214    String serverPassword = "serverP";
215    String trustKS = keystoresDir + "/trustKS.jks";
216    String trustPassword = "trustP";
217
218    File sslClientConfFile = new File(
219        sslConfDir + "/ssl-client-" + System.nanoTime() + "-" + HBaseCommonTestingUtility
220            .getRandomUUID() + ".xml");
221    File sslServerConfFile = new File(
222        sslConfDir + "/ssl-server-" + System.nanoTime() + "-" + HBaseCommonTestingUtility
223            .getRandomUUID() + ".xml");
224
225    Map<String, X509Certificate> certs = new HashMap<>();
226
227    if (useClientCert) {
228      KeyPair cKP = KeyStoreTestUtil.generateKeyPair("RSA");
229      X509Certificate cCert =
230        KeyStoreTestUtil.generateCertificate("CN=localhost, O=client", cKP, 30,
231                                             "SHA1withRSA");
232      KeyStoreTestUtil.createKeyStore(clientKS, clientPassword, "client",
233                                      cKP.getPrivate(), cCert);
234      certs.put("client", cCert);
235    }
236
237    KeyPair sKP = KeyStoreTestUtil.generateKeyPair("RSA");
238    X509Certificate sCert =
239      KeyStoreTestUtil.generateCertificate("CN=localhost, O=server", sKP, 30,
240                                           "SHA1withRSA");
241    KeyStoreTestUtil.createKeyStore(serverKS, serverPassword, "server",
242                                    sKP.getPrivate(), sCert);
243    certs.put("server", sCert);
244
245    KeyStoreTestUtil.createTrustStore(trustKS, trustPassword, certs);
246
247    Configuration clientSSLConf = createClientSSLConfig(clientKS, clientPassword,
248      clientPassword, trustKS);
249    Configuration serverSSLConf = createServerSSLConfig(serverKS, serverPassword,
250      serverPassword, trustKS);
251
252    saveConfig(sslClientConfFile, clientSSLConf);
253    saveConfig(sslServerConfFile, serverSSLConf);
254
255    conf.set(SSLFactory.SSL_HOSTNAME_VERIFIER_KEY, "ALLOW_ALL");
256    conf.set(SSLFactory.SSL_CLIENT_CONF_KEY, sslClientConfFile.getName());
257    conf.set(SSLFactory.SSL_SERVER_CONF_KEY, sslServerConfFile.getName());
258    conf.set("dfs.https.server.keystore.resource", sslServerConfFile.getName());
259
260
261    conf.setBoolean(SSLFactory.SSL_REQUIRE_CLIENT_CERT_KEY, useClientCert);
262  }
263
264  /**
265   * Creates SSL configuration for a client.
266   *
267   * @param clientKS String client keystore file
268   * @param password String store password, or null to avoid setting store
269   *   password
270   * @param keyPassword String key password, or null to avoid setting key
271   *   password
272   * @param trustKS String truststore file
273   * @return Configuration for client SSL
274   */
275  public static Configuration createClientSSLConfig(String clientKS,
276      String password, String keyPassword, String trustKS) {
277    Configuration clientSSLConf = createSSLConfig(SSLFactory.Mode.CLIENT,
278      clientKS, password, keyPassword, trustKS);
279    return clientSSLConf;
280  }
281
282  /**
283   * Creates SSL configuration for a server.
284   *
285   * @param serverKS String server keystore file
286   * @param password String store password, or null to avoid setting store
287   *   password
288   * @param keyPassword String key password, or null to avoid setting key
289   *   password
290   * @param trustKS String truststore file
291   * @return Configuration for server SSL
292   */
293  public static Configuration createServerSSLConfig(String serverKS,
294      String password, String keyPassword, String trustKS) throws IOException {
295    Configuration serverSSLConf = createSSLConfig(SSLFactory.Mode.SERVER,
296      serverKS, password, keyPassword, trustKS);
297    return serverSSLConf;
298  }
299
300  /**
301   * Creates SSL configuration.
302   *
303   * @param mode SSLFactory.Mode mode to configure
304   * @param keystore String keystore file
305   * @param password String store password, or null to avoid setting store
306   *   password
307   * @param keyPassword String key password, or null to avoid setting key
308   *   password
309   * @param trustKS String truststore file
310   * @return Configuration for SSL
311   */
312  private static Configuration createSSLConfig(SSLFactory.Mode mode,
313      String keystore, String password, String keyPassword, String trustKS) {
314    String trustPassword = "trustP";
315
316    Configuration sslConf = new Configuration(false);
317    if (keystore != null) {
318      sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(mode,
319        FileBasedKeyStoresFactory.SSL_KEYSTORE_LOCATION_TPL_KEY), keystore);
320    }
321    if (password != null) {
322      sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(mode,
323        FileBasedKeyStoresFactory.SSL_KEYSTORE_PASSWORD_TPL_KEY), password);
324    }
325    if (keyPassword != null) {
326      sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(mode,
327        FileBasedKeyStoresFactory.SSL_KEYSTORE_KEYPASSWORD_TPL_KEY),
328        keyPassword);
329    }
330    if (trustKS != null) {
331      sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(mode,
332        FileBasedKeyStoresFactory.SSL_TRUSTSTORE_LOCATION_TPL_KEY), trustKS);
333    }
334    if (trustPassword != null) {
335      sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(mode,
336        FileBasedKeyStoresFactory.SSL_TRUSTSTORE_PASSWORD_TPL_KEY),
337        trustPassword);
338    }
339    sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(mode,
340      FileBasedKeyStoresFactory.SSL_TRUSTSTORE_RELOAD_INTERVAL_TPL_KEY), "1000");
341
342    return sslConf;
343  }
344
345  /**
346   * Saves configuration to a file.
347   *
348   * @param file File to save
349   * @param conf Configuration contents to write to file
350   * @throws IOException if there is an I/O error saving the file
351   */
352  public static void saveConfig(File file, Configuration conf)
353      throws IOException {
354    Writer writer = new FileWriter(file);
355    try {
356      conf.writeXml(writer);
357    } finally {
358      writer.close();
359    }
360  }
361}