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 2019/07/15 12:49:41 UTC

[zookeeper] branch branch-3.5 updated: ZOOKEEPER-3443: Add support of PKCS12 trust/key stores

This is an automated email from the ASF dual-hosted git repository.

andor pushed a commit to branch branch-3.5
in repository https://gitbox.apache.org/repos/asf/zookeeper.git


The following commit(s) were added to refs/heads/branch-3.5 by this push:
     new 427899c  ZOOKEEPER-3443: Add support of PKCS12 trust/key stores
427899c is described below

commit 427899ce4a14fd97673a36c0a3ac2282da656589
Author: Ivan Yurchenko <iv...@aiven.io>
AuthorDate: Mon Jul 15 14:46:48 2019 +0200

    ZOOKEEPER-3443: Add support of PKCS12 trust/key stores
    
    This commit adds support of PKCS12 trust store and key store type.
    The existing mechanism for trust/key store types that support JKS and
    PEM were extended with PKCS12.
    
    The implementations of JKSFileLoader and PKCS12FileLoader were almost
    identical so most of it were abstracted away in
    StandardTypeFileKeyStoreLoader.
    
    Author: Ivan Yurchenko <iv...@aiven.io>
    
    Reviewers: eolivelli@apache.org, andor@apache.org
    
    Closes #1003 from ivanyu/ZOOKEEPER-3443
    
    (cherry picked from commit 1c83846615701e88749690f06993a6e77452b83c)
    Signed-off-by: Andor Molnar <an...@apache.org>
---
 .../src/main/resources/markdown/zookeeperAdmin.md  |   4 +-
 .../common/FileKeyStoreLoaderBuilderProvider.java  |   2 +
 .../org/apache/zookeeper/common/JKSFileLoader.java |  56 +------
 .../apache/zookeeper/common/KeyStoreFileType.java  |  12 +-
 .../apache/zookeeper/common/PKCS12FileLoader.java  |  46 ++++++
 ...er.java => StandardTypeFileKeyStoreLoader.java} |  59 ++------
 .../FileKeyStoreLoaderBuilderProviderTest.java     |   8 +
 .../zookeeper/common/KeyStoreFileTypeTest.java     |  13 ++
 .../zookeeper/common/PKCS12FileLoaderTest.java     | 165 +++++++++++++++++++++
 .../apache/zookeeper/common/X509TestContext.java   |  88 +++++++----
 .../apache/zookeeper/common/X509TestHelpers.java   |  51 +++++++
 .../org/apache/zookeeper/common/X509UtilTest.java  |  94 ++++++++++++
 12 files changed, 467 insertions(+), 131 deletions(-)

diff --git a/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md b/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md
index 0f117f2..f96faa6 100644
--- a/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md
+++ b/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md
@@ -934,7 +934,7 @@ encryption/authentication/authorization performed by the service.
 * *ssl.keyStore.type* and *ssl.quorum.keyStore.type* :
     (Java system properties: **zookeeper.ssl.keyStore.type** and **zookeeper.ssl.quorum.keyStore.type**)
     **New in 3.5.5:**
-    Specifies the file format of client and quorum keystores. Values: JKS, PEM or null (detect by filename).    
+    Specifies the file format of client and quorum keystores. Values: JKS, PEM, PKCS12 or null (detect by filename).    
     Default: null     
     
 * *ssl.trustStore.location* and *ssl.trustStore.password* and *ssl.quorum.trustStore.location* and *ssl.quorum.trustStore.password* :
@@ -947,7 +947,7 @@ encryption/authentication/authorization performed by the service.
 * *ssl.trustStore.type* and *ssl.quorum.trustStore.type* :
     (Java system properties: **zookeeper.ssl.trustStore.type** and **zookeeper.ssl.quorum.trustStore.type**)
     **New in 3.5.5:**
-    Specifies the file format of client and quorum trustStores. Values: JKS, PEM or null (detect by filename).    
+    Specifies the file format of client and quorum trustStores. Values: JKS, PEM, PKCS12 or null (detect by filename).    
     Default: null     
 
 * *ssl.protocol* and *ssl.quorum.protocol* :
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
index bcbefe2..0a03d62 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/common/FileKeyStoreLoaderBuilderProvider.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/FileKeyStoreLoaderBuilderProvider.java
@@ -36,6 +36,8 @@ public class FileKeyStoreLoaderBuilderProvider {
                 return new JKSFileLoader.Builder();
             case PEM:
                 return new PEMFileLoader.Builder();
+            case PKCS12:
+                return new PKCS12FileLoader.Builder();
             default:
                 throw new AssertionError(
                         "Unexpected StoreFileType: " + type.name());
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
index f391c7b..cf11736 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/common/JKSFileLoader.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/JKSFileLoader.java
@@ -18,25 +18,13 @@
 
 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;
+import java.security.KeyStoreException;
 
 /**
  * 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";
-
+class JKSFileLoader extends StandardTypeFileKeyStoreLoader {
     private JKSFileLoader(String keyStorePath,
                           String trustStorePath,
                           String keyStorePassword,
@@ -45,44 +33,8 @@ class JKSFileLoader extends FileKeyStoreLoader {
     }
 
     @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);
-        }
+    protected KeyStore keyStoreInstance() throws KeyStoreException {
+        return KeyStore.getInstance("JKS");
     }
 
     static class Builder extends FileKeyStoreLoader.Builder<JKSFileLoader> {
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
index f006468..cc49013 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/common/KeyStoreFileType.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/KeyStoreFileType.java
@@ -20,11 +20,10 @@ 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.
+ * Currently, JKS (Java keystore), PEM, and PKCS12 types are supported.
  */
 public enum KeyStoreFileType {
-    // TODO: consider adding support for PKCS12
-    JKS(".jks"), PEM(".pem");
+    JKS(".jks"), PEM(".pem"), PKCS12(".p12");
 
     private final String defaultFileExtension;
 
@@ -54,7 +53,7 @@ public enum KeyStoreFileType {
      * @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.
+     *         one of "JKS", "PEM", "PKCS12", or empty/null.
      */
     public static KeyStoreFileType fromPropertyValue(String propertyValue) {
         if (propertyValue == null || propertyValue.length() == 0) {
@@ -67,11 +66,12 @@ public enum KeyStoreFileType {
      * 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>.
+     * If the file name ends with ".p12", returns <code>StoreFileType.PKCS12</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".
+     *         ".jks", ".pem", or "p12".
      */
     public static KeyStoreFileType fromFilename(String filename) {
         int i = filename.lastIndexOf('.');
@@ -99,7 +99,7 @@ public enum KeyStoreFileType {
      *                 <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.
+     *         one of "JKS", "PEM", "PKCS12", or empty/null.
      * @throws IllegalArgumentException if <code>propertyValue</code>is empty
      *         or null and the type could not be determined from the file name.
      */
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/PKCS12FileLoader.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/PKCS12FileLoader.java
new file mode 100644
index 0000000..402ecd8
--- /dev/null
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/PKCS12FileLoader.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 java.security.KeyStore;
+import java.security.KeyStoreException;
+
+/**
+ * Implementation of {@link FileKeyStoreLoader} that loads from PKCS12 files.
+ */
+class PKCS12FileLoader extends StandardTypeFileKeyStoreLoader {
+    private PKCS12FileLoader(String keyStorePath,
+                             String trustStorePath,
+                             String keyStorePassword,
+                             String trustStorePassword) {
+        super(keyStorePath, trustStorePath, keyStorePassword, trustStorePassword);
+    }
+
+    @Override
+    protected KeyStore keyStoreInstance() throws KeyStoreException {
+        return KeyStore.getInstance("PKCS12");
+    }
+
+    static class Builder extends FileKeyStoreLoader.Builder<PKCS12FileLoader> {
+        @Override
+        PKCS12FileLoader build() {
+            return new PKCS12FileLoader(keyStorePath, trustStorePath, keyStorePassword, trustStorePassword);
+        }
+    }
+}
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/JKSFileLoader.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/StandardTypeFileKeyStoreLoader.java
similarity index 51%
copy from zookeeper-server/src/main/java/org/apache/zookeeper/common/JKSFileLoader.java
copy to zookeeper-server/src/main/java/org/apache/zookeeper/common/StandardTypeFileKeyStoreLoader.java
index f391c7b..3a8cb2e 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/common/JKSFileLoader.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/StandardTypeFileKeyStoreLoader.java
@@ -24,71 +24,44 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.security.GeneralSecurityException;
 import java.security.KeyStore;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import java.security.KeyStoreException;
 
 /**
- * Implementation of {@link FileKeyStoreLoader} that loads from JKS files.
+ * Base class for instances of {@link KeyStoreLoader} which load the key/trust
+ * stores from files on a filesystem using standard {@link KeyStore} types like
+ * JKS or PKCS12.
  */
-class JKSFileLoader extends FileKeyStoreLoader {
-    private static final Logger LOG = LoggerFactory.getLogger(JKSFileLoader.class);
-
+abstract class StandardTypeFileKeyStoreLoader extends FileKeyStoreLoader {
     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) {
+    StandardTypeFileKeyStoreLoader(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));
+        try (InputStream inputStream = new FileInputStream(new File(keyStorePath))) {
+            KeyStore ks = keyStoreInstance();
             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));
+        try (InputStream inputStream = new FileInputStream(new File(trustStorePath))) {
+            KeyStore ts = keyStoreInstance();
             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);
-        }
-    }
+    protected abstract KeyStore keyStoreInstance() throws KeyStoreException;
 
-    static class Builder extends FileKeyStoreLoader.Builder<JKSFileLoader> {
-        @Override
-        JKSFileLoader build() {
-            return new JKSFileLoader(keyStorePath, trustStorePath, keyStorePassword, trustStorePassword);
-        }
+    private static char[] passwordStringToCharArray(String password) {
+        return password == null ? EMPTY_CHAR_ARRAY : password.toCharArray();
     }
 }
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
index 59c27b2..612eb0d 100644
--- a/zookeeper-server/src/test/java/org/apache/zookeeper/common/FileKeyStoreLoaderBuilderProviderTest.java
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/FileKeyStoreLoaderBuilderProviderTest.java
@@ -39,6 +39,14 @@ public class FileKeyStoreLoaderBuilderProviderTest extends ZKTestCase {
         Assert.assertTrue(builder instanceof PEMFileLoader.Builder);
     }
 
+    @Test
+    public void testGetBuilderForPKCS12FileType() {
+        FileKeyStoreLoader.Builder<?> builder =
+            FileKeyStoreLoaderBuilderProvider.getBuilderForKeyStoreFileType(
+                KeyStoreFileType.PKCS12);
+        Assert.assertTrue(builder instanceof PKCS12FileLoader.Builder);
+    }
+
     @Test(expected = NullPointerException.class)
     public void testGetBuilderForNullFileType() {
         FileKeyStoreLoaderBuilderProvider.getBuilderForKeyStoreFileType(null);
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
index 53aa0b0..9265af9 100644
--- a/zookeeper-server/src/test/java/org/apache/zookeeper/common/KeyStoreFileTypeTest.java
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/KeyStoreFileTypeTest.java
@@ -27,12 +27,14 @@ public class KeyStoreFileTypeTest extends ZKTestCase {
     public void testGetPropertyValue() {
         Assert.assertEquals("PEM", KeyStoreFileType.PEM.getPropertyValue());
         Assert.assertEquals("JKS", KeyStoreFileType.JKS.getPropertyValue());
+        Assert.assertEquals("PKCS12", KeyStoreFileType.PKCS12.getPropertyValue());
     }
 
     @Test
     public void testFromPropertyValue() {
         Assert.assertEquals(KeyStoreFileType.PEM, KeyStoreFileType.fromPropertyValue("PEM"));
         Assert.assertEquals(KeyStoreFileType.JKS, KeyStoreFileType.fromPropertyValue("JKS"));
+        Assert.assertEquals(KeyStoreFileType.PKCS12, KeyStoreFileType.fromPropertyValue("PKCS12"));
         Assert.assertNull(KeyStoreFileType.fromPropertyValue(""));
         Assert.assertNull(KeyStoreFileType.fromPropertyValue(null));
     }
@@ -41,6 +43,7 @@ public class KeyStoreFileTypeTest extends ZKTestCase {
     public void testFromPropertyValueIgnoresCase() {
         Assert.assertEquals(KeyStoreFileType.PEM, KeyStoreFileType.fromPropertyValue("pem"));
         Assert.assertEquals(KeyStoreFileType.JKS, KeyStoreFileType.fromPropertyValue("jks"));
+        Assert.assertEquals(KeyStoreFileType.PKCS12, KeyStoreFileType.fromPropertyValue("pkcs12"));
         Assert.assertNull(KeyStoreFileType.fromPropertyValue(""));
         Assert.assertNull(KeyStoreFileType.fromPropertyValue(null));
     }
@@ -60,6 +63,10 @@ public class KeyStoreFileTypeTest extends ZKTestCase {
                 KeyStoreFileType.fromFilename("mykey.pem"));
         Assert.assertEquals(KeyStoreFileType.PEM,
                 KeyStoreFileType.fromFilename("/path/to/key/dir/mykey.pem"));
+        Assert.assertEquals(KeyStoreFileType.PKCS12,
+            KeyStoreFileType.fromFilename("mykey.p12"));
+        Assert.assertEquals(KeyStoreFileType.PKCS12,
+            KeyStoreFileType.fromFilename("/path/to/key/dir/mykey.p12"));
     }
 
     @Test(expected = IllegalArgumentException.class)
@@ -73,6 +80,12 @@ public class KeyStoreFileTypeTest extends ZKTestCase {
         Assert.assertEquals(KeyStoreFileType.JKS,
                 KeyStoreFileType.fromPropertyValueOrFileName(
                         "JKS", "prod.key"));
+        Assert.assertEquals(KeyStoreFileType.PEM,
+            KeyStoreFileType.fromPropertyValueOrFileName(
+                "PEM", "prod.key"));
+        Assert.assertEquals(KeyStoreFileType.PKCS12,
+            KeyStoreFileType.fromPropertyValueOrFileName(
+                "PKCS12", "prod.key"));
         // Falls back to filename detection if no property value
         Assert.assertEquals(KeyStoreFileType.JKS,
                 KeyStoreFileType.fromPropertyValueOrFileName("", "prod.jks"));
diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/PKCS12FileLoaderTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/PKCS12FileLoaderTest.java
new file mode 100644
index 0000000..f2b6efa
--- /dev/null
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/PKCS12FileLoaderTest.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 PKCS12FileLoaderTest extends BaseX509ParameterizedTestCase {
+
+    @Parameterized.Parameters
+    public static Collection<Object[]> params() {
+        return BaseX509ParameterizedTestCase.defaultParams();
+    }
+
+    public PKCS12FileLoaderTest(
+        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.PKCS12)
+            .getAbsolutePath();
+        KeyStore ks = new PKCS12FileLoader.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.PKCS12)
+            .getAbsolutePath();
+        new PKCS12FileLoader.Builder()
+            .setKeyStorePath(path)
+            .setKeyStorePassword("wrong password")
+            .build()
+            .loadKeyStore();
+    }
+
+    @Test(expected = IOException.class)
+    public void testLoadKeyStoreWithWrongFilePath() throws Exception {
+        String path = x509TestContext.getKeyStoreFile(KeyStoreFileType.PKCS12)
+            .getAbsolutePath();
+        new PKCS12FileLoader.Builder()
+            .setKeyStorePath(path + ".does_not_exist")
+            .setKeyStorePassword(x509TestContext.getKeyStorePassword())
+            .build()
+            .loadKeyStore();
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testLoadKeyStoreWithNullFilePath() throws Exception {
+        new PKCS12FileLoader.Builder()
+            .setKeyStorePassword(x509TestContext.getKeyStorePassword())
+            .build()
+            .loadKeyStore();
+    }
+
+    @Test(expected = IOException.class)
+    public void testLoadKeyStoreWithWrongFileType() throws Exception {
+        // Trying to load a PEM file with PKCS12 loader should fail
+        String path = x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM)
+            .getAbsolutePath();
+        new PKCS12FileLoader.Builder()
+            .setKeyStorePath(path)
+            .setKeyStorePassword(x509TestContext.getKeyStorePassword())
+            .build()
+            .loadKeyStore();
+    }
+
+    @Test
+    public void testLoadTrustStore() throws Exception {
+        String path = x509TestContext.getTrustStoreFile(KeyStoreFileType.PKCS12)
+            .getAbsolutePath();
+        KeyStore ts = new PKCS12FileLoader.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.PKCS12)
+            .getAbsolutePath();
+        new PKCS12FileLoader.Builder()
+            .setTrustStorePath(path)
+            .setTrustStorePassword("wrong password")
+            .build()
+            .loadTrustStore();
+    }
+
+    @Test(expected = IOException.class)
+    public void testLoadTrustStoreWithWrongFilePath() throws Exception {
+        String path = x509TestContext.getTrustStoreFile(KeyStoreFileType.PKCS12)
+            .getAbsolutePath();
+        new PKCS12FileLoader.Builder()
+            .setTrustStorePath(path + ".does_not_exist")
+            .setTrustStorePassword(x509TestContext.getTrustStorePassword())
+            .build()
+            .loadTrustStore();
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testLoadTrustStoreWithNullFilePath() throws Exception {
+        new PKCS12FileLoader.Builder()
+            .setTrustStorePassword(x509TestContext.getTrustStorePassword())
+            .build()
+            .loadTrustStore();
+    }
+
+    @Test(expected = IOException.class)
+    public void testLoadTrustStoreWithWrongFileType() throws Exception {
+        // Trying to load a PEM file with PKCS12 loader should fail
+        String path = x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM)
+            .getAbsolutePath();
+        new PKCS12FileLoader.Builder()
+            .setTrustStorePath(path)
+            .setTrustStorePassword(x509TestContext.getTrustStorePassword())
+            .build()
+            .loadTrustStore();
+    }
+}
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
index 5a86bb4..3a899f4 100644
--- a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java
@@ -53,6 +53,7 @@ public class X509TestContext {
     private final String trustStorePassword;
     private File trustStoreJksFile;
     private File trustStorePemFile;
+    private File trustStorePkcs12File;
 
     private final X509KeyType keyStoreKeyType;
     private final KeyPair keyStoreKeyPair;
@@ -61,6 +62,7 @@ public class X509TestContext {
     private final String keyStorePassword;
     private File keyStoreJksFile;
     private File keyStorePemFile;
+    private File keyStorePkcs12File;
 
     private final Boolean hostnameVerification;
 
@@ -116,7 +118,8 @@ public class X509TestContext {
                 nameBuilder.build(),
                 keyStoreKeyPair.getPublic(),
                 keyStoreCertExpirationMillis);
-        trustStorePemFile = trustStoreJksFile = keyStorePemFile = keyStoreJksFile = null;
+        trustStorePkcs12File = trustStorePemFile = trustStoreJksFile = null;
+        keyStorePkcs12File = keyStorePemFile = keyStoreJksFile = null;
 
         this.hostnameVerification = hostnameVerification;
     }
@@ -171,6 +174,8 @@ public class X509TestContext {
                 return getTrustStoreJksFile();
             case PEM:
                 return getTrustStorePemFile();
+            case PKCS12:
+                return getTrustStorePkcs12File();
             default:
                 throw new IllegalArgumentException("Invalid trust store type: " + storeFileType + ", must be one of: " +
                         Arrays.toString(KeyStoreFileType.values()));
@@ -179,22 +184,17 @@ public class X509TestContext {
 
     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;
+            File trustStoreJksFile = File.createTempFile(
+                TRUST_STORE_PREFIX, KeyStoreFileType.JKS.getDefaultFileExtension(), tempDir);
+            trustStoreJksFile.deleteOnExit();
+            try (final FileOutputStream trustStoreOutputStream = new FileOutputStream(trustStoreJksFile)) {
+                byte[] bytes = X509TestHelpers.certToJavaTrustStoreBytes(trustStoreCertificate, trustStorePassword);
+                trustStoreOutputStream.write(bytes);
+                trustStoreOutputStream.flush();
             } catch (GeneralSecurityException e) {
                 throw new IOException(e);
             }
+            this.trustStoreJksFile = trustStoreJksFile;
         }
         return trustStoreJksFile;
     }
@@ -214,6 +214,23 @@ public class X509TestContext {
         return trustStorePemFile;
     }
 
+    private File getTrustStorePkcs12File() throws IOException {
+        if (trustStorePkcs12File == null) {
+            File trustStorePkcs12File = File.createTempFile(
+                TRUST_STORE_PREFIX, KeyStoreFileType.PKCS12.getDefaultFileExtension(), tempDir);
+            trustStorePkcs12File.deleteOnExit();
+            try (final FileOutputStream trustStoreOutputStream = new FileOutputStream(trustStorePkcs12File)) {
+                byte[] bytes = X509TestHelpers.certToPKCS12TrustStoreBytes(trustStoreCertificate, trustStorePassword);
+                trustStoreOutputStream.write(bytes);
+                trustStoreOutputStream.flush();
+            } catch (GeneralSecurityException e) {
+                throw new IOException(e);
+            }
+            this.trustStorePkcs12File = trustStorePkcs12File;
+        }
+        return trustStorePkcs12File;
+    }
+
     public X509KeyType getKeyStoreKeyType() {
         return keyStoreKeyType;
     }
@@ -251,6 +268,8 @@ public class X509TestContext {
                 return getKeyStoreJksFile();
             case PEM:
                 return getKeyStorePemFile();
+            case PKCS12:
+                return getKeyStorePkcs12File();
             default:
                 throw new IllegalArgumentException("Invalid key store type: " + storeFileType + ", must be one of: " +
                         Arrays.toString(KeyStoreFileType.values()));
@@ -259,23 +278,18 @@ public class X509TestContext {
 
     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;
+            File keyStoreJksFile = File.createTempFile(
+                KEY_STORE_PREFIX, KeyStoreFileType.JKS.getDefaultFileExtension(), tempDir);
+            keyStoreJksFile.deleteOnExit();
+            try (final FileOutputStream keyStoreOutputStream = new FileOutputStream(keyStoreJksFile)) {
+                byte[] bytes = X509TestHelpers.certAndPrivateKeyToJavaKeyStoreBytes(
+                    keyStoreCertificate, keyStoreKeyPair.getPrivate(), keyStorePassword);
+                keyStoreOutputStream.write(bytes);
+                keyStoreOutputStream.flush();
             } catch (GeneralSecurityException e) {
                 throw new IOException(e);
             }
+            this.keyStoreJksFile = keyStoreJksFile;
         }
         return keyStoreJksFile;
     }
@@ -300,6 +314,24 @@ public class X509TestContext {
         return keyStorePemFile;
     }
 
+    private File getKeyStorePkcs12File() throws IOException {
+        if (keyStorePkcs12File == null) {
+            File keyStorePkcs12File = File.createTempFile(
+                KEY_STORE_PREFIX, KeyStoreFileType.PKCS12.getDefaultFileExtension(), tempDir);
+            keyStorePkcs12File.deleteOnExit();
+            try (final FileOutputStream keyStoreOutputStream = new FileOutputStream(keyStorePkcs12File)) {
+                byte[] bytes = X509TestHelpers.certAndPrivateKeyToPKCS12Bytes(
+                    keyStoreCertificate, keyStoreKeyPair.getPrivate(), keyStorePassword);
+                keyStoreOutputStream.write(bytes);
+                keyStoreOutputStream.flush();
+            } catch (GeneralSecurityException e) {
+                throw new IOException(e);
+            }
+            this.keyStorePkcs12File = keyStorePkcs12File;
+        }
+        return keyStorePkcs12File;
+    }
+
     /**
      * 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:
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
index 2ca250d..9e8dcf6 100644
--- a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java
@@ -348,6 +348,30 @@ public class X509TestHelpers {
             X509Certificate cert,
             String keyPassword) throws IOException, GeneralSecurityException {
         KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
+        return certToTrustStoreBytes(cert, keyPassword, trustStore);
+    }
+
+    /**
+     * Encodes the given X509Certificate as a PKCS12 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 PKCS12 trust store.
+     * @throws IOException
+     * @throws GeneralSecurityException
+     */
+    public static byte[] certToPKCS12TrustStoreBytes(
+            X509Certificate cert,
+            String keyPassword) throws IOException, GeneralSecurityException {
+        KeyStore trustStore = KeyStore.getInstance("PKCS12");
+        return certToTrustStoreBytes(cert, keyPassword, trustStore);
+    }
+
+    private static byte[] certToTrustStoreBytes(X509Certificate cert,
+                                                String keyPassword,
+                                                KeyStore trustStore) throws IOException, GeneralSecurityException {
         char[] keyPasswordChars = keyPassword == null ? new char[0] : keyPassword.toCharArray();
         trustStore.load(null, keyPasswordChars);
         trustStore.setCertificateEntry(cert.getSubjectDN().toString(), cert);
@@ -375,6 +399,33 @@ public class X509TestHelpers {
             PrivateKey privateKey,
             String keyPassword) throws IOException, GeneralSecurityException {
         KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+        return certAndPrivateKeyToPKCS12Bytes(cert, privateKey, keyPassword, keyStore);
+    }
+
+    /**
+     * Encodes the given X509Certificate and private key as a PKCS12 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 PKCS12 key store.
+     * @throws IOException
+     * @throws GeneralSecurityException
+     */
+    public static byte[] certAndPrivateKeyToPKCS12Bytes(
+            X509Certificate cert,
+            PrivateKey privateKey,
+            String keyPassword) throws IOException, GeneralSecurityException {
+        KeyStore keyStore = KeyStore.getInstance("PKCS12");
+        return certAndPrivateKeyToPKCS12Bytes(cert, privateKey, keyPassword, keyStore);
+    }
+
+    private static byte[] certAndPrivateKeyToPKCS12Bytes(
+            X509Certificate cert,
+            PrivateKey privateKey,
+            String keyPassword,
+            KeyStore keyStore) throws IOException, GeneralSecurityException {
         char[] keyPasswordChars = keyPassword == null ? new char[0] : keyPassword.toCharArray();
         keyStore.load(null, keyPasswordChars);
         keyStore.setKeyEntry(
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 0a09795..004b8df 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
@@ -375,6 +375,100 @@ public class X509UtilTest extends BaseX509ParameterizedTestCase {
     }
 
     @Test
+    public void testLoadPKCS12KeyStore() throws Exception {
+        // Make sure we can instantiate a key manager from the PKCS12 file on disk
+        X509KeyManager km = X509Util.createKeyManager(
+            x509TestContext.getKeyStoreFile(KeyStoreFileType.PKCS12).getAbsolutePath(),
+            x509TestContext.getKeyStorePassword(),
+            KeyStoreFileType.PKCS12.getPropertyValue());
+    }
+
+    @Test
+    public void testLoadPKCS12KeyStoreNullPassword() 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.PKCS12).getAbsolutePath(),
+            null,
+            KeyStoreFileType.PKCS12.getPropertyValue());
+    }
+
+    @Test
+    public void testLoadPKCS12KeyStoreAutodetectStoreFileType() throws Exception {
+        // Make sure we can instantiate a key manager from the PKCS12 file on disk
+        X509KeyManager km = X509Util.createKeyManager(
+            x509TestContext.getKeyStoreFile(KeyStoreFileType.PKCS12).getAbsolutePath(),
+            x509TestContext.getKeyStorePassword(),
+            null /* null StoreFileType means 'autodetect from file extension' */);
+    }
+
+    @Test(expected = X509Exception.KeyManagerException.class)
+    public void testLoadPKCS12KeyStoreWithWrongPassword() throws Exception {
+        // Attempting to load with the wrong key password should fail
+        X509KeyManager km = X509Util.createKeyManager(
+            x509TestContext.getKeyStoreFile(KeyStoreFileType.PKCS12).getAbsolutePath(),
+            "wrong password",
+            KeyStoreFileType.PKCS12.getPropertyValue());
+    }
+
+    @Test
+    public void testLoadPKCS12TrustStore() throws Exception {
+        // Make sure we can instantiate a trust manager from the PKCS12 file on disk
+        X509TrustManager tm = X509Util.createTrustManager(
+            x509TestContext.getTrustStoreFile(KeyStoreFileType.PKCS12).getAbsolutePath(),
+            x509TestContext.getTrustStorePassword(),
+            KeyStoreFileType.PKCS12.getPropertyValue(),
+            true,
+            true,
+            true,
+            true);
+    }
+
+    @Test
+    public void testLoadPKCS12TrustStoreNullPassword() 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.PKCS12).getAbsolutePath(),
+            null,
+            KeyStoreFileType.PKCS12.getPropertyValue(),
+            false,
+            false,
+            true,
+            true);
+    }
+
+    @Test
+    public void testLoadPKCS12TrustStoreAutodetectStoreFileType() throws Exception {
+        // Make sure we can instantiate a trust manager from the PKCS12 file on disk
+        X509TrustManager tm = X509Util.createTrustManager(
+            x509TestContext.getTrustStoreFile(KeyStoreFileType.PKCS12).getAbsolutePath(),
+            x509TestContext.getTrustStorePassword(),
+            null,  // null StoreFileType means 'autodetect from file extension'
+            true,
+            true,
+            true,
+            true);
+    }
+
+    @Test(expected = X509Exception.TrustManagerException.class)
+    public void testLoadPKCS12TrustStoreWithWrongPassword() throws Exception {
+        // Attempting to load with the wrong key password should fail
+        X509TrustManager tm = X509Util.createTrustManager(
+            x509TestContext.getTrustStoreFile(KeyStoreFileType.PKCS12).getAbsolutePath(),
+            "wrong password",
+            KeyStoreFileType.PKCS12.getPropertyValue(),
+            true,
+            true,
+            true,
+            true);
+    }
+
+    @Test
     public void testGetSslHandshakeDetectionTimeoutMillisProperty() {
         Assert.assertEquals(
                 X509Util.DEFAULT_HANDSHAKE_DETECTION_TIMEOUT_MILLIS,