From d2cc840edf4888d090e6777a621fd976e7abccd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andor=20Moln=C3=A1r?= Date: Tue, 6 Sep 2022 21:43:41 +0200 Subject: [PATCH] HBASE-27346 Autodetect key/truststore file type from file extension (#4757) Signed-off-by: Duo Zhang Signed-off-by: Bryan Beaudreault --- .../hbase/io/crypto/tls/BCFKSFileLoader.java | 42 ++++ .../io/crypto/tls/FileKeyStoreLoader.java | 80 +++++++ .../FileKeyStoreLoaderBuilderProvider.java | 54 +++++ .../hbase/io/crypto/tls/JKSFileLoader.java | 41 ++++ .../hbase/io/crypto/tls/KeyStoreLoader.java | 53 +++++ .../hbase/io/crypto/tls/PEMFileLoader.java | 56 +++++ .../hbase/io/crypto/tls/PKCS12FileLoader.java | 42 ++++ .../hadoop/hbase/io/crypto/tls/PemReader.java | 218 ++++++++++++++++++ .../tls/StandardTypeFileKeyStoreLoader.java | 79 +++++++ .../hadoop/hbase/io/crypto/tls/X509Util.java | 29 +-- .../tls/AbstractTestX509Parameterized.java | 128 ++++++++++ .../io/crypto/tls/TestBCFKSFileLoader.java | 118 ++++++++++ ...TestFileKeyStoreLoaderBuilderProvider.java | 66 ++++++ .../io/crypto/tls/TestJKSFileLoader.java | 117 ++++++++++ .../io/crypto/tls/TestKeyStoreFileType.java | 120 ++++++++++ .../io/crypto/tls/TestPEMFileLoader.java | 112 +++++++++ .../io/crypto/tls/TestPKCS12FileLoader.java | 117 ++++++++++ .../hbase/io/crypto/tls/TestX509Util.java | 196 ++++++++-------- .../hbase/io/crypto/tls/X509TestContext.java | 52 ++++- .../crypto/tls/X509TestContextProvider.java | 17 ++ .../hbase/io/crypto/tls/X509TestHelpers.java | 36 ++- 21 files changed, 1647 insertions(+), 126 deletions(-) create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/BCFKSFileLoader.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/FileKeyStoreLoader.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/FileKeyStoreLoaderBuilderProvider.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/JKSFileLoader.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/KeyStoreLoader.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/PEMFileLoader.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/PKCS12FileLoader.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/PemReader.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/StandardTypeFileKeyStoreLoader.java create mode 100644 hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/AbstractTestX509Parameterized.java create mode 100644 hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestBCFKSFileLoader.java create mode 100644 hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestFileKeyStoreLoaderBuilderProvider.java create mode 100644 hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestJKSFileLoader.java create mode 100644 hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestKeyStoreFileType.java create mode 100644 hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestPEMFileLoader.java create mode 100644 hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/tls/TestPKCS12FileLoader.java 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. + *

+ * This file has been copied from the Apache ZooKeeper project. + * @see Base + * revision + */ +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 { + @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. + *

+ * This file has been copied from the Apache ZooKeeper project. + * @see Base + * revision + */ +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 the subtype of FileKeyStoreLoader created by the Builder. + */ + static abstract class Builder { + String keyStorePath; + String trustStorePath; + char[] keyStorePassword; + char[] trustStorePassword; + + Builder() { + } + + Builder setKeyStorePath(String keyStorePath) { + this.keyStorePath = Objects.requireNonNull(keyStorePath); + return this; + } + + Builder setTrustStorePath(String trustStorePath) { + this.trustStorePath = Objects.requireNonNull(trustStorePath); + return this; + } + + Builder setKeyStorePassword(char[] keyStorePassword) { + this.keyStorePassword = Objects.requireNonNull(keyStorePassword); + return this; + } + + Builder 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 Base + * revision + */ +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 + 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. + *

+ * This file has been copied from the Apache ZooKeeper project. + * @see Base + * revision + */ +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 { + @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. + *

+ * This file has been copied from the Apache ZooKeeper project. + * @see Base + * revision + */ +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. + *

+ * This file has been copied from the Apache ZooKeeper project. + * @see Base + * revision + */ +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 { + @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. + *

+ * This file has been copied from the Apache ZooKeeper project. + * @see Base + * revision + */ +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 { + @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 Base + * revision + */ +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 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 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 readCertificateChain(File certificateChainFile) + throws IOException, GeneralSecurityException { + String contents = new String(Files.readAllBytes(certificateChainFile.toPath()), US_ASCII); + return readCertificateChain(contents); + } + + public static List readCertificateChain(String certificateChain) + throws CertificateException { + Matcher matcher = CERT_PATTERN.matcher(certificateChain); + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + List 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. + *

+ * This file has been copied from the Apache ZooKeeper project. + * @see Base + * revision + */ +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). + *

+ * This base class takes care of setting up / cleaning up the test environment, and caching the + * X509TestContext objects used by the tests. + *

+ * This file has been copied from the Apache ZooKeeper project. + * @see Base + * revision + */ +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 defaultParams() { + List 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 Base + * revision + */ +@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 Base + * revision + */ +@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 Base + * revision + */ +@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 Base + * revision + */ +@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 Base + * revision + */ +@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 Base + * revision + */ +@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 */ @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 data() { - List 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 defaultParams() { + List 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);