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 java.io.ByteArrayOutputStream;
021import java.io.IOException;
022import java.io.StringWriter;
023import java.math.BigInteger;
024import java.net.InetAddress;
025import java.net.UnknownHostException;
026import java.security.GeneralSecurityException;
027import java.security.KeyPair;
028import java.security.KeyPairGenerator;
029import java.security.KeyStore;
030import java.security.PrivateKey;
031import java.security.PublicKey;
032import java.security.SecureRandom;
033import java.security.cert.Certificate;
034import java.security.cert.CertificateException;
035import java.security.cert.X509Certificate;
036import java.security.spec.ECGenParameterSpec;
037import java.security.spec.RSAKeyGenParameterSpec;
038import java.time.LocalDate;
039import java.time.ZoneId;
040import org.apache.yetus.audience.InterfaceAudience;
041import org.bouncycastle.asn1.DERIA5String;
042import org.bouncycastle.asn1.DEROctetString;
043import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
044import org.bouncycastle.asn1.x500.X500Name;
045import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
046import org.bouncycastle.asn1.x509.BasicConstraints;
047import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
048import org.bouncycastle.asn1.x509.Extension;
049import org.bouncycastle.asn1.x509.GeneralName;
050import org.bouncycastle.asn1.x509.GeneralNames;
051import org.bouncycastle.asn1.x509.KeyPurposeId;
052import org.bouncycastle.asn1.x509.KeyUsage;
053import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
054import org.bouncycastle.cert.X509CertificateHolder;
055import org.bouncycastle.cert.X509v3CertificateBuilder;
056import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
057import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
058import org.bouncycastle.crypto.util.PrivateKeyFactory;
059import org.bouncycastle.jce.provider.BouncyCastleProvider;
060import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
061import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
062import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder;
063import org.bouncycastle.operator.ContentSigner;
064import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
065import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
066import org.bouncycastle.operator.OperatorCreationException;
067import org.bouncycastle.operator.OutputEncryptor;
068import org.bouncycastle.operator.bc.BcContentSignerBuilder;
069import org.bouncycastle.operator.bc.BcECContentSignerBuilder;
070import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder;
071
072/**
073 * This class contains helper methods for creating X509 certificates and key pairs, and for
074 * serializing them to JKS, PEM or other keystore type files.
075 * <p/>
076 * This file has been copied from the Apache ZooKeeper project.
077 * @see <a href=
078 *      "https://github.com/apache/zookeeper/blob/c74658d398cdc1d207aa296cb6e20de00faec03e/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java">Base
079 *      revision</a>
080 */
081@InterfaceAudience.Private
082final class X509TestHelpers {
083
084  private static final SecureRandom PRNG = new SecureRandom();
085  private static final int DEFAULT_RSA_KEY_SIZE_BITS = 2048;
086  private static final BigInteger DEFAULT_RSA_PUB_EXPONENT = RSAKeyGenParameterSpec.F4; // 65537
087  private static final String DEFAULT_ELLIPTIC_CURVE_NAME = "secp256r1";
088  // Per RFC 5280 section 4.1.2.2, X509 certificates can use up to 20 bytes == 160 bits for serial
089  // numbers.
090  private static final int SERIAL_NUMBER_MAX_BITS = 20 * Byte.SIZE;
091
092  /**
093   * Uses the private key of the given key pair to create a self-signed CA certificate with the
094   * public half of the key pair and the given subject and expiration. The issuer of the new cert
095   * will be equal to the subject. Returns the new certificate. The returned certificate should be
096   * used as the trust store. The private key of the input key pair should be used to sign
097   * certificates that are used by test peers to establish TLS connections to each other.
098   * @param subject the subject of the new certificate being created.
099   * @param keyPair the key pair to use. The public key will be embedded in the new certificate, and
100   *                the private key will be used to self-sign the certificate.
101   * @return a new self-signed CA certificate.
102   */
103  public static X509Certificate newSelfSignedCACert(X500Name subject, KeyPair keyPair)
104    throws IOException, OperatorCreationException, GeneralSecurityException {
105    LocalDate now = LocalDate.now(ZoneId.systemDefault());
106    X509v3CertificateBuilder builder = initCertBuilder(subject, // for self-signed certs,
107      // issuer == subject
108      now, now.plusDays(1), subject, keyPair.getPublic());
109    builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true)); // is a CA
110    builder.addExtension(Extension.keyUsage, true,
111      new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign | KeyUsage.cRLSign));
112    return buildAndSignCertificate(keyPair.getPrivate(), builder);
113  }
114
115  /**
116   * Using the private key of the given CA key pair and the Subject of the given CA cert as the
117   * Issuer, issues a new cert with the given subject and public key. The returned certificate,
118   * combined with the private key half of the <code>certPublicKey</code>, should be used as the key
119   * store.
120   * @param caCert        the certificate of the CA that's doing the signing.
121   * @param caKeyPair     the key pair of the CA. The private key will be used to sign. The public
122   *                      key must match the public key in the <code>caCert</code>.
123   * @param certSubject   the subject field of the new cert being issued.
124   * @param certPublicKey the public key of the new cert being issued.
125   * @return a new certificate signed by the CA's private key.
126   */
127  public static X509Certificate newCert(X509Certificate caCert, KeyPair caKeyPair,
128    X500Name certSubject, PublicKey certPublicKey)
129    throws IOException, OperatorCreationException, GeneralSecurityException {
130    return newCert(caCert, caKeyPair, certSubject, certPublicKey, getLocalhostSubjectAltNames());
131  }
132
133  /**
134   * Using the private key of the given CA key pair and the Subject of the given CA cert as the
135   * Issuer, issues a new cert with the given subject and public key. The returned certificate,
136   * combined with the private key half of the <code>certPublicKey</code>, should be used as the key
137   * store.
138   * @param caCert          the certificate of the CA that's doing the signing.
139   * @param caKeyPair       the key pair of the CA. The private key will be used to sign. The public
140   *                        key must match the public key in the <code>caCert</code>.
141   * @param certSubject     the subject field of the new cert being issued.
142   * @param certPublicKey   the public key of the new cert being issued.
143   * @param subjectAltNames the subject alternative names to use, or null if none
144   * @return a new certificate signed by the CA's private key.
145   */
146  public static X509Certificate newCert(X509Certificate caCert, KeyPair caKeyPair,
147    X500Name certSubject, PublicKey certPublicKey, GeneralNames subjectAltNames)
148    throws IOException, OperatorCreationException, GeneralSecurityException {
149    if (!caKeyPair.getPublic().equals(caCert.getPublicKey())) {
150      throw new IllegalArgumentException(
151        "CA private key does not match the public key in " + "the CA cert");
152    }
153    LocalDate now = LocalDate.now(ZoneId.systemDefault());
154    X509v3CertificateBuilder builder =
155      initCertBuilder(new X500Name(caCert.getIssuerX500Principal().getName()), now, now.plusDays(1),
156        certSubject, certPublicKey);
157    builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false)); // not a CA
158    builder.addExtension(Extension.keyUsage, true,
159      new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment));
160    builder.addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(
161      new KeyPurposeId[] { KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth }));
162
163    if (subjectAltNames != null) {
164      builder.addExtension(Extension.subjectAlternativeName, false, subjectAltNames);
165    }
166    return buildAndSignCertificate(caKeyPair.getPrivate(), builder);
167  }
168
169  /**
170   * Returns subject alternative names for "localhost".
171   * @return the subject alternative names for "localhost".
172   */
173  private static GeneralNames getLocalhostSubjectAltNames() throws UnknownHostException {
174    InetAddress[] localAddresses = InetAddress.getAllByName("localhost");
175    GeneralName[] generalNames = new GeneralName[localAddresses.length + 1];
176    for (int i = 0; i < localAddresses.length; i++) {
177      generalNames[i] =
178        new GeneralName(GeneralName.iPAddress, new DEROctetString(localAddresses[i].getAddress()));
179    }
180    generalNames[generalNames.length - 1] =
181      new GeneralName(GeneralName.dNSName, new DERIA5String("localhost"));
182    return new GeneralNames(generalNames);
183  }
184
185  /**
186   * Helper method for newSelfSignedCACert() and newCert(). Initializes a X509v3CertificateBuilder
187   * with logic that's common to both methods.
188   * @param issuer           Issuer field of the new cert.
189   * @param notBefore        date before which the new cert is not valid.
190   * @param notAfter         date after which the new cert is not valid.
191   * @param subject          Subject field of the new cert.
192   * @param subjectPublicKey public key to store in the new cert.
193   * @return a X509v3CertificateBuilder that can be further customized to finish creating the new
194   *         cert.
195   */
196  private static X509v3CertificateBuilder initCertBuilder(X500Name issuer, LocalDate notBefore,
197    LocalDate notAfter, X500Name subject, PublicKey subjectPublicKey) {
198    return new X509v3CertificateBuilder(issuer, new BigInteger(SERIAL_NUMBER_MAX_BITS, PRNG),
199      java.sql.Date.valueOf(notBefore), java.sql.Date.valueOf(notAfter), subject,
200      SubjectPublicKeyInfo.getInstance(subjectPublicKey.getEncoded()));
201  }
202
203  /**
204   * Signs the certificate being built by the given builder using the given private key and returns
205   * the certificate.
206   * @param privateKey the private key to sign the certificate with.
207   * @param builder    the cert builder that contains the certificate data.
208   * @return the signed certificate.
209   */
210  private static X509Certificate buildAndSignCertificate(PrivateKey privateKey,
211    X509v3CertificateBuilder builder)
212    throws IOException, OperatorCreationException, CertificateException {
213    BcContentSignerBuilder signerBuilder;
214    if (privateKey.getAlgorithm().contains("RSA")) { // a little hacky way to detect key type, but
215      // it works
216      AlgorithmIdentifier signatureAlgorithm =
217        new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256WithRSAEncryption");
218      AlgorithmIdentifier digestAlgorithm =
219        new DefaultDigestAlgorithmIdentifierFinder().find(signatureAlgorithm);
220      signerBuilder = new BcRSAContentSignerBuilder(signatureAlgorithm, digestAlgorithm);
221    } else { // if not RSA, assume EC
222      AlgorithmIdentifier signatureAlgorithm =
223        new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withECDSA");
224      AlgorithmIdentifier digestAlgorithm =
225        new DefaultDigestAlgorithmIdentifierFinder().find(signatureAlgorithm);
226      signerBuilder = new BcECContentSignerBuilder(signatureAlgorithm, digestAlgorithm);
227    }
228    AsymmetricKeyParameter privateKeyParam = PrivateKeyFactory.createKey(privateKey.getEncoded());
229    ContentSigner signer = signerBuilder.build(privateKeyParam);
230    return toX509Cert(builder.build(signer));
231  }
232
233  /**
234   * Generates a new asymmetric key pair of the given type.
235   * @param keyType the type of key pair to generate.
236   * @return the new key pair.
237   * @throws GeneralSecurityException if your java crypto providers are messed up.
238   */
239  public static KeyPair generateKeyPair(X509KeyType keyType) throws GeneralSecurityException {
240    switch (keyType) {
241      case RSA:
242        return generateRSAKeyPair();
243      case EC:
244        return generateECKeyPair();
245      default:
246        throw new IllegalArgumentException("Invalid X509KeyType");
247    }
248  }
249
250  /**
251   * Generates an RSA key pair with a 2048-bit private key and F4 (65537) as the public exponent.
252   * @return the key pair.
253   */
254  public static KeyPair generateRSAKeyPair() throws GeneralSecurityException {
255    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
256    RSAKeyGenParameterSpec keyGenSpec =
257      new RSAKeyGenParameterSpec(DEFAULT_RSA_KEY_SIZE_BITS, DEFAULT_RSA_PUB_EXPONENT);
258    keyGen.initialize(keyGenSpec, PRNG);
259    return keyGen.generateKeyPair();
260  }
261
262  /**
263   * Generates an elliptic curve key pair using the "secp256r1" aka "prime256v1" aka "NIST P-256"
264   * curve.
265   * @return the key pair.
266   */
267  public static KeyPair generateECKeyPair() throws GeneralSecurityException {
268    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
269    keyGen.initialize(new ECGenParameterSpec(DEFAULT_ELLIPTIC_CURVE_NAME), PRNG);
270    return keyGen.generateKeyPair();
271  }
272
273  /**
274   * PEM-encodes the given X509 certificate and private key (compatible with OpenSSL), optionally
275   * protecting the private key with a password. Concatenates them both and returns the result as a
276   * single string. This creates the PEM encoding of a key store.
277   * @param cert        the X509 certificate to PEM-encode.
278   * @param privateKey  the private key to PEM-encode.
279   * @param keyPassword an optional key password. If empty or null, the private key will not be
280   *                    encrypted.
281   * @return a String containing the PEM encodings of the certificate and private key.
282   * @throws IOException               if converting the certificate or private key to PEM format
283   *                                   fails.
284   * @throws OperatorCreationException if constructing the encryptor from the given password fails.
285   */
286  public static String pemEncodeCertAndPrivateKey(X509Certificate cert, PrivateKey privateKey,
287    char[] keyPassword) throws IOException, OperatorCreationException {
288    return pemEncodeX509Certificate(cert) + "\n" + pemEncodePrivateKey(privateKey, keyPassword);
289  }
290
291  /**
292   * PEM-encodes the given private key (compatible with OpenSSL), optionally protecting it with a
293   * password, and returns the result as a String.
294   * @param key      the private key.
295   * @param password an optional key password. If empty or null, the private key will not be
296   *                 encrypted.
297   * @return a String containing the PEM encoding of the private key.
298   * @throws IOException               if converting the key to PEM format fails.
299   * @throws OperatorCreationException if constructing the encryptor from the given password fails.
300   */
301  public static String pemEncodePrivateKey(PrivateKey key, char[] password)
302    throws IOException, OperatorCreationException {
303    StringWriter stringWriter = new StringWriter();
304    JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter);
305    OutputEncryptor encryptor = null;
306    if (password != null && password.length > 0) {
307      encryptor =
308        new JceOpenSSLPKCS8EncryptorBuilder(PKCSObjectIdentifiers.pbeWithSHAAnd3_KeyTripleDES_CBC)
309          .setProvider(BouncyCastleProvider.PROVIDER_NAME).setRandom(PRNG).setPassword(password)
310          .build();
311    }
312    pemWriter.writeObject(new JcaPKCS8Generator(key, encryptor));
313    pemWriter.close();
314    return stringWriter.toString();
315  }
316
317  /**
318   * PEM-encodes the given X509 certificate (compatible with OpenSSL) and returns the result as a
319   * String.
320   * @param cert the certificate.
321   * @return a String containing the PEM encoding of the certificate.
322   * @throws IOException if converting the certificate to PEM format fails.
323   */
324  public static String pemEncodeX509Certificate(X509Certificate cert) throws IOException {
325    StringWriter stringWriter = new StringWriter();
326    JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter);
327    pemWriter.writeObject(cert);
328    pemWriter.close();
329    return stringWriter.toString();
330  }
331
332  /**
333   * Encodes the given X509Certificate as a JKS TrustStore, optionally protecting the cert with a
334   * password (though it's unclear why one would do this since certificates only contain public
335   * information and do not need to be kept secret). Returns the byte array encoding of the trust
336   * store, which may be written to a file and loaded to instantiate the trust store at a later
337   * point or in another process.
338   * @param cert        the certificate to serialize.
339   * @param keyPassword an optional password to encrypt the trust store. If empty or null, the cert
340   *                    will not be encrypted.
341   * @return the serialized bytes of the JKS trust store.
342   */
343  public static byte[] certToJavaTrustStoreBytes(X509Certificate cert, char[] keyPassword)
344    throws IOException, GeneralSecurityException {
345    KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
346    return certToTrustStoreBytes(cert, keyPassword, trustStore);
347  }
348
349  /**
350   * Encodes the given X509Certificate as a PKCS12 TrustStore, optionally protecting the cert with a
351   * password (though it's unclear why one would do this since certificates only contain public
352   * information and do not need to be kept secret). Returns the byte array encoding of the trust
353   * store, which may be written to a file and loaded to instantiate the trust store at a later
354   * point or in another process.
355   * @param cert        the certificate to serialize.
356   * @param keyPassword an optional password to encrypt the trust store. If empty or null, the cert
357   *                    will not be encrypted.
358   * @return the serialized bytes of the PKCS12 trust store.
359   */
360  public static byte[] certToPKCS12TrustStoreBytes(X509Certificate cert, char[] keyPassword)
361    throws IOException, GeneralSecurityException {
362    KeyStore trustStore = KeyStore.getInstance("PKCS12");
363    return certToTrustStoreBytes(cert, keyPassword, trustStore);
364  }
365
366  /**
367   * Encodes the given X509Certificate as a BCFKS TrustStore, optionally protecting the cert with a
368   * password (though it's unclear why one would do this since certificates only contain public
369   * information and do not need to be kept secret). Returns the byte array encoding of the trust
370   * store, which may be written to a file and loaded to instantiate the trust store at a later
371   * point or in another process.
372   * @param cert        the certificate to serialize.
373   * @param keyPassword an optional password to encrypt the trust store. If empty or null, the cert
374   *                    will not be encrypted.
375   * @return the serialized bytes of the BCFKS trust store.
376   */
377  public static byte[] certToBCFKSTrustStoreBytes(X509Certificate cert, char[] keyPassword)
378    throws IOException, GeneralSecurityException {
379    KeyStore trustStore = KeyStore.getInstance("BCFKS");
380    return certToTrustStoreBytes(cert, keyPassword, trustStore);
381  }
382
383  private static byte[] certToTrustStoreBytes(X509Certificate cert, char[] keyPassword,
384    KeyStore trustStore) throws IOException, GeneralSecurityException {
385    trustStore.load(null, keyPassword);
386    trustStore.setCertificateEntry(cert.getSubjectX500Principal().toString(), cert);
387    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
388    trustStore.store(outputStream, keyPassword);
389    outputStream.flush();
390    byte[] result = outputStream.toByteArray();
391    outputStream.close();
392    return result;
393  }
394
395  /**
396   * Encodes the given X509Certificate and private key as a JKS KeyStore, optionally protecting the
397   * private key (and possibly the cert?) with a password. Returns the byte array encoding of the
398   * key store, which may be written to a file and loaded to instantiate the key store at a later
399   * point or in another process.
400   * @param cert        the X509 certificate to serialize.
401   * @param privateKey  the private key to serialize.
402   * @param keyPassword an optional key password. If empty or null, the private key will not be
403   *                    encrypted.
404   * @return the serialized bytes of the JKS key store.
405   */
406  public static byte[] certAndPrivateKeyToJavaKeyStoreBytes(X509Certificate cert,
407    PrivateKey privateKey, char[] keyPassword) throws IOException, GeneralSecurityException {
408    KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
409    return certAndPrivateKeyToBytes(cert, privateKey, keyPassword, keyStore);
410  }
411
412  /**
413   * Encodes the given X509Certificate and private key as a PKCS12 KeyStore, optionally protecting
414   * the private key (and possibly the cert?) with a password. Returns the byte array encoding of
415   * the key store, which may be written to a file and loaded to instantiate the key store at a
416   * later point or in another process.
417   * @param cert        the X509 certificate to serialize.
418   * @param privateKey  the private key to serialize.
419   * @param keyPassword an optional key password. If empty or null, the private key will not be
420   *                    encrypted.
421   * @return the serialized bytes of the PKCS12 key store.
422   */
423  public static byte[] certAndPrivateKeyToPKCS12Bytes(X509Certificate cert, PrivateKey privateKey,
424    char[] keyPassword) throws IOException, GeneralSecurityException {
425    KeyStore keyStore = KeyStore.getInstance("PKCS12");
426    return certAndPrivateKeyToBytes(cert, privateKey, keyPassword, keyStore);
427  }
428
429  /**
430   * Encodes the given X509Certificate and private key as a BCFKS KeyStore, optionally protecting
431   * the private key (and possibly the cert?) with a password. Returns the byte array encoding of
432   * the key store, which may be written to a file and loaded to instantiate the key store at a
433   * later point or in another process.
434   * @param cert        the X509 certificate to serialize.
435   * @param privateKey  the private key to serialize.
436   * @param keyPassword an optional key password. If empty or null, the private key will not be
437   *                    encrypted.
438   * @return the serialized bytes of the BCFKS key store.
439   */
440  public static byte[] certAndPrivateKeyToBCFKSBytes(X509Certificate cert, PrivateKey privateKey,
441    char[] keyPassword) throws IOException, GeneralSecurityException {
442    KeyStore keyStore = KeyStore.getInstance("BCFKS");
443    return certAndPrivateKeyToBytes(cert, privateKey, keyPassword, keyStore);
444  }
445
446  private static byte[] certAndPrivateKeyToBytes(X509Certificate cert, PrivateKey privateKey,
447    char[] keyPassword, KeyStore keyStore) throws IOException, GeneralSecurityException {
448    keyStore.load(null, keyPassword);
449    keyStore.setKeyEntry("key", privateKey, keyPassword, new Certificate[] { cert });
450    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
451    keyStore.store(outputStream, keyPassword);
452    outputStream.flush();
453    byte[] result = outputStream.toByteArray();
454    outputStream.close();
455    return result;
456  }
457
458  /**
459   * Convenience method to convert a bouncycastle X509CertificateHolder to a java X509Certificate.
460   * @param certHolder a bouncycastle X509CertificateHolder.
461   * @return a java X509Certificate
462   * @throws CertificateException if the conversion fails.
463   */
464  public static X509Certificate toX509Cert(X509CertificateHolder certHolder)
465    throws CertificateException {
466    return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME)
467      .getCertificate(certHolder);
468  }
469
470  private X509TestHelpers() {
471    // empty
472  }
473}