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 = initCertBuilder(new X500Name(caCert.getIssuerDN().getName()),
155      now, now.plusDays(1), certSubject, certPublicKey);
156    builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false)); // not a CA
157    builder.addExtension(Extension.keyUsage, true,
158      new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment));
159    builder.addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(
160      new KeyPurposeId[] { KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth }));
161
162    if (subjectAltNames != null) {
163      builder.addExtension(Extension.subjectAlternativeName, false, subjectAltNames);
164    }
165    return buildAndSignCertificate(caKeyPair.getPrivate(), builder);
166  }
167
168  /**
169   * Returns subject alternative names for "localhost".
170   * @return the subject alternative names for "localhost".
171   */
172  private static GeneralNames getLocalhostSubjectAltNames() throws UnknownHostException {
173    InetAddress[] localAddresses = InetAddress.getAllByName("localhost");
174    GeneralName[] generalNames = new GeneralName[localAddresses.length + 1];
175    for (int i = 0; i < localAddresses.length; i++) {
176      generalNames[i] =
177        new GeneralName(GeneralName.iPAddress, new DEROctetString(localAddresses[i].getAddress()));
178    }
179    generalNames[generalNames.length - 1] =
180      new GeneralName(GeneralName.dNSName, new DERIA5String("localhost"));
181    return new GeneralNames(generalNames);
182  }
183
184  /**
185   * Helper method for newSelfSignedCACert() and newCert(). Initializes a X509v3CertificateBuilder
186   * with logic that's common to both methods.
187   * @param issuer           Issuer field of the new cert.
188   * @param notBefore        date before which the new cert is not valid.
189   * @param notAfter         date after which the new cert is not valid.
190   * @param subject          Subject field of the new cert.
191   * @param subjectPublicKey public key to store in the new cert.
192   * @return a X509v3CertificateBuilder that can be further customized to finish creating the new
193   *         cert.
194   */
195  private static X509v3CertificateBuilder initCertBuilder(X500Name issuer, LocalDate notBefore,
196    LocalDate notAfter, X500Name subject, PublicKey subjectPublicKey) {
197    return new X509v3CertificateBuilder(issuer, new BigInteger(SERIAL_NUMBER_MAX_BITS, PRNG),
198      java.sql.Date.valueOf(notBefore), java.sql.Date.valueOf(notAfter), subject,
199      SubjectPublicKeyInfo.getInstance(subjectPublicKey.getEncoded()));
200  }
201
202  /**
203   * Signs the certificate being built by the given builder using the given private key and returns
204   * the certificate.
205   * @param privateKey the private key to sign the certificate with.
206   * @param builder    the cert builder that contains the certificate data.
207   * @return the signed certificate.
208   */
209  private static X509Certificate buildAndSignCertificate(PrivateKey privateKey,
210    X509v3CertificateBuilder builder)
211    throws IOException, OperatorCreationException, CertificateException {
212    BcContentSignerBuilder signerBuilder;
213    if (privateKey.getAlgorithm().contains("RSA")) { // a little hacky way to detect key type, but
214      // it works
215      AlgorithmIdentifier signatureAlgorithm =
216        new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256WithRSAEncryption");
217      AlgorithmIdentifier digestAlgorithm =
218        new DefaultDigestAlgorithmIdentifierFinder().find(signatureAlgorithm);
219      signerBuilder = new BcRSAContentSignerBuilder(signatureAlgorithm, digestAlgorithm);
220    } else { // if not RSA, assume EC
221      AlgorithmIdentifier signatureAlgorithm =
222        new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withECDSA");
223      AlgorithmIdentifier digestAlgorithm =
224        new DefaultDigestAlgorithmIdentifierFinder().find(signatureAlgorithm);
225      signerBuilder = new BcECContentSignerBuilder(signatureAlgorithm, digestAlgorithm);
226    }
227    AsymmetricKeyParameter privateKeyParam = PrivateKeyFactory.createKey(privateKey.getEncoded());
228    ContentSigner signer = signerBuilder.build(privateKeyParam);
229    return toX509Cert(builder.build(signer));
230  }
231
232  /**
233   * Generates a new asymmetric key pair of the given type.
234   * @param keyType the type of key pair to generate.
235   * @return the new key pair.
236   * @throws GeneralSecurityException if your java crypto providers are messed up.
237   */
238  public static KeyPair generateKeyPair(X509KeyType keyType) throws GeneralSecurityException {
239    switch (keyType) {
240      case RSA:
241        return generateRSAKeyPair();
242      case EC:
243        return generateECKeyPair();
244      default:
245        throw new IllegalArgumentException("Invalid X509KeyType");
246    }
247  }
248
249  /**
250   * Generates an RSA key pair with a 2048-bit private key and F4 (65537) as the public exponent.
251   * @return the key pair.
252   */
253  public static KeyPair generateRSAKeyPair() throws GeneralSecurityException {
254    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
255    RSAKeyGenParameterSpec keyGenSpec =
256      new RSAKeyGenParameterSpec(DEFAULT_RSA_KEY_SIZE_BITS, DEFAULT_RSA_PUB_EXPONENT);
257    keyGen.initialize(keyGenSpec, PRNG);
258    return keyGen.generateKeyPair();
259  }
260
261  /**
262   * Generates an elliptic curve key pair using the "secp256r1" aka "prime256v1" aka "NIST P-256"
263   * curve.
264   * @return the key pair.
265   */
266  public static KeyPair generateECKeyPair() throws GeneralSecurityException {
267    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
268    keyGen.initialize(new ECGenParameterSpec(DEFAULT_ELLIPTIC_CURVE_NAME), PRNG);
269    return keyGen.generateKeyPair();
270  }
271
272  /**
273   * PEM-encodes the given X509 certificate and private key (compatible with OpenSSL), optionally
274   * protecting the private key with a password. Concatenates them both and returns the result as a
275   * single string. This creates the PEM encoding of a key store.
276   * @param cert        the X509 certificate to PEM-encode.
277   * @param privateKey  the private key to PEM-encode.
278   * @param keyPassword an optional key password. If empty or null, the private key will not be
279   *                    encrypted.
280   * @return a String containing the PEM encodings of the certificate and private key.
281   * @throws IOException               if converting the certificate or private key to PEM format
282   *                                   fails.
283   * @throws OperatorCreationException if constructing the encryptor from the given password fails.
284   */
285  public static String pemEncodeCertAndPrivateKey(X509Certificate cert, PrivateKey privateKey,
286    char[] keyPassword) throws IOException, OperatorCreationException {
287    return pemEncodeX509Certificate(cert) + "\n" + pemEncodePrivateKey(privateKey, keyPassword);
288  }
289
290  /**
291   * PEM-encodes the given private key (compatible with OpenSSL), optionally protecting it with a
292   * password, and returns the result as a String.
293   * @param key      the private key.
294   * @param password an optional key password. If empty or null, the private key will not be
295   *                 encrypted.
296   * @return a String containing the PEM encoding of the private key.
297   * @throws IOException               if converting the key to PEM format fails.
298   * @throws OperatorCreationException if constructing the encryptor from the given password fails.
299   */
300  public static String pemEncodePrivateKey(PrivateKey key, char[] password)
301    throws IOException, OperatorCreationException {
302    StringWriter stringWriter = new StringWriter();
303    JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter);
304    OutputEncryptor encryptor = null;
305    if (password != null && password.length > 0) {
306      encryptor =
307        new JceOpenSSLPKCS8EncryptorBuilder(PKCSObjectIdentifiers.pbeWithSHAAnd3_KeyTripleDES_CBC)
308          .setProvider(BouncyCastleProvider.PROVIDER_NAME).setRandom(PRNG).setPasssword(password)
309          .build();
310    }
311    pemWriter.writeObject(new JcaPKCS8Generator(key, encryptor));
312    pemWriter.close();
313    return stringWriter.toString();
314  }
315
316  /**
317   * PEM-encodes the given X509 certificate (compatible with OpenSSL) and returns the result as a
318   * String.
319   * @param cert the certificate.
320   * @return a String containing the PEM encoding of the certificate.
321   * @throws IOException if converting the certificate to PEM format fails.
322   */
323  public static String pemEncodeX509Certificate(X509Certificate cert) throws IOException {
324    StringWriter stringWriter = new StringWriter();
325    JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter);
326    pemWriter.writeObject(cert);
327    pemWriter.close();
328    return stringWriter.toString();
329  }
330
331  /**
332   * Encodes the given X509Certificate as a JKS TrustStore, optionally protecting the cert with a
333   * password (though it's unclear why one would do this since certificates only contain public
334   * information and do not need to be kept secret). Returns the byte array encoding of the trust
335   * store, which may be written to a file and loaded to instantiate the trust store at a later
336   * point or in another process.
337   * @param cert        the certificate to serialize.
338   * @param keyPassword an optional password to encrypt the trust store. If empty or null, the cert
339   *                    will not be encrypted.
340   * @return the serialized bytes of the JKS trust store.
341   */
342  public static byte[] certToJavaTrustStoreBytes(X509Certificate cert, char[] keyPassword)
343    throws IOException, GeneralSecurityException {
344    KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
345    return certToTrustStoreBytes(cert, keyPassword, trustStore);
346  }
347
348  /**
349   * Encodes the given X509Certificate as a PKCS12 TrustStore, optionally protecting the cert with a
350   * password (though it's unclear why one would do this since certificates only contain public
351   * information and do not need to be kept secret). Returns the byte array encoding of the trust
352   * store, which may be written to a file and loaded to instantiate the trust store at a later
353   * point or in another process.
354   * @param cert        the certificate to serialize.
355   * @param keyPassword an optional password to encrypt the trust store. If empty or null, the cert
356   *                    will not be encrypted.
357   * @return the serialized bytes of the PKCS12 trust store.
358   */
359  public static byte[] certToPKCS12TrustStoreBytes(X509Certificate cert, char[] keyPassword)
360    throws IOException, GeneralSecurityException {
361    KeyStore trustStore = KeyStore.getInstance("PKCS12");
362    return certToTrustStoreBytes(cert, keyPassword, trustStore);
363  }
364
365  /**
366   * Encodes the given X509Certificate as a BCFKS TrustStore, optionally protecting the cert with a
367   * password (though it's unclear why one would do this since certificates only contain public
368   * information and do not need to be kept secret). Returns the byte array encoding of the trust
369   * store, which may be written to a file and loaded to instantiate the trust store at a later
370   * point or in another process.
371   * @param cert        the certificate to serialize.
372   * @param keyPassword an optional password to encrypt the trust store. If empty or null, the cert
373   *                    will not be encrypted.
374   * @return the serialized bytes of the BCFKS trust store.
375   */
376  public static byte[] certToBCFKSTrustStoreBytes(X509Certificate cert, char[] keyPassword)
377    throws IOException, GeneralSecurityException {
378    KeyStore trustStore = KeyStore.getInstance("BCFKS");
379    return certToTrustStoreBytes(cert, keyPassword, trustStore);
380  }
381
382  private static byte[] certToTrustStoreBytes(X509Certificate cert, char[] keyPassword,
383    KeyStore trustStore) throws IOException, GeneralSecurityException {
384    trustStore.load(null, keyPassword);
385    trustStore.setCertificateEntry(cert.getSubjectDN().toString(), cert);
386    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
387    trustStore.store(outputStream, keyPassword);
388    outputStream.flush();
389    byte[] result = outputStream.toByteArray();
390    outputStream.close();
391    return result;
392  }
393
394  /**
395   * Encodes the given X509Certificate and private key as a JKS KeyStore, optionally protecting the
396   * private key (and possibly the cert?) with a password. Returns the byte array encoding of the
397   * key store, which may be written to a file and loaded to instantiate the key store at a later
398   * point or in another process.
399   * @param cert        the X509 certificate to serialize.
400   * @param privateKey  the private key to serialize.
401   * @param keyPassword an optional key password. If empty or null, the private key will not be
402   *                    encrypted.
403   * @return the serialized bytes of the JKS key store.
404   */
405  public static byte[] certAndPrivateKeyToJavaKeyStoreBytes(X509Certificate cert,
406    PrivateKey privateKey, char[] keyPassword) throws IOException, GeneralSecurityException {
407    KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
408    return certAndPrivateKeyToBytes(cert, privateKey, keyPassword, keyStore);
409  }
410
411  /**
412   * Encodes the given X509Certificate and private key as a PKCS12 KeyStore, optionally protecting
413   * the private key (and possibly the cert?) with a password. Returns the byte array encoding of
414   * the key store, which may be written to a file and loaded to instantiate the key store at a
415   * later point or in another process.
416   * @param cert        the X509 certificate to serialize.
417   * @param privateKey  the private key to serialize.
418   * @param keyPassword an optional key password. If empty or null, the private key will not be
419   *                    encrypted.
420   * @return the serialized bytes of the PKCS12 key store.
421   */
422  public static byte[] certAndPrivateKeyToPKCS12Bytes(X509Certificate cert, PrivateKey privateKey,
423    char[] keyPassword) throws IOException, GeneralSecurityException {
424    KeyStore keyStore = KeyStore.getInstance("PKCS12");
425    return certAndPrivateKeyToBytes(cert, privateKey, keyPassword, keyStore);
426  }
427
428  /**
429   * Encodes the given X509Certificate and private key as a BCFKS KeyStore, optionally protecting
430   * the private key (and possibly the cert?) with a password. Returns the byte array encoding of
431   * the key store, which may be written to a file and loaded to instantiate the key store at a
432   * later point or in another process.
433   * @param cert        the X509 certificate to serialize.
434   * @param privateKey  the private key to serialize.
435   * @param keyPassword an optional key password. If empty or null, the private key will not be
436   *                    encrypted.
437   * @return the serialized bytes of the BCFKS key store.
438   */
439  public static byte[] certAndPrivateKeyToBCFKSBytes(X509Certificate cert, PrivateKey privateKey,
440    char[] keyPassword) throws IOException, GeneralSecurityException {
441    KeyStore keyStore = KeyStore.getInstance("BCFKS");
442    return certAndPrivateKeyToBytes(cert, privateKey, keyPassword, keyStore);
443  }
444
445  private static byte[] certAndPrivateKeyToBytes(X509Certificate cert, PrivateKey privateKey,
446    char[] keyPassword, KeyStore keyStore) throws IOException, GeneralSecurityException {
447    keyStore.load(null, keyPassword);
448    keyStore.setKeyEntry("key", privateKey, keyPassword, new Certificate[] { cert });
449    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
450    keyStore.store(outputStream, keyPassword);
451    outputStream.flush();
452    byte[] result = outputStream.toByteArray();
453    outputStream.close();
454    return result;
455  }
456
457  /**
458   * Convenience method to convert a bouncycastle X509CertificateHolder to a java X509Certificate.
459   * @param certHolder a bouncycastle X509CertificateHolder.
460   * @return a java X509Certificate
461   * @throws CertificateException if the conversion fails.
462   */
463  public static X509Certificate toX509Cert(X509CertificateHolder certHolder)
464    throws CertificateException {
465    return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME)
466      .getCertificate(certHolder);
467  }
468
469  private X509TestHelpers() {
470    // empty
471  }
472}