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 static java.util.Objects.requireNonNull; 021 022import java.io.File; 023import java.io.IOException; 024import java.nio.charset.StandardCharsets; 025import java.nio.file.Files; 026import java.nio.file.StandardCopyOption; 027import java.nio.file.StandardOpenOption; 028import java.security.GeneralSecurityException; 029import java.security.KeyPair; 030import java.security.Security; 031import java.security.cert.X509Certificate; 032import java.util.Arrays; 033import org.apache.hadoop.conf.Configuration; 034import org.apache.yetus.audience.InterfaceAudience; 035import org.bouncycastle.asn1.x500.X500Name; 036import org.bouncycastle.asn1.x500.X500NameBuilder; 037import org.bouncycastle.asn1.x500.style.BCStyle; 038import org.bouncycastle.asn1.x509.GeneralName; 039import org.bouncycastle.asn1.x509.GeneralNames; 040import org.bouncycastle.jce.provider.BouncyCastleProvider; 041import org.bouncycastle.operator.OperatorCreationException; 042 043/** 044 * This class simplifies the creation of certificates and private keys for SSL/TLS connections. 045 * <p/> 046 * This file has been copied from the Apache ZooKeeper project. 047 * @see <a href= 048 * "https://github.com/apache/zookeeper/blob/c74658d398cdc1d207aa296cb6e20de00faec03e/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java">Base 049 * revision</a> 050 */ 051@InterfaceAudience.Private 052public final class X509TestContext { 053 054 private static final String TRUST_STORE_PREFIX = "hbase_test_ca"; 055 private static final String KEY_STORE_PREFIX = "hbase_test_key"; 056 057 private final File tempDir; 058 private final Configuration conf; 059 060 private X509Certificate trustStoreCertificate; 061 private final char[] trustStorePassword; 062 private KeyPair trustStoreKeyPair; 063 private File trustStoreJksFile; 064 private File trustStorePemFile; 065 private File trustStorePkcs12File; 066 private File trustStoreBcfksFile; 067 068 private KeyPair keyStoreKeyPair; 069 private X509Certificate keyStoreCertificate; 070 private final char[] keyStorePassword; 071 private File keyStoreJksFile; 072 private File keyStorePemFile; 073 private File keyStorePkcs12File; 074 private File keyStoreBcfksFile; 075 076 /** 077 * Constructor is intentionally private, use the Builder class instead. 078 * @param conf the configuration 079 * @param tempDir the directory in which key store and trust store temp files will be 080 * written. 081 * @param trustStoreKeyPair the key pair for the trust store. 082 * @param trustStorePassword the password to protect a JKS trust store (ignored for PEM trust 083 * stores). 084 * @param keyStoreKeyPair the key pair for the key store. 085 * @param keyStorePassword the password to protect the key store private key. 086 */ 087 private X509TestContext(Configuration conf, File tempDir, KeyPair trustStoreKeyPair, 088 char[] trustStorePassword, KeyPair keyStoreKeyPair, char[] keyStorePassword) 089 throws IOException, GeneralSecurityException, OperatorCreationException { 090 if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { 091 throw new IllegalStateException("BC Security provider was not found"); 092 } 093 this.conf = conf; 094 this.tempDir = requireNonNull(tempDir); 095 if (!tempDir.isDirectory()) { 096 throw new IllegalArgumentException("Not a directory: " + tempDir); 097 } 098 099 this.trustStoreKeyPair = trustStoreKeyPair; 100 this.trustStorePassword = requireNonNull(trustStorePassword); 101 this.keyStoreKeyPair = requireNonNull(keyStoreKeyPair); 102 this.keyStorePassword = requireNonNull(keyStorePassword); 103 104 createCertificates(); 105 106 trustStorePkcs12File = null; 107 trustStorePemFile = null; 108 trustStoreJksFile = null; 109 keyStorePkcs12File = null; 110 keyStorePemFile = null; 111 keyStoreJksFile = null; 112 } 113 114 /** 115 * Used by {@link #cloneWithNewKeystoreCert(X509Certificate)}. Should set all fields except 116 * generated keystore path fields 117 */ 118 private X509TestContext(File tempDir, Configuration conf, X509Certificate trustStoreCertificate, 119 char[] trustStorePassword, KeyPair trustStoreKeyPair, File trustStoreJksFile, 120 File trustStorePemFile, File trustStorePkcs12File, KeyPair keyStoreKeyPair, 121 char[] keyStorePassword, X509Certificate keyStoreCertificate) { 122 this.tempDir = tempDir; 123 this.conf = conf; 124 this.trustStoreCertificate = trustStoreCertificate; 125 this.trustStorePassword = trustStorePassword; 126 this.trustStoreKeyPair = trustStoreKeyPair; 127 this.trustStoreJksFile = trustStoreJksFile; 128 this.trustStorePemFile = trustStorePemFile; 129 this.trustStorePkcs12File = trustStorePkcs12File; 130 this.keyStoreKeyPair = keyStoreKeyPair; 131 this.keyStoreCertificate = keyStoreCertificate; 132 this.keyStorePassword = keyStorePassword; 133 keyStorePkcs12File = null; 134 keyStorePemFile = null; 135 keyStoreJksFile = null; 136 } 137 138 /** 139 * Generates a new certificate using this context's CA and keystoreKeyPair. By default, the cert 140 * will have localhost in the subjectAltNames. This can be overridden by passing one or more 141 * string arguments after the cert name. The expectation for those arguments is that they are 142 * valid DNS names. 143 */ 144 public X509Certificate newCert(X500Name name, String... subjectAltNames) 145 throws GeneralSecurityException, IOException, OperatorCreationException { 146 if (subjectAltNames.length == 0) { 147 return X509TestHelpers.newCert(trustStoreCertificate, trustStoreKeyPair, name, 148 keyStoreKeyPair.getPublic()); 149 } 150 GeneralName[] names = new GeneralName[subjectAltNames.length]; 151 for (int i = 0; i < subjectAltNames.length; i++) { 152 names[i] = new GeneralName(GeneralName.dNSName, subjectAltNames[i]); 153 } 154 return X509TestHelpers.newCert(trustStoreCertificate, trustStoreKeyPair, name, 155 keyStoreKeyPair.getPublic(), new GeneralNames(names)); 156 } 157 158 public File getTempDir() { 159 return tempDir; 160 } 161 162 public char[] getTrustStorePassword() { 163 return trustStorePassword; 164 } 165 166 /** 167 * Returns the path to the trust store file in the given format (JKS or PEM). Note that the file 168 * is created lazily, the first time this method is called. 169 * @param storeFileType the store file type (JKS or PEM). 170 * @return the path to the trust store file. 171 * @throws Exception if there is an error creating the trust store file. 172 */ 173 public File getTrustStoreFile(KeyStoreFileType storeFileType) throws Exception { 174 switch (storeFileType) { 175 case JKS: 176 return getTrustStoreJksFile(); 177 case PEM: 178 return getTrustStorePemFile(); 179 case PKCS12: 180 return getTrustStorePkcs12File(); 181 case BCFKS: 182 return getTrustStoreBcfksFile(); 183 default: 184 throw new IllegalArgumentException("Invalid trust store type: " + storeFileType 185 + ", must be one of: " + Arrays.toString(KeyStoreFileType.values())); 186 } 187 } 188 189 private void atomicWriteFile(File file, byte[] content) throws IOException { 190 File tmpFile = new File(file.getParentFile(), file.getName().concat(".tmp")); 191 Files.write(tmpFile.toPath(), content, StandardOpenOption.CREATE, 192 StandardOpenOption.TRUNCATE_EXISTING); 193 Files.move(tmpFile.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING, 194 StandardCopyOption.ATOMIC_MOVE); 195 } 196 197 private File getTrustStoreJksFile() throws Exception { 198 if (trustStoreJksFile == null) { 199 trustStoreJksFile = File.createTempFile(TRUST_STORE_PREFIX, 200 KeyStoreFileType.JKS.getDefaultFileExtension(), tempDir); 201 generateTrustStoreJksFile(); 202 } 203 return trustStoreJksFile; 204 } 205 206 private void generateTrustStoreJksFile() throws Exception { 207 atomicWriteFile(trustStoreJksFile, 208 X509TestHelpers.certToJavaTrustStoreBytes(trustStoreCertificate, trustStorePassword)); 209 } 210 211 private File getTrustStorePemFile() throws IOException { 212 if (trustStorePemFile == null) { 213 trustStorePemFile = File.createTempFile(TRUST_STORE_PREFIX, 214 KeyStoreFileType.PEM.getDefaultFileExtension(), tempDir); 215 generateTrustStorePemFile(); 216 } 217 return trustStorePemFile; 218 } 219 220 private void generateTrustStorePemFile() throws IOException { 221 atomicWriteFile(trustStorePemFile, X509TestHelpers 222 .pemEncodeX509Certificate(trustStoreCertificate).getBytes(StandardCharsets.US_ASCII)); 223 } 224 225 private File getTrustStorePkcs12File() throws Exception { 226 if (trustStorePkcs12File == null) { 227 trustStorePkcs12File = File.createTempFile(TRUST_STORE_PREFIX, 228 KeyStoreFileType.PKCS12.getDefaultFileExtension(), tempDir); 229 generateTrustStorePkcs12File(); 230 } 231 return trustStorePkcs12File; 232 } 233 234 private void generateTrustStorePkcs12File() throws Exception { 235 atomicWriteFile(trustStorePkcs12File, 236 X509TestHelpers.certToPKCS12TrustStoreBytes(trustStoreCertificate, trustStorePassword)); 237 } 238 239 private File getTrustStoreBcfksFile() throws Exception { 240 if (trustStoreBcfksFile == null) { 241 trustStoreBcfksFile = File.createTempFile(TRUST_STORE_PREFIX, 242 KeyStoreFileType.BCFKS.getDefaultFileExtension(), tempDir); 243 generateTrustStoreBcfksFile(); 244 } 245 return trustStoreBcfksFile; 246 } 247 248 private void generateTrustStoreBcfksFile() throws Exception { 249 atomicWriteFile(trustStoreBcfksFile, 250 X509TestHelpers.certToBCFKSTrustStoreBytes(trustStoreCertificate, trustStorePassword)); 251 } 252 253 public X509Certificate getKeyStoreCertificate() { 254 return keyStoreCertificate; 255 } 256 257 public char[] getKeyStorePassword() { 258 return keyStorePassword; 259 } 260 261 public boolean isKeyStoreEncrypted() { 262 return keyStorePassword != null; 263 } 264 265 public Configuration getConf() { 266 return conf; 267 } 268 269 /** 270 * Returns the path to the key store file in the given format (JKS, PEM, ...). Note that the file 271 * is created lazily, the first time this method is called. 272 * @param storeFileType the store file type (JKS, PEM, ...). 273 * @return the path to the key store file. 274 * @throws Exception if there is an error creating the key store file. 275 */ 276 public File getKeyStoreFile(KeyStoreFileType storeFileType) throws Exception { 277 switch (storeFileType) { 278 case JKS: 279 return getKeyStoreJksFile(); 280 case PEM: 281 return getKeyStorePemFile(); 282 case PKCS12: 283 return getKeyStorePkcs12File(); 284 case BCFKS: 285 return getKeyStoreBcfksFile(); 286 default: 287 throw new IllegalArgumentException("Invalid key store type: " + storeFileType 288 + ", must be one of: " + Arrays.toString(KeyStoreFileType.values())); 289 } 290 } 291 292 private File getKeyStoreJksFile() throws Exception { 293 if (keyStoreJksFile == null) { 294 keyStoreJksFile = File.createTempFile(KEY_STORE_PREFIX, 295 KeyStoreFileType.JKS.getDefaultFileExtension(), tempDir); 296 generateKeyStoreJksFile(); 297 } 298 return keyStoreJksFile; 299 } 300 301 private void generateKeyStoreJksFile() throws Exception { 302 atomicWriteFile(keyStoreJksFile, X509TestHelpers.certAndPrivateKeyToJavaKeyStoreBytes( 303 keyStoreCertificate, keyStoreKeyPair.getPrivate(), keyStorePassword)); 304 } 305 306 private File getKeyStorePemFile() throws Exception { 307 if (keyStorePemFile == null) { 308 keyStorePemFile = File.createTempFile(KEY_STORE_PREFIX, 309 KeyStoreFileType.PEM.getDefaultFileExtension(), tempDir); 310 generateKeyStorePemFile(); 311 } 312 return keyStorePemFile; 313 } 314 315 private void generateKeyStorePemFile() throws Exception { 316 atomicWriteFile(keyStorePemFile, X509TestHelpers.pemEncodeCertAndPrivateKey(keyStoreCertificate, 317 keyStoreKeyPair.getPrivate(), keyStorePassword).getBytes(StandardCharsets.US_ASCII)); 318 } 319 320 private File getKeyStorePkcs12File() throws Exception { 321 if (keyStorePkcs12File == null) { 322 keyStorePkcs12File = File.createTempFile(KEY_STORE_PREFIX, 323 KeyStoreFileType.PKCS12.getDefaultFileExtension(), tempDir); 324 generateKeyStorePkcs12File(); 325 } 326 return keyStorePkcs12File; 327 } 328 329 private void generateKeyStorePkcs12File() throws Exception { 330 atomicWriteFile(keyStorePkcs12File, X509TestHelpers.certAndPrivateKeyToPKCS12Bytes( 331 keyStoreCertificate, keyStoreKeyPair.getPrivate(), keyStorePassword)); 332 } 333 334 private File getKeyStoreBcfksFile() throws Exception { 335 if (keyStoreBcfksFile == null) { 336 keyStoreBcfksFile = File.createTempFile(KEY_STORE_PREFIX, 337 KeyStoreFileType.BCFKS.getDefaultFileExtension(), tempDir); 338 generateKeyStoreBcfksFile(); 339 } 340 return keyStoreBcfksFile; 341 } 342 343 private void generateKeyStoreBcfksFile() throws Exception { 344 atomicWriteFile(keyStoreBcfksFile, X509TestHelpers.certAndPrivateKeyToBCFKSBytes( 345 keyStoreCertificate, keyStoreKeyPair.getPrivate(), keyStorePassword)); 346 } 347 348 /** 349 * Sets the SSL system properties such that the given X509Util object can be used to create SSL 350 * Contexts that will use the trust store and key store files created by this test context. 351 * Example usage: 352 * 353 * <pre> 354 * X509TestContext testContext = ...; // create the test context 355 * X509Util x509Util = new QuorumX509Util(); 356 * testContext.setSystemProperties(x509Util, KeyStoreFileType.JKS, KeyStoreFileType.JKS); 357 * // The returned context will use the key store and trust store created by the test context. 358 * SSLContext ctx = x509Util.getDefaultSSLContext(); 359 * </pre> 360 * 361 * @param keyStoreFileType the store file type to use for the key store (JKS, PEM, ...). 362 * @param trustStoreFileType the store file type to use for the trust store (JKS, PEM, ...). 363 * @throws Exception if there is an error creating the key store file or trust store file. 364 */ 365 public void setConfigurations(KeyStoreFileType keyStoreFileType, 366 KeyStoreFileType trustStoreFileType) throws Exception { 367 setKeystoreConfigurations(keyStoreFileType, conf); 368 conf.set(X509Util.TLS_CONFIG_TRUSTSTORE_LOCATION, 369 this.getTrustStoreFile(trustStoreFileType).getAbsolutePath()); 370 conf.set(X509Util.TLS_CONFIG_TRUSTSTORE_PASSWORD, String.valueOf(this.getTrustStorePassword())); 371 conf.set(X509Util.TLS_CONFIG_TRUSTSTORE_TYPE, trustStoreFileType.getPropertyValue()); 372 } 373 374 /** 375 * Sets the KeyStore-related SSL system properties onto the given Configuration such that X509Util 376 * can be used to create SSL Contexts using that KeyStore. This can be used in special 377 * circumstances to inject a "bad" certificate where the keystore doesn't match the CA in the 378 * truststore. Or use it to create a connection without a truststore. 379 * @see #setConfigurations(KeyStoreFileType, KeyStoreFileType) which sets both keystore and 380 * truststore and is more applicable to general use. 381 */ 382 public void setKeystoreConfigurations(KeyStoreFileType keyStoreFileType, Configuration confToSet) 383 throws Exception { 384 385 confToSet.set(X509Util.TLS_CONFIG_KEYSTORE_LOCATION, 386 this.getKeyStoreFile(keyStoreFileType).getAbsolutePath()); 387 confToSet.set(X509Util.TLS_CONFIG_KEYSTORE_PASSWORD, 388 String.valueOf(this.getKeyStorePassword())); 389 confToSet.set(X509Util.TLS_CONFIG_KEYSTORE_TYPE, keyStoreFileType.getPropertyValue()); 390 } 391 392 public void clearConfigurations() { 393 conf.unset(X509Util.TLS_CONFIG_KEYSTORE_LOCATION); 394 conf.unset(X509Util.TLS_CONFIG_KEYSTORE_PASSWORD); 395 conf.unset(X509Util.TLS_CONFIG_KEYSTORE_TYPE); 396 conf.unset(X509Util.TLS_CONFIG_TRUSTSTORE_LOCATION); 397 conf.unset(X509Util.TLS_CONFIG_TRUSTSTORE_PASSWORD); 398 conf.unset(X509Util.TLS_CONFIG_TRUSTSTORE_TYPE); 399 } 400 401 /** 402 * Creates a clone of the current context, but injecting the passed certificate as the KeyStore 403 * cert. The new context's keystore path fields are nulled, so the next call to 404 * {@link #setConfigurations(KeyStoreFileType, KeyStoreFileType)}, 405 * {@link #setKeystoreConfigurations(KeyStoreFileType, Configuration)} , or 406 * {@link #getKeyStoreFile(KeyStoreFileType)} will create a new keystore with this certificate in 407 * place. 408 * @param cert the cert to replace 409 */ 410 public X509TestContext cloneWithNewKeystoreCert(X509Certificate cert) { 411 return new X509TestContext(tempDir, conf, trustStoreCertificate, trustStorePassword, 412 trustStoreKeyPair, trustStoreJksFile, trustStorePemFile, trustStorePkcs12File, 413 keyStoreKeyPair, keyStorePassword, cert); 414 } 415 416 public void regenerateStores(X509KeyType keyStoreKeyType, X509KeyType trustStoreKeyType, 417 KeyStoreFileType keyStoreFileType, KeyStoreFileType trustStoreFileType, 418 String... subjectAltNames) throws Exception { 419 trustStoreKeyPair = X509TestHelpers.generateKeyPair(trustStoreKeyType); 420 keyStoreKeyPair = X509TestHelpers.generateKeyPair(keyStoreKeyType); 421 createCertificates(subjectAltNames); 422 423 switch (keyStoreFileType) { 424 case JKS: 425 generateKeyStoreJksFile(); 426 break; 427 case PEM: 428 generateKeyStorePemFile(); 429 break; 430 case BCFKS: 431 generateKeyStoreBcfksFile(); 432 break; 433 case PKCS12: 434 generateKeyStorePkcs12File(); 435 break; 436 } 437 438 switch (trustStoreFileType) { 439 case JKS: 440 generateTrustStoreJksFile(); 441 break; 442 case PEM: 443 generateTrustStorePemFile(); 444 break; 445 case PKCS12: 446 generateTrustStorePkcs12File(); 447 break; 448 case BCFKS: 449 generateTrustStoreBcfksFile(); 450 break; 451 } 452 } 453 454 private void createCertificates(String... subjectAltNames) 455 throws GeneralSecurityException, IOException, OperatorCreationException { 456 X500NameBuilder caNameBuilder = new X500NameBuilder(BCStyle.INSTANCE); 457 caNameBuilder.addRDN(BCStyle.CN, getClass().getSimpleName() + " Root CA"); 458 trustStoreCertificate = 459 X509TestHelpers.newSelfSignedCACert(caNameBuilder.build(), trustStoreKeyPair); 460 461 X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE); 462 nameBuilder.addRDN(BCStyle.CN, getClass().getSimpleName() + " Zookeeper Test"); 463 keyStoreCertificate = newCert(nameBuilder.build(), subjectAltNames); 464 } 465 466 /** 467 * Builder class, used for creating new instances of X509TestContext. 468 */ 469 public static class Builder { 470 471 private final Configuration conf; 472 private File tempDir; 473 private X509KeyType trustStoreKeyType; 474 private char[] trustStorePassword; 475 private X509KeyType keyStoreKeyType; 476 private char[] keyStorePassword; 477 478 /** 479 * Creates an empty builder with the given Configuration. 480 */ 481 public Builder(Configuration conf) { 482 this.conf = conf; 483 trustStoreKeyType = X509KeyType.EC; 484 keyStoreKeyType = X509KeyType.EC; 485 } 486 487 /** 488 * Builds a new X509TestContext from this builder. 489 * @return a new X509TestContext 490 */ 491 public X509TestContext build() 492 throws IOException, GeneralSecurityException, OperatorCreationException { 493 KeyPair trustStoreKeyPair = X509TestHelpers.generateKeyPair(trustStoreKeyType); 494 KeyPair keyStoreKeyPair = X509TestHelpers.generateKeyPair(keyStoreKeyType); 495 return new X509TestContext(conf, tempDir, trustStoreKeyPair, trustStorePassword, 496 keyStoreKeyPair, keyStorePassword); 497 } 498 499 /** 500 * Sets the temporary directory. Certificate and private key files will be created in this 501 * directory. 502 * @param tempDir the temp directory. 503 * @return this Builder. 504 */ 505 public Builder setTempDir(File tempDir) { 506 this.tempDir = tempDir; 507 return this; 508 } 509 510 /** 511 * Sets the trust store key type. The CA key generated for the test context will be of this 512 * type. 513 * @param keyType the key type. 514 * @return this Builder. 515 */ 516 public Builder setTrustStoreKeyType(X509KeyType keyType) { 517 trustStoreKeyType = keyType; 518 return this; 519 } 520 521 /** 522 * Sets the trust store password. Ignored for PEM trust stores, JKS trust stores will be 523 * encrypted with this password. 524 * @param password the password. 525 * @return this Builder. 526 */ 527 public Builder setTrustStorePassword(char[] password) { 528 trustStorePassword = password; 529 return this; 530 } 531 532 /** 533 * Sets the key store key type. The private key generated for the test context will be of this 534 * type. 535 * @param keyType the key type. 536 * @return this Builder. 537 */ 538 public Builder setKeyStoreKeyType(X509KeyType keyType) { 539 keyStoreKeyType = keyType; 540 return this; 541 } 542 543 /** 544 * Sets the key store password. The private key (PEM, JKS) and certificate (JKS only) will be 545 * encrypted with this password. 546 * @param password the password. 547 * @return this Builder. 548 */ 549 public Builder setKeyStorePassword(char[] password) { 550 keyStorePassword = password; 551 return this; 552 } 553 } 554 555 /** 556 * Returns a new default-constructed Builder. 557 * @return a new Builder. 558 */ 559 public static Builder newBuilder(Configuration conf) { 560 return new Builder(conf); 561 } 562}