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}