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}