zookeeper-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From an...@apache.org
Subject [1/2] zookeeper git commit: ZOOKEEPER-3173: Quorum TLS - support PEM trust/key stores
Date Wed, 07 Nov 2018 01:30:08 GMT
Repository: zookeeper
Updated Branches:
  refs/heads/branch-3.5 427f13678 -> 39fbc58f1


http://git-wip-us.apache.org/repos/asf/zookeeper/blob/39fbc58f/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java
new file mode 100644
index 0000000..5a86bb4
--- /dev/null
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java
@@ -0,0 +1,492 @@
+/**
+ * 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 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;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * 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 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 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);
+        trustStorePemFile = trustStoreJksFile = 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();
+            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) {
+            try {
+                File trustStoreJksFile = File.createTempFile(
+                        TRUST_STORE_PREFIX, KeyStoreFileType.JKS.getDefaultFileExtension(), tempDir);
+                trustStoreJksFile.deleteOnExit();
+                final FileOutputStream trustStoreOutputStream = new FileOutputStream(trustStoreJksFile);
+                try {
+                    byte[] bytes = X509TestHelpers.certToJavaTrustStoreBytes(trustStoreCertificate, trustStorePassword);
+                    trustStoreOutputStream.write(bytes);
+                    trustStoreOutputStream.flush();
+                } finally {
+                    trustStoreOutputStream.close();
+                }
+                this.trustStoreJksFile = trustStoreJksFile;
+            } catch (GeneralSecurityException e) {
+                throw new IOException(e);
+            }
+        }
+        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;
+    }
+
+    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 or 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 or 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();
+            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) {
+            try {
+                File keyStoreJksFile = File.createTempFile(
+                        KEY_STORE_PREFIX, KeyStoreFileType.JKS.getDefaultFileExtension(), tempDir);
+                keyStoreJksFile.deleteOnExit();
+                final FileOutputStream keyStoreOutputStream = new FileOutputStream(keyStoreJksFile);
+                try {
+                    byte[] bytes = X509TestHelpers.certAndPrivateKeyToJavaKeyStoreBytes(
+                            keyStoreCertificate, keyStoreKeyPair.getPrivate(), keyStorePassword);
+                    keyStoreOutputStream.write(bytes);
+                    keyStoreOutputStream.flush();
+                } finally {
+                    keyStoreOutputStream.close();
+                }
+                this.keyStoreJksFile = keyStoreJksFile;
+            } catch (GeneralSecurityException e) {
+                throw new IOException(e);
+            }
+        }
+        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;
+    }
+
+    /**
+     * 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 or PEM).
+     * @param trustStoreFileType the store file type to use for the trust store (JKS or 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.getSslKeystoreTypeProperty());
+        System.clearProperty(x509Util.getSslTruststoreLocationProperty());
+        System.clearProperty(x509Util.getSslTruststorePasswdProperty());
+        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();
+    }
+}

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/39fbc58f/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java
new file mode 100644
index 0000000..59b7634
--- /dev/null
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java
@@ -0,0 +1,402 @@
+/**
+ * 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 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.AlgorithmIdentifier;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.asn1.x509.KeyPurposeId;
+import org.bouncycastle.asn1.x509.KeyUsage;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+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.ContentSigner;
+import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
+import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.OutputEncryptor;
+import org.bouncycastle.operator.bc.BcContentSignerBuilder;
+import org.bouncycastle.operator.bc.BcECContentSignerBuilder;
+import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+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.util.Date;
+
+/**
+ * This class contains helper methods for creating X509 certificates and key pairs, and for serializing them
+ * to JKS or PEM files.
+ */
+public class X509TestHelpers {
+    private static final Logger LOG = LoggerFactory.getLogger(X509TestHelpers.class);
+
+    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.
+     * @param expirationMillis expiration of the new certificate, in milliseconds from now.
+     * @return a new self-signed CA certificate.
+     * @throws IOException
+     * @throws OperatorCreationException
+     * @throws GeneralSecurityException
+     */
+    public static X509Certificate newSelfSignedCACert(
+            X500Name subject,
+            KeyPair keyPair,
+            long expirationMillis) throws IOException, OperatorCreationException, GeneralSecurityException {
+        Date now = new Date();
+        X509v3CertificateBuilder builder = initCertBuilder(
+                subject, // for self-signed certs, issuer == subject
+                now,
+                new Date(now.getTime() + expirationMillis),
+                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.
+     * @param expirationMillis the expiration of the cert being issued, in milliseconds from now.
+     * @return a new certificate signed by the CA's private key.
+     * @throws IOException
+     * @throws OperatorCreationException
+     * @throws GeneralSecurityException
+     */
+    public static X509Certificate newCert(
+            X509Certificate caCert,
+            KeyPair caKeyPair,
+            X500Name certSubject,
+            PublicKey certPublicKey,
+            long expirationMillis) 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");
+        }
+        Date now = new Date();
+        X509v3CertificateBuilder builder = initCertBuilder(
+                new X500Name(caCert.getIssuerDN().getName()),
+                now,
+                new Date(now.getTime() + expirationMillis),
+                certSubject,
+                certPublicKey);
+        builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false)); // not a CA
+        builder.addExtension(
+                Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyAgreement));
+        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,
+            Date notBefore,
+            Date notAfter,
+            X500Name subject,
+            PublicKey subjectPublicKey) {
+        return new X509v3CertificateBuilder(
+                issuer,
+                new BigInteger(SERIAL_NUMBER_MAX_BITS, PRNG),
+                notBefore,
+                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.
+     * @throws IOException
+     * @throws OperatorCreationException
+     * @throws CertificateException
+     */
+    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.
+     * @throws IOException
+     * @throws GeneralSecurityException
+     */
+    public static byte[] certToJavaTrustStoreBytes(
+            X509Certificate cert,
+            String keyPassword) throws IOException, GeneralSecurityException {
+        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
+        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.
+     * @throws IOException
+     * @throws GeneralSecurityException
+     */
+    public static byte[] certAndPrivateKeyToJavaKeyStoreBytes(
+            X509Certificate cert,
+            PrivateKey privateKey,
+            String keyPassword) throws IOException, GeneralSecurityException {
+        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+        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);
+    }
+}

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/39fbc58f/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java
index e726caa..6b343c3 100644
--- a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java
@@ -17,150 +17,78 @@
  */
 package org.apache.zookeeper.common;
 
+import java.security.Security;
+import java.util.Collection;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.X509TrustManager;
+
 import org.apache.zookeeper.PortAssignment;
-import org.apache.zookeeper.ZKTestCase;
 import org.apache.zookeeper.client.ZKClientConfig;
 import org.apache.zookeeper.server.ServerCnxnFactory;
-import org.bouncycastle.asn1.x500.X500NameBuilder;
-import org.bouncycastle.asn1.x500.style.BCStyle;
-import org.bouncycastle.asn1.x509.BasicConstraints;
-import org.bouncycastle.asn1.x509.Extension;
-import org.bouncycastle.asn1.x509.KeyUsage;
-import org.bouncycastle.cert.X509v3CertificateBuilder;
-import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
-import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.bouncycastle.operator.ContentSigner;
-import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
 import org.junit.After;
-import org.junit.AfterClass;
 import org.junit.Assert;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLServerSocket;
-import javax.net.ssl.SSLSocket;
-import java.io.FileOutputStream;
-import java.math.BigInteger;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.KeyStore;
-import java.security.Security;
-import java.security.cert.Certificate;
-import java.security.cert.X509Certificate;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Random;
-
-import static org.apache.zookeeper.test.ClientBase.createTmpDir;
-
-public class X509UtilTest extends ZKTestCase {
-
-    private static final char[] PASSWORD = "password".toCharArray();
-    private X509Certificate rootCertificate;
-
-    private String truststorePath;
-    private String keystorePath;
-    private static KeyPair rootKeyPair;
-
+@RunWith(Parameterized.class)
+public class X509UtilTest extends BaseX509ParameterizedTestCase {
     private X509Util x509Util;
-    private String[] customCipherSuites = new String[]{"SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", "SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA"};
-
-    @BeforeClass
-    public static void createKeyPair() throws Exception {
-        Security.addProvider(new BouncyCastleProvider());
-        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
-        keyPairGenerator.initialize(4096);
-        rootKeyPair = keyPairGenerator.genKeyPair();
+    private static final String[] customCipherSuites = new String[]{
+            "SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA",
+            "SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA"};
+
+    @Parameterized.Parameters
+    public static Collection<Object[]> params() {
+        return BaseX509ParameterizedTestCase.defaultParams();
     }
 
-    @AfterClass
-    public static void removeBouncyCastleProvider() throws Exception {
-        Security.removeProvider("BC");
+    public X509UtilTest(
+            X509KeyType caKeyType,
+            X509KeyType certKeyType,
+            String keyPassword,
+            Integer paramIndex) {
+        super(paramIndex, () -> {
+            try {
+                return X509TestContext.newBuilder()
+                        .setTempDir(tempDir)
+                        .setKeyStorePassword(keyPassword)
+                        .setKeyStoreKeyType(certKeyType)
+                        .setTrustStorePassword(keyPassword)
+                        .setTrustStoreKeyType(caKeyType)
+                        .build();
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        });
     }
 
     @Before
     public void setUp() throws Exception {
-        rootCertificate = createSelfSignedCertifcate(rootKeyPair);
-
-        String tmpDir = createTmpDir().getAbsolutePath();
-        truststorePath = tmpDir + "/truststore.jks";
-        keystorePath = tmpDir + "/keystore.jks";
-
-        x509Util = new ClientX509Util();
-
-        writeKeystore(rootCertificate, rootKeyPair, keystorePath);
-
+        x509TestContext.setSystemProperties(new ClientX509Util(), KeyStoreFileType.JKS, KeyStoreFileType.JKS);
         System.setProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY, "org.apache.zookeeper.server.NettyServerCnxnFactory");
         System.setProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET, "org.apache.zookeeper.ClientCnxnSocketNetty");
-        System.setProperty(x509Util.getSslKeystoreLocationProperty(), keystorePath);
-        System.setProperty(x509Util.getSslKeystorePasswdProperty(), new String(PASSWORD));
-        System.setProperty(x509Util.getSslTruststoreLocationProperty(), truststorePath);
-        System.setProperty(x509Util.getSslTruststorePasswdProperty(), new String(PASSWORD));
-        System.setProperty(x509Util.getSslHostnameVerificationEnabledProperty(), "false");
-
-        writeTrustStore(PASSWORD);
-    }
-
-    private void writeKeystore(X509Certificate certificate, KeyPair keyPair, String path) throws Exception {
-        KeyStore keyStore = KeyStore.getInstance("JKS");
-        keyStore.load(null, PASSWORD);
-        keyStore.setKeyEntry("alias", keyPair.getPrivate(), PASSWORD, new Certificate[] { certificate });
-        FileOutputStream outputStream = new FileOutputStream(path);
-        keyStore.store(outputStream, PASSWORD);
-        outputStream.flush();
-        outputStream.close();
-    }
-
-    private void writeTrustStore(char[] password) throws Exception {
-        KeyStore trustStore = KeyStore.getInstance("JKS");
-        trustStore.load(null, password);
-        trustStore.setCertificateEntry(rootCertificate.getSubjectDN().toString(), rootCertificate);
-        FileOutputStream outputStream = new FileOutputStream(truststorePath);
-        if (password == null) {
-            trustStore.store(outputStream, new char[0]);
-        } else {
-            trustStore.store(outputStream, password);
-        }
-        outputStream.flush();
-        outputStream.close();
-    }
-
-    private X509Certificate createSelfSignedCertifcate(KeyPair keyPair) throws Exception {
-        X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
-        nameBuilder.addRDN(BCStyle.CN, "localhost");
-        Date notBefore = new Date();
-        Calendar cal = Calendar.getInstance();
-        cal.setTime(notBefore);
-        cal.add(Calendar.YEAR, 1);
-        Date notAfter = cal.getTime();
-        BigInteger serialNumber = new BigInteger(128, new Random());
-
-        X509v3CertificateBuilder certificateBuilder =
-                new JcaX509v3CertificateBuilder(nameBuilder.build(), serialNumber, notBefore, notAfter, nameBuilder.build(), keyPair.getPublic())
-                        .addExtension(Extension.basicConstraints, true, new BasicConstraints(0))
-                        .addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign | KeyUsage.cRLSign));
-
-        ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(keyPair.getPrivate());
-
-        return new JcaX509CertificateConverter().getCertificate(certificateBuilder.build(contentSigner));
+        x509Util = new ClientX509Util();
     }
 
     @After
-    public void cleanUp() throws Exception {
-        System.clearProperty(x509Util.getSslKeystoreLocationProperty());
-        System.clearProperty(x509Util.getSslKeystorePasswdProperty());
-        System.clearProperty(x509Util.getSslTruststoreLocationProperty());
-        System.clearProperty(x509Util.getSslTruststorePasswdProperty());
-        System.clearProperty(x509Util.getSslHostnameVerificationEnabledProperty());
+    public void cleanUp() {
+        x509TestContext.clearSystemProperties(x509Util);
         System.clearProperty(x509Util.getSslOcspEnabledProperty());
         System.clearProperty(x509Util.getSslCrlEnabledProperty());
         System.clearProperty(x509Util.getCipherSuitesProperty());
+        System.clearProperty(x509Util.getSslProtocolProperty());
         System.clearProperty("com.sun.net.ssl.checkRevocation");
         System.clearProperty("com.sun.security.enableCRLDP");
-        Security.setProperty("com.sun.security.enableCRLDP", "false");
+        Security.setProperty("ocsp.enable", Boolean.FALSE.toString());
+        Security.setProperty("com.sun.security.enableCRLDP", Boolean.FALSE.toString());
+        System.clearProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY);
+        System.clearProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET);
     }
 
     @Test(timeout = 5000)
@@ -178,13 +106,6 @@ public class X509UtilTest extends ZKTestCase {
     }
 
     @Test(timeout = 5000)
-    public void testCreateSSLContextWithoutTrustStorePassword() throws Exception {
-        writeTrustStore(null);
-        System.clearProperty(x509Util.getSslTruststorePasswdProperty());
-        x509Util.getDefaultSSLContext();
-    }
-
-    @Test(timeout = 5000, expected = X509Exception.SSLContextException.class)
     public void testCreateSSLContextWithoutKeyStoreLocation() throws Exception {
         System.clearProperty(x509Util.getSslKeystoreLocationProperty());
         x509Util.getDefaultSSLContext();
@@ -192,6 +113,9 @@ public class X509UtilTest extends ZKTestCase {
 
     @Test(timeout = 5000, expected = X509Exception.SSLContextException.class)
     public void testCreateSSLContextWithoutKeyStorePassword() throws Exception {
+        if (!x509TestContext.isKeyStoreEncrypted()) {
+            throw new X509Exception.SSLContextException("");
+        }
         System.clearProperty(x509Util.getSslKeystorePasswdProperty());
         x509Util.getDefaultSSLContext();
     }
@@ -256,6 +180,182 @@ public class X509UtilTest extends ZKTestCase {
         Assert.assertTrue(sslServerSocket.getNeedClientAuth());
     }
 
+    @Test
+    public void testLoadPEMKeyStore() throws Exception {
+        // Make sure we can instantiate a key manager from the PEM file on disk
+        X509KeyManager km = X509Util.createKeyManager(
+                x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath(),
+                x509TestContext.getKeyStorePassword(),
+                KeyStoreFileType.PEM.getPropertyValue());
+    }
+
+    @Test
+    public void testLoadPEMKeyStoreNullPassword() throws Exception {
+        if (!x509TestContext.getKeyStorePassword().isEmpty()) {
+            return;
+        }
+        // Make sure that empty password and null password are treated the same
+        X509KeyManager km = X509Util.createKeyManager(
+                x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath(),
+                null,
+                KeyStoreFileType.PEM.getPropertyValue());
+    }
+
+    @Test
+    public void testLoadPEMKeyStoreAutodetectStoreFileType() throws Exception {
+        // Make sure we can instantiate a key manager from the PEM file on disk
+        X509KeyManager km = X509Util.createKeyManager(
+                x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath(),
+                x509TestContext.getKeyStorePassword(),
+                null /* null StoreFileType means 'autodetect from file extension' */);
+    }
+
+    @Test(expected = X509Exception.KeyManagerException.class)
+    public void testLoadPEMKeyStoreWithWrongPassword() throws Exception {
+        // Attempting to load with the wrong key password should fail
+        X509KeyManager km = X509Util.createKeyManager(
+                x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath(),
+                "wrong password", // intentionally use the wrong password
+                KeyStoreFileType.PEM.getPropertyValue());
+    }
+
+    @Test
+    public void testLoadPEMTrustStore() throws Exception {
+        // Make sure we can instantiate a trust manager from the PEM file on disk
+        X509TrustManager tm = X509Util.createTrustManager(
+                x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM).getAbsolutePath(),
+                x509TestContext.getTrustStorePassword(),
+                KeyStoreFileType.PEM.getPropertyValue(),
+                false,
+                false,
+                true,
+                true);
+    }
+
+    @Test
+    public void testLoadPEMTrustStoreNullPassword() throws Exception {
+        if (!x509TestContext.getTrustStorePassword().isEmpty()) {
+            return;
+        }
+        // Make sure that empty password and null password are treated the same
+        X509TrustManager tm = X509Util.createTrustManager(
+                x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM).getAbsolutePath(),
+                null,
+                KeyStoreFileType.PEM.getPropertyValue(),
+                false,
+                false,
+                true,
+                true);
+
+    }
+
+    @Test
+    public void testLoadPEMTrustStoreAutodetectStoreFileType() throws Exception {
+        // Make sure we can instantiate a trust manager from the PEM file on disk
+        X509TrustManager tm = X509Util.createTrustManager(
+                x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM).getAbsolutePath(),
+                x509TestContext.getTrustStorePassword(),
+                null,  // null StoreFileType means 'autodetect from file extension'
+                false,
+                false,
+                true,
+                true);
+    }
+
+    @Test
+    public void testLoadJKSKeyStore() throws Exception {
+        // Make sure we can instantiate a key manager from the JKS file on disk
+        X509KeyManager km = X509Util.createKeyManager(
+                x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS).getAbsolutePath(),
+                x509TestContext.getKeyStorePassword(),
+                KeyStoreFileType.JKS.getPropertyValue());
+    }
+
+    @Test
+    public void testLoadJKSKeyStoreNullPassword() throws Exception {
+        if (!x509TestContext.getKeyStorePassword().isEmpty()) {
+            return;
+        }
+        // Make sure that empty password and null password are treated the same
+        X509KeyManager km = X509Util.createKeyManager(
+                x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS).getAbsolutePath(),
+                null,
+                KeyStoreFileType.JKS.getPropertyValue());
+    }
+
+    @Test
+    public void testLoadJKSKeyStoreAutodetectStoreFileType() throws Exception {
+        // Make sure we can instantiate a key manager from the JKS file on disk
+        X509KeyManager km = X509Util.createKeyManager(
+                x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS).getAbsolutePath(),
+                x509TestContext.getKeyStorePassword(),
+                null /* null StoreFileType means 'autodetect from file extension' */);
+    }
+
+    @Test(expected = X509Exception.KeyManagerException.class)
+    public void testLoadJKSKeyStoreWithWrongPassword() throws Exception {
+        // Attempting to load with the wrong key password should fail
+        X509KeyManager km = X509Util.createKeyManager(
+                x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS).getAbsolutePath(),
+                "wrong password",
+                KeyStoreFileType.JKS.getPropertyValue());
+    }
+
+    @Test
+    public void testLoadJKSTrustStore() throws Exception {
+        // Make sure we can instantiate a trust manager from the JKS file on disk
+        X509TrustManager tm = X509Util.createTrustManager(
+                x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS).getAbsolutePath(),
+                x509TestContext.getTrustStorePassword(),
+                KeyStoreFileType.JKS.getPropertyValue(),
+                true,
+                true,
+                true,
+                true);
+    }
+
+    @Test
+    public void testLoadJKSTrustStoreNullPassword() throws Exception {
+        if (!x509TestContext.getTrustStorePassword().isEmpty()) {
+            return;
+        }
+        // Make sure that empty password and null password are treated the same
+        X509TrustManager tm = X509Util.createTrustManager(
+                x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS).getAbsolutePath(),
+                null,
+                KeyStoreFileType.JKS.getPropertyValue(),
+                false,
+                false,
+                true,
+                true);
+    }
+
+    @Test
+    public void testLoadJKSTrustStoreAutodetectStoreFileType() throws Exception {
+        // Make sure we can instantiate a trust manager from the JKS file on disk
+        X509TrustManager tm = X509Util.createTrustManager(
+                x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS).getAbsolutePath(),
+                x509TestContext.getTrustStorePassword(),
+                null,  // null StoreFileType means 'autodetect from file extension'
+                true,
+                true,
+                true,
+                true);
+    }
+
+    @Test(expected = X509Exception.TrustManagerException.class)
+    public void testLoadJKSTrustStoreWithWrongPassword() throws Exception {
+        // Attempting to load with the wrong key password should fail
+        X509TrustManager tm = X509Util.createTrustManager(
+                x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS).getAbsolutePath(),
+                "wrong password",
+                KeyStoreFileType.JKS.getPropertyValue(),
+                true,
+                true,
+                true,
+                true);
+    }
+
     // Warning: this will reset the x509Util
     private void setCustomCipherSuites() {
         System.setProperty(x509Util.getCipherSuitesProperty(), customCipherSuites[0] + "," + customCipherSuites[1]);

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/39fbc58f/zookeeper-server/src/test/java/org/apache/zookeeper/util/PemReaderTest.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/util/PemReaderTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/util/PemReaderTest.java
new file mode 100644
index 0000000..d0d3dc7
--- /dev/null
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/util/PemReaderTest.java
@@ -0,0 +1,137 @@
+/**
+ * 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.util;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyStoreException;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.zookeeper.common.BaseX509ParameterizedTestCase;
+import org.apache.zookeeper.common.KeyStoreFileType;
+import org.apache.zookeeper.common.X509KeyType;
+import org.apache.zookeeper.common.X509TestContext;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class PemReaderTest extends BaseX509ParameterizedTestCase {
+
+    @Parameterized.Parameters
+    public static Collection<Object[]> params() {
+        return BaseX509ParameterizedTestCase.defaultParams();
+    }
+
+    public PemReaderTest(
+            X509KeyType caKeyType,
+            X509KeyType certKeyType,
+            String keyPassword,
+            Integer paramIndex) {
+        super(paramIndex, () -> {
+            try {
+                return X509TestContext.newBuilder()
+                        .setTempDir(tempDir)
+                        .setKeyStorePassword(keyPassword)
+                        .setKeyStoreKeyType(certKeyType)
+                        .setTrustStorePassword(keyPassword)
+                        .setTrustStoreKeyType(caKeyType)
+                        .build();
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        });
+    }
+
+    @Test
+    public void testLoadPrivateKeyFromKeyStore() throws IOException, GeneralSecurityException {
+        Optional<String> optPassword = x509TestContext.getKeyStorePassword().length() > 0
+                ? Optional.of(x509TestContext.getKeyStorePassword())
+                : Optional.empty();
+        PrivateKey privateKey = PemReader.loadPrivateKey(
+                x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM), optPassword);
+        Assert.assertEquals(x509TestContext.getKeyStoreKeyPair().getPrivate(), privateKey);
+    }
+
+    // Try to load a password-protected private key without providing a password
+    @Test(expected = GeneralSecurityException.class)
+    public void testLoadEncryptedPrivateKeyFromKeyStoreWithoutPassword() throws GeneralSecurityException, IOException {
+        if (!x509TestContext.isKeyStoreEncrypted()) {
+            throw new GeneralSecurityException(); // this case is not tested so throw the expected exception
+        }
+        PemReader.loadPrivateKey(x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM), Optional.empty());
+    }
+
+    // Try to load a password-protected private key with the wrong password
+    @Test(expected = GeneralSecurityException.class)
+    public void testLoadEncryptedPrivateKeyFromKeyStoreWithWrongPassword() throws GeneralSecurityException, IOException {
+        if (!x509TestContext.isKeyStoreEncrypted()) {
+            throw new GeneralSecurityException(); // this case is not tested so throw the expected exception
+        }
+        PemReader.loadPrivateKey(
+                x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM),
+                Optional.of("wrong password"));
+    }
+
+    // Try to load a non-protected private key while providing a password
+    @Test(expected = IOException.class)
+    public void testLoadUnencryptedPrivateKeyFromKeyStoreWithWrongPassword() throws GeneralSecurityException, IOException {
+        if (x509TestContext.isKeyStoreEncrypted()) {
+            throw new IOException();
+        }
+        PemReader.loadPrivateKey(
+                x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM),
+                Optional.of("wrong password"));
+    }
+
+    // Expect this to fail, the trust store does not contain a private key
+    @Test(expected = KeyStoreException.class)
+    public void testLoadPrivateKeyFromTrustStore() throws IOException, GeneralSecurityException {
+        PemReader.loadPrivateKey(
+                x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM), Optional.empty());
+    }
+
+    // Expect this to fail, the trust store does not contain a private key
+    @Test(expected = KeyStoreException.class)
+    public void testLoadPrivateKeyFromTrustStoreWithPassword() throws IOException, GeneralSecurityException {
+        PemReader.loadPrivateKey(
+                x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM), Optional.of("foobar"));
+    }
+
+    @Test
+    public void testLoadCertificateFromKeyStore() throws IOException, GeneralSecurityException {
+        List<X509Certificate> certs = PemReader.readCertificateChain(
+                x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM));
+        Assert.assertEquals(1, certs.size());
+        Assert.assertEquals(x509TestContext.getKeyStoreCertificate(), certs.get(0));
+    }
+
+    @Test
+    public void testLoadCertificateFromTrustStore() throws IOException, GeneralSecurityException {
+        List<X509Certificate> certs = PemReader.readCertificateChain(
+                x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM));
+        Assert.assertEquals(1, certs.size());
+        Assert.assertEquals(x509TestContext.getTrustStoreCertificate(), certs.get(0));
+    }
+}


Mime
View raw message