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.io.crypto.tls;
019
020import static java.util.Objects.requireNonNull;
021
022import java.io.File;
023import java.io.IOException;
024import java.nio.charset.StandardCharsets;
025import java.nio.file.Files;
026import java.nio.file.StandardCopyOption;
027import java.nio.file.StandardOpenOption;
028import java.security.GeneralSecurityException;
029import java.security.KeyPair;
030import java.security.Security;
031import java.security.cert.X509Certificate;
032import java.util.Arrays;
033import org.apache.hadoop.conf.Configuration;
034import org.apache.yetus.audience.InterfaceAudience;
035import org.bouncycastle.asn1.x500.X500Name;
036import org.bouncycastle.asn1.x500.X500NameBuilder;
037import org.bouncycastle.asn1.x500.style.BCStyle;
038import org.bouncycastle.asn1.x509.GeneralName;
039import org.bouncycastle.asn1.x509.GeneralNames;
040import org.bouncycastle.jce.provider.BouncyCastleProvider;
041import org.bouncycastle.operator.OperatorCreationException;
042
043/**
044 * This class simplifies the creation of certificates and private keys for SSL/TLS connections.
045 * <p/>
046 * This file has been copied from the Apache ZooKeeper project.
047 * @see <a href=
048 *      "https://github.com/apache/zookeeper/blob/c74658d398cdc1d207aa296cb6e20de00faec03e/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java">Base
049 *      revision</a>
050 */
051@InterfaceAudience.Private
052public final class X509TestContext {
053
054  private static final String TRUST_STORE_PREFIX = "hbase_test_ca";
055  private static final String KEY_STORE_PREFIX = "hbase_test_key";
056
057  private final File tempDir;
058  private final Configuration conf;
059
060  private X509Certificate trustStoreCertificate;
061  private final char[] trustStorePassword;
062  private KeyPair trustStoreKeyPair;
063  private File trustStoreJksFile;
064  private File trustStorePemFile;
065  private File trustStorePkcs12File;
066  private File trustStoreBcfksFile;
067
068  private KeyPair keyStoreKeyPair;
069  private X509Certificate keyStoreCertificate;
070  private final char[] keyStorePassword;
071  private File keyStoreJksFile;
072  private File keyStorePemFile;
073  private File keyStorePkcs12File;
074  private File keyStoreBcfksFile;
075
076  /**
077   * Constructor is intentionally private, use the Builder class instead.
078   * @param conf               the configuration
079   * @param tempDir            the directory in which key store and trust store temp files will be
080   *                           written.
081   * @param trustStoreKeyPair  the key pair for the trust store.
082   * @param trustStorePassword the password to protect a JKS trust store (ignored for PEM trust
083   *                           stores).
084   * @param keyStoreKeyPair    the key pair for the key store.
085   * @param keyStorePassword   the password to protect the key store private key.
086   */
087  private X509TestContext(Configuration conf, File tempDir, KeyPair trustStoreKeyPair,
088    char[] trustStorePassword, KeyPair keyStoreKeyPair, char[] keyStorePassword)
089    throws IOException, GeneralSecurityException, OperatorCreationException {
090    if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
091      throw new IllegalStateException("BC Security provider was not found");
092    }
093    this.conf = conf;
094    this.tempDir = requireNonNull(tempDir);
095    if (!tempDir.isDirectory()) {
096      throw new IllegalArgumentException("Not a directory: " + tempDir);
097    }
098
099    this.trustStoreKeyPair = trustStoreKeyPair;
100    this.trustStorePassword = requireNonNull(trustStorePassword);
101    this.keyStoreKeyPair = requireNonNull(keyStoreKeyPair);
102    this.keyStorePassword = requireNonNull(keyStorePassword);
103
104    createCertificates();
105
106    trustStorePkcs12File = null;
107    trustStorePemFile = null;
108    trustStoreJksFile = null;
109    keyStorePkcs12File = null;
110    keyStorePemFile = null;
111    keyStoreJksFile = null;
112  }
113
114  /**
115   * Used by {@link #cloneWithNewKeystoreCert(X509Certificate)}. Should set all fields except
116   * generated keystore path fields
117   */
118  private X509TestContext(File tempDir, Configuration conf, X509Certificate trustStoreCertificate,
119    char[] trustStorePassword, KeyPair trustStoreKeyPair, File trustStoreJksFile,
120    File trustStorePemFile, File trustStorePkcs12File, KeyPair keyStoreKeyPair,
121    char[] keyStorePassword, X509Certificate keyStoreCertificate) {
122    this.tempDir = tempDir;
123    this.conf = conf;
124    this.trustStoreCertificate = trustStoreCertificate;
125    this.trustStorePassword = trustStorePassword;
126    this.trustStoreKeyPair = trustStoreKeyPair;
127    this.trustStoreJksFile = trustStoreJksFile;
128    this.trustStorePemFile = trustStorePemFile;
129    this.trustStorePkcs12File = trustStorePkcs12File;
130    this.keyStoreKeyPair = keyStoreKeyPair;
131    this.keyStoreCertificate = keyStoreCertificate;
132    this.keyStorePassword = keyStorePassword;
133    keyStorePkcs12File = null;
134    keyStorePemFile = null;
135    keyStoreJksFile = null;
136  }
137
138  /**
139   * Generates a new certificate using this context's CA and keystoreKeyPair. By default, the cert
140   * will have localhost in the subjectAltNames. This can be overridden by passing one or more
141   * string arguments after the cert name. The expectation for those arguments is that they are
142   * valid DNS names.
143   */
144  public X509Certificate newCert(X500Name name, String... subjectAltNames)
145    throws GeneralSecurityException, IOException, OperatorCreationException {
146    if (subjectAltNames.length == 0) {
147      return X509TestHelpers.newCert(trustStoreCertificate, trustStoreKeyPair, name,
148        keyStoreKeyPair.getPublic());
149    }
150    GeneralName[] names = new GeneralName[subjectAltNames.length];
151    for (int i = 0; i < subjectAltNames.length; i++) {
152      names[i] = new GeneralName(GeneralName.dNSName, subjectAltNames[i]);
153    }
154    return X509TestHelpers.newCert(trustStoreCertificate, trustStoreKeyPair, name,
155      keyStoreKeyPair.getPublic(), new GeneralNames(names));
156  }
157
158  public File getTempDir() {
159    return tempDir;
160  }
161
162  public char[] getTrustStorePassword() {
163    return trustStorePassword;
164  }
165
166  /**
167   * Returns the path to the trust store file in the given format (JKS or PEM). Note that the file
168   * is created lazily, the first time this method is called.
169   * @param storeFileType the store file type (JKS or PEM).
170   * @return the path to the trust store file.
171   * @throws Exception if there is an error creating the trust store file.
172   */
173  public File getTrustStoreFile(KeyStoreFileType storeFileType) throws Exception {
174    switch (storeFileType) {
175      case JKS:
176        return getTrustStoreJksFile();
177      case PEM:
178        return getTrustStorePemFile();
179      case PKCS12:
180        return getTrustStorePkcs12File();
181      case BCFKS:
182        return getTrustStoreBcfksFile();
183      default:
184        throw new IllegalArgumentException("Invalid trust store type: " + storeFileType
185          + ", must be one of: " + Arrays.toString(KeyStoreFileType.values()));
186    }
187  }
188
189  private void atomicWriteFile(File file, byte[] content) throws IOException {
190    File tmpFile = new File(file.getParentFile(), file.getName().concat(".tmp"));
191    Files.write(tmpFile.toPath(), content, StandardOpenOption.CREATE,
192      StandardOpenOption.TRUNCATE_EXISTING);
193    Files.move(tmpFile.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING,
194      StandardCopyOption.ATOMIC_MOVE);
195  }
196
197  private File getTrustStoreJksFile() throws Exception {
198    if (trustStoreJksFile == null) {
199      trustStoreJksFile = File.createTempFile(TRUST_STORE_PREFIX,
200        KeyStoreFileType.JKS.getDefaultFileExtension(), tempDir);
201      generateTrustStoreJksFile();
202    }
203    return trustStoreJksFile;
204  }
205
206  private void generateTrustStoreJksFile() throws Exception {
207    atomicWriteFile(trustStoreJksFile,
208      X509TestHelpers.certToJavaTrustStoreBytes(trustStoreCertificate, trustStorePassword));
209  }
210
211  private File getTrustStorePemFile() throws IOException {
212    if (trustStorePemFile == null) {
213      trustStorePemFile = File.createTempFile(TRUST_STORE_PREFIX,
214        KeyStoreFileType.PEM.getDefaultFileExtension(), tempDir);
215      generateTrustStorePemFile();
216    }
217    return trustStorePemFile;
218  }
219
220  private void generateTrustStorePemFile() throws IOException {
221    atomicWriteFile(trustStorePemFile, X509TestHelpers
222      .pemEncodeX509Certificate(trustStoreCertificate).getBytes(StandardCharsets.US_ASCII));
223  }
224
225  private File getTrustStorePkcs12File() throws Exception {
226    if (trustStorePkcs12File == null) {
227      trustStorePkcs12File = File.createTempFile(TRUST_STORE_PREFIX,
228        KeyStoreFileType.PKCS12.getDefaultFileExtension(), tempDir);
229      generateTrustStorePkcs12File();
230    }
231    return trustStorePkcs12File;
232  }
233
234  private void generateTrustStorePkcs12File() throws Exception {
235    atomicWriteFile(trustStorePkcs12File,
236      X509TestHelpers.certToPKCS12TrustStoreBytes(trustStoreCertificate, trustStorePassword));
237  }
238
239  private File getTrustStoreBcfksFile() throws Exception {
240    if (trustStoreBcfksFile == null) {
241      trustStoreBcfksFile = File.createTempFile(TRUST_STORE_PREFIX,
242        KeyStoreFileType.BCFKS.getDefaultFileExtension(), tempDir);
243      generateTrustStoreBcfksFile();
244    }
245    return trustStoreBcfksFile;
246  }
247
248  private void generateTrustStoreBcfksFile() throws Exception {
249    atomicWriteFile(trustStoreBcfksFile,
250      X509TestHelpers.certToBCFKSTrustStoreBytes(trustStoreCertificate, trustStorePassword));
251  }
252
253  public X509Certificate getKeyStoreCertificate() {
254    return keyStoreCertificate;
255  }
256
257  public char[] getKeyStorePassword() {
258    return keyStorePassword;
259  }
260
261  public boolean isKeyStoreEncrypted() {
262    return keyStorePassword != null;
263  }
264
265  public Configuration getConf() {
266    return conf;
267  }
268
269  /**
270   * Returns the path to the key store file in the given format (JKS, PEM, ...). Note that the file
271   * is created lazily, the first time this method is called.
272   * @param storeFileType the store file type (JKS, PEM, ...).
273   * @return the path to the key store file.
274   * @throws Exception if there is an error creating the key store file.
275   */
276  public File getKeyStoreFile(KeyStoreFileType storeFileType) throws Exception {
277    switch (storeFileType) {
278      case JKS:
279        return getKeyStoreJksFile();
280      case PEM:
281        return getKeyStorePemFile();
282      case PKCS12:
283        return getKeyStorePkcs12File();
284      case BCFKS:
285        return getKeyStoreBcfksFile();
286      default:
287        throw new IllegalArgumentException("Invalid key store type: " + storeFileType
288          + ", must be one of: " + Arrays.toString(KeyStoreFileType.values()));
289    }
290  }
291
292  private File getKeyStoreJksFile() throws Exception {
293    if (keyStoreJksFile == null) {
294      keyStoreJksFile = File.createTempFile(KEY_STORE_PREFIX,
295        KeyStoreFileType.JKS.getDefaultFileExtension(), tempDir);
296      generateKeyStoreJksFile();
297    }
298    return keyStoreJksFile;
299  }
300
301  private void generateKeyStoreJksFile() throws Exception {
302    atomicWriteFile(keyStoreJksFile, X509TestHelpers.certAndPrivateKeyToJavaKeyStoreBytes(
303      keyStoreCertificate, keyStoreKeyPair.getPrivate(), keyStorePassword));
304  }
305
306  private File getKeyStorePemFile() throws Exception {
307    if (keyStorePemFile == null) {
308      keyStorePemFile = File.createTempFile(KEY_STORE_PREFIX,
309        KeyStoreFileType.PEM.getDefaultFileExtension(), tempDir);
310      generateKeyStorePemFile();
311    }
312    return keyStorePemFile;
313  }
314
315  private void generateKeyStorePemFile() throws Exception {
316    atomicWriteFile(keyStorePemFile, X509TestHelpers.pemEncodeCertAndPrivateKey(keyStoreCertificate,
317      keyStoreKeyPair.getPrivate(), keyStorePassword).getBytes(StandardCharsets.US_ASCII));
318  }
319
320  private File getKeyStorePkcs12File() throws Exception {
321    if (keyStorePkcs12File == null) {
322      keyStorePkcs12File = File.createTempFile(KEY_STORE_PREFIX,
323        KeyStoreFileType.PKCS12.getDefaultFileExtension(), tempDir);
324      generateKeyStorePkcs12File();
325    }
326    return keyStorePkcs12File;
327  }
328
329  private void generateKeyStorePkcs12File() throws Exception {
330    atomicWriteFile(keyStorePkcs12File, X509TestHelpers.certAndPrivateKeyToPKCS12Bytes(
331      keyStoreCertificate, keyStoreKeyPair.getPrivate(), keyStorePassword));
332  }
333
334  private File getKeyStoreBcfksFile() throws Exception {
335    if (keyStoreBcfksFile == null) {
336      keyStoreBcfksFile = File.createTempFile(KEY_STORE_PREFIX,
337        KeyStoreFileType.BCFKS.getDefaultFileExtension(), tempDir);
338      generateKeyStoreBcfksFile();
339    }
340    return keyStoreBcfksFile;
341  }
342
343  private void generateKeyStoreBcfksFile() throws Exception {
344    atomicWriteFile(keyStoreBcfksFile, X509TestHelpers.certAndPrivateKeyToBCFKSBytes(
345      keyStoreCertificate, keyStoreKeyPair.getPrivate(), keyStorePassword));
346  }
347
348  /**
349   * Sets the SSL system properties such that the given X509Util object can be used to create SSL
350   * Contexts that will use the trust store and key store files created by this test context.
351   * Example usage:
352   *
353   * <pre>
354   *     X509TestContext testContext = ...; // create the test context
355   *     X509Util x509Util = new QuorumX509Util();
356   *     testContext.setSystemProperties(x509Util, KeyStoreFileType.JKS, KeyStoreFileType.JKS);
357   *     // The returned context will use the key store and trust store created by the test context.
358   *     SSLContext ctx = x509Util.getDefaultSSLContext();
359   * </pre>
360   *
361   * @param keyStoreFileType   the store file type to use for the key store (JKS, PEM, ...).
362   * @param trustStoreFileType the store file type to use for the trust store (JKS, PEM, ...).
363   * @throws Exception if there is an error creating the key store file or trust store file.
364   */
365  public void setConfigurations(KeyStoreFileType keyStoreFileType,
366    KeyStoreFileType trustStoreFileType) throws Exception {
367    setKeystoreConfigurations(keyStoreFileType, conf);
368    conf.set(X509Util.TLS_CONFIG_TRUSTSTORE_LOCATION,
369      this.getTrustStoreFile(trustStoreFileType).getAbsolutePath());
370    conf.set(X509Util.TLS_CONFIG_TRUSTSTORE_PASSWORD, String.valueOf(this.getTrustStorePassword()));
371    conf.set(X509Util.TLS_CONFIG_TRUSTSTORE_TYPE, trustStoreFileType.getPropertyValue());
372  }
373
374  /**
375   * Sets the KeyStore-related SSL system properties onto the given Configuration such that X509Util
376   * can be used to create SSL Contexts using that KeyStore. This can be used in special
377   * circumstances to inject a "bad" certificate where the keystore doesn't match the CA in the
378   * truststore. Or use it to create a connection without a truststore.
379   * @see #setConfigurations(KeyStoreFileType, KeyStoreFileType) which sets both keystore and
380   *      truststore and is more applicable to general use.
381   */
382  public void setKeystoreConfigurations(KeyStoreFileType keyStoreFileType, Configuration confToSet)
383    throws Exception {
384
385    confToSet.set(X509Util.TLS_CONFIG_KEYSTORE_LOCATION,
386      this.getKeyStoreFile(keyStoreFileType).getAbsolutePath());
387    confToSet.set(X509Util.TLS_CONFIG_KEYSTORE_PASSWORD,
388      String.valueOf(this.getKeyStorePassword()));
389    confToSet.set(X509Util.TLS_CONFIG_KEYSTORE_TYPE, keyStoreFileType.getPropertyValue());
390  }
391
392  public void clearConfigurations() {
393    conf.unset(X509Util.TLS_CONFIG_KEYSTORE_LOCATION);
394    conf.unset(X509Util.TLS_CONFIG_KEYSTORE_PASSWORD);
395    conf.unset(X509Util.TLS_CONFIG_KEYSTORE_TYPE);
396    conf.unset(X509Util.TLS_CONFIG_TRUSTSTORE_LOCATION);
397    conf.unset(X509Util.TLS_CONFIG_TRUSTSTORE_PASSWORD);
398    conf.unset(X509Util.TLS_CONFIG_TRUSTSTORE_TYPE);
399  }
400
401  /**
402   * Creates a clone of the current context, but injecting the passed certificate as the KeyStore
403   * cert. The new context's keystore path fields are nulled, so the next call to
404   * {@link #setConfigurations(KeyStoreFileType, KeyStoreFileType)},
405   * {@link #setKeystoreConfigurations(KeyStoreFileType, Configuration)} , or
406   * {@link #getKeyStoreFile(KeyStoreFileType)} will create a new keystore with this certificate in
407   * place.
408   * @param cert the cert to replace
409   */
410  public X509TestContext cloneWithNewKeystoreCert(X509Certificate cert) {
411    return new X509TestContext(tempDir, conf, trustStoreCertificate, trustStorePassword,
412      trustStoreKeyPair, trustStoreJksFile, trustStorePemFile, trustStorePkcs12File,
413      keyStoreKeyPair, keyStorePassword, cert);
414  }
415
416  public void regenerateStores(X509KeyType keyStoreKeyType, X509KeyType trustStoreKeyType,
417    KeyStoreFileType keyStoreFileType, KeyStoreFileType trustStoreFileType,
418    String... subjectAltNames) throws Exception {
419    trustStoreKeyPair = X509TestHelpers.generateKeyPair(trustStoreKeyType);
420    keyStoreKeyPair = X509TestHelpers.generateKeyPair(keyStoreKeyType);
421    createCertificates(subjectAltNames);
422
423    switch (keyStoreFileType) {
424      case JKS:
425        generateKeyStoreJksFile();
426        break;
427      case PEM:
428        generateKeyStorePemFile();
429        break;
430      case BCFKS:
431        generateKeyStoreBcfksFile();
432        break;
433      case PKCS12:
434        generateKeyStorePkcs12File();
435        break;
436    }
437
438    switch (trustStoreFileType) {
439      case JKS:
440        generateTrustStoreJksFile();
441        break;
442      case PEM:
443        generateTrustStorePemFile();
444        break;
445      case PKCS12:
446        generateTrustStorePkcs12File();
447        break;
448      case BCFKS:
449        generateTrustStoreBcfksFile();
450        break;
451    }
452  }
453
454  private void createCertificates(String... subjectAltNames)
455    throws GeneralSecurityException, IOException, OperatorCreationException {
456    X500NameBuilder caNameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
457    caNameBuilder.addRDN(BCStyle.CN, getClass().getSimpleName() + " Root CA");
458    trustStoreCertificate =
459      X509TestHelpers.newSelfSignedCACert(caNameBuilder.build(), trustStoreKeyPair);
460
461    X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
462    nameBuilder.addRDN(BCStyle.CN, getClass().getSimpleName() + " Zookeeper Test");
463    keyStoreCertificate = newCert(nameBuilder.build(), subjectAltNames);
464  }
465
466  /**
467   * Builder class, used for creating new instances of X509TestContext.
468   */
469  public static class Builder {
470
471    private final Configuration conf;
472    private File tempDir;
473    private X509KeyType trustStoreKeyType;
474    private char[] trustStorePassword;
475    private X509KeyType keyStoreKeyType;
476    private char[] keyStorePassword;
477
478    /**
479     * Creates an empty builder with the given Configuration.
480     */
481    public Builder(Configuration conf) {
482      this.conf = conf;
483      trustStoreKeyType = X509KeyType.EC;
484      keyStoreKeyType = X509KeyType.EC;
485    }
486
487    /**
488     * Builds a new X509TestContext from this builder.
489     * @return a new X509TestContext
490     */
491    public X509TestContext build()
492      throws IOException, GeneralSecurityException, OperatorCreationException {
493      KeyPair trustStoreKeyPair = X509TestHelpers.generateKeyPair(trustStoreKeyType);
494      KeyPair keyStoreKeyPair = X509TestHelpers.generateKeyPair(keyStoreKeyType);
495      return new X509TestContext(conf, tempDir, trustStoreKeyPair, trustStorePassword,
496        keyStoreKeyPair, keyStorePassword);
497    }
498
499    /**
500     * Sets the temporary directory. Certificate and private key files will be created in this
501     * directory.
502     * @param tempDir the temp directory.
503     * @return this Builder.
504     */
505    public Builder setTempDir(File tempDir) {
506      this.tempDir = tempDir;
507      return this;
508    }
509
510    /**
511     * Sets the trust store key type. The CA key generated for the test context will be of this
512     * type.
513     * @param keyType the key type.
514     * @return this Builder.
515     */
516    public Builder setTrustStoreKeyType(X509KeyType keyType) {
517      trustStoreKeyType = keyType;
518      return this;
519    }
520
521    /**
522     * Sets the trust store password. Ignored for PEM trust stores, JKS trust stores will be
523     * encrypted with this password.
524     * @param password the password.
525     * @return this Builder.
526     */
527    public Builder setTrustStorePassword(char[] password) {
528      trustStorePassword = password;
529      return this;
530    }
531
532    /**
533     * Sets the key store key type. The private key generated for the test context will be of this
534     * type.
535     * @param keyType the key type.
536     * @return this Builder.
537     */
538    public Builder setKeyStoreKeyType(X509KeyType keyType) {
539      keyStoreKeyType = keyType;
540      return this;
541    }
542
543    /**
544     * Sets the key store password. The private key (PEM, JKS) and certificate (JKS only) will be
545     * encrypted with this password.
546     * @param password the password.
547     * @return this Builder.
548     */
549    public Builder setKeyStorePassword(char[] password) {
550      keyStorePassword = password;
551      return this;
552    }
553  }
554
555  /**
556   * Returns a new default-constructed Builder.
557   * @return a new Builder.
558   */
559  public static Builder newBuilder(Configuration conf) {
560    return new Builder(conf);
561  }
562}