X509TestContext.java
/*
* 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.zookeeper.common;
import static java.util.Objects.requireNonNull;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import org.apache.commons.io.FileUtils;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.OperatorCreationException;
/**
* This class simplifies the creation of certificates and private keys for SSL/TLS connections.
*/
public class X509TestContext {
private static final String TRUST_STORE_PREFIX = "zk_test_ca";
private static final String KEY_STORE_PREFIX = "zk_test_key";
private final File tempDir;
private final X509KeyType trustStoreKeyType;
private final KeyPair trustStoreKeyPair;
private final long trustStoreCertExpirationMillis;
private final X509Certificate trustStoreCertificate;
private final String trustStorePassword;
private File trustStoreJksFile;
private File trustStorePemFile;
private File trustStorePkcs12File;
private File trustStoreBcfksFile;
private final X509KeyType keyStoreKeyType;
private final KeyPair keyStoreKeyPair;
private final long keyStoreCertExpirationMillis;
private final X509Certificate keyStoreCertificate;
private final String keyStorePassword;
private File keyStoreJksFile;
private File keyStorePemFile;
private File keyStorePkcs12File;
private File keyStoreBcfksFile;
private final Boolean hostnameVerification;
/**
* Constructor is intentionally private, use the Builder class instead.
* @param tempDir the directory in which key store and trust store temp files will be written.
* @param trustStoreKeyPair the key pair for the trust store.
* @param trustStoreCertExpirationMillis the expiration of the trust store cert, in milliseconds from now.
* @param trustStorePassword the password to protect a JKS trust store (ignored for PEM trust stores).
* @param keyStoreKeyPair the key pair for the key store.
* @param keyStoreCertExpirationMillis the expiration of the key store cert, in milliseconds from now.
* @param keyStorePassword the password to protect the key store private key.
* @throws IOException
* @throws GeneralSecurityException
* @throws OperatorCreationException
*/
private X509TestContext(File tempDir, KeyPair trustStoreKeyPair, long trustStoreCertExpirationMillis, String trustStorePassword, KeyPair keyStoreKeyPair, long keyStoreCertExpirationMillis, String keyStorePassword, Boolean hostnameVerification) throws IOException, GeneralSecurityException, OperatorCreationException {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
throw new IllegalStateException("BC Security provider was not found");
}
this.tempDir = requireNonNull(tempDir);
if (!tempDir.isDirectory()) {
throw new IllegalArgumentException("Not a directory: " + tempDir);
}
this.trustStoreKeyPair = requireNonNull(trustStoreKeyPair);
this.trustStoreKeyType = keyPairToType(trustStoreKeyPair);
this.trustStoreCertExpirationMillis = trustStoreCertExpirationMillis;
this.trustStorePassword = requireNonNull(trustStorePassword);
this.keyStoreKeyPair = requireNonNull(keyStoreKeyPair);
this.keyStoreKeyType = keyPairToType(keyStoreKeyPair);
this.keyStoreCertExpirationMillis = keyStoreCertExpirationMillis;
this.keyStorePassword = requireNonNull(keyStorePassword);
X500NameBuilder caNameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
caNameBuilder.addRDN(BCStyle.CN, MethodHandles.lookup().lookupClass().getCanonicalName() + " Root CA");
trustStoreCertificate = X509TestHelpers.newSelfSignedCACert(caNameBuilder.build(), trustStoreKeyPair, trustStoreCertExpirationMillis);
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
nameBuilder.addRDN(BCStyle.CN, MethodHandles.lookup().lookupClass().getCanonicalName() + " Zookeeper Test");
keyStoreCertificate = X509TestHelpers.newCert(trustStoreCertificate, trustStoreKeyPair, nameBuilder.build(), keyStoreKeyPair.getPublic(), keyStoreCertExpirationMillis);
trustStorePkcs12File = trustStorePemFile = trustStoreJksFile = null;
keyStorePkcs12File = keyStorePemFile = keyStoreJksFile = null;
this.hostnameVerification = hostnameVerification;
}
/**
* Returns the X509KeyType of the given key pair.
* @param keyPair the key pair.
* @return <code>X509KeyType.RSA</code> if given an RSA key pair, and <code>X509KeyType.EC</code> otherwise.
*/
private X509KeyType keyPairToType(KeyPair keyPair) {
if (keyPair.getPrivate().getAlgorithm().contains("RSA")) {
return X509KeyType.RSA;
} else {
return X509KeyType.EC;
}
}
public File getTempDir() {
return tempDir;
}
public X509KeyType getTrustStoreKeyType() {
return trustStoreKeyType;
}
public KeyPair getTrustStoreKeyPair() {
return trustStoreKeyPair;
}
public long getTrustStoreCertExpirationMillis() {
return trustStoreCertExpirationMillis;
}
public X509Certificate getTrustStoreCertificate() {
return trustStoreCertificate;
}
public String getTrustStorePassword() {
return trustStorePassword;
}
/**
* Returns the path to the trust store file in the given format (JKS or PEM). Note that the file is created lazily,
* the first time this method is called. The trust store file is temporary and will be deleted on exit.
* @param storeFileType the store file type (JKS or PEM).
* @return the path to the trust store file.
* @throws IOException if there is an error creating the trust store file.
*/
public File getTrustStoreFile(KeyStoreFileType storeFileType) throws IOException {
switch (storeFileType) {
case JKS:
return getTrustStoreJksFile();
case PEM:
return getTrustStorePemFile();
case PKCS12:
return getTrustStorePkcs12File();
case BCFKS:
return getTrustStoreBcfksFile();
default:
throw new IllegalArgumentException("Invalid trust store type: "
+ storeFileType
+ ", must be one of: "
+ Arrays.toString(KeyStoreFileType.values()));
}
}
private File getTrustStoreJksFile() throws IOException {
if (trustStoreJksFile == null) {
File trustStoreJksFile = File.createTempFile(TRUST_STORE_PREFIX, KeyStoreFileType.JKS.getDefaultFileExtension(), tempDir);
trustStoreJksFile.deleteOnExit();
try (final FileOutputStream trustStoreOutputStream = new FileOutputStream(trustStoreJksFile)) {
byte[] bytes = X509TestHelpers.certToJavaTrustStoreBytes(trustStoreCertificate, trustStorePassword);
trustStoreOutputStream.write(bytes);
trustStoreOutputStream.flush();
} catch (GeneralSecurityException e) {
throw new IOException(e);
}
this.trustStoreJksFile = trustStoreJksFile;
}
return trustStoreJksFile;
}
private File getTrustStorePemFile() throws IOException {
if (trustStorePemFile == null) {
File trustStorePemFile = File.createTempFile(TRUST_STORE_PREFIX, KeyStoreFileType.PEM.getDefaultFileExtension(), tempDir);
trustStorePemFile.deleteOnExit();
FileUtils.writeStringToFile(trustStorePemFile, X509TestHelpers.pemEncodeX509Certificate(trustStoreCertificate), StandardCharsets.US_ASCII, false);
this.trustStorePemFile = trustStorePemFile;
}
return trustStorePemFile;
}
private File getTrustStorePkcs12File() throws IOException {
if (trustStorePkcs12File == null) {
File trustStorePkcs12File = File.createTempFile(TRUST_STORE_PREFIX, KeyStoreFileType.PKCS12.getDefaultFileExtension(), tempDir);
trustStorePkcs12File.deleteOnExit();
try (final FileOutputStream trustStoreOutputStream = new FileOutputStream(trustStorePkcs12File)) {
byte[] bytes = X509TestHelpers.certToPKCS12TrustStoreBytes(trustStoreCertificate, trustStorePassword);
trustStoreOutputStream.write(bytes);
trustStoreOutputStream.flush();
} catch (GeneralSecurityException e) {
throw new IOException(e);
}
this.trustStorePkcs12File = trustStorePkcs12File;
}
return trustStorePkcs12File;
}
private File getTrustStoreBcfksFile() throws IOException {
if (trustStoreBcfksFile == null) {
File trustStoreBcfksFile = File.createTempFile(
TRUST_STORE_PREFIX, KeyStoreFileType.BCFKS.getDefaultFileExtension(), tempDir);
trustStoreBcfksFile.deleteOnExit();
try (final FileOutputStream trustStoreOutputStream = new FileOutputStream(trustStoreBcfksFile)) {
byte[] bytes = X509TestHelpers.certToBCFKSTrustStoreBytes(trustStoreCertificate, trustStorePassword);
trustStoreOutputStream.write(bytes);
trustStoreOutputStream.flush();
} catch (GeneralSecurityException e) {
throw new IOException(e);
}
this.trustStoreBcfksFile = trustStoreBcfksFile;
}
return trustStoreBcfksFile;
}
public X509KeyType getKeyStoreKeyType() {
return keyStoreKeyType;
}
public KeyPair getKeyStoreKeyPair() {
return keyStoreKeyPair;
}
public long getKeyStoreCertExpirationMillis() {
return keyStoreCertExpirationMillis;
}
public X509Certificate getKeyStoreCertificate() {
return keyStoreCertificate;
}
public String getKeyStorePassword() {
return keyStorePassword;
}
public boolean isKeyStoreEncrypted() {
return keyStorePassword.length() > 0;
}
/**
* Returns the path to the key store file in the given format (JKS, PEM, ...). Note that the file is created lazily,
* the first time this method is called. The key store file is temporary and will be deleted on exit.
* @param storeFileType the store file type (JKS, PEM, ...).
* @return the path to the key store file.
* @throws IOException if there is an error creating the key store file.
*/
public File getKeyStoreFile(KeyStoreFileType storeFileType) throws IOException {
switch (storeFileType) {
case JKS:
return getKeyStoreJksFile();
case PEM:
return getKeyStorePemFile();
case PKCS12:
return getKeyStorePkcs12File();
case BCFKS:
return getKeyStoreBcfksFile();
default:
throw new IllegalArgumentException("Invalid key store type: "
+ storeFileType
+ ", must be one of: "
+ Arrays.toString(KeyStoreFileType.values()));
}
}
private File getKeyStoreJksFile() throws IOException {
if (keyStoreJksFile == null) {
File keyStoreJksFile = File.createTempFile(KEY_STORE_PREFIX, KeyStoreFileType.JKS.getDefaultFileExtension(), tempDir);
keyStoreJksFile.deleteOnExit();
try (final FileOutputStream keyStoreOutputStream = new FileOutputStream(keyStoreJksFile)) {
byte[] bytes = X509TestHelpers.certAndPrivateKeyToJavaKeyStoreBytes(keyStoreCertificate, keyStoreKeyPair.getPrivate(), keyStorePassword);
keyStoreOutputStream.write(bytes);
keyStoreOutputStream.flush();
} catch (GeneralSecurityException e) {
throw new IOException(e);
}
this.keyStoreJksFile = keyStoreJksFile;
}
return keyStoreJksFile;
}
private File getKeyStorePemFile() throws IOException {
if (keyStorePemFile == null) {
try {
File keyStorePemFile = File.createTempFile(KEY_STORE_PREFIX, KeyStoreFileType.PEM.getDefaultFileExtension(), tempDir);
keyStorePemFile.deleteOnExit();
FileUtils.writeStringToFile(keyStorePemFile, X509TestHelpers.pemEncodeCertAndPrivateKey(keyStoreCertificate, keyStoreKeyPair.getPrivate(), keyStorePassword), StandardCharsets.US_ASCII, false);
this.keyStorePemFile = keyStorePemFile;
} catch (OperatorCreationException e) {
throw new IOException(e);
}
}
return keyStorePemFile;
}
private File getKeyStorePkcs12File() throws IOException {
if (keyStorePkcs12File == null) {
File keyStorePkcs12File = File.createTempFile(KEY_STORE_PREFIX, KeyStoreFileType.PKCS12.getDefaultFileExtension(), tempDir);
keyStorePkcs12File.deleteOnExit();
try (final FileOutputStream keyStoreOutputStream = new FileOutputStream(keyStorePkcs12File)) {
byte[] bytes = X509TestHelpers.certAndPrivateKeyToPKCS12Bytes(keyStoreCertificate, keyStoreKeyPair.getPrivate(), keyStorePassword);
keyStoreOutputStream.write(bytes);
keyStoreOutputStream.flush();
} catch (GeneralSecurityException e) {
throw new IOException(e);
}
this.keyStorePkcs12File = keyStorePkcs12File;
}
return keyStorePkcs12File;
}
private File getKeyStoreBcfksFile() throws IOException {
if (keyStoreBcfksFile == null) {
File keyStoreBcfksFile = File.createTempFile(
KEY_STORE_PREFIX, KeyStoreFileType.BCFKS.getDefaultFileExtension(), tempDir);
keyStoreBcfksFile.deleteOnExit();
try (final FileOutputStream keyStoreOutputStream = new FileOutputStream(keyStoreBcfksFile)) {
byte[] bytes = X509TestHelpers.certAndPrivateKeyToBCFKSBytes(
keyStoreCertificate, keyStoreKeyPair.getPrivate(), keyStorePassword);
keyStoreOutputStream.write(bytes);
keyStoreOutputStream.flush();
} catch (GeneralSecurityException e) {
throw new IOException(e);
}
this.keyStoreBcfksFile = keyStoreBcfksFile;
}
return keyStoreBcfksFile;
}
/**
* Sets the SSL system properties such that the given X509Util object can be used to create SSL Contexts that
* will use the trust store and key store files created by this test context. Example usage:
* <pre>
* X509TestContext testContext = ...; // create the test context
* X509Util x509Util = new QuorumX509Util();
* testContext.setSystemProperties(x509Util, KeyStoreFileType.JKS, KeyStoreFileType.JKS);
* // The returned context will use the key store and trust store created by the test context.
* SSLContext ctx = x509Util.getDefaultSSLContext();
* </pre>
* @param x509Util the X509Util.
* @param keyStoreFileType the store file type to use for the key store (JKS, PEM, ...).
* @param trustStoreFileType the store file type to use for the trust store (JKS, PEM, ...).
* @throws IOException if there is an error creating the key store file or trust store file.
*/
public void setSystemProperties(X509Util x509Util, KeyStoreFileType keyStoreFileType, KeyStoreFileType trustStoreFileType) throws IOException {
System.setProperty(x509Util.getSslKeystoreLocationProperty(), this.getKeyStoreFile(keyStoreFileType).getAbsolutePath());
System.setProperty(x509Util.getSslKeystorePasswdProperty(), this.getKeyStorePassword());
System.setProperty(x509Util.getSslKeystoreTypeProperty(), keyStoreFileType.getPropertyValue());
System.setProperty(x509Util.getSslTruststoreLocationProperty(), this.getTrustStoreFile(trustStoreFileType).getAbsolutePath());
System.setProperty(x509Util.getSslTruststorePasswdProperty(), this.getTrustStorePassword());
System.setProperty(x509Util.getSslTruststoreTypeProperty(), trustStoreFileType.getPropertyValue());
if (hostnameVerification != null) {
System.setProperty(x509Util.getSslHostnameVerificationEnabledProperty(), hostnameVerification.toString());
} else {
System.clearProperty(x509Util.getSslHostnameVerificationEnabledProperty());
}
}
/**
* Clears system properties set by
* {@link #setSystemProperties(X509Util, KeyStoreFileType, KeyStoreFileType)}.
* @param x509Util the X509Util to read property keys from.
*/
public void clearSystemProperties(X509Util x509Util) {
System.clearProperty(x509Util.getSslKeystoreLocationProperty());
System.clearProperty(x509Util.getSslKeystorePasswdProperty());
System.clearProperty(x509Util.getSslKeystorePasswdPathProperty());
System.clearProperty(x509Util.getSslKeystoreTypeProperty());
System.clearProperty(x509Util.getSslTruststoreLocationProperty());
System.clearProperty(x509Util.getSslTruststorePasswdProperty());
System.clearProperty(x509Util.getSslTruststorePasswdPathProperty());
System.clearProperty(x509Util.getSslTruststoreTypeProperty());
System.clearProperty(x509Util.getSslHostnameVerificationEnabledProperty());
}
/**
* Builder class, used for creating new instances of X509TestContext.
*/
public static class Builder {
public static final long DEFAULT_CERT_EXPIRATION_MILLIS = 1000L * 60 * 60 * 24; // 1 day
private File tempDir;
private X509KeyType trustStoreKeyType;
private String trustStorePassword;
private long trustStoreCertExpirationMillis;
private X509KeyType keyStoreKeyType;
private String keyStorePassword;
private long keyStoreCertExpirationMillis;
private Boolean hostnameVerification;
/**
* Creates an empty builder.
*/
public Builder() {
trustStoreKeyType = X509KeyType.EC;
trustStorePassword = "";
trustStoreCertExpirationMillis = DEFAULT_CERT_EXPIRATION_MILLIS;
keyStoreKeyType = X509KeyType.EC;
keyStorePassword = "";
keyStoreCertExpirationMillis = DEFAULT_CERT_EXPIRATION_MILLIS;
hostnameVerification = null;
}
/**
* Builds a new X509TestContext from this builder.
* @return a new X509TestContext
* @throws IOException
* @throws GeneralSecurityException
* @throws OperatorCreationException
*/
public X509TestContext build() throws IOException, GeneralSecurityException, OperatorCreationException {
KeyPair trustStoreKeyPair = X509TestHelpers.generateKeyPair(trustStoreKeyType);
KeyPair keyStoreKeyPair = X509TestHelpers.generateKeyPair(keyStoreKeyType);
return new X509TestContext(tempDir, trustStoreKeyPair, trustStoreCertExpirationMillis, trustStorePassword, keyStoreKeyPair, keyStoreCertExpirationMillis, keyStorePassword, hostnameVerification);
}
/**
* Sets the temporary directory. Certificate and private key files will be created in this directory.
* @param tempDir the temp directory.
* @return this Builder.
*/
public Builder setTempDir(File tempDir) {
this.tempDir = tempDir;
return this;
}
/**
* Sets the trust store key type. The CA key generated for the test context will be of this type.
* @param keyType the key type.
* @return this Builder.
*/
public Builder setTrustStoreKeyType(X509KeyType keyType) {
trustStoreKeyType = keyType;
return this;
}
/**
* Sets the trust store password. Ignored for PEM trust stores, JKS trust stores will be encrypted with this
* password.
* @param password the password.
* @return this Builder.
*/
public Builder setTrustStorePassword(String password) {
trustStorePassword = password;
return this;
}
/**
* Sets the trust store certificate's expiration, in milliseconds from when <code>build()</code> is called.
* @param expirationMillis expiration in milliseconds.
* @return this Builder.
*/
public Builder setTrustStoreCertExpirationMillis(long expirationMillis) {
trustStoreCertExpirationMillis = expirationMillis;
return this;
}
/**
* Sets the key store key type. The private key generated for the test context will be of this type.
* @param keyType the key type.
* @return this Builder.
*/
public Builder setKeyStoreKeyType(X509KeyType keyType) {
keyStoreKeyType = keyType;
return this;
}
/**
* Sets the key store password. The private key (PEM, JKS) and certificate (JKS only) will be encrypted with
* this password.
* @param password the password.
* @return this Builder.
*/
public Builder setKeyStorePassword(String password) {
keyStorePassword = password;
return this;
}
/**
* Sets the key store certificate's expiration, in milliseconds from when <code>build()</code> is called.
* @param expirationMillis expiration in milliseconds.
* @return this Builder.
*/
public Builder setKeyStoreCertExpirationMillis(long expirationMillis) {
keyStoreCertExpirationMillis = expirationMillis;
return this;
}
/**
* Sets the hostname verification behavior. If null is provided, reverts the behavior to the default, otherwise
* explicitly sets hostname verification to true or false.
* @param hostnameVerification new value for the hostname verification setting.
* @return this Builder.
*/
public Builder setHostnameVerification(Boolean hostnameVerification) {
this.hostnameVerification = hostnameVerification;
return this;
}
}
/**
* Returns a new default-constructed Builder.
* @return a new Builder.
*/
public static Builder newBuilder() {
return new Builder();
}
}