You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hbase.apache.org by bb...@apache.org on 2022/09/06 19:43:47 UTC

[hbase] branch master updated: HBASE-27346 Autodetect key/truststore file type from file extension (#4757)

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

bbeaudreault pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hbase.git


The following commit(s) were added to refs/heads/master by this push:
     new d2cc840edf4 HBASE-27346 Autodetect key/truststore file type from file extension (#4757)
d2cc840edf4 is described below

commit d2cc840edf4888d090e6777a621fd976e7abccd7
Author: Andor Molnár <an...@cloudera.com>
AuthorDate: Tue Sep 6 21:43:41 2022 +0200

    HBASE-27346 Autodetect key/truststore file type from file extension (#4757)
    
    Signed-off-by: Duo Zhang <zh...@apache.org>
    Signed-off-by: Bryan Beaudreault <bb...@apache.org>
---
 .../hbase/io/crypto/tls/BCFKSFileLoader.java       |  42 ++++
 .../hbase/io/crypto/tls/FileKeyStoreLoader.java    |  80 ++++++++
 .../tls/FileKeyStoreLoaderBuilderProvider.java     |  54 +++++
 .../hadoop/hbase/io/crypto/tls/JKSFileLoader.java  |  41 ++++
 .../hadoop/hbase/io/crypto/tls/KeyStoreLoader.java |  53 +++++
 .../hadoop/hbase/io/crypto/tls/PEMFileLoader.java  |  56 ++++++
 .../hbase/io/crypto/tls/PKCS12FileLoader.java      |  42 ++++
 .../hadoop/hbase/io/crypto/tls/PemReader.java      | 218 +++++++++++++++++++++
 .../crypto/tls/StandardTypeFileKeyStoreLoader.java |  79 ++++++++
 .../hadoop/hbase/io/crypto/tls/X509Util.java       |  29 +--
 .../crypto/tls/AbstractTestX509Parameterized.java  | 128 ++++++++++++
 .../hbase/io/crypto/tls/TestBCFKSFileLoader.java   | 118 +++++++++++
 .../tls/TestFileKeyStoreLoaderBuilderProvider.java |  66 +++++++
 .../hbase/io/crypto/tls/TestJKSFileLoader.java     | 117 +++++++++++
 .../hbase/io/crypto/tls/TestKeyStoreFileType.java  | 120 ++++++++++++
 .../hbase/io/crypto/tls/TestPEMFileLoader.java     | 112 +++++++++++
 .../hbase/io/crypto/tls/TestPKCS12FileLoader.java  | 117 +++++++++++
 .../hadoop/hbase/io/crypto/tls/TestX509Util.java   | 196 +++++++++---------
 .../hbase/io/crypto/tls/X509TestContext.java       |  52 ++++-
 .../io/crypto/tls/X509TestContextProvider.java     |  17 ++
 .../hbase/io/crypto/tls/X509TestHelpers.java       |  36 +++-
 21 files changed, 1647 insertions(+), 126 deletions(-)

diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/BCFKSFileLoader.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/BCFKSFileLoader.java
new file mode 100644
index 00000000000..cefa4135c90
--- /dev/null
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/BCFKSFileLoader.java
@@ -0,0 +1,42 @@
+/*
+ * 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.hadoop.hbase.io.crypto.tls;
+
+/**
+ * Implementation of {@link FileKeyStoreLoader} that loads from BCKFS files.
+ * <p/>
+ * This file has been copied from the Apache ZooKeeper project.
+ * @see <a href=
+ *      "https://github.com/apache/zookeeper/blob/c74658d398cdc1d207aa296cb6e20de00faec03e/zookeeper-server/src/main/java/org/apache/zookeeper/common/BCFKSFileLoader.java">Base
+ *      revision</a>
+ */
+final class BCFKSFileLoader extends StandardTypeFileKeyStoreLoader {
+  private BCFKSFileLoader(String keyStorePath, String trustStorePath, char[] keyStorePassword,
+    char[] trustStorePassword) {
+    super(keyStorePath, trustStorePath, keyStorePassword, trustStorePassword,
+      SupportedStandardKeyFormat.BCFKS);
+  }
+
+  static class Builder extends FileKeyStoreLoader.Builder<BCFKSFileLoader> {
+    @Override
+    BCFKSFileLoader build() {
+      return new BCFKSFileLoader(keyStorePath, trustStorePath, keyStorePassword,
+        trustStorePassword);
+    }
+  }
+}
diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/FileKeyStoreLoader.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/FileKeyStoreLoader.java
new file mode 100644
index 00000000000..3a1740b4faf
--- /dev/null
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/FileKeyStoreLoader.java
@@ -0,0 +1,80 @@
+/*
+ * 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.hadoop.hbase.io.crypto.tls;
+
+import java.util.Objects;
+
+/**
+ * Base class for instances of {@link KeyStoreLoader} which load the key/trust stores from files on
+ * a filesystem.
+ * <p/>
+ * This file has been copied from the Apache ZooKeeper project.
+ * @see <a href=
+ *      "https://github.com/apache/zookeeper/blob/c74658d398cdc1d207aa296cb6e20de00faec03e/zookeeper-server/src/main/java/org/apache/zookeeper/common/FileKeyStoreLoader.java">Base
+ *      revision</a>
+ */
+abstract class FileKeyStoreLoader implements KeyStoreLoader {
+  final String keyStorePath;
+  final String trustStorePath;
+  final char[] keyStorePassword;
+  final char[] trustStorePassword;
+
+  FileKeyStoreLoader(String keyStorePath, String trustStorePath, char[] keyStorePassword,
+    char[] 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;
+    char[] keyStorePassword;
+    char[] 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(char[] keyStorePassword) {
+      this.keyStorePassword = Objects.requireNonNull(keyStorePassword);
+      return this;
+    }
+
+    Builder<T> setTrustStorePassword(char[] trustStorePassword) {
+      this.trustStorePassword = Objects.requireNonNull(trustStorePassword);
+      return this;
+    }
+
+    abstract T build();
+  }
+}
diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/FileKeyStoreLoaderBuilderProvider.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/FileKeyStoreLoaderBuilderProvider.java
new file mode 100644
index 00000000000..432c8a06d11
--- /dev/null
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/FileKeyStoreLoaderBuilderProvider.java
@@ -0,0 +1,54 @@
+/*
+ * 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.hadoop.hbase.io.crypto.tls;
+
+import java.util.Objects;
+
+/**
+ * This file has been copied from the Apache ZooKeeper project.
+ * @see <a href=
+ *      "https://github.com/apache/zookeeper/blob/c74658d398cdc1d207aa296cb6e20de00faec03e/zookeeper-server/src/main/java/org/apache/zookeeper/common/FileKeyStoreLoaderBuilderProvider.java">Base
+ *      revision</a>
+ */
+final 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();
+      case PKCS12:
+        return new PKCS12FileLoader.Builder();
+      case BCFKS:
+        return new BCFKSFileLoader.Builder();
+      default:
+        throw new AssertionError("Unexpected StoreFileType: " + type.name());
+    }
+  }
+
+  private FileKeyStoreLoaderBuilderProvider() {
+    // disabled
+  }
+}
diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/JKSFileLoader.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/JKSFileLoader.java
new file mode 100644
index 00000000000..36e9643cbfe
--- /dev/null
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/JKSFileLoader.java
@@ -0,0 +1,41 @@
+/*
+ * 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.hadoop.hbase.io.crypto.tls;
+
+/**
+ * Implementation of {@link FileKeyStoreLoader} that loads from JKS files.
+ * <p/>
+ * This file has been copied from the Apache ZooKeeper project.
+ * @see <a href=
+ *      "https://github.com/apache/zookeeper/blob/c74658d398cdc1d207aa296cb6e20de00faec03e/zookeeper-server/src/main/java/org/apache/zookeeper/common/JKSFileLoader.java">Base
+ *      revision</a>
+ */
+final class JKSFileLoader extends StandardTypeFileKeyStoreLoader {
+  private JKSFileLoader(String keyStorePath, String trustStorePath, char[] keyStorePassword,
+    char[] trustStorePassword) {
+    super(keyStorePath, trustStorePath, keyStorePassword, trustStorePassword,
+      SupportedStandardKeyFormat.JKS);
+  }
+
+  static class Builder extends FileKeyStoreLoader.Builder<JKSFileLoader> {
+    @Override
+    JKSFileLoader build() {
+      return new JKSFileLoader(keyStorePath, trustStorePath, keyStorePassword, trustStorePassword);
+    }
+  }
+}
diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/KeyStoreLoader.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/KeyStoreLoader.java
new file mode 100644
index 00000000000..928b3b9d046
--- /dev/null
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/KeyStoreLoader.java
@@ -0,0 +1,53 @@
+/*
+ * 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.hadoop.hbase.io.crypto.tls;
+
+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.
+ * <p/>
+ * This file has been copied from the Apache ZooKeeper project.
+ * @see <a href=
+ *      "https://github.com/apache/zookeeper/blob/c74658d398cdc1d207aa296cb6e20de00faec03e/zookeeper-server/src/main/java/org/apache/zookeeper/common/KeyStoreLoader.java">Base
+ *      revision</a>
+ */
+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;
+}
diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/PEMFileLoader.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/PEMFileLoader.java
new file mode 100644
index 00000000000..06d264b67da
--- /dev/null
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/PEMFileLoader.java
@@ -0,0 +1,56 @@
+/*
+ * 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.hadoop.hbase.io.crypto.tls;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+
+/**
+ * Implementation of {@link FileKeyStoreLoader} that loads from PEM files.
+ * <p/>
+ * This file has been copied from the Apache ZooKeeper project.
+ * @see <a href=
+ *      "https://github.com/apache/zookeeper/blob/c74658d398cdc1d207aa296cb6e20de00faec03e/zookeeper-server/src/main/java/org/apache/zookeeper/common/PEMFileLoader.java">Base
+ *      revision</a>
+ */
+final class PEMFileLoader extends FileKeyStoreLoader {
+  private PEMFileLoader(String keyStorePath, String trustStorePath, char[] keyStorePassword,
+    char[] trustStorePassword) {
+    super(keyStorePath, trustStorePath, keyStorePassword, trustStorePassword);
+  }
+
+  @Override
+  public KeyStore loadKeyStore() throws IOException, GeneralSecurityException {
+    File file = new File(keyStorePath);
+    return PemReader.loadKeyStore(file, file, keyStorePassword);
+  }
+
+  @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);
+    }
+  }
+}
diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/PKCS12FileLoader.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/PKCS12FileLoader.java
new file mode 100644
index 00000000000..ab5a532787e
--- /dev/null
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/PKCS12FileLoader.java
@@ -0,0 +1,42 @@
+/*
+ * 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.hadoop.hbase.io.crypto.tls;
+
+/**
+ * Implementation of {@link FileKeyStoreLoader} that loads from PKCS12 files.
+ * <p/>
+ * This file has been copied from the Apache ZooKeeper project.
+ * @see <a href=
+ *      "https://github.com/apache/zookeeper/blob/c74658d398cdc1d207aa296cb6e20de00faec03e/zookeeper-server/src/main/java/org/apache/zookeeper/common/PKCS12FileLoader.java">Base
+ *      revision</a>
+ */
+final class PKCS12FileLoader extends StandardTypeFileKeyStoreLoader {
+  private PKCS12FileLoader(String keyStorePath, String trustStorePath, char[] keyStorePassword,
+    char[] trustStorePassword) {
+    super(keyStorePath, trustStorePath, keyStorePassword, trustStorePassword,
+      SupportedStandardKeyFormat.PKCS12);
+  }
+
+  static class Builder extends FileKeyStoreLoader.Builder<PKCS12FileLoader> {
+    @Override
+    PKCS12FileLoader build() {
+      return new PKCS12FileLoader(keyStorePath, trustStorePath, keyStorePassword,
+        trustStorePassword);
+    }
+  }
+}
diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/PemReader.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/PemReader.java
new file mode 100644
index 00000000000..b4f7aa5565a
--- /dev/null
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/PemReader.java
@@ -0,0 +1,218 @@
+/*
+ * 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.hadoop.hbase.io.crypto.tls;
+
+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;
+
+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.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;
+
+/**
+ * This file has been copied from the Apache ZooKeeper project.
+ * @see <a href=
+ *      "https://github.com/apache/zookeeper/blob/c74658d398cdc1d207aa296cb6e20de00faec03e/zookeeper-server/src/main/java/org/apache/zookeeper/util/PemReader.java">Base
+ *      revision</a>
+ */
+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,
+    char[] 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, 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, char[] keyPassword)
+    throws IOException, GeneralSecurityException {
+    String privateKey = new String(Files.readAllBytes(privateKeyFile.toPath()), US_ASCII);
+    return loadPrivateKey(privateKey, keyPassword);
+  }
+
+  public static PrivateKey loadPrivateKey(String privateKey, char[] 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 != null && keyPassword.length > 0) {
+      EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(encodedKey);
+      SecretKeyFactory keyFactory =
+        SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName());
+      SecretKey secretKey = keyFactory.generateSecret(new PBEKeySpec(keyPassword));
+
+      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) {
+      // ignore
+    }
+
+    try {
+      KeyFactory keyFactory = KeyFactory.getInstance("EC");
+      return keyFactory.generatePrivate(encodedKeySpec);
+    } catch (InvalidKeySpecException ignore) {
+      // 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) {
+      // ignore
+    }
+
+    try {
+      KeyFactory keyFactory = KeyFactory.getInstance("EC");
+      return keyFactory.generatePublic(encodedKeySpec);
+    } catch (InvalidKeySpecException ignore) {
+      // ignore
+    }
+
+    KeyFactory keyFactory = KeyFactory.getInstance("DSA");
+    return keyFactory.generatePublic(encodedKeySpec);
+  }
+
+  private static byte[] base64Decode(String base64) {
+    return getMimeDecoder().decode(base64.getBytes(US_ASCII));
+  }
+}
diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/StandardTypeFileKeyStoreLoader.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/StandardTypeFileKeyStoreLoader.java
new file mode 100644
index 00000000000..67aebdd6c7b
--- /dev/null
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/StandardTypeFileKeyStoreLoader.java
@@ -0,0 +1,79 @@
+/*
+ * 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.hadoop.hbase.io.crypto.tls;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+
+/**
+ * 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.
+ * <p/>
+ * This file has been copied from the Apache ZooKeeper project.
+ * @see <a href=
+ *      "https://github.com/apache/zookeeper/blob/c74658d398cdc1d207aa296cb6e20de00faec03e/zookeeper-server/src/main/java/org/apache/zookeeper/common/StandardTypeFileKeyStoreLoader.java">Base
+ *      revision</a>
+ */
+abstract class StandardTypeFileKeyStoreLoader extends FileKeyStoreLoader {
+  private static final char[] EMPTY_CHAR_ARRAY = new char[0];
+
+  protected final SupportedStandardKeyFormat format;
+
+  protected enum SupportedStandardKeyFormat {
+    JKS,
+    PKCS12,
+    BCFKS
+  }
+
+  StandardTypeFileKeyStoreLoader(String keyStorePath, String trustStorePath,
+    char[] keyStorePassword, char[] trustStorePassword, SupportedStandardKeyFormat format) {
+    super(keyStorePath, trustStorePath, keyStorePassword, trustStorePassword);
+    this.format = format;
+  }
+
+  @Override
+  public KeyStore loadKeyStore() throws IOException, GeneralSecurityException {
+    try (InputStream inputStream = Files.newInputStream(new File(keyStorePath).toPath())) {
+      KeyStore ks = keyStoreInstance();
+      ks.load(inputStream, passwordStringToCharArray(keyStorePassword));
+      return ks;
+    }
+  }
+
+  @Override
+  public KeyStore loadTrustStore() throws IOException, GeneralSecurityException {
+    try (InputStream inputStream = Files.newInputStream(new File(trustStorePath).toPath())) {
+      KeyStore ts = keyStoreInstance();
+      ts.load(inputStream, passwordStringToCharArray(trustStorePassword));
+      return ts;
+    }
+  }
+
+  private KeyStore keyStoreInstance() throws KeyStoreException {
+    return KeyStore.getInstance(format.name());
+  }
+
+  private static char[] passwordStringToCharArray(char[] password) {
+    return password == null ? EMPTY_CHAR_ARRAY : password;
+  }
+}
diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/X509Util.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/X509Util.java
index 76b7fad4c59..471ad41d06f 100644
--- a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/X509Util.java
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/X509Util.java
@@ -17,10 +17,7 @@
  */
 package org.apache.hadoop.hbase.io.crypto.tls;
 
-import java.io.File;
 import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
 import java.security.GeneralSecurityException;
 import java.security.KeyStore;
 import java.security.Security;
@@ -227,19 +224,16 @@ public final class X509Util {
   static X509KeyManager createKeyManager(String keyStoreLocation, char[] keyStorePassword,
     String keyStoreType) throws KeyManagerException {
 
-    if (keyStoreType == null) {
-      keyStoreType = "jks";
-    }
-
     if (keyStorePassword == null) {
       keyStorePassword = EMPTY_CHAR_ARRAY;
     }
 
     try {
-      KeyStore ks = KeyStore.getInstance(keyStoreType);
-      try (InputStream inputStream = Files.newInputStream(new File(keyStoreLocation).toPath())) {
-        ks.load(inputStream, keyStorePassword);
-      }
+      KeyStoreFileType storeFileType =
+        KeyStoreFileType.fromPropertyValueOrFileName(keyStoreType, keyStoreLocation);
+      KeyStore ks = FileKeyStoreLoaderBuilderProvider.getBuilderForKeyStoreFileType(storeFileType)
+        .setKeyStorePath(keyStoreLocation).setKeyStorePassword(keyStorePassword).build()
+        .loadKeyStore();
 
       KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
       kmf.init(ks, keyStorePassword);
@@ -272,19 +266,16 @@ public final class X509Util {
   static X509TrustManager createTrustManager(String trustStoreLocation, char[] trustStorePassword,
     String trustStoreType, boolean crlEnabled, boolean ocspEnabled) throws TrustManagerException {
 
-    if (trustStoreType == null) {
-      trustStoreType = "jks";
-    }
-
     if (trustStorePassword == null) {
       trustStorePassword = EMPTY_CHAR_ARRAY;
     }
 
     try {
-      KeyStore ts = KeyStore.getInstance(trustStoreType);
-      try (InputStream inputStream = Files.newInputStream(new File(trustStoreLocation).toPath())) {
-        ts.load(inputStream, trustStorePassword);
-      }
+      KeyStoreFileType storeFileType =
+        KeyStoreFileType.fromPropertyValueOrFileName(trustStoreType, trustStoreLocation);
+      KeyStore ts = FileKeyStoreLoaderBuilderProvider.getBuilderForKeyStoreFileType(storeFileType)
+        .setTrustStorePath(trustStoreLocation).setTrustStorePassword(trustStorePassword).build()
+        .loadTrustStore();
 
       PKIXBuilderParameters pbParams = new PKIXBuilderParameters(ts, new X509CertSelector());
       if (crlEnabled || ocspEnabled) {
diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/AbstractTestX509Parameterized.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/AbstractTestX509Parameterized.java
new file mode 100644
index 00000000000..821a6854135
--- /dev/null
+++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/AbstractTestX509Parameterized.java
@@ -0,0 +1,128 @@
+/*
+ * 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.hadoop.hbase.io.crypto.tls;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.Security;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import org.apache.commons.io.FileUtils;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.HBaseCommonTestingUtil;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.runners.Parameterized;
+
+/**
+ * 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).
+ * <p/>
+ * This base class takes care of setting up / cleaning up the test environment, and caching the
+ * X509TestContext objects used by the tests.
+ * <p/>
+ * This file has been copied from the Apache ZooKeeper project.
+ * @see <a href=
+ *      "https://github.com/apache/zookeeper/blob/master/zookeeper-server/src/test/java/org/apache/zookeeper/common/BaseX509ParameterizedTestCase.java">Base
+ *      revision</a>
+ */
+public abstract class AbstractTestX509Parameterized {
+
+  private static final HBaseCommonTestingUtil UTIL = new HBaseCommonTestingUtil();
+  private static X509TestContextProvider PROVIDER;
+
+  @Parameterized.Parameter()
+  public X509KeyType caKeyType;
+
+  @Parameterized.Parameter(value = 1)
+  public X509KeyType certKeyType;
+
+  @Parameterized.Parameter(value = 2)
+  public char[] keyPassword;
+
+  @Parameterized.Parameter(value = 3)
+  public Integer paramIndex;
+
+  /**
+   * Default parameters suitable for most subclasses. See example usage in {@link TestX509Util}.
+   * @return an array of parameter combinations to test with.
+   */
+  @Parameterized.Parameters(
+      name = "{index}: caKeyType={0}, certKeyType={1}, keyPassword={2}, paramIndex={3}")
+  public static Collection<Object[]> defaultParams() {
+    List<Object[]> result = new ArrayList<>();
+    int paramIndex = 0;
+    for (X509KeyType caKeyType : X509KeyType.values()) {
+      for (X509KeyType certKeyType : X509KeyType.values()) {
+        for (char[] keyPassword : new char[][] { "".toCharArray(), "pa$$w0rd".toCharArray() }) {
+          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 Configuration conf;
+
+  protected X509TestContext x509TestContext;
+
+  @BeforeClass
+  public static void setUpBaseClass() throws Exception {
+    Security.addProvider(new BouncyCastleProvider());
+    File dir = new File(UTIL.getDataTestDir(TestX509Util.class.getSimpleName()).toString())
+      .getCanonicalFile();
+    FileUtils.forceMkdir(dir);
+    PROVIDER = new X509TestContextProvider(UTIL.getConfiguration(), dir);
+  }
+
+  @AfterClass
+  public static void cleanUpBaseClass() {
+    Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
+    UTIL.cleanupTestDir();
+  }
+
+  @Before
+  public void setUp() throws IOException {
+    x509TestContext = PROVIDER.get(caKeyType, certKeyType, keyPassword);
+    x509TestContext.setConfigurations(KeyStoreFileType.JKS, KeyStoreFileType.JKS);
+    conf = new Configuration(UTIL.getConfiguration());
+  }
+
+  @After
+  public void cleanUp() {
+    x509TestContext.clearConfigurations();
+    x509TestContext.getConf().unset(X509Util.TLS_CONFIG_OCSP);
+    x509TestContext.getConf().unset(X509Util.TLS_CONFIG_CLR);
+    x509TestContext.getConf().unset(X509Util.TLS_CONFIG_PROTOCOL);
+    System.clearProperty("com.sun.net.ssl.checkRevocation");
+    System.clearProperty("com.sun.security.enableCRLDP");
+    Security.setProperty("ocsp.enable", Boolean.FALSE.toString());
+    Security.setProperty("com.sun.security.enableCRLDP", Boolean.FALSE.toString());
+  }
+}
diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestBCFKSFileLoader.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestBCFKSFileLoader.java
new file mode 100644
index 00000000000..060c60a7a0c
--- /dev/null
+++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestBCFKSFileLoader.java
@@ -0,0 +1,118 @@
+/*
+ * 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.hadoop.hbase.io.crypto.tls;
+
+import java.io.IOException;
+import java.security.KeyStore;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+/**
+ * This file has been copied from the Apache ZooKeeper project.
+ * @see <a href=
+ *      "https://github.com/apache/zookeeper/blob/master/zookeeper-server/src/test/java/org/apache/zookeeper/common/BCFKSFileLoaderTest.java">Base
+ *      revision</a>
+ */
+@RunWith(Parameterized.class)
+@Category({ SecurityTests.class, SmallTests.class })
+public class TestBCFKSFileLoader extends AbstractTestX509Parameterized {
+
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(TestBCFKSFileLoader.class);
+
+  @Test
+  public void testLoadKeyStore() throws Exception {
+    String path = x509TestContext.getKeyStoreFile(KeyStoreFileType.BCFKS).getAbsolutePath();
+    KeyStore ks = new BCFKSFileLoader.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.BCFKS).getAbsolutePath();
+    new BCFKSFileLoader.Builder().setKeyStorePath(path)
+      .setKeyStorePassword("wrong password".toCharArray()).build().loadKeyStore();
+  }
+
+  @Test(expected = IOException.class)
+  public void testLoadKeyStoreWithWrongFilePath() throws Exception {
+    String path = x509TestContext.getKeyStoreFile(KeyStoreFileType.BCFKS).getAbsolutePath();
+    new BCFKSFileLoader.Builder().setKeyStorePath(path + ".does_not_exist")
+      .setKeyStorePassword(x509TestContext.getKeyStorePassword()).build().loadKeyStore();
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testLoadKeyStoreWithNullFilePath() throws Exception {
+    new BCFKSFileLoader.Builder().setKeyStorePassword(x509TestContext.getKeyStorePassword()).build()
+      .loadKeyStore();
+  }
+
+  @Test(expected = IOException.class)
+  public void testLoadKeyStoreWithWrongFileType() throws Exception {
+    // Trying to load a PEM file with BCFKS loader should fail
+    String path = x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath();
+    new BCFKSFileLoader.Builder().setKeyStorePath(path)
+      .setKeyStorePassword(x509TestContext.getKeyStorePassword()).build().loadKeyStore();
+  }
+
+  @Test
+  public void testLoadTrustStore() throws Exception {
+    String path = x509TestContext.getTrustStoreFile(KeyStoreFileType.BCFKS).getAbsolutePath();
+    KeyStore ts = new BCFKSFileLoader.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.BCFKS).getAbsolutePath();
+    new BCFKSFileLoader.Builder().setTrustStorePath(path)
+      .setTrustStorePassword("wrong password".toCharArray()).build().loadTrustStore();
+  }
+
+  @Test(expected = IOException.class)
+  public void testLoadTrustStoreWithWrongFilePath() throws Exception {
+    String path = x509TestContext.getTrustStoreFile(KeyStoreFileType.BCFKS).getAbsolutePath();
+    new BCFKSFileLoader.Builder().setTrustStorePath(path + ".does_not_exist")
+      .setTrustStorePassword(x509TestContext.getTrustStorePassword()).build().loadTrustStore();
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testLoadTrustStoreWithNullFilePath() throws Exception {
+    new BCFKSFileLoader.Builder().setTrustStorePassword(x509TestContext.getTrustStorePassword())
+      .build().loadTrustStore();
+  }
+
+  @Test(expected = IOException.class)
+  public void testLoadTrustStoreWithWrongFileType() throws Exception {
+    // Trying to load a PEM file with BCFKS loader should fail
+    String path = x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM).getAbsolutePath();
+    new BCFKSFileLoader.Builder().setTrustStorePath(path)
+      .setTrustStorePassword(x509TestContext.getTrustStorePassword()).build().loadTrustStore();
+  }
+
+}
diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestFileKeyStoreLoaderBuilderProvider.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestFileKeyStoreLoaderBuilderProvider.java
new file mode 100644
index 00000000000..a8010348334
--- /dev/null
+++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestFileKeyStoreLoaderBuilderProvider.java
@@ -0,0 +1,66 @@
+/*
+ * 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.hadoop.hbase.io.crypto.tls;
+
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+/**
+ * This file has been copied from the Apache ZooKeeper project.
+ * @see <a href=
+ *      "https://github.com/apache/zookeeper/blob/master/zookeeper-server/src/test/java/org/apache/zookeeper/common/FileKeyStoreLoaderBuilderProviderTest.java">Base
+ *      revision</a>
+ */
+@Category({ SecurityTests.class, SmallTests.class })
+public class TestFileKeyStoreLoaderBuilderProvider {
+
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(TestFileKeyStoreLoaderBuilderProvider.class);
+
+  @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
+  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/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestJKSFileLoader.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestJKSFileLoader.java
new file mode 100644
index 00000000000..6640e3b22f9
--- /dev/null
+++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestJKSFileLoader.java
@@ -0,0 +1,117 @@
+/*
+ * 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.hadoop.hbase.io.crypto.tls;
+
+import java.io.IOException;
+import java.security.KeyStore;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+/**
+ * This file has been copied from the Apache ZooKeeper project.
+ * @see <a href=
+ *      "https://github.com/apache/zookeeper/blob/master/zookeeper-server/src/test/java/org/apache/zookeeper/common/JKSFileLoaderTest.java">Base
+ *      revision</a>
+ */
+@RunWith(Parameterized.class)
+@Category({ SecurityTests.class, SmallTests.class })
+public class TestJKSFileLoader extends AbstractTestX509Parameterized {
+
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(TestJKSFileLoader.class);
+
+  @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".toCharArray()).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".toCharArray()).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();
+  }
+}
diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestKeyStoreFileType.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestKeyStoreFileType.java
new file mode 100644
index 00000000000..d3f457fb4d3
--- /dev/null
+++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestKeyStoreFileType.java
@@ -0,0 +1,120 @@
+/*
+ * 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.hadoop.hbase.io.crypto.tls;
+
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+/**
+ * This file has been copied from the Apache ZooKeeper project.
+ * @see <a href=
+ *      "https://github.com/apache/zookeeper/blob/master/zookeeper-server/src/test/java/org/apache/zookeeper/common/KeyStoreFileTypeTest.java">Base
+ *      revision</a>
+ */
+@Category({ SecurityTests.class, SmallTests.class })
+public class TestKeyStoreFileType {
+
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(TestKeyStoreFileType.class);
+
+  @Test
+  public void testGetPropertyValue() {
+    Assert.assertEquals("PEM", KeyStoreFileType.PEM.getPropertyValue());
+    Assert.assertEquals("JKS", KeyStoreFileType.JKS.getPropertyValue());
+    Assert.assertEquals("PKCS12", KeyStoreFileType.PKCS12.getPropertyValue());
+    Assert.assertEquals("BCFKS", KeyStoreFileType.BCFKS.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.assertEquals(KeyStoreFileType.BCFKS, KeyStoreFileType.fromPropertyValue("BCFKS"));
+    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.assertEquals(KeyStoreFileType.PKCS12, KeyStoreFileType.fromPropertyValue("pkcs12"));
+    Assert.assertEquals(KeyStoreFileType.BCFKS, KeyStoreFileType.fromPropertyValue("bcfks"));
+    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"));
+    Assert.assertEquals(KeyStoreFileType.PKCS12, KeyStoreFileType.fromFilename("mykey.p12"));
+    Assert.assertEquals(KeyStoreFileType.PKCS12,
+      KeyStoreFileType.fromFilename("/path/to/key/dir/mykey.p12"));
+    Assert.assertEquals(KeyStoreFileType.BCFKS, KeyStoreFileType.fromFilename("mykey.bcfks"));
+    Assert.assertEquals(KeyStoreFileType.BCFKS,
+      KeyStoreFileType.fromFilename("/path/to/key/dir/mykey.bcfks"));
+  }
+
+  @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"));
+    Assert.assertEquals(KeyStoreFileType.PEM,
+      KeyStoreFileType.fromPropertyValueOrFileName("PEM", "prod.key"));
+    Assert.assertEquals(KeyStoreFileType.PKCS12,
+      KeyStoreFileType.fromPropertyValueOrFileName("PKCS12", "prod.key"));
+    Assert.assertEquals(KeyStoreFileType.BCFKS,
+      KeyStoreFileType.fromPropertyValueOrFileName("BCFKS", "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");
+  }
+}
diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestPEMFileLoader.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestPEMFileLoader.java
new file mode 100644
index 00000000000..0c9924f0907
--- /dev/null
+++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestPEMFileLoader.java
@@ -0,0 +1,112 @@
+/*
+ * 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.hadoop.hbase.io.crypto.tls;
+
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+/**
+ * This file has been copied from the Apache ZooKeeper project.
+ * @see <a href=
+ *      "https://github.com/apache/zookeeper/blob/master/zookeeper-server/src/test/java/org/apache/zookeeper/common/PEMFileLoaderTest.java">Base
+ *      revision</a>
+ */
+@RunWith(Parameterized.class)
+@Category({ SecurityTests.class, SmallTests.class })
+public class TestPEMFileLoader extends AbstractTestX509Parameterized {
+
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(TestPEMFileLoader.class);
+
+  @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".toCharArray()).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());
+  }
+}
diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestPKCS12FileLoader.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestPKCS12FileLoader.java
new file mode 100644
index 00000000000..a0ff83833e2
--- /dev/null
+++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestPKCS12FileLoader.java
@@ -0,0 +1,117 @@
+/*
+ * 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.hadoop.hbase.io.crypto.tls;
+
+import java.io.IOException;
+import java.security.KeyStore;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+/**
+ * This file has been copied from the Apache ZooKeeper project.
+ * @see <a href=
+ *      "https://github.com/apache/zookeeper/blob/master/zookeeper-server/src/test/java/org/apache/zookeeper/common/PKCS12FileLoaderTest.java">Base
+ *      revision</a>
+ */
+@RunWith(Parameterized.class)
+@Category({ SecurityTests.class, SmallTests.class })
+public class TestPKCS12FileLoader extends AbstractTestX509Parameterized {
+
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(TestPKCS12FileLoader.class);
+
+  @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".toCharArray()).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".toCharArray()).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/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestX509Util.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestX509Util.java
index 61134390e8a..a847db98a04 100644
--- a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestX509Util.java
+++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestX509Util.java
@@ -28,28 +28,15 @@ import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeThat;
 import static org.mockito.Mockito.mock;
 
-import java.io.File;
-import java.io.IOException;
 import java.security.Security;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
-import java.util.List;
-import org.apache.commons.io.FileUtils;
-import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.hbase.HBaseClassTestRule;
-import org.apache.hadoop.hbase.HBaseCommonTestingUtil;
 import org.apache.hadoop.hbase.exceptions.KeyManagerException;
 import org.apache.hadoop.hbase.exceptions.SSLContextException;
 import org.apache.hadoop.hbase.exceptions.TrustManagerException;
-import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
 import org.apache.hadoop.hbase.testclassification.SmallTests;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.ClassRule;
 import org.junit.Test;
 import org.junit.experimental.categories.Category;
@@ -66,83 +53,15 @@ import org.apache.hbase.thirdparty.io.netty.handler.ssl.SslContext;
  *      revision</a>
  */
 @RunWith(Parameterized.class)
-@Category({ MiscTests.class, SmallTests.class })
-public class TestX509Util {
+@Category({ SecurityTests.class, SmallTests.class })
+public class TestX509Util extends AbstractTestX509Parameterized {
 
   @ClassRule
   public static final HBaseClassTestRule CLASS_RULE =
     HBaseClassTestRule.forClass(TestX509Util.class);
 
-  private static final HBaseCommonTestingUtil UTIL = new HBaseCommonTestingUtil();
   private static final char[] EMPTY_CHAR_ARRAY = new char[0];
 
-  private static X509TestContextProvider PROVIDER;
-
-  @Parameterized.Parameter()
-  public X509KeyType caKeyType;
-
-  @Parameterized.Parameter(value = 1)
-  public X509KeyType certKeyType;
-
-  @Parameterized.Parameter(value = 2)
-  public char[] keyPassword;
-
-  @Parameterized.Parameter(value = 3)
-  public Integer paramIndex;
-
-  private X509TestContext x509TestContext;
-
-  private Configuration conf;
-
-  @Parameterized.Parameters(
-      name = "{index}: caKeyType={0}, certKeyType={1}, keyPassword={2}, paramIndex={3}")
-  public static Collection<Object[]> data() {
-    List<Object[]> params = new ArrayList<>();
-    int paramIndex = 0;
-    for (X509KeyType caKeyType : X509KeyType.values()) {
-      for (X509KeyType certKeyType : X509KeyType.values()) {
-        for (char[] keyPassword : new char[][] { "".toCharArray(), "pa$$w0rd".toCharArray() }) {
-          params.add(new Object[] { caKeyType, certKeyType, keyPassword, paramIndex++ });
-        }
-      }
-    }
-    return params;
-  }
-
-  @BeforeClass
-  public static void setUpBeforeClass() throws IOException {
-    Security.addProvider(new BouncyCastleProvider());
-    File dir = new File(UTIL.getDataTestDir(TestX509Util.class.getSimpleName()).toString())
-      .getCanonicalFile();
-    FileUtils.forceMkdir(dir);
-    PROVIDER = new X509TestContextProvider(UTIL.getConfiguration(), dir);
-  }
-
-  @AfterClass
-  public static void tearDownAfterClass() {
-    Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
-    UTIL.cleanupTestDir();
-  }
-
-  @Before
-  public void setUp() throws IOException {
-    x509TestContext = PROVIDER.get(caKeyType, certKeyType, keyPassword);
-    x509TestContext.setConfigurations(KeyStoreFileType.JKS, KeyStoreFileType.JKS);
-    conf = new Configuration(UTIL.getConfiguration());
-  }
-
-  @After
-  public void cleanUp() {
-    x509TestContext.clearConfigurations();
-    x509TestContext.getConf().unset(X509Util.TLS_CONFIG_OCSP);
-    x509TestContext.getConf().unset(X509Util.TLS_CONFIG_CLR);
-    x509TestContext.getConf().unset(X509Util.TLS_CONFIG_PROTOCOL);
-    System.clearProperty("com.sun.net.ssl.checkRevocation");
-    System.clearProperty("com.sun.security.enableCRLDP");
-    Security.setProperty("ocsp.enable", Boolean.FALSE.toString());
-    Security.setProperty("com.sun.security.enableCRLDP", Boolean.FALSE.toString());
-  }
-
   @Test
   public void testCreateSSLContextWithoutCustomProtocol() throws Exception {
     SslContext sslContext = X509Util.createSslContextForClient(conf);
@@ -204,6 +123,69 @@ public class TestX509Util {
     assertFalse(Boolean.valueOf(Security.getProperty("ocsp.enable")));
   }
 
+  @Test
+  public void testLoadPEMKeyStore() throws Exception {
+    // Make sure we can instantiate a key manager from the PEM file on disk
+    X509Util.createKeyManager(
+      x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath(),
+      x509TestContext.getKeyStorePassword(), KeyStoreFileType.PEM.getPropertyValue());
+  }
+
+  @Test
+  public void testLoadPEMKeyStoreNullPassword() throws Exception {
+    assumeThat(x509TestContext.getKeyStorePassword(), equalTo(EMPTY_CHAR_ARRAY));
+    // Make sure that empty password and null password are treated the same
+    X509Util.createKeyManager(
+      x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath(), null,
+      KeyStoreFileType.PEM.getPropertyValue());
+  }
+
+  @Test
+  public void testLoadPEMKeyStoreAutodetectStoreFileType() throws Exception {
+    // Make sure we can instantiate a key manager from the PEM file on disk
+    X509Util.createKeyManager(
+      x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath(),
+      x509TestContext.getKeyStorePassword(),
+      null /* null StoreFileType means 'autodetect from file extension' */);
+  }
+
+  @Test(expected = KeyManagerException.class)
+  public void testLoadPEMKeyStoreWithWrongPassword() throws Exception {
+    // Attempting to load with the wrong key password should fail
+    X509Util.createKeyManager(
+      x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath(),
+      "wrong password".toCharArray(), // intentionally use the wrong password
+      KeyStoreFileType.PEM.getPropertyValue());
+  }
+
+  @Test
+  public void testLoadPEMTrustStore() throws Exception {
+    // Make sure we can instantiate a trust manager from the PEM file on disk
+    X509Util.createTrustManager(
+      x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM).getAbsolutePath(),
+      x509TestContext.getTrustStorePassword(), KeyStoreFileType.PEM.getPropertyValue(), false,
+      false);
+  }
+
+  @Test
+  public void testLoadPEMTrustStoreNullPassword() throws Exception {
+    assumeThat(x509TestContext.getTrustStorePassword(), equalTo(EMPTY_CHAR_ARRAY));
+    // Make sure that empty password and null password are treated the same
+    X509Util.createTrustManager(
+      x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM).getAbsolutePath(), null,
+      KeyStoreFileType.PEM.getPropertyValue(), false, false);
+  }
+
+  @Test
+  public void testLoadPEMTrustStoreAutodetectStoreFileType() throws Exception {
+    // Make sure we can instantiate a trust manager from the PEM file on disk
+    X509Util.createTrustManager(
+      x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM).getAbsolutePath(),
+      x509TestContext.getTrustStorePassword(), null, // null StoreFileType means 'autodetect from
+                                                     // file extension'
+      false, false);
+  }
+
   @Test
   public void testLoadJKSKeyStore() throws Exception {
     // Make sure we can instantiate a key manager from the JKS file on disk
@@ -222,7 +204,7 @@ public class TestX509Util {
   }
 
   @Test
-  public void testLoadJKSKeyStoreFileTypeDefaultToJks() throws Exception {
+  public void testLoadJKSKeyStoreAutodetectStoreFileType() throws Exception {
     // Make sure we can instantiate a key manager from the JKS file on disk
     X509Util.createKeyManager(
       x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS).getAbsolutePath(),
@@ -258,16 +240,17 @@ public class TestX509Util {
   }
 
   @Test
-  public void testLoadJKSTrustStoreFileTypeDefaultToJks() throws Exception {
+  public void testLoadJKSTrustStoreAutodetectStoreFileType() throws Exception {
     // Make sure we can instantiate a trust manager from the JKS file on disk
     X509Util.createTrustManager(
       x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS).getAbsolutePath(),
-      // null StoreFileType means 'autodetect from file extension'
-      x509TestContext.getTrustStorePassword(), null, true, true);
+      x509TestContext.getTrustStorePassword(), null, // null StoreFileType means 'autodetect from
+                                                     // file extension'
+      true, true);
   }
 
   @Test
-  public void testLoadJKSTrustStoreWithWrongPassword() throws Exception {
+  public void testLoadJKSTrustStoreWithWrongPassword() {
     assertThrows(TrustManagerException.class, () -> {
       // Attempting to load with the wrong key password should fail
       X509Util.createTrustManager(
@@ -294,7 +277,16 @@ public class TestX509Util {
   }
 
   @Test
-  public void testLoadPKCS12KeyStoreWithWrongPassword() throws Exception {
+  public void testLoadPKCS12KeyStoreAutodetectStoreFileType() throws Exception {
+    // Make sure we can instantiate a key manager from the PKCS12 file on disk
+    X509Util.createKeyManager(
+      x509TestContext.getKeyStoreFile(KeyStoreFileType.PKCS12).getAbsolutePath(),
+      x509TestContext.getKeyStorePassword(),
+      null /* null StoreFileType means 'autodetect from file extension' */);
+  }
+
+  @Test
+  public void testLoadPKCS12KeyStoreWithWrongPassword() {
     assertThrows(KeyManagerException.class, () -> {
       // Attempting to load with the wrong key password should fail
       X509Util.createKeyManager(
@@ -322,7 +314,17 @@ public class TestX509Util {
   }
 
   @Test
-  public void testLoadPKCS12TrustStoreWithWrongPassword() throws Exception {
+  public void testLoadPKCS12TrustStoreAutodetectStoreFileType() throws Exception {
+    // Make sure we can instantiate a trust manager from the PKCS12 file on disk
+    X509Util.createTrustManager(
+      x509TestContext.getTrustStoreFile(KeyStoreFileType.PKCS12).getAbsolutePath(),
+      x509TestContext.getTrustStorePassword(), null, // null StoreFileType means 'autodetect from
+                                                     // file extension'
+      true, true);
+  }
+
+  @Test
+  public void testLoadPKCS12TrustStoreWithWrongPassword() {
     assertThrows(TrustManagerException.class, () -> {
       // Attempting to load with the wrong key password should fail
       X509Util.createTrustManager(
@@ -332,42 +334,42 @@ public class TestX509Util {
   }
 
   @Test
-  public void testGetDefaultCipherSuitesJava8() throws Exception {
+  public void testGetDefaultCipherSuitesJava8() {
     String[] cipherSuites = X509Util.getDefaultCipherSuitesForJavaVersion("1.8");
     // Java 8 default should have the CBC suites first
     assertThat(cipherSuites[0], containsString("CBC"));
   }
 
   @Test
-  public void testGetDefaultCipherSuitesJava9() throws Exception {
+  public void testGetDefaultCipherSuitesJava9() {
     String[] cipherSuites = X509Util.getDefaultCipherSuitesForJavaVersion("9");
     // Java 9+ default should have the GCM suites first
     assertThat(cipherSuites[0], containsString("GCM"));
   }
 
   @Test
-  public void testGetDefaultCipherSuitesJava10() throws Exception {
+  public void testGetDefaultCipherSuitesJava10() {
     String[] cipherSuites = X509Util.getDefaultCipherSuitesForJavaVersion("10");
     // Java 9+ default should have the GCM suites first
     assertThat(cipherSuites[0], containsString("GCM"));
   }
 
   @Test
-  public void testGetDefaultCipherSuitesJava11() throws Exception {
+  public void testGetDefaultCipherSuitesJava11() {
     String[] cipherSuites = X509Util.getDefaultCipherSuitesForJavaVersion("11");
     // Java 9+ default should have the GCM suites first
     assertThat(cipherSuites[0], containsString("GCM"));
   }
 
   @Test
-  public void testGetDefaultCipherSuitesUnknownVersion() throws Exception {
+  public void testGetDefaultCipherSuitesUnknownVersion() {
     String[] cipherSuites = X509Util.getDefaultCipherSuitesForJavaVersion("notaversion");
     // If version can't be parsed, use the more conservative Java 8 default
     assertThat(cipherSuites[0], containsString("CBC"));
   }
 
   @Test
-  public void testGetDefaultCipherSuitesNullVersion() throws Exception {
+  public void testGetDefaultCipherSuitesNullVersion() {
     assertThrows(NullPointerException.class, () -> {
       X509Util.getDefaultCipherSuitesForJavaVersion(null);
     });
diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/X509TestContext.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/X509TestContext.java
index b2085078860..27cb5bde3ac 100644
--- a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/X509TestContext.java
+++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/X509TestContext.java
@@ -31,7 +31,6 @@ import java.security.cert.X509Certificate;
 import java.util.Arrays;
 import org.apache.commons.io.FileUtils;
 import org.apache.hadoop.conf.Configuration;
-import org.apache.hadoop.hbase.HBaseConfiguration;
 import org.apache.yetus.audience.InterfaceAudience;
 import org.bouncycastle.asn1.x500.X500NameBuilder;
 import org.bouncycastle.asn1.x500.style.BCStyle;
@@ -60,6 +59,7 @@ public final class X509TestContext {
   private File trustStoreJksFile;
   private File trustStorePemFile;
   private File trustStorePkcs12File;
+  private File trustStoreBcfksFile;
 
   private final KeyPair keyStoreKeyPair;
   private final X509Certificate keyStoreCertificate;
@@ -67,6 +67,7 @@ public final class X509TestContext {
   private File keyStoreJksFile;
   private File keyStorePemFile;
   private File keyStorePkcs12File;
+  private File keyStoreBcfksFile;
 
   /**
    * Constructor is intentionally private, use the Builder class instead.
@@ -137,6 +138,8 @@ public final class X509TestContext {
         return getTrustStorePemFile();
       case PKCS12:
         return getTrustStorePkcs12File();
+      case BCFKS:
+        return getTrustStoreBcfksFile();
       default:
         throw new IllegalArgumentException("Invalid trust store type: " + storeFileType
           + ", must be one of: " + Arrays.toString(KeyStoreFileType.values()));
@@ -194,6 +197,25 @@ public final class X509TestContext {
     return trustStorePkcs12File;
   }
 
+  private File getTrustStoreBcfksFile() throws IOException {
+    if (trustStoreBcfksFile == null) {
+      File trustStoreBcfksFile = File.createTempFile(TRUST_STORE_PREFIX,
+        KeyStoreFileType.BCFKS.getDefaultFileExtension(), tempDir);
+      trustStoreBcfksFile.deleteOnExit();
+      try (
+        final FileOutputStream trustStoreOutputStream = new FileOutputStream(trustStoreBcfksFile)) {
+        byte[] bytes =
+          X509TestHelpers.certToBCFKSTrustStoreBytes(trustStoreCertificate, trustStorePassword);
+        trustStoreOutputStream.write(bytes);
+        trustStoreOutputStream.flush();
+      } catch (GeneralSecurityException e) {
+        throw new IOException(e);
+      }
+      this.trustStoreBcfksFile = trustStoreBcfksFile;
+    }
+    return trustStoreBcfksFile;
+  }
+
   public X509Certificate getKeyStoreCertificate() {
     return keyStoreCertificate;
   }
@@ -226,6 +248,8 @@ public final class X509TestContext {
         return getKeyStorePemFile();
       case PKCS12:
         return getKeyStorePkcs12File();
+      case BCFKS:
+        return getKeyStoreBcfksFile();
       default:
         throw new IllegalArgumentException("Invalid key store type: " + storeFileType
           + ", must be one of: " + Arrays.toString(KeyStoreFileType.values()));
@@ -286,6 +310,24 @@ public final class X509TestContext {
     return keyStorePkcs12File;
   }
 
+  private File getKeyStoreBcfksFile() throws IOException {
+    if (keyStoreBcfksFile == null) {
+      File keyStoreBcfksFile = File.createTempFile(KEY_STORE_PREFIX,
+        KeyStoreFileType.BCFKS.getDefaultFileExtension(), tempDir);
+      keyStoreBcfksFile.deleteOnExit();
+      try (final FileOutputStream keyStoreOutputStream = new FileOutputStream(keyStoreBcfksFile)) {
+        byte[] bytes = X509TestHelpers.certAndPrivateKeyToBCFKSBytes(keyStoreCertificate,
+          keyStoreKeyPair.getPrivate(), keyStorePassword);
+        keyStoreOutputStream.write(bytes);
+        keyStoreOutputStream.flush();
+      } catch (GeneralSecurityException e) {
+        throw new IOException(e);
+      }
+      this.keyStoreBcfksFile = keyStoreBcfksFile;
+    }
+    return keyStoreBcfksFile;
+  }
+
   /**
    * Sets the SSL system properties such that the given X509Util object can be used to create SSL
    * Contexts that will use the trust store and key store files created by this test context.
@@ -413,14 +455,6 @@ public final class X509TestContext {
     }
   }
 
-  /**
-   * Returns a new default-constructed Builder.
-   * @return a new Builder.
-   */
-  public static Builder newBuilder() {
-    return newBuilder(HBaseConfiguration.create());
-  }
-
   /**
    * Returns a new default-constructed Builder.
    * @return a new Builder.
diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/X509TestContextProvider.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/X509TestContextProvider.java
index 3024755a2e3..d65cdbe689d 100644
--- a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/X509TestContextProvider.java
+++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/X509TestContextProvider.java
@@ -18,7 +18,10 @@
 package org.apache.hadoop.hbase.io.crypto.tls;
 
 import java.io.File;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
 import java.util.Objects;
 import org.apache.hadoop.conf.Configuration;
 
@@ -83,4 +86,18 @@ public class X509TestContextProvider {
   public X509TestContext get(X509KeyType caKeyType, X509KeyType certKeyType, char[] keyPassword) {
     return ctxs.getUnchecked(new CacheKey(caKeyType, certKeyType, keyPassword));
   }
+
+  static Collection<Object[]> defaultParams() {
+    List<Object[]> params = new ArrayList<>();
+    int paramIndex = 0;
+    for (X509KeyType caKeyType : X509KeyType.values()) {
+      for (X509KeyType certKeyType : X509KeyType.values()) {
+        for (char[] keyPassword : new char[][] { "".toCharArray(), "pa$$w0rd".toCharArray() }) {
+          params.add(new Object[] { caKeyType, certKeyType, keyPassword, paramIndex++ });
+        }
+      }
+    }
+    return params;
+  }
+
 }
diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/X509TestHelpers.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/X509TestHelpers.java
index 1697dca8669..968e616ef56 100644
--- a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/X509TestHelpers.java
+++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/X509TestHelpers.java
@@ -281,7 +281,7 @@ final class X509TestHelpers {
     StringWriter stringWriter = new StringWriter();
     JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter);
     OutputEncryptor encryptor = null;
-    if (password != null) {
+    if (password != null && password.length > 0) {
       encryptor =
         new JceOpenSSLPKCS8EncryptorBuilder(PKCSObjectIdentifiers.pbeWithSHAAnd3_KeyTripleDES_CBC)
           .setProvider(BouncyCastleProvider.PROVIDER_NAME).setRandom(PRNG).setPasssword(password)
@@ -341,6 +341,23 @@ final class X509TestHelpers {
     return certToTrustStoreBytes(cert, keyPassword, trustStore);
   }
 
+  /**
+   * Encodes the given X509Certificate as a BCFKS 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 BCFKS trust store. nn
+   */
+  public static byte[] certToBCFKSTrustStoreBytes(X509Certificate cert, char[] keyPassword)
+    throws IOException, GeneralSecurityException {
+    KeyStore trustStore = KeyStore.getInstance("BCFKS");
+    return certToTrustStoreBytes(cert, keyPassword, trustStore);
+  }
+
   private static byte[] certToTrustStoreBytes(X509Certificate cert, char[] keyPassword,
     KeyStore trustStore) throws IOException, GeneralSecurityException {
     trustStore.load(null, keyPassword);
@@ -387,6 +404,23 @@ final class X509TestHelpers {
     return certAndPrivateKeyToBytes(cert, privateKey, keyPassword, keyStore);
   }
 
+  /**
+   * Encodes the given X509Certificate and private key as a BCFKS 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 BCFKS key store. nn
+   */
+  public static byte[] certAndPrivateKeyToBCFKSBytes(X509Certificate cert, PrivateKey privateKey,
+    char[] keyPassword) throws IOException, GeneralSecurityException {
+    KeyStore keyStore = KeyStore.getInstance("BCFKS");
+    return certAndPrivateKeyToBytes(cert, privateKey, keyPassword, keyStore);
+  }
+
   private static byte[] certAndPrivateKeyToBytes(X509Certificate cert, PrivateKey privateKey,
     char[] keyPassword, KeyStore keyStore) throws IOException, GeneralSecurityException {
     keyStore.load(null, keyPassword);