| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.omid.tls; |
| |
| import org.bouncycastle.asn1.DERIA5String; |
| import org.bouncycastle.asn1.DEROctetString; |
| import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; |
| import org.bouncycastle.asn1.x500.X500Name; |
| import org.bouncycastle.asn1.x509.*; |
| import org.bouncycastle.cert.X509CertificateHolder; |
| import org.bouncycastle.cert.X509v3CertificateBuilder; |
| import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; |
| import org.bouncycastle.crypto.params.AsymmetricKeyParameter; |
| import org.bouncycastle.crypto.util.PrivateKeyFactory; |
| import org.bouncycastle.jce.provider.BouncyCastleProvider; |
| import org.bouncycastle.openssl.jcajce.JcaPEMWriter; |
| import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator; |
| import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder; |
| import org.bouncycastle.operator.*; |
| import org.bouncycastle.operator.bc.BcContentSignerBuilder; |
| import org.bouncycastle.operator.bc.BcECContentSignerBuilder; |
| import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.StringWriter; |
| import java.math.BigInteger; |
| import java.net.InetAddress; |
| import java.net.UnknownHostException; |
| import java.security.*; |
| import java.security.cert.Certificate; |
| import java.security.cert.CertificateException; |
| import java.security.cert.X509Certificate; |
| import java.security.spec.ECGenParameterSpec; |
| import java.security.spec.RSAKeyGenParameterSpec; |
| import java.time.LocalDate; |
| import java.time.ZoneId; |
| |
| /** |
| * This class contains helper methods for creating X509 certificates and key pairs, and for |
| * serializing them to JKS, PEM or other keystore type files. |
| * <p/> |
| * This file has is based on the one in HBase project. |
| * @see <a href= |
| * "https://github.com/apache/hbase/blob/d2b0074f7ad4c43d31a1a511a0d74feda72451d1/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/X509TestHelpers.java">Base |
| * revision</a> |
| */ |
| final class X509TestHelpers { |
| |
| private static final SecureRandom PRNG = new SecureRandom(); |
| private static final int DEFAULT_RSA_KEY_SIZE_BITS = 2048; |
| private static final BigInteger DEFAULT_RSA_PUB_EXPONENT = RSAKeyGenParameterSpec.F4; // 65537 |
| private static final String DEFAULT_ELLIPTIC_CURVE_NAME = "secp256r1"; |
| // Per RFC 5280 section 4.1.2.2, X509 certificates can use up to 20 bytes == 160 bits for serial |
| // numbers. |
| private static final int SERIAL_NUMBER_MAX_BITS = 20 * Byte.SIZE; |
| |
| /** |
| * Uses the private key of the given key pair to create a self-signed CA certificate with the |
| * public half of the key pair and the given subject and expiration. The issuer of the new cert |
| * will be equal to the subject. Returns the new certificate. The returned certificate should be |
| * used as the trust store. The private key of the input key pair should be used to sign |
| * certificates that are used by test peers to establish TLS connections to each other. |
| * @param subject the subject of the new certificate being created. |
| * @param keyPair the key pair to use. The public key will be embedded in the new certificate, and |
| * the private key will be used to self-sign the certificate. |
| * @return a new self-signed CA certificate. |
| */ |
| public static X509Certificate newSelfSignedCACert(X500Name subject, KeyPair keyPair) |
| throws IOException, OperatorCreationException, GeneralSecurityException { |
| LocalDate now = LocalDate.now(ZoneId.systemDefault()); |
| X509v3CertificateBuilder builder = initCertBuilder(subject, // for self-signed certs, |
| // issuer == subject |
| now, now.plusDays(1), subject, keyPair.getPublic()); |
| builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true)); // is a CA |
| builder.addExtension(Extension.keyUsage, true, |
| new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign | KeyUsage.cRLSign)); |
| return buildAndSignCertificate(keyPair.getPrivate(), builder); |
| } |
| |
| /** |
| * Using the private key of the given CA key pair and the Subject of the given CA cert as the |
| * Issuer, issues a new cert with the given subject and public key. The returned certificate, |
| * combined with the private key half of the <code>certPublicKey</code>, should be used as the key |
| * store. |
| * @param caCert the certificate of the CA that's doing the signing. |
| * @param caKeyPair the key pair of the CA. The private key will be used to sign. The public |
| * key must match the public key in the <code>caCert</code>. |
| * @param certSubject the subject field of the new cert being issued. |
| * @param certPublicKey the public key of the new cert being issued. |
| * @return a new certificate signed by the CA's private key. |
| */ |
| public static X509Certificate newCert(X509Certificate caCert, KeyPair caKeyPair, |
| X500Name certSubject, PublicKey certPublicKey) |
| throws IOException, OperatorCreationException, GeneralSecurityException { |
| if (!caKeyPair.getPublic().equals(caCert.getPublicKey())) { |
| throw new IllegalArgumentException( |
| "CA private key does not match the public key in " + "the CA cert"); |
| } |
| LocalDate now = LocalDate.now(ZoneId.systemDefault()); |
| X509v3CertificateBuilder builder = initCertBuilder(new X500Name(caCert.getIssuerDN().getName()), |
| now, now.plusDays(1), certSubject, certPublicKey); |
| builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false)); // not a CA |
| builder.addExtension(Extension.keyUsage, true, |
| new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment)); |
| builder.addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage( |
| new KeyPurposeId[] { KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth })); |
| |
| builder.addExtension(Extension.subjectAlternativeName, false, getLocalhostSubjectAltNames()); |
| return buildAndSignCertificate(caKeyPair.getPrivate(), builder); |
| } |
| |
| /** |
| * Returns subject alternative names for "localhost". |
| * @return the subject alternative names for "localhost". |
| */ |
| private static GeneralNames getLocalhostSubjectAltNames() throws UnknownHostException { |
| InetAddress[] localAddresses = InetAddress.getAllByName("localhost"); |
| GeneralName[] generalNames = new GeneralName[localAddresses.length + 1]; |
| for (int i = 0; i < localAddresses.length; i++) { |
| generalNames[i] = |
| new GeneralName(GeneralName.iPAddress, new DEROctetString(localAddresses[i].getAddress())); |
| } |
| generalNames[generalNames.length - 1] = |
| new GeneralName(GeneralName.dNSName, new DERIA5String("localhost")); |
| return new GeneralNames(generalNames); |
| } |
| |
| /** |
| * Helper method for newSelfSignedCACert() and newCert(). Initializes a X509v3CertificateBuilder |
| * with logic that's common to both methods. |
| * @param issuer Issuer field of the new cert. |
| * @param notBefore date before which the new cert is not valid. |
| * @param notAfter date after which the new cert is not valid. |
| * @param subject Subject field of the new cert. |
| * @param subjectPublicKey public key to store in the new cert. |
| * @return a X509v3CertificateBuilder that can be further customized to finish creating the new |
| * cert. |
| */ |
| private static X509v3CertificateBuilder initCertBuilder(X500Name issuer, LocalDate notBefore, |
| LocalDate notAfter, X500Name subject, PublicKey subjectPublicKey) { |
| return new X509v3CertificateBuilder(issuer, new BigInteger(SERIAL_NUMBER_MAX_BITS, PRNG), |
| java.sql.Date.valueOf(notBefore), java.sql.Date.valueOf(notAfter), subject, |
| SubjectPublicKeyInfo.getInstance(subjectPublicKey.getEncoded())); |
| } |
| |
| /** |
| * Signs the certificate being built by the given builder using the given private key and returns |
| * the certificate. |
| * @param privateKey the private key to sign the certificate with. |
| * @param builder the cert builder that contains the certificate data. |
| * @return the signed certificate. |
| */ |
| private static X509Certificate buildAndSignCertificate(PrivateKey privateKey, |
| X509v3CertificateBuilder builder) |
| throws IOException, OperatorCreationException, CertificateException { |
| BcContentSignerBuilder signerBuilder; |
| if (privateKey.getAlgorithm().contains("RSA")) { // a little hacky way to detect key type, but |
| // it works |
| AlgorithmIdentifier signatureAlgorithm = |
| new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256WithRSAEncryption"); |
| AlgorithmIdentifier digestAlgorithm = |
| new DefaultDigestAlgorithmIdentifierFinder().find(signatureAlgorithm); |
| signerBuilder = new BcRSAContentSignerBuilder(signatureAlgorithm, digestAlgorithm); |
| } else { // if not RSA, assume EC |
| AlgorithmIdentifier signatureAlgorithm = |
| new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withECDSA"); |
| AlgorithmIdentifier digestAlgorithm = |
| new DefaultDigestAlgorithmIdentifierFinder().find(signatureAlgorithm); |
| signerBuilder = new BcECContentSignerBuilder(signatureAlgorithm, digestAlgorithm); |
| } |
| AsymmetricKeyParameter privateKeyParam = PrivateKeyFactory.createKey(privateKey.getEncoded()); |
| ContentSigner signer = signerBuilder.build(privateKeyParam); |
| return toX509Cert(builder.build(signer)); |
| } |
| |
| /** |
| * Generates a new asymmetric key pair of the given type. |
| * @param keyType the type of key pair to generate. |
| * @return the new key pair. |
| * @throws GeneralSecurityException if your java crypto providers are messed up. |
| */ |
| public static KeyPair generateKeyPair(X509KeyType keyType) throws GeneralSecurityException { |
| switch (keyType) { |
| case RSA: |
| return generateRSAKeyPair(); |
| case EC: |
| return generateECKeyPair(); |
| default: |
| throw new IllegalArgumentException("Invalid X509KeyType"); |
| } |
| } |
| |
| /** |
| * Generates an RSA key pair with a 2048-bit private key and F4 (65537) as the public exponent. |
| * @return the key pair. |
| */ |
| public static KeyPair generateRSAKeyPair() throws GeneralSecurityException { |
| KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); |
| RSAKeyGenParameterSpec keyGenSpec = |
| new RSAKeyGenParameterSpec(DEFAULT_RSA_KEY_SIZE_BITS, DEFAULT_RSA_PUB_EXPONENT); |
| keyGen.initialize(keyGenSpec, PRNG); |
| return keyGen.generateKeyPair(); |
| } |
| |
| /** |
| * Generates an elliptic curve key pair using the "secp256r1" aka "prime256v1" aka "NIST P-256" |
| * curve. |
| * @return the key pair. |
| */ |
| public static KeyPair generateECKeyPair() throws GeneralSecurityException { |
| KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC"); |
| keyGen.initialize(new ECGenParameterSpec(DEFAULT_ELLIPTIC_CURVE_NAME), PRNG); |
| return keyGen.generateKeyPair(); |
| } |
| |
| /** |
| * PEM-encodes the given X509 certificate and private key (compatible with OpenSSL), optionally |
| * protecting the private key with a password. Concatenates them both and returns the result as a |
| * single string. This creates the PEM encoding of a key store. |
| * @param cert the X509 certificate to PEM-encode. |
| * @param privateKey the private key to PEM-encode. |
| * @param keyPassword an optional key password. If empty or null, the private key will not be |
| * encrypted. |
| * @return a String containing the PEM encodings of the certificate and private key. |
| * @throws IOException if converting the certificate or private key to PEM format |
| * fails. |
| * @throws OperatorCreationException if constructing the encryptor from the given password fails. |
| */ |
| public static String pemEncodeCertAndPrivateKey(X509Certificate cert, PrivateKey privateKey, |
| String keyPassword) throws IOException, OperatorCreationException { |
| return pemEncodeX509Certificate(cert) + "\n" + pemEncodePrivateKey(privateKey, keyPassword); |
| } |
| |
| /** |
| * PEM-encodes the given private key (compatible with OpenSSL), optionally protecting it with a |
| * password, and returns the result as a String. |
| * @param key the private key. |
| * @param password an optional key password. If empty or null, the private key will not be |
| * encrypted. |
| * @return a String containing the PEM encoding of the private key. |
| * @throws IOException if converting the key to PEM format fails. |
| * @throws OperatorCreationException if constructing the encryptor from the given password fails. |
| */ |
| public static String pemEncodePrivateKey(PrivateKey key, String password) |
| throws IOException, OperatorCreationException { |
| StringWriter stringWriter = new StringWriter(); |
| JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter); |
| OutputEncryptor encryptor = null; |
| if (password != null && password.length() > 0) { |
| encryptor = |
| new JceOpenSSLPKCS8EncryptorBuilder(PKCSObjectIdentifiers.pbeWithSHAAnd3_KeyTripleDES_CBC) |
| .setProvider(BouncyCastleProvider.PROVIDER_NAME).setRandom(PRNG) |
| .setPasssword(password.toCharArray()).build(); |
| } |
| pemWriter.writeObject(new JcaPKCS8Generator(key, encryptor)); |
| pemWriter.close(); |
| return stringWriter.toString(); |
| } |
| |
| /** |
| * PEM-encodes the given X509 certificate (compatible with OpenSSL) and returns the result as a |
| * String. |
| * @param cert the certificate. |
| * @return a String containing the PEM encoding of the certificate. |
| * @throws IOException if converting the certificate to PEM format fails. |
| */ |
| public static String pemEncodeX509Certificate(X509Certificate cert) throws IOException { |
| StringWriter stringWriter = new StringWriter(); |
| JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter); |
| pemWriter.writeObject(cert); |
| pemWriter.close(); |
| return stringWriter.toString(); |
| } |
| |
| /** |
| * Encodes the given X509Certificate as a JKS TrustStore, optionally protecting the cert with a |
| * password (though it's unclear why one would do this since certificates only contain public |
| * information and do not need to be kept secret). Returns the byte array encoding of the trust |
| * store, which may be written to a file and loaded to instantiate the trust store at a later |
| * point or in another process. |
| * @param cert the certificate to serialize. |
| * @param keyPassword an optional password to encrypt the trust store. If empty or null, the cert |
| * will not be encrypted. |
| * @return the serialized bytes of the JKS trust store. |
| */ |
| public static byte[] certToJavaTrustStoreBytes(X509Certificate cert, String keyPassword) |
| throws IOException, GeneralSecurityException { |
| KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); |
| return certToTrustStoreBytes(cert, keyPassword, trustStore); |
| } |
| |
| /** |
| * Encodes the given X509Certificate as a PKCS12 TrustStore, optionally protecting the cert with a |
| * password (though it's unclear why one would do this since certificates only contain public |
| * information and do not need to be kept secret). Returns the byte array encoding of the trust |
| * store, which may be written to a file and loaded to instantiate the trust store at a later |
| * point or in another process. |
| * @param cert the certificate to serialize. |
| * @param keyPassword an optional password to encrypt the trust store. If empty or null, the cert |
| * will not be encrypted. |
| * @return the serialized bytes of the PKCS12 trust store. |
| */ |
| public static byte[] certToPKCS12TrustStoreBytes(X509Certificate cert, String keyPassword) |
| throws IOException, GeneralSecurityException { |
| KeyStore trustStore = KeyStore.getInstance("PKCS12"); |
| return certToTrustStoreBytes(cert, keyPassword, trustStore); |
| } |
| |
| |
| /** |
| * Encodes the given X509Certificate as a BCFKS TrustStore, optionally protecting the cert with a |
| * password (though it's unclear why one would do this since certificates only contain public |
| * information and do not need to be kept secret). Returns the byte array encoding of the trust |
| * store, which may be written to a file and loaded to instantiate the trust store at a later |
| * point or in another process. |
| * @param cert the certificate to serialize. |
| * @param keyPassword an optional password to encrypt the trust store. If empty or null, the cert |
| * will not be encrypted. |
| * @return the serialized bytes of the BCFKS trust store. nn |
| */ |
| public static byte[] certToBCFKSTrustStoreBytes(X509Certificate cert, String keyPassword) |
| throws IOException, GeneralSecurityException { |
| KeyStore trustStore = KeyStore.getInstance("BCFKS"); |
| return certToTrustStoreBytes(cert, keyPassword, trustStore); |
| } |
| |
| private static byte[] certToTrustStoreBytes(X509Certificate cert, String keyPassword, |
| KeyStore trustStore) throws IOException, GeneralSecurityException { |
| char[] keyPasswordChars = keyPassword == null ? new char[0] : keyPassword.toCharArray(); |
| trustStore.load(null, keyPasswordChars); |
| trustStore.setCertificateEntry(cert.getSubjectDN().toString(), cert); |
| ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); |
| trustStore.store(outputStream, keyPasswordChars); |
| outputStream.flush(); |
| byte[] result = outputStream.toByteArray(); |
| outputStream.close(); |
| return result; |
| } |
| |
| /** |
| * Encodes the given X509Certificate and private key as a JKS KeyStore, optionally protecting the |
| * private key (and possibly the cert?) with a password. Returns the byte array encoding of the |
| * key store, which may be written to a file and loaded to instantiate the key store at a later |
| * point or in another process. |
| * @param cert the X509 certificate to serialize. |
| * @param privateKey the private key to serialize. |
| * @param keyPassword an optional key password. If empty or null, the private key will not be |
| * encrypted. |
| * @return the serialized bytes of the JKS key store. |
| */ |
| public static byte[] certAndPrivateKeyToJavaKeyStoreBytes(X509Certificate cert, |
| PrivateKey privateKey, String keyPassword) throws IOException, GeneralSecurityException { |
| KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); |
| return certAndPrivateKeyToBytes(cert, privateKey, keyPassword, keyStore); |
| } |
| |
| /** |
| * Encodes the given X509Certificate and private key as a PKCS12 KeyStore, optionally protecting |
| * the private key (and possibly the cert?) with a password. Returns the byte array encoding of |
| * the key store, which may be written to a file and loaded to instantiate the key store at a |
| * later point or in another process. |
| * @param cert the X509 certificate to serialize. |
| * @param privateKey the private key to serialize. |
| * @param keyPassword an optional key password. If empty or null, the private key will not be |
| * encrypted. |
| * @return the serialized bytes of the PKCS12 key store. |
| */ |
| public static byte[] certAndPrivateKeyToPKCS12Bytes(X509Certificate cert, PrivateKey privateKey, |
| String keyPassword) throws IOException, GeneralSecurityException { |
| KeyStore keyStore = KeyStore.getInstance("PKCS12"); |
| return certAndPrivateKeyToBytes(cert, privateKey, keyPassword, keyStore); |
| } |
| |
| |
| /** |
| * Encodes the given X509Certificate and private key as a BCFKS KeyStore, optionally protecting |
| * the private key (and possibly the cert?) with a password. Returns the byte array encoding of |
| * the key store, which may be written to a file and loaded to instantiate the key store at a |
| * later point or in another process. |
| * @param cert the X509 certificate to serialize. |
| * @param privateKey the private key to serialize. |
| * @param keyPassword an optional key password. If empty or null, the private key will not be |
| * encrypted. |
| * @return the serialized bytes of the BCFKS key store. nn |
| */ |
| public static byte[] certAndPrivateKeyToBCFKSBytes(X509Certificate cert, PrivateKey privateKey, |
| String keyPassword) throws IOException, GeneralSecurityException { |
| KeyStore keyStore = KeyStore.getInstance("BCFKS"); |
| return certAndPrivateKeyToBytes(cert, privateKey, keyPassword, keyStore); |
| } |
| |
| private static byte[] certAndPrivateKeyToBytes(X509Certificate cert, PrivateKey privateKey, |
| String keyPassword, KeyStore keyStore) throws IOException, GeneralSecurityException { |
| char[] keyPasswordChars = keyPassword == null ? new char[0] : keyPassword.toCharArray(); |
| keyStore.load(null, keyPasswordChars); |
| keyStore.setKeyEntry("key", privateKey, keyPasswordChars, new Certificate[] { cert }); |
| ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); |
| keyStore.store(outputStream, keyPasswordChars); |
| outputStream.flush(); |
| byte[] result = outputStream.toByteArray(); |
| outputStream.close(); |
| return result; |
| } |
| |
| /** |
| * Convenience method to convert a bouncycastle X509CertificateHolder to a java X509Certificate. |
| * @param certHolder a bouncycastle X509CertificateHolder. |
| * @return a java X509Certificate |
| * @throws CertificateException if the conversion fails. |
| */ |
| public static X509Certificate toX509Cert(X509CertificateHolder certHolder) |
| throws CertificateException { |
| return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME) |
| .getCertificate(certHolder); |
| } |
| |
| private X509TestHelpers() { |
| // empty |
| } |
| } |