You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zookeeper.apache.org by an...@apache.org on 2018/11/07 01:29:54 UTC

[2/2] zookeeper git commit: ZOOKEEPER-3173: Quorum TLS - support PEM trust/key stores

ZOOKEEPER-3173: Quorum TLS - support PEM trust/key stores

Add support for loading key and trust stores from PEM files.

Unfortunately, this PR includes 2 JIRAs, because it was quite difficult
to untangle the two features as they were developed at the same time
originally:

- ZOOKEEPER-3173: Quorum TLS - support PEM trust/key stores
- ZOOKEEPER-3175: Quorum TLS - test improvements

## Added support for PEM formatted key stores and trust stores
- key store and trust store files can now be in PEM format as well as JKS.
- Added config properties to tell ZK what type of trust/key store to load:
- `zookeeper.ssl.keyStore.type` and `zookeeper.ssl.trustStore.type` for ClientX509Util
- `zookeeper.ssl.quorum.keyStore.type` and `zookeeper.ssl.quorum.trustStore.type` for QuorumX509Util
- store type properties could have the values "JKS", "PEM", or not set
- leaving the type properties unset will cause auto-detection of the store type based on the file extension (".jks" or ".pem")

## Added test utilities for easily creating X509 certs and using them in unit tests
- added new class `X509TestContext` and its friend, `X509TestHelpers`
- rewrote some existing unit tests to use these classes, and added new tests that use them
- some existing tests (i.e. `QuorumSSLTest`) should probably be ported to use this as well, haven't got around to it yet

Author: Ilya Maykov <il...@fb.com>

Reviewers: andor@apache.org

Closes #678 from ivmaykov/ZOOKEEPER-3173


Project: http://git-wip-us.apache.org/repos/asf/zookeeper/repo
Commit: http://git-wip-us.apache.org/repos/asf/zookeeper/commit/03286f29
Tree: http://git-wip-us.apache.org/repos/asf/zookeeper/tree/03286f29
Diff: http://git-wip-us.apache.org/repos/asf/zookeeper/diff/03286f29

Branch: refs/heads/master
Commit: 03286f29d29f4c1d3496ce87d1441df91422220e
Parents: e5fc122
Author: Ilya Maykov <il...@fb.com>
Authored: Tue Nov 6 17:29:49 2018 -0800
Committer: Andor Molnar <an...@apache.org>
Committed: Tue Nov 6 17:29:49 2018 -0800

----------------------------------------------------------------------
 NOTICE.txt                                      |   6 +
 .../zookeeper/common/FileKeyStoreLoader.java    |  77 +++
 .../FileKeyStoreLoaderBuilderProvider.java      |  45 ++
 .../apache/zookeeper/common/JKSFileLoader.java  |  94 ++++
 .../zookeeper/common/KeyStoreFileType.java      | 114 +++++
 .../apache/zookeeper/common/KeyStoreLoader.java |  52 ++
 .../apache/zookeeper/common/PEMFileLoader.java  |  64 +++
 .../org/apache/zookeeper/common/X509Util.java   | 201 +++++---
 .../org/apache/zookeeper/common/ZKConfig.java   |   4 +
 .../server/auth/X509AuthenticationProvider.java |  39 +-
 .../org/apache/zookeeper/util/PemReader.java    | 236 +++++++++
 .../common/BaseX509ParameterizedTestCase.java   | 109 ++++
 .../FileKeyStoreLoaderBuilderProviderTest.java  |  46 ++
 .../zookeeper/common/JKSFileLoaderTest.java     | 165 +++++++
 .../zookeeper/common/KeyStoreFileTypeTest.java  |  90 ++++
 .../zookeeper/common/PEMFileLoaderTest.java     | 156 ++++++
 .../apache/zookeeper/common/X509KeyType.java    |  26 +
 .../zookeeper/common/X509TestContext.java       | 492 +++++++++++++++++++
 .../zookeeper/common/X509TestHelpers.java       | 402 +++++++++++++++
 .../apache/zookeeper/common/X509UtilTest.java   | 352 ++++++++-----
 .../apache/zookeeper/util/PemReaderTest.java    | 137 ++++++
 21 files changed, 2676 insertions(+), 231 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/NOTICE.txt
----------------------------------------------------------------------
diff --git a/NOTICE.txt b/NOTICE.txt
index 5689ab6..9ce75ba 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -3,3 +3,9 @@ Copyright 2009-2014 The Apache Software Foundation
 
 This product includes software developed at
 The Apache Software Foundation (http://www.apache.org/).
+
+This product includes software components originally
+developed for Airlift (https://github.com/airlift/airlift),
+licensed under the Apache 2.0 license. The licensing terms
+for Airlift code can be found at:
+https://github.com/airlift/airlift/blob/master/LICENSE

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/main/java/org/apache/zookeeper/common/FileKeyStoreLoader.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/FileKeyStoreLoader.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/FileKeyStoreLoader.java
new file mode 100644
index 0000000..6cc37b6
--- /dev/null
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/FileKeyStoreLoader.java
@@ -0,0 +1,77 @@
+/**
+ * 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.util.Objects;
+
+/**
+ * Base class for instances of {@link KeyStoreLoader} which load the key/trust
+ * stores from files on a filesystem.
+ */
+abstract class FileKeyStoreLoader implements KeyStoreLoader {
+    final String keyStorePath;
+    final String trustStorePath;
+    final String keyStorePassword;
+    final String trustStorePassword;
+
+    FileKeyStoreLoader(String keyStorePath,
+                       String trustStorePath,
+                       String keyStorePassword,
+                       String trustStorePassword) {
+        this.keyStorePath = keyStorePath;
+        this.trustStorePath = trustStorePath;
+        this.keyStorePassword = keyStorePassword;
+        this.trustStorePassword = trustStorePassword;
+    }
+
+    /**
+     * Base class for builder pattern used by subclasses.
+     * @param <T> the subtype of FileKeyStoreLoader created by the Builder.
+     */
+    static abstract class Builder<T extends FileKeyStoreLoader> {
+        String keyStorePath;
+        String trustStorePath;
+        String keyStorePassword;
+        String trustStorePassword;
+
+        Builder() {}
+
+        Builder<T> setKeyStorePath(String keyStorePath) {
+            this.keyStorePath = Objects.requireNonNull(keyStorePath);
+            return this;
+        }
+
+        Builder<T> setTrustStorePath(String trustStorePath) {
+            this.trustStorePath = Objects.requireNonNull(trustStorePath);
+            return this;
+        }
+
+        Builder<T> setKeyStorePassword(String keyStorePassword) {
+            this.keyStorePassword = Objects.requireNonNull(keyStorePassword);
+            return this;
+        }
+
+        Builder<T> setTrustStorePassword(String trustStorePassword) {
+            this.trustStorePassword = Objects.requireNonNull(trustStorePassword);
+            return this;
+        }
+
+        abstract T build();
+    }
+}

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/main/java/org/apache/zookeeper/common/FileKeyStoreLoaderBuilderProvider.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/FileKeyStoreLoaderBuilderProvider.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/FileKeyStoreLoaderBuilderProvider.java
new file mode 100644
index 0000000..bcbefe2
--- /dev/null
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/FileKeyStoreLoaderBuilderProvider.java
@@ -0,0 +1,45 @@
+/**
+ * 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.util.Objects;
+
+public class FileKeyStoreLoaderBuilderProvider {
+    /**
+     * Returns a {@link FileKeyStoreLoader.Builder} that can build a loader
+     * which loads keys and certs from files of the given
+     * {@link KeyStoreFileType}.
+     *
+     * @param type the file type to load keys/certs from.
+     * @return a new Builder.
+     */
+    static FileKeyStoreLoader.Builder<? extends FileKeyStoreLoader>
+    getBuilderForKeyStoreFileType(KeyStoreFileType type) {
+        switch (Objects.requireNonNull(type)) {
+            case JKS:
+                return new JKSFileLoader.Builder();
+            case PEM:
+                return new PEMFileLoader.Builder();
+            default:
+                throw new AssertionError(
+                        "Unexpected StoreFileType: " + type.name());
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/main/java/org/apache/zookeeper/common/JKSFileLoader.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/JKSFileLoader.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/JKSFileLoader.java
new file mode 100644
index 0000000..f391c7b
--- /dev/null
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/JKSFileLoader.java
@@ -0,0 +1,94 @@
+/**
+ * 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.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implementation of {@link FileKeyStoreLoader} that loads from JKS files.
+ */
+class JKSFileLoader extends FileKeyStoreLoader {
+    private static final Logger LOG = LoggerFactory.getLogger(JKSFileLoader.class);
+
+    private static final char[] EMPTY_CHAR_ARRAY = new char[0];
+    private static final String JKS_KEY_STORE_TYPE = "JKS";
+
+    private JKSFileLoader(String keyStorePath,
+                          String trustStorePath,
+                          String keyStorePassword,
+                          String trustStorePassword) {
+        super(keyStorePath, trustStorePath, keyStorePassword, trustStorePassword);
+    }
+
+    @Override
+    public KeyStore loadKeyStore() throws IOException, GeneralSecurityException {
+        KeyStore ks = KeyStore.getInstance(JKS_KEY_STORE_TYPE);
+        InputStream inputStream = null;
+        try {
+            inputStream = new FileInputStream(new File(keyStorePath));
+            ks.load(inputStream, passwordStringToCharArray(keyStorePassword));
+            return ks;
+        } finally {
+            forceClose(inputStream);
+        }
+    }
+
+    @Override
+    public KeyStore loadTrustStore() throws IOException, GeneralSecurityException {
+        KeyStore ts = KeyStore.getInstance(JKS_KEY_STORE_TYPE);
+        InputStream inputStream = null;
+        try {
+            inputStream = new FileInputStream(new File(trustStorePath));
+            ts.load(inputStream, passwordStringToCharArray(trustStorePassword));
+            return ts;
+        } finally {
+            forceClose(inputStream);
+        }
+    }
+
+    private char[] passwordStringToCharArray(String password) {
+        return password == null ? EMPTY_CHAR_ARRAY : password.toCharArray();
+    }
+
+    private void forceClose(InputStream stream) {
+        if (stream == null) {
+            return;
+        }
+        try {
+            stream.close();
+        } catch (IOException e) {
+            LOG.info("Failed to close key store input stream", e);
+        }
+    }
+
+    static class Builder extends FileKeyStoreLoader.Builder<JKSFileLoader> {
+        @Override
+        JKSFileLoader build() {
+            return new JKSFileLoader(keyStorePath, trustStorePath, keyStorePassword, trustStorePassword);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/main/java/org/apache/zookeeper/common/KeyStoreFileType.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/KeyStoreFileType.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/KeyStoreFileType.java
new file mode 100644
index 0000000..f006468
--- /dev/null
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/KeyStoreFileType.java
@@ -0,0 +1,114 @@
+/**
+ * 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;
+
+/**
+ * This enum represents the file type of a KeyStore or TrustStore.
+ * Currently, JKS (java keystore) and PEM types are supported.
+ */
+public enum KeyStoreFileType {
+    // TODO: consider adding support for PKCS12
+    JKS(".jks"), PEM(".pem");
+
+    private final String defaultFileExtension;
+
+    KeyStoreFileType(String defaultFileExtension) {
+        this.defaultFileExtension = defaultFileExtension;
+    }
+
+    /**
+     * The property string that specifies that a key store or trust store
+     * should use this store file type.
+     */
+    public String getPropertyValue() {
+        return this.name();
+    }
+
+    /**
+     * The file extension that is associated with this file type.
+     */
+    public String getDefaultFileExtension() {
+        return defaultFileExtension;
+    }
+
+    /**
+     * Converts a property value to a StoreFileType enum. If the property value
+     * is <code>null</code> or an empty string, returns <code>null</code>.
+     * @param propertyValue the property value.
+     * @return the KeyStoreFileType, or <code>null</code> if
+     *         <code>propertyValue</code> is <code>null</code> or empty.
+     * @throws IllegalArgumentException if <code>propertyValue</code> is not
+     *         one of "JKS", "PEM", or empty/null.
+     */
+    public static KeyStoreFileType fromPropertyValue(String propertyValue) {
+        if (propertyValue == null || propertyValue.length() == 0) {
+            return null;
+        }
+        return KeyStoreFileType.valueOf(propertyValue.toUpperCase());
+    }
+
+    /**
+     * Detects the type of KeyStore / TrustStore file from the file extension.
+     * If the file name ends with ".jks", returns <code>StoreFileType.JKS</code>.
+     * If the file name ends with ".pem", returns <code>StoreFileType.PEM</code>.
+     * Otherwise, throws an IllegalArgumentException.
+     * @param filename the filename of the key store or trust store file.
+     * @return a KeyStoreFileType.
+     * @throws IllegalArgumentException if the filename does not end with
+     *         ".jks" or ".pem".
+     */
+    public static KeyStoreFileType fromFilename(String filename) {
+        int i = filename.lastIndexOf('.');
+        if (i >= 0) {
+            String extension = filename.substring(i);
+            for (KeyStoreFileType storeFileType : KeyStoreFileType.values()) {
+                if (storeFileType.getDefaultFileExtension().equals(extension)) {
+                    return storeFileType;
+                }
+            }
+        }
+        throw new IllegalArgumentException(
+                "Unable to auto-detect store file type from file name: " + filename);
+    }
+
+    /**
+     * If <code>propertyValue</code> is not null or empty, returns the result
+     * of <code>KeyStoreFileType.fromPropertyValue(propertyValue)</code>. Else,
+     * returns the result of <code>KeyStoreFileType.fromFileName(filename)</code>.
+     * @param propertyValue property value describing the KeyStoreFileType, or
+     *                      null/empty to auto-detect the type from the file
+     *                      name.
+     * @param filename file name of the key store file. The file extension is
+     *                 used to auto-detect the KeyStoreFileType when
+     *                 <code>propertyValue</code> is null or empty.
+     * @return a KeyStoreFileType.
+     * @throws IllegalArgumentException if <code>propertyValue</code> is not
+     *         one of "JKS", "PEM", or empty/null.
+     * @throws IllegalArgumentException if <code>propertyValue</code>is empty
+     *         or null and the type could not be determined from the file name.
+     */
+    public static KeyStoreFileType fromPropertyValueOrFileName(String propertyValue,
+                                                               String filename) {
+        KeyStoreFileType result = KeyStoreFileType.fromPropertyValue(propertyValue);
+        if (result == null) {
+            result = KeyStoreFileType.fromFilename(filename);
+        }
+        return result;
+    }
+}

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/main/java/org/apache/zookeeper/common/KeyStoreLoader.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/KeyStoreLoader.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/KeyStoreLoader.java
new file mode 100644
index 0000000..ad5d9d0
--- /dev/null
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/KeyStoreLoader.java
@@ -0,0 +1,52 @@
+/**
+ * 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.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+
+/**
+ * An interface for an object that can load key stores or trust stores.
+ */
+interface KeyStoreLoader {
+    /**
+     * Loads a KeyStore which contains at least one private key and the
+     * associated X509 cert chain.
+     *
+     * @return a new KeyStore
+     * @throws IOException if loading the key store fails due to an IO error,
+     *         such as "file not found".
+     * @throws GeneralSecurityException if loading the key store fails due to
+     *         a security error, such as "unsupported crypto algorithm".
+     */
+    KeyStore loadKeyStore() throws IOException, GeneralSecurityException;
+
+    /**
+     * Loads a KeyStore which contains at least one X509 cert chain for a
+     * trusted Certificate Authority (CA).
+     *
+     * @return a new KeyStore
+     * @throws IOException if loading the trust store fails due to an IO error,
+     *         such as "file not found".
+     * @throws GeneralSecurityException if loading the trust store fails due to
+     *         a security error, such as "unsupported crypto algorithm".
+     */
+    KeyStore loadTrustStore() throws IOException, GeneralSecurityException;
+}

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/main/java/org/apache/zookeeper/common/PEMFileLoader.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/PEMFileLoader.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/PEMFileLoader.java
new file mode 100644
index 0000000..637e8a8
--- /dev/null
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/PEMFileLoader.java
@@ -0,0 +1,64 @@
+/**
+ * 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.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.util.Optional;
+
+import org.apache.zookeeper.util.PemReader;
+
+/**
+ * Implementation of {@link FileKeyStoreLoader} that loads from PEM files.
+ */
+class PEMFileLoader extends FileKeyStoreLoader {
+    private PEMFileLoader(String keyStorePath,
+                          String trustStorePath,
+                          String keyStorePassword,
+                          String trustStorePassword) {
+        super(keyStorePath, trustStorePath, keyStorePassword, trustStorePassword);
+    }
+
+    @Override
+    public KeyStore loadKeyStore() throws IOException, GeneralSecurityException {
+        Optional<String> passwordOption;
+        if (keyStorePassword == null || keyStorePassword.length() == 0) {
+            passwordOption = Optional.empty();
+        } else {
+            passwordOption = Optional.of(keyStorePassword);
+        }
+        File file = new File(keyStorePath);
+        return PemReader.loadKeyStore(file, file, passwordOption);
+    }
+
+    @Override
+    public KeyStore loadTrustStore() throws IOException, GeneralSecurityException {
+        return PemReader.loadTrustStore(new File(trustStorePath));
+    }
+
+
+    static class Builder extends FileKeyStoreLoader.Builder<PEMFileLoader> {
+        @Override
+        PEMFileLoader build() {
+            return new PEMFileLoader(keyStorePath, trustStorePath, keyStorePassword, trustStorePassword);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
index 2112c70..5b97ac6 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
@@ -18,8 +18,17 @@
 package org.apache.zookeeper.common;
 
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import java.io.IOException;
+import java.net.Socket;
+import java.security.GeneralSecurityException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.security.Security;
+import java.security.cert.PKIXBuilderParameters;
+import java.security.cert.X509CertSelector;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicReference;
 
 import javax.net.ssl.CertPathTrustManagerParameters;
 import javax.net.ssl.KeyManager;
@@ -33,26 +42,12 @@ import javax.net.ssl.TrustManagerFactory;
 import javax.net.ssl.X509ExtendedTrustManager;
 import javax.net.ssl.X509KeyManager;
 import javax.net.ssl.X509TrustManager;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.net.Socket;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.KeyManagementException;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.Security;
-import java.security.UnrecoverableKeyException;
-import java.security.cert.CertificateException;
-import java.security.cert.PKIXBuilderParameters;
-import java.security.cert.X509CertSelector;
-import java.util.Arrays;
-import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.zookeeper.common.X509Exception.KeyManagerException;
 import org.apache.zookeeper.common.X509Exception.SSLContextException;
 import org.apache.zookeeper.common.X509Exception.TrustManagerException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Utility code for X509 handling
@@ -83,8 +78,10 @@ public abstract class X509Util {
     private String cipherSuitesProperty = getConfigPrefix() + "ciphersuites";
     private String sslKeystoreLocationProperty = getConfigPrefix() + "keyStore.location";
     private String sslKeystorePasswdProperty = getConfigPrefix() + "keyStore.password";
+    private String sslKeystoreTypeProperty = getConfigPrefix() + "keyStore.type";
     private String sslTruststoreLocationProperty = getConfigPrefix() + "trustStore.location";
     private String sslTruststorePasswdProperty = getConfigPrefix() + "trustStore.password";
+    private String sslTruststoreTypeProperty = getConfigPrefix() + "trustStore.type";
     private String sslHostnameVerificationEnabledProperty = getConfigPrefix() + "hostnameVerification";
     private String sslCrlEnabledProperty = getConfigPrefix() + "crl";
     private String sslOcspEnabledProperty = getConfigPrefix() + "ocsp";
@@ -121,6 +118,10 @@ public abstract class X509Util {
         return sslKeystorePasswdProperty;
     }
 
+    public String getSslKeystoreTypeProperty() {
+        return sslKeystoreTypeProperty;
+    }
+
     public String getSslTruststoreLocationProperty() {
         return sslTruststoreLocationProperty;
     }
@@ -129,6 +130,10 @@ public abstract class X509Util {
         return sslTruststorePasswdProperty;
     }
 
+    public String getSslTruststoreTypeProperty() {
+        return sslTruststoreTypeProperty;
+    }
+
     public String getSslHostnameVerificationEnabledProperty() {
         return sslHostnameVerificationEnabledProperty;
     }
@@ -167,47 +172,49 @@ public abstract class X509Util {
         KeyManager[] keyManagers = null;
         TrustManager[] trustManagers = null;
 
-        String keyStoreLocationProp = config.getProperty(sslKeystoreLocationProperty);
-        String keyStorePasswordProp = config.getProperty(sslKeystorePasswdProperty);
+        String keyStoreLocationProp = config.getProperty(sslKeystoreLocationProperty, "");
+        String keyStorePasswordProp = config.getProperty(sslKeystorePasswdProperty, "");
+        String keyStoreTypeProp = config.getProperty(sslKeystoreTypeProperty);
 
         // There are legal states in some use cases for null KeyManager or TrustManager.
-        // But if a user wanna specify one, location and password are required.
+        // But if a user wanna specify one, location is required. Password defaults to empty string if it is not
+        // specified by the user.
 
-        if (keyStoreLocationProp == null && keyStorePasswordProp == null) {
+        if (keyStoreLocationProp.isEmpty()) {
             LOG.warn(getSslKeystoreLocationProperty() + " not specified");
         } else {
-            if (keyStoreLocationProp == null) {
-                throw new SSLContextException(getSslKeystoreLocationProperty() + " not specified");
-            }
-            if (keyStorePasswordProp == null) {
-                throw new SSLContextException(getSslKeystorePasswdProperty() + " not specified");
-            }
             try {
                 keyManagers = new KeyManager[]{
-                        createKeyManager(keyStoreLocationProp, keyStorePasswordProp)};
+                        createKeyManager(keyStoreLocationProp, keyStorePasswordProp, keyStoreTypeProp)};
             } catch (KeyManagerException keyManagerException) {
                 throw new SSLContextException("Failed to create KeyManager", keyManagerException);
+            } catch (IllegalArgumentException e) {
+                throw new SSLContextException("Bad value for " + sslKeystoreTypeProperty + ": " + keyStoreTypeProp, e);
             }
         }
 
-        String trustStoreLocationProp = config.getProperty(sslTruststoreLocationProperty);
-        String trustStorePasswordProp = config.getProperty(sslTruststorePasswdProperty);
+        String trustStoreLocationProp = config.getProperty(sslTruststoreLocationProperty, "");
+        String trustStorePasswordProp = config.getProperty(sslTruststorePasswdProperty, "");
+        String trustStoreTypeProp = config.getProperty(sslTruststoreTypeProperty);
 
         boolean sslCrlEnabled = config.getBoolean(this.sslCrlEnabledProperty);
         boolean sslOcspEnabled = config.getBoolean(this.sslOcspEnabledProperty);
         boolean sslServerHostnameVerificationEnabled =
-                config.getBoolean(this.getSslHostnameVerificationEnabledProperty(),true);
-        boolean sslClientHostnameVerificationEnabled = sslServerHostnameVerificationEnabled && shouldVerifyClientHostname();
+                config.getBoolean(this.getSslHostnameVerificationEnabledProperty(), true);
+        boolean sslClientHostnameVerificationEnabled =
+                sslServerHostnameVerificationEnabled && shouldVerifyClientHostname();
 
-        if (trustStoreLocationProp == null) {
+        if (trustStoreLocationProp.isEmpty()) {
             LOG.warn(getSslTruststoreLocationProperty() + " not specified");
         } else {
             try {
                 trustManagers = new TrustManager[]{
-                        createTrustManager(trustStoreLocationProp, trustStorePasswordProp, sslCrlEnabled, sslOcspEnabled,
+                        createTrustManager(trustStoreLocationProp, trustStorePasswordProp, trustStoreTypeProp, sslCrlEnabled, sslOcspEnabled,
                                 sslServerHostnameVerificationEnabled, sslClientHostnameVerificationEnabled)};
             } catch (TrustManagerException trustManagerException) {
                 throw new SSLContextException("Failed to create TrustManager", trustManagerException);
+            } catch (IllegalArgumentException e) {
+                throw new SSLContextException("Bad value for " + sslTruststoreTypeProperty + ": " + trustStoreTypeProp, e);
             }
         }
 
@@ -221,17 +228,38 @@ public abstract class X509Util {
         }
     }
 
-    public static X509KeyManager createKeyManager(String keyStoreLocation, String keyStorePassword)
+    /**
+     * Creates a key manager by loading the key store from the given file of
+     * the given type, optionally decrypting it using the given password.
+     * @param keyStoreLocation the location of the key store file.
+     * @param keyStorePassword optional password to decrypt the key store. If
+     *                         empty, assumes the key store is not encrypted.
+     * @param keyStoreTypeProp must be JKS, PEM, or null. If null, attempts to
+     *                         autodetect the key store type from the file
+     *                         extension (.jks / .pem).
+     * @return the key manager.
+     * @throws KeyManagerException if something goes wrong.
+     */
+    public static X509KeyManager createKeyManager(
+            String keyStoreLocation,
+            String keyStorePassword,
+            String keyStoreTypeProp)
             throws KeyManagerException {
-        FileInputStream inputStream = null;
+        if (keyStorePassword == null) {
+            keyStorePassword = "";
+        }
         try {
-            char[] keyStorePasswordChars = keyStorePassword.toCharArray();
-            File keyStoreFile = new File(keyStoreLocation);
-            KeyStore ks = KeyStore.getInstance("JKS");
-            inputStream = new FileInputStream(keyStoreFile);
-            ks.load(inputStream, keyStorePasswordChars);
+            KeyStoreFileType storeFileType =
+                    KeyStoreFileType.fromPropertyValueOrFileName(
+                            keyStoreTypeProp, keyStoreLocation);
+            KeyStore ks = FileKeyStoreLoaderBuilderProvider
+                    .getBuilderForKeyStoreFileType(storeFileType)
+                    .setKeyStorePath(keyStoreLocation)
+                    .setKeyStorePassword(keyStorePassword)
+                    .build()
+                    .loadKeyStore();
             KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
-            kmf.init(ks, keyStorePasswordChars);
+            kmf.init(ks, keyStorePassword.toCharArray());
 
             for (KeyManager km : kmf.getKeyManagers()) {
                 if (km instanceof X509KeyManager) {
@@ -239,38 +267,58 @@ public abstract class X509Util {
                 }
             }
             throw new KeyManagerException("Couldn't find X509KeyManager");
-
-        } catch (IOException|CertificateException|UnrecoverableKeyException|NoSuchAlgorithmException|KeyStoreException
-                keyManagerCreationException) {
-            throw new KeyManagerException(keyManagerCreationException);
-        } finally {
-            if (inputStream != null) {
-                try {
-                    inputStream.close();
-                } catch (IOException ioException) {
-                    LOG.info("Failed to close key store input stream", ioException);
-                }
-            }
+        } catch (IOException | GeneralSecurityException | IllegalArgumentException e) {
+            throw new KeyManagerException(e);
         }
     }
 
-    public static X509TrustManager createTrustManager(String trustStoreLocation, String trustStorePassword,
-                                                      boolean crlEnabled, boolean ocspEnabled,
-                                                      final boolean serverHostnameVerificationEnabled,
-                                                      final boolean clientHostnameVerificationEnabled)
+    /**
+     * Creates a trust manager by loading the trust store from the given file
+     * of the given type, optionally decrypting it using the given password.
+     * @param trustStoreLocation the location of the trust store file.
+     * @param trustStorePassword optional password to decrypt the trust store
+     *                           (only applies to JKS trust stores). If empty,
+     *                           assumes the trust store is not encrypted.
+     * @param trustStoreTypeProp must be JKS, PEM, or null. If null, attempts
+     *                           to autodetect the trust store type from the
+     *                           file extension (.jks / .pem).
+     * @param crlEnabled enable CRL (certificate revocation list) checks.
+     * @param ocspEnabled enable OCSP (online certificate status protocol)
+     *                    checks.
+     * @param serverHostnameVerificationEnabled if true, verify hostnames of
+     *                                          remote servers that client
+     *                                          sockets created by this
+     *                                          X509Util connect to.
+     * @param clientHostnameVerificationEnabled if true, verify hostnames of
+     *                                          remote clients that server
+     *                                          sockets created by this
+     *                                          X509Util accept connections
+     *                                          from.
+     * @return the trust manager.
+     * @throws TrustManagerException if something goes wrong.
+     */
+    public static X509TrustManager createTrustManager(
+            String trustStoreLocation,
+            String trustStorePassword,
+            String trustStoreTypeProp,
+            boolean crlEnabled,
+            boolean ocspEnabled,
+            final boolean serverHostnameVerificationEnabled,
+            final boolean clientHostnameVerificationEnabled)
             throws TrustManagerException {
-        FileInputStream inputStream = null;
+        if (trustStorePassword == null) {
+            trustStorePassword = "";
+        }
         try {
-            File trustStoreFile = new File(trustStoreLocation);
-            KeyStore ts = KeyStore.getInstance("JKS");
-            inputStream = new FileInputStream(trustStoreFile);
-            if (trustStorePassword != null) {
-                char[] trustStorePasswordChars = trustStorePassword.toCharArray();
-                ts.load(inputStream, trustStorePasswordChars);
-            } else {
-                ts.load(inputStream, null);
-            }
-
+            KeyStoreFileType storeFileType =
+                    KeyStoreFileType.fromPropertyValueOrFileName(
+                            trustStoreTypeProp, trustStoreLocation);
+            KeyStore ts = FileKeyStoreLoaderBuilderProvider
+                    .getBuilderForKeyStoreFileType(storeFileType)
+                    .setTrustStorePath(trustStoreLocation)
+                    .setTrustStorePassword(trustStorePassword)
+                    .build()
+                    .loadTrustStore();
             PKIXBuilderParameters pbParams = new PKIXBuilderParameters(ts, new X509CertSelector());
             if (crlEnabled || ocspEnabled) {
                 pbParams.setRevocationEnabled(true);
@@ -294,17 +342,8 @@ public abstract class X509Util {
                 }
             }
             throw new TrustManagerException("Couldn't find X509TrustManager");
-        } catch (IOException|CertificateException|NoSuchAlgorithmException|InvalidAlgorithmParameterException|KeyStoreException
-                 trustManagerCreationException) {
-            throw new TrustManagerException(trustManagerCreationException);
-        } finally {
-            if (inputStream != null) {
-                try {
-                    inputStream.close();
-                } catch (IOException ioException) {
-                    LOG.info("failed to close TrustStore input stream", ioException);
-                }
-            }
+        } catch (IOException | GeneralSecurityException | IllegalArgumentException e) {
+            throw new TrustManagerException(e);
         }
     }
 

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/main/java/org/apache/zookeeper/common/ZKConfig.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/ZKConfig.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ZKConfig.java
index dc24b19..01bac69 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/common/ZKConfig.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ZKConfig.java
@@ -116,10 +116,14 @@ public class ZKConfig {
                 System.getProperty(x509Util.getSslKeystoreLocationProperty()));
         properties.put(x509Util.getSslKeystorePasswdProperty(),
                 System.getProperty(x509Util.getSslKeystorePasswdProperty()));
+        properties.put(x509Util.getSslKeystoreTypeProperty(),
+                System.getProperty(x509Util.getSslKeystoreTypeProperty()));
         properties.put(x509Util.getSslTruststoreLocationProperty(),
                 System.getProperty(x509Util.getSslTruststoreLocationProperty()));
         properties.put(x509Util.getSslTruststorePasswdProperty(),
                 System.getProperty(x509Util.getSslTruststorePasswdProperty()));
+        properties.put(x509Util.getSslTruststoreTypeProperty(),
+                System.getProperty(x509Util.getSslTruststoreTypeProperty()));
         properties.put(x509Util.getSslHostnameVerificationEnabledProperty(),
                 System.getProperty(x509Util.getSslHostnameVerificationEnabledProperty()));
         properties.put(x509Util.getSslCrlEnabledProperty(),

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java
index 8a699ce..d0ca079 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java
@@ -27,11 +27,11 @@ import javax.security.auth.x500.X500Principal;
 
 import org.apache.zookeeper.KeeperException;
 import org.apache.zookeeper.common.ClientX509Util;
-import org.apache.zookeeper.common.ZKConfig;
 import org.apache.zookeeper.common.X509Exception;
 import org.apache.zookeeper.common.X509Exception.KeyManagerException;
 import org.apache.zookeeper.common.X509Exception.TrustManagerException;
 import org.apache.zookeeper.common.X509Util;
+import org.apache.zookeeper.common.ZKConfig;
 import org.apache.zookeeper.data.Id;
 import org.apache.zookeeper.server.ServerCnxn;
 import org.slf4j.Logger;
@@ -70,46 +70,37 @@ public class X509AuthenticationProvider implements AuthenticationProvider {
         ZKConfig config = new ZKConfig();
         X509Util x509Util = new ClientX509Util();
 
-        String keyStoreLocation = config.getProperty(x509Util.getSslKeystoreLocationProperty());
-        String keyStorePassword = config.getProperty(x509Util.getSslKeystorePasswdProperty());
+        String keyStoreLocation = config.getProperty(x509Util.getSslKeystoreLocationProperty(), "");
+        String keyStorePassword = config.getProperty(x509Util.getSslKeystorePasswdProperty(), "");
+        String keyStoreTypeProp = config.getProperty(x509Util.getSslKeystoreTypeProperty());
 
-        boolean crlEnabled = Boolean.parseBoolean(System.getProperty(x509Util.getSslCrlEnabledProperty()));
-        boolean ocspEnabled = Boolean.parseBoolean(System.getProperty(x509Util.getSslOcspEnabledProperty()));
-        boolean hostnameVerificationEnabled = Boolean.parseBoolean(System.getProperty(x509Util.getSslHostnameVerificationEnabledProperty()));
+        boolean crlEnabled = Boolean.parseBoolean(config.getProperty(x509Util.getSslCrlEnabledProperty()));
+        boolean ocspEnabled = Boolean.parseBoolean(config.getProperty(x509Util.getSslOcspEnabledProperty()));
+        boolean hostnameVerificationEnabled = Boolean.parseBoolean(
+                config.getProperty(x509Util.getSslHostnameVerificationEnabledProperty()));
 
         X509KeyManager km = null;
         X509TrustManager tm = null;
-        if (keyStoreLocation == null && keyStorePassword == null) {
+        if (keyStoreLocation.isEmpty()) {
             LOG.warn("keystore not specified for client connection");
         } else {
-            if (keyStoreLocation == null) {
-                throw new X509Exception("keystore location not specified for client connection");
-            }
-            if (keyStorePassword == null) {
-                throw new X509Exception("keystore password not specified for client connection");
-            }
             try {
-                km = X509Util.createKeyManager(keyStoreLocation, keyStorePassword);
+                km = X509Util.createKeyManager(keyStoreLocation, keyStorePassword, keyStoreTypeProp);
             } catch (KeyManagerException e) {
                 LOG.error("Failed to create key manager", e);
             }
         }
         
-        String trustStoreLocation = config.getProperty(x509Util.getSslTruststoreLocationProperty());
-        String trustStorePassword = config.getProperty(x509Util.getSslTruststorePasswdProperty());
+        String trustStoreLocation = config.getProperty(x509Util.getSslTruststoreLocationProperty(), "");
+        String trustStorePassword = config.getProperty(x509Util.getSslTruststorePasswdProperty(), "");
+        String trustStoreTypeProp = config.getProperty(x509Util.getSslTruststoreTypeProperty());
 
-        if (trustStoreLocation == null && trustStorePassword == null) {
+        if (trustStoreLocation.isEmpty()) {
             LOG.warn("Truststore not specified for client connection");
         } else {
-            if (trustStoreLocation == null) {
-                throw new X509Exception("Truststore location not specified for client connection");
-            }
-            if (trustStorePassword == null) {
-                throw new X509Exception("Truststore password not specified for client connection");
-            }
             try {
                 tm = X509Util.createTrustManager(
-                        trustStoreLocation, trustStorePassword, crlEnabled, ocspEnabled,
+                        trustStoreLocation, trustStorePassword, trustStoreTypeProp, crlEnabled, ocspEnabled,
                         hostnameVerificationEnabled, false);
             } catch (TrustManagerException e) {
                 LOG.error("Failed to create trust manager", e);

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/main/java/org/apache/zookeeper/util/PemReader.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/util/PemReader.java b/zookeeper-server/src/main/java/org/apache/zookeeper/util/PemReader.java
new file mode 100644
index 0000000..25017ea
--- /dev/null
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/util/PemReader.java
@@ -0,0 +1,236 @@
+/**
+ * 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.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.crypto.Cipher;
+import javax.crypto.EncryptedPrivateKeyInfo;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.security.auth.x500.X500Principal;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.util.Base64.getMimeDecoder;
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+import static javax.crypto.Cipher.DECRYPT_MODE;
+
+/**
+ * Note: this class is copied from io.airlift.security.pem.PemReader (see
+ * https://github.com/airlift/airlift/blob/master/security/src/main/java/io/airlift/security/pem/PemReader.java) with
+ * permission of the authors, to avoid adding an extra library dependency to Zookeeper.
+ * The file was copied from commit hash 86348546af43217f4d04a0cdad624b0ae4751c2c.
+ *
+ * The following modifications have been made to the original source code:
+ * <ul>
+ * <li>imports have been rearranged to match Zookeeper import order style.</li>
+ * <li>The dependency on <code>com.google.common.io.Files.asCharSource</code> has been removed.</li>
+ * <li>A dependency on <code>java.nio.file.Files</code> has been added.</li>
+ * </ul>
+ */
+public final class PemReader
+{
+    private static final Pattern CERT_PATTERN = Pattern.compile(
+            "-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+" + // Header
+                    "([a-z0-9+/=\\r\\n]+)" +                    // Base64 text
+                    "-+END\\s+.*CERTIFICATE[^-]*-+",            // Footer
+            CASE_INSENSITIVE);
+
+    private static final Pattern PRIVATE_KEY_PATTERN = Pattern.compile(
+            "-+BEGIN\\s+.*PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + // Header
+                    "([a-z0-9+/=\\r\\n]+)" +                       // Base64 text
+                    "-+END\\s+.*PRIVATE\\s+KEY[^-]*-+",            // Footer
+            CASE_INSENSITIVE);
+
+    private static final Pattern PUBLIC_KEY_PATTERN = Pattern.compile(
+            "-+BEGIN\\s+.*PUBLIC\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + // Header
+                    "([a-z0-9+/=\\r\\n]+)" +                      // Base64 text
+                    "-+END\\s+.*PUBLIC\\s+KEY[^-]*-+",            // Footer
+            CASE_INSENSITIVE);
+
+    private PemReader() {}
+
+    public static KeyStore loadTrustStore(File certificateChainFile)
+            throws IOException, GeneralSecurityException
+    {
+        KeyStore keyStore = KeyStore.getInstance("JKS");
+        keyStore.load(null, null);
+
+        List<X509Certificate> certificateChain = readCertificateChain(certificateChainFile);
+        for (X509Certificate certificate : certificateChain) {
+            X500Principal principal = certificate.getSubjectX500Principal();
+            keyStore.setCertificateEntry(principal.getName("RFC2253"), certificate);
+        }
+        return keyStore;
+    }
+
+    public static KeyStore loadKeyStore(File certificateChainFile, File privateKeyFile, Optional<String> keyPassword)
+            throws IOException, GeneralSecurityException
+    {
+        PrivateKey key = loadPrivateKey(privateKeyFile, keyPassword);
+
+        List<X509Certificate> certificateChain = readCertificateChain(certificateChainFile);
+        if (certificateChain.isEmpty()) {
+            throw new CertificateException("Certificate file does not contain any certificates: " + certificateChainFile);
+        }
+
+        KeyStore keyStore = KeyStore.getInstance("JKS");
+        keyStore.load(null, null);
+        keyStore.setKeyEntry("key", key, keyPassword.orElse("").toCharArray(), certificateChain.toArray(new Certificate[0]));
+        return keyStore;
+    }
+
+    public static List<X509Certificate> readCertificateChain(File certificateChainFile)
+            throws IOException, GeneralSecurityException
+    {
+        String contents = new String(Files.readAllBytes(certificateChainFile.toPath()), US_ASCII);
+        return readCertificateChain(contents);
+    }
+
+    public static List<X509Certificate> readCertificateChain(String certificateChain)
+            throws CertificateException
+    {
+        Matcher matcher = CERT_PATTERN.matcher(certificateChain);
+        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+        List<X509Certificate> certificates = new ArrayList<>();
+
+        int start = 0;
+        while (matcher.find(start)) {
+            byte[] buffer = base64Decode(matcher.group(1));
+            certificates.add((X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(buffer)));
+            start = matcher.end();
+        }
+
+        return certificates;
+    }
+
+    public static PrivateKey loadPrivateKey(File privateKeyFile, Optional<String> keyPassword)
+            throws IOException, GeneralSecurityException
+    {
+        String privateKey = new String(Files.readAllBytes(privateKeyFile.toPath()), US_ASCII);
+        return loadPrivateKey(privateKey, keyPassword);
+    }
+
+    public static PrivateKey loadPrivateKey(String privateKey, Optional<String> keyPassword)
+            throws IOException, GeneralSecurityException
+    {
+        Matcher matcher = PRIVATE_KEY_PATTERN.matcher(privateKey);
+        if (!matcher.find()) {
+            throw new KeyStoreException("did not find a private key");
+        }
+        byte[] encodedKey = base64Decode(matcher.group(1));
+
+        PKCS8EncodedKeySpec encodedKeySpec;
+        if (keyPassword.isPresent()) {
+            EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(encodedKey);
+            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName());
+            SecretKey secretKey = keyFactory.generateSecret(new PBEKeySpec(keyPassword.get().toCharArray()));
+
+            Cipher cipher = Cipher.getInstance(encryptedPrivateKeyInfo.getAlgName());
+            cipher.init(DECRYPT_MODE, secretKey, encryptedPrivateKeyInfo.getAlgParameters());
+
+            encodedKeySpec = encryptedPrivateKeyInfo.getKeySpec(cipher);
+        }
+        else {
+            encodedKeySpec = new PKCS8EncodedKeySpec(encodedKey);
+        }
+
+        // this code requires a key in PKCS8 format which is not the default openssl format
+        // to convert to the PKCS8 format you use : openssl pkcs8 -topk8 ...
+        try {
+            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+            return keyFactory.generatePrivate(encodedKeySpec);
+        }
+        catch (InvalidKeySpecException ignore) {
+        }
+
+        try {
+            KeyFactory keyFactory = KeyFactory.getInstance("EC");
+            return keyFactory.generatePrivate(encodedKeySpec);
+        }
+        catch (InvalidKeySpecException ignore) {
+        }
+
+        KeyFactory keyFactory = KeyFactory.getInstance("DSA");
+        return keyFactory.generatePrivate(encodedKeySpec);
+    }
+
+    public static PublicKey loadPublicKey(File publicKeyFile)
+            throws IOException, GeneralSecurityException
+    {
+        String publicKey = new String(Files.readAllBytes(publicKeyFile.toPath()), US_ASCII);
+        return loadPublicKey(publicKey);
+    }
+
+    public static PublicKey loadPublicKey(String publicKey)
+            throws GeneralSecurityException
+    {
+        Matcher matcher = PUBLIC_KEY_PATTERN.matcher(publicKey);
+        if (!matcher.find()) {
+            throw new KeyStoreException("did not find a public key");
+        }
+        String data = matcher.group(1);
+        byte[] encodedKey = base64Decode(data);
+
+        X509EncodedKeySpec encodedKeySpec = new X509EncodedKeySpec(encodedKey);
+        try {
+            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+            return keyFactory.generatePublic(encodedKeySpec);
+        }
+        catch (InvalidKeySpecException ignore) {
+        }
+
+        try {
+            KeyFactory keyFactory = KeyFactory.getInstance("EC");
+            return keyFactory.generatePublic(encodedKeySpec);
+        }
+        catch (InvalidKeySpecException ignore) {
+        }
+
+        KeyFactory keyFactory = KeyFactory.getInstance("DSA");
+        return keyFactory.generatePublic(encodedKeySpec);
+    }
+
+    private static byte[] base64Decode(String base64)
+    {
+        return getMimeDecoder().decode(base64.getBytes(US_ASCII));
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/test/java/org/apache/zookeeper/common/BaseX509ParameterizedTestCase.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/BaseX509ParameterizedTestCase.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/BaseX509ParameterizedTestCase.java
new file mode 100644
index 0000000..73b9196
--- /dev/null
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/BaseX509ParameterizedTestCase.java
@@ -0,0 +1,109 @@
+/**
+ * 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.IOException;
+import java.security.Security;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.zookeeper.ZKTestCase;
+import org.apache.zookeeper.test.ClientBase;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+/**
+ * Base class for parameterized unit tests that use X509TestContext for testing
+ * different X509 parameter combinations (CA key type, cert key type, with/without
+ * a password, with/without hostname verification, etc).
+ *
+ * This base class takes care of setting up / cleaning up the test environment,
+ * and caching the X509TestContext objects used by the tests.
+ */
+public abstract class BaseX509ParameterizedTestCase extends ZKTestCase {
+    /**
+     * Default parameters suitable for most subclasses. See example usage
+     * in {@link X509UtilTest}.
+     * @return an array of parameter combinations to test with.
+     */
+    public static Collection<Object[]> defaultParams() {
+        ArrayList<Object[]> result = new ArrayList<>();
+        int paramIndex = 0;
+        for (X509KeyType caKeyType : X509KeyType.values()) {
+            for (X509KeyType certKeyType : X509KeyType.values()) {
+                for (String keyPassword : new String[]{"", "pa$$w0rd"}) {
+                    result.add(new Object[]{caKeyType, certKeyType, keyPassword, paramIndex++});
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Because key generation and writing / deleting files is kind of expensive, we cache the certs and on-disk files
+     * between test cases. None of the test cases modify any of this data so it's safe to reuse between tests. This
+     * caching makes all test cases after the first one for a given parameter combination complete almost instantly.
+     */
+    protected static Map<Integer, X509TestContext> cachedTestContexts;
+    protected static File tempDir;
+
+    protected X509TestContext x509TestContext;
+
+    @BeforeClass
+    public static void setUpBaseClass() throws Exception {
+        Security.addProvider(new BouncyCastleProvider());
+        cachedTestContexts = new HashMap<>();
+        tempDir = ClientBase.createEmptyTestDir();
+    }
+
+    @AfterClass
+    public static void cleanUpBaseClass() {
+        Security.removeProvider("BC");
+        cachedTestContexts.clear();
+        cachedTestContexts = null;
+        try {
+            FileUtils.deleteDirectory(tempDir);
+        } catch (IOException e) {
+            // ignore
+        }
+    }
+
+    /**
+     * Constructor. See example usage in {@link X509UtilTest}.
+     *
+     * @param paramIndex the index under which the X509TestContext should be cached.
+     * @param contextSupplier a function that creates and returns the X509TestContext
+     *                        for the current index if one is not already cached.
+     */
+    protected BaseX509ParameterizedTestCase(
+            Integer paramIndex,
+            java.util.function.Supplier<X509TestContext> contextSupplier) {
+        if (cachedTestContexts.containsKey(paramIndex)) {
+            x509TestContext = cachedTestContexts.get(paramIndex);
+        } else {
+            x509TestContext = contextSupplier.get();
+            cachedTestContexts.put(paramIndex, x509TestContext);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/test/java/org/apache/zookeeper/common/FileKeyStoreLoaderBuilderProviderTest.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/FileKeyStoreLoaderBuilderProviderTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/FileKeyStoreLoaderBuilderProviderTest.java
new file mode 100644
index 0000000..59c27b2
--- /dev/null
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/FileKeyStoreLoaderBuilderProviderTest.java
@@ -0,0 +1,46 @@
+/**
+ * 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.apache.zookeeper.ZKTestCase;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class FileKeyStoreLoaderBuilderProviderTest extends ZKTestCase {
+    @Test
+    public void testGetBuilderForJKSFileType() {
+        FileKeyStoreLoader.Builder<?> builder =
+                FileKeyStoreLoaderBuilderProvider.getBuilderForKeyStoreFileType(
+                        KeyStoreFileType.JKS);
+        Assert.assertTrue(builder instanceof JKSFileLoader.Builder);
+    }
+
+    @Test
+    public void testGetBuilderForPEMFileType() {
+        FileKeyStoreLoader.Builder<?> builder =
+                FileKeyStoreLoaderBuilderProvider.getBuilderForKeyStoreFileType(
+                        KeyStoreFileType.PEM);
+        Assert.assertTrue(builder instanceof PEMFileLoader.Builder);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testGetBuilderForNullFileType() {
+        FileKeyStoreLoaderBuilderProvider.getBuilderForKeyStoreFileType(null);
+    }
+}

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/test/java/org/apache/zookeeper/common/JKSFileLoaderTest.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/JKSFileLoaderTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/JKSFileLoaderTest.java
new file mode 100644
index 0000000..5e916f3
--- /dev/null
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/JKSFileLoaderTest.java
@@ -0,0 +1,165 @@
+/**
+ * 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.IOException;
+import java.security.KeyStore;
+import java.util.Collection;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class JKSFileLoaderTest extends BaseX509ParameterizedTestCase {
+
+    @Parameterized.Parameters
+    public static Collection<Object[]> params() {
+        return BaseX509ParameterizedTestCase.defaultParams();
+    }
+
+    public JKSFileLoaderTest(
+            final X509KeyType caKeyType,
+            final X509KeyType certKeyType,
+            final String keyPassword,
+            final 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 testLoadKeyStore() throws Exception {
+        String path = x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS)
+                .getAbsolutePath();
+        KeyStore ks = new JKSFileLoader.Builder()
+                .setKeyStorePath(path)
+                .setKeyStorePassword(x509TestContext.getKeyStorePassword())
+                .build()
+                .loadKeyStore();
+        Assert.assertEquals(1, ks.size());
+    }
+
+    @Test(expected = Exception.class)
+    public void testLoadKeyStoreWithWrongPassword() throws Exception {
+        String path = x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS)
+                .getAbsolutePath();
+        new JKSFileLoader.Builder()
+                .setKeyStorePath(path)
+                .setKeyStorePassword("wrong password")
+                .build()
+                .loadKeyStore();
+    }
+
+    @Test(expected = IOException.class)
+    public void testLoadKeyStoreWithWrongFilePath() throws Exception {
+        String path = x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS)
+                .getAbsolutePath();
+        new JKSFileLoader.Builder()
+                .setKeyStorePath(path + ".does_not_exist")
+                .setKeyStorePassword(x509TestContext.getKeyStorePassword())
+                .build()
+                .loadKeyStore();
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testLoadKeyStoreWithNullFilePath() throws Exception {
+        new JKSFileLoader.Builder()
+                .setKeyStorePassword(x509TestContext.getKeyStorePassword())
+                .build()
+                .loadKeyStore();
+    }
+
+    @Test(expected = IOException.class)
+    public void testLoadKeyStoreWithWrongFileType() throws Exception {
+        // Trying to load a PEM file with JKS loader should fail
+        String path = x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM)
+                .getAbsolutePath();
+        new JKSFileLoader.Builder()
+                .setKeyStorePath(path)
+                .setKeyStorePassword(x509TestContext.getKeyStorePassword())
+                .build()
+                .loadKeyStore();
+    }
+
+    @Test
+    public void testLoadTrustStore() throws Exception {
+        String path = x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS)
+                .getAbsolutePath();
+        KeyStore ts = new JKSFileLoader.Builder()
+                .setTrustStorePath(path)
+                .setTrustStorePassword(x509TestContext.getTrustStorePassword())
+                .build()
+                .loadTrustStore();
+        Assert.assertEquals(1, ts.size());
+    }
+
+    @Test(expected = Exception.class)
+    public void testLoadTrustStoreWithWrongPassword() throws Exception {
+        String path = x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS)
+                .getAbsolutePath();
+        new JKSFileLoader.Builder()
+                .setTrustStorePath(path)
+                .setTrustStorePassword("wrong password")
+                .build()
+                .loadTrustStore();
+    }
+
+    @Test(expected = IOException.class)
+    public void testLoadTrustStoreWithWrongFilePath() throws Exception {
+        String path = x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS)
+                .getAbsolutePath();
+        new JKSFileLoader.Builder()
+                .setTrustStorePath(path + ".does_not_exist")
+                .setTrustStorePassword(x509TestContext.getTrustStorePassword())
+                .build()
+                .loadTrustStore();
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testLoadTrustStoreWithNullFilePath() throws Exception {
+        new JKSFileLoader.Builder()
+                .setTrustStorePassword(x509TestContext.getTrustStorePassword())
+                .build()
+                .loadTrustStore();
+    }
+
+    @Test(expected = IOException.class)
+    public void testLoadTrustStoreWithWrongFileType() throws Exception {
+        // Trying to load a PEM file with JKS loader should fail
+        String path = x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM)
+                .getAbsolutePath();
+        new JKSFileLoader.Builder()
+                .setTrustStorePath(path)
+                .setTrustStorePassword(x509TestContext.getTrustStorePassword())
+                .build()
+                .loadTrustStore();
+    }
+}

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/test/java/org/apache/zookeeper/common/KeyStoreFileTypeTest.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/KeyStoreFileTypeTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/KeyStoreFileTypeTest.java
new file mode 100644
index 0000000..53aa0b0
--- /dev/null
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/KeyStoreFileTypeTest.java
@@ -0,0 +1,90 @@
+/**
+ * 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.apache.zookeeper.ZKTestCase;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class KeyStoreFileTypeTest extends ZKTestCase {
+    @Test
+    public void testGetPropertyValue() {
+        Assert.assertEquals("PEM", KeyStoreFileType.PEM.getPropertyValue());
+        Assert.assertEquals("JKS", KeyStoreFileType.JKS.getPropertyValue());
+    }
+
+    @Test
+    public void testFromPropertyValue() {
+        Assert.assertEquals(KeyStoreFileType.PEM, KeyStoreFileType.fromPropertyValue("PEM"));
+        Assert.assertEquals(KeyStoreFileType.JKS, KeyStoreFileType.fromPropertyValue("JKS"));
+        Assert.assertNull(KeyStoreFileType.fromPropertyValue(""));
+        Assert.assertNull(KeyStoreFileType.fromPropertyValue(null));
+    }
+
+    @Test
+    public void testFromPropertyValueIgnoresCase() {
+        Assert.assertEquals(KeyStoreFileType.PEM, KeyStoreFileType.fromPropertyValue("pem"));
+        Assert.assertEquals(KeyStoreFileType.JKS, KeyStoreFileType.fromPropertyValue("jks"));
+        Assert.assertNull(KeyStoreFileType.fromPropertyValue(""));
+        Assert.assertNull(KeyStoreFileType.fromPropertyValue(null));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testFromPropertyValueThrowsOnBadPropertyValue() {
+        KeyStoreFileType.fromPropertyValue("foobar");
+    }
+
+    @Test
+    public void testFromFilename() {
+        Assert.assertEquals(KeyStoreFileType.JKS,
+                KeyStoreFileType.fromFilename("mykey.jks"));
+        Assert.assertEquals(KeyStoreFileType.JKS,
+                KeyStoreFileType.fromFilename("/path/to/key/dir/mykey.jks"));
+        Assert.assertEquals(KeyStoreFileType.PEM,
+                KeyStoreFileType.fromFilename("mykey.pem"));
+        Assert.assertEquals(KeyStoreFileType.PEM,
+                KeyStoreFileType.fromFilename("/path/to/key/dir/mykey.pem"));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testFromFilenameThrowsOnBadFileExtension() {
+        KeyStoreFileType.fromFilename("prod.key");
+    }
+
+    @Test
+    public void testFromPropertyValueOrFileName() {
+        // Property value takes precedence if provided
+        Assert.assertEquals(KeyStoreFileType.JKS,
+                KeyStoreFileType.fromPropertyValueOrFileName(
+                        "JKS", "prod.key"));
+        // Falls back to filename detection if no property value
+        Assert.assertEquals(KeyStoreFileType.JKS,
+                KeyStoreFileType.fromPropertyValueOrFileName("", "prod.jks"));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testFromPropertyValueOrFileNameThrowsOnBadPropertyValue() {
+        KeyStoreFileType.fromPropertyValueOrFileName("foobar", "prod.jks");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testFromPropertyValueOrFileNameThrowsOnBadFileExtension() {
+        KeyStoreFileType.fromPropertyValueOrFileName("", "prod.key");
+    }
+}

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/test/java/org/apache/zookeeper/common/PEMFileLoaderTest.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/PEMFileLoaderTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/PEMFileLoaderTest.java
new file mode 100644
index 0000000..e78c750
--- /dev/null
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/PEMFileLoaderTest.java
@@ -0,0 +1,156 @@
+/**
+ * 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.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.util.Collection;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class PEMFileLoaderTest extends BaseX509ParameterizedTestCase {
+
+    @Parameterized.Parameters
+    public static Collection<Object[]> params() {
+        return BaseX509ParameterizedTestCase.defaultParams();
+    }
+
+    public PEMFileLoaderTest(
+            final X509KeyType caKeyType,
+            final X509KeyType certKeyType,
+            final String keyPassword,
+            final 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 testLoadKeyStore() throws Exception {
+        String path = x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM)
+                .getAbsolutePath();
+        KeyStore ks = new PEMFileLoader.Builder()
+                .setKeyStorePath(path)
+                .setKeyStorePassword(x509TestContext.getKeyStorePassword())
+                .build()
+                .loadKeyStore();
+        Assert.assertEquals(1, ks.size());
+    }
+
+    @Test(expected = Exception.class)
+    public void testLoadKeyStoreWithWrongPassword() throws Exception {
+        String path = x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM)
+                .getAbsolutePath();
+        new PEMFileLoader.Builder()
+                .setKeyStorePath(path)
+                .setKeyStorePassword("wrong password")
+                .build()
+                .loadKeyStore();
+    }
+
+    @Test(expected = IOException.class)
+    public void testLoadKeyStoreWithWrongFilePath() throws Exception {
+        String path = x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM)
+                .getAbsolutePath();
+        new PEMFileLoader.Builder()
+                .setKeyStorePath(path + ".does_not_exist")
+                .setKeyStorePassword(x509TestContext.getKeyStorePassword())
+                .build()
+                .loadKeyStore();
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testLoadKeyStoreWithNullFilePath() throws Exception {
+        new PEMFileLoader.Builder()
+                .setKeyStorePassword(x509TestContext.getKeyStorePassword())
+                .build()
+                .loadKeyStore();
+    }
+
+    @Test(expected = KeyStoreException.class)
+    public void testLoadKeyStoreWithWrongFileType() throws Exception {
+        // Trying to load a JKS file with PEM loader should fail
+        String path = x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS)
+                .getAbsolutePath();
+        new PEMFileLoader.Builder()
+                .setKeyStorePath(path)
+                .setKeyStorePassword(x509TestContext.getKeyStorePassword())
+                .build()
+                .loadKeyStore();
+    }
+
+    @Test
+    public void testLoadTrustStore() throws Exception {
+        String path = x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM)
+                .getAbsolutePath();
+        KeyStore ts = new PEMFileLoader.Builder()
+                .setTrustStorePath(path)
+                .setTrustStorePassword(x509TestContext.getTrustStorePassword())
+                .build()
+                .loadTrustStore();
+        Assert.assertEquals(1, ts.size());
+    }
+
+    @Test(expected = IOException.class)
+    public void testLoadTrustStoreWithWrongFilePath() throws Exception {
+        String path = x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM)
+                .getAbsolutePath();
+        new PEMFileLoader.Builder()
+                .setTrustStorePath(path + ".does_not_exist")
+                .setTrustStorePassword(x509TestContext.getTrustStorePassword())
+                .build()
+                .loadTrustStore();
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testLoadTrustStoreWithNullFilePath() throws Exception {
+        new PEMFileLoader.Builder()
+                .setTrustStorePassword(x509TestContext.getTrustStorePassword())
+                .build()
+                .loadTrustStore();
+    }
+
+    @Test
+    public void testLoadTrustStoreWithWrongFileType() throws Exception {
+        // Trying to load a JKS file with PEM loader should fail
+        String path = x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS)
+                .getAbsolutePath();
+        KeyStore ts = new PEMFileLoader.Builder()
+                .setTrustStorePath(path)
+                .setTrustStorePassword(x509TestContext.getTrustStorePassword())
+                .build()
+                .loadTrustStore();
+        Assert.assertEquals(0, ts.size());
+    }
+}

http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509KeyType.java
----------------------------------------------------------------------
diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509KeyType.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509KeyType.java
new file mode 100644
index 0000000..e2cdc81
--- /dev/null
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509KeyType.java
@@ -0,0 +1,26 @@
+/**
+ * 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;
+
+/**
+ * Represents a type of key pair used for X509 certs in tests. The two options are RSA or EC (elliptic curve).
+ */
+public enum X509KeyType {
+    RSA, EC;
+}