NIFI-8451 Updated KeyStoreUtils to use KeyStore.getInstance() with provider

- Refactored and consolidated KeyStoreUtils unit tests
- Corrected KeyStoreUtils.loadEmptyKeyStore() to use KeyStoreUtils.getKeyStore()

Signed-off-by: Nathan Gough <thenatog@gmail.com>

This closes #5015.
This commit is contained in:
exceptionfactory 2021-04-21 08:07:24 -05:00 committed by Nathan Gough
parent 1ef1905461
commit ed6d5bacba
3 changed files with 72 additions and 326 deletions

View File

@ -41,7 +41,6 @@ import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
@ -110,13 +109,12 @@ public class KeyStoreUtils {
* @throws KeyStoreException if a KeyStore of the given type cannot be instantiated
*/
public static KeyStore getKeyStore(String keyStoreType) throws KeyStoreException {
String keyStoreProvider = getKeyStoreProvider(keyStoreType);
final String keyStoreProvider = getKeyStoreProvider(keyStoreType);
if (StringUtils.isNotEmpty(keyStoreProvider)) {
try {
return KeyStore.getInstance(keyStoreType, keyStoreProvider);
} catch (Exception e) {
logger.error("Unable to load " + keyStoreProvider + " " + keyStoreType
+ " keystore. This may cause issues getting trusted CA certificates as well as Certificate Chains for use in TLS.", e);
} catch (final Exception e) {
logger.error("KeyStore Type [{}] Provider [{}] instance creation failed", keyStoreType, keyStoreProvider, e);
}
}
return KeyStore.getInstance(keyStoreType);
@ -526,15 +524,12 @@ public class KeyStoreUtils {
* @return an empty keystore
* @throws KeyStoreException if a keystore of the given type cannot be instantiated
*/
private static KeyStore loadEmptyKeyStore(KeystoreType keyStoreType) throws KeyStoreException, CertificateException, NoSuchAlgorithmException {
final KeyStore keyStore;
private static KeyStore loadEmptyKeyStore(final KeystoreType keyStoreType) throws KeyStoreException, CertificateException, NoSuchAlgorithmException {
try {
keyStore = KeyStore.getInstance(
Objects.requireNonNull(keyStoreType).getType());
final KeyStore keyStore = getKeyStore(keyStoreType.getType());
keyStore.load(null, null);
return keyStore;
} catch (IOException e) {
logger.error("Encountered an error loading keystore: {}", e.getLocalizedMessage());
} catch (final IOException e) {
throw new UncheckedIOException("Error loading keystore", e);
}
}

View File

@ -1,226 +0,0 @@
/*
* 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.nifi.security.util
import org.apache.nifi.util.StringUtils
import org.junit.After
import org.junit.AfterClass
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
import java.nio.file.Files
import java.nio.file.Paths
import java.security.KeyStore
import java.security.cert.Certificate
@RunWith(JUnit4.class)
class KeyStoreUtilsGroovyTest extends GroovyTestCase {
private static final Logger logger = LoggerFactory.getLogger(KeyStoreUtilsGroovyTest.class)
private static final String TEST_KEYSTORE_PASSWORD = "keystorepassword"
private static final String TEST_KEY_PASSWORD = "keypassword"
private static final String TEST_TRUSTSTORE_PASSWORD = "truststorepassword"
private static final KeystoreType DEFAULT_STORE_TYPE = KeystoreType.JKS
private static final KeystoreType PKCS12_STORE_TYPE = KeystoreType.PKCS12
private static TlsConfiguration tlsConfigParam
private static TlsConfiguration tlsConfiguration
@BeforeClass
static void setUpOnce() {
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
tlsConfigParam = new StandardTlsConfiguration(null, TEST_KEYSTORE_PASSWORD, TEST_KEY_PASSWORD, DEFAULT_STORE_TYPE, null, TEST_TRUSTSTORE_PASSWORD, null)
tlsConfiguration = KeyStoreUtils.createTlsConfigAndNewKeystoreTruststore(tlsConfigParam)
}
@AfterClass
static void afterClass() throws Exception {
deleteKeystoreTruststore(tlsConfiguration);
}
@Before
void setUp() {
}
@After
void tearDown() {
}
@Test
void testShouldVerifyKeystoreIsValid() {
// Arrange
final URL ksUrl = getKeystorePathAsUrl(tlsConfiguration.getKeystorePath())
// Act
boolean keystoreIsValid = KeyStoreUtils.isStoreValid(ksUrl, DEFAULT_STORE_TYPE, TEST_KEYSTORE_PASSWORD.toCharArray())
// Assert
assert keystoreIsValid
}
@Test
void testShouldVerifyKeystoreIsNotValid() {
// Arrange
final URL ksUrl = getKeystorePathAsUrl(tlsConfiguration.getKeystorePath())
// Act
boolean keystoreIsValid = KeyStoreUtils.isStoreValid(ksUrl, DEFAULT_STORE_TYPE, TEST_KEYSTORE_PASSWORD.reverse().toCharArray())
// Assert
assert !keystoreIsValid
}
@Test
void testShouldVerifyKeyPasswordIsValid() {
// Arrange
final URL ksUrl = getKeystorePathAsUrl(tlsConfiguration.getKeystorePath())
// Act
boolean keyPasswordIsValid = KeyStoreUtils.isKeyPasswordCorrect(ksUrl, DEFAULT_STORE_TYPE, TEST_KEYSTORE_PASSWORD.toCharArray(), TEST_KEY_PASSWORD.toCharArray())
// Assert
assert keyPasswordIsValid
}
@Test
void testShouldVerifyKeyPasswordIsNotValid() {
// Arrange
final URL ksUrl = getKeystorePathAsUrl(tlsConfiguration.getKeystorePath())
// Act
boolean keyPasswordIsValid = KeyStoreUtils.isKeyPasswordCorrect(ksUrl, tlsConfiguration.getKeystoreType(), TEST_KEYSTORE_PASSWORD.toCharArray(), TEST_KEY_PASSWORD.reverse().toCharArray())
// Assert
assert !keyPasswordIsValid
}
@Test
@Ignore("Used to create passwordless truststore file for testing NIFI-6770")
void createPasswordlessTruststore() {
// Retrieve the public certificate from https://nifi.apache.org
String hostname = "nifi.apache.org"
SSLSocketFactory factory = HttpsURLConnection.getDefaultSSLSocketFactory()
SSLSocket socket = (SSLSocket) factory.createSocket(hostname, 443)
socket.startHandshake()
List<Certificate> certs = socket.session.peerCertificateChain as List<Certificate>
Certificate nodeCert = CertificateUtils.formX509Certificate(certs.first().encoded)
// Create a JKS truststore containing that cert as a trustedCertEntry and do not put a password on the truststore
KeyStore truststore = KeyStore.getInstance("JKS")
// Explicitly set the second parameter to empty to avoid a password
truststore.load(null, "".chars)
truststore.setCertificateEntry("nifi.apache.org", nodeCert)
// Save the truststore to disk
FileOutputStream fos = new FileOutputStream("target/nifi.apache.org.ts.jks")
truststore.store(fos, "".chars)
}
@Test
@Ignore("Used to create passwordless truststore file for testing NIFI-6770")
void createLocalPasswordlessTruststore() {
KeyStore truststoreWithPassword = KeyStore.getInstance("JKS")
truststoreWithPassword.load(new FileInputStream("/Users/alopresto/Workspace/nifi/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/truststore.jks"), "passwordpassword".chars)
Certificate nodeCert = truststoreWithPassword.getCertificate("nifi-cert")
// Create a JKS truststore containing that cert as a trustedCertEntry and do not put a password on the truststore
KeyStore truststore = KeyStore.getInstance("JKS")
// Explicitly set the second parameter to empty to avoid a password
truststore.load(null, "".chars)
truststore.setCertificateEntry("nifi.apache.org", nodeCert)
// Save the truststore to disk
FileOutputStream fos = new FileOutputStream("/Users/alopresto/Workspace/nifi/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/truststore.no-password.jks")
truststore.store(fos, "".chars)
}
@Test
void testShouldValidateTlsConfigAndNewKeystoreTruststoreWithParams() {
// Assert
assert tlsConfiguration.getKeystorePath()
assert tlsConfiguration.getTruststorePath()
assert tlsConfiguration.getKeystoreType() == DEFAULT_STORE_TYPE
assert tlsConfiguration.getTruststoreType() == PKCS12_STORE_TYPE
assert tlsConfiguration.getKeystorePassword() == TEST_KEYSTORE_PASSWORD
}
@Test
void testShouldValidateTlsConfigAndNewKeystoreTruststoreWithoutParams() {
// Act
TlsConfiguration testTlsConfig = KeyStoreUtils.createTlsConfigAndNewKeystoreTruststore()
deleteKeystoreTruststore(testTlsConfig)
// Assert
assert testTlsConfig.getKeystorePath()
assert testTlsConfig.getKeyPassword() == testTlsConfig.getKeystorePassword()
assert testTlsConfig.getTruststorePassword()
assert testTlsConfig.getKeystoreType() == PKCS12_STORE_TYPE
assert testTlsConfig.getTruststoreType() == PKCS12_STORE_TYPE
}
@Test
void testShouldValidateTlsConfigWithoutKeyPasswordParam() {
// Arrange
TlsConfiguration testTlsConfigParam = new StandardTlsConfiguration(null, TEST_KEYSTORE_PASSWORD, null, DEFAULT_STORE_TYPE, null, TEST_TRUSTSTORE_PASSWORD, DEFAULT_STORE_TYPE)
// Act
final TlsConfiguration testTlsConfig = KeyStoreUtils.createTlsConfigAndNewKeystoreTruststore(testTlsConfigParam)
deleteKeystoreTruststore(testTlsConfig)
// Assert
assert testTlsConfig.getKeyPassword() == testTlsConfig.getKeystorePassword()
}
private static URL getKeystorePathAsUrl(String path) {
return new File(path).toURI().toURL()
}
private static void deleteKeystoreTruststore(TlsConfiguration tlsConfig) {
if (tlsConfig != null) {
try {
if (StringUtils.isNotBlank(tlsConfig.getKeystorePath())) {
Files.deleteIfExists(Paths.get(tlsConfig.getKeystorePath()))
}
} catch (IOException e) {
throw new IOException("There was an error deleting a keystore: ${e.getMessage()}, ${e}");
}
try {
if (StringUtils.isNotBlank(tlsConfig.getTruststorePath())) {
Files.deleteIfExists(Paths.get(tlsConfig.getTruststorePath()))
}
} catch (IOException e) {
throw new IOException("There was an error deleting a truststore: ${e.getMessage()}, ${e}");
}
}
}
}

View File

@ -19,6 +19,7 @@ package org.apache.nifi.security.util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
@ -28,6 +29,8 @@ import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.UUID;
import org.junit.BeforeClass;
import org.junit.Test;
@ -36,113 +39,87 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class KeyStoreUtilsTest {
public static final String SIGNING_ALGORITHM = "SHA256withRSA";
public static final int DURATION_DAYS = 365;
public static final char[] BAD_TEST_PASSWORD_DONT_USE_THIS = "changek".toCharArray();
public static final char[] BAD_KEY_STORE_TEST_PASSWORD_DONT_USE_THIS = "changes".toCharArray();
public static final String ALIAS = "alias";
private static KeyPair caCertKeyPair;
private static X509Certificate caCertificate;
private static KeyPair issuedCertificateKeyPair;
private static X509Certificate issuedCertificate;
private static final String SIGNING_ALGORITHM = "SHA256withRSA";
private static final int DURATION_DAYS = 365;
private static final char[] KEY_PASSWORD = UUID.randomUUID().toString().toCharArray();
private static final char[] STORE_PASSWORD = UUID.randomUUID().toString().toCharArray();
private static final String ALIAS = "alias";
private static final String KEY_ALGORITHM = "RSA";
private static final String SUBJECT_DN = "CN=localhost";
private static KeyPair keyPair;
private static X509Certificate certificate;
@BeforeClass
public static void generateKeysAndCertificates() throws NoSuchAlgorithmException, CertificateException {
caCertKeyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
issuedCertificateKeyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
caCertificate = CertificateUtils.generateSelfSignedX509Certificate(caCertKeyPair, "CN=testca,O=Apache,OU=NiFi", SIGNING_ALGORITHM, DURATION_DAYS);
issuedCertificate = CertificateUtils.generateIssuedCertificate("CN=testcert,O=Apache,OU=NiFi", issuedCertificateKeyPair.getPublic(), caCertificate, caCertKeyPair, SIGNING_ALGORITHM,
DURATION_DAYS);
keyPair = KeyPairGenerator.getInstance(KEY_ALGORITHM).generateKeyPair();
certificate = CertificateUtils.generateSelfSignedX509Certificate(keyPair, SUBJECT_DN, SIGNING_ALGORITHM, DURATION_DAYS);
}
@Test
public void testBcfksKeyStoreRoundTrip() throws GeneralSecurityException, IOException {
testKeyStoreRoundTrip(() -> KeyStoreUtils.getKeyStore(KeystoreType.BCFKS.toString().toLowerCase()));
public void testCreateTlsConfigAndNewKeystoreTruststore() throws GeneralSecurityException, IOException {
final TlsConfiguration configuration = KeyStoreUtils.createTlsConfigAndNewKeystoreTruststore();
final File keystoreFile = new File(configuration.getKeystorePath());
assertTrue("Keystore File not found", keystoreFile.exists());
keystoreFile.deleteOnExit();
final File truststoreFile = new File(configuration.getTruststorePath());
assertTrue("Truststore File not found", truststoreFile.exists());
truststoreFile.deleteOnExit();
assertEquals("Keystore Type not matched", KeystoreType.PKCS12, configuration.getKeystoreType());
assertEquals("Truststore Type not matched", KeystoreType.PKCS12, configuration.getTruststoreType());
assertTrue("Keystore not valid", KeyStoreUtils.isStoreValid(keystoreFile.toURI().toURL(), configuration.getKeystoreType(), configuration.getKeystorePassword().toCharArray()));
assertTrue("Truststore not valid", KeyStoreUtils.isStoreValid(truststoreFile.toURI().toURL(), configuration.getTruststoreType(), configuration.getTruststorePassword().toCharArray()));
}
@Test
public void testJksKeyStoreRoundTrip() throws GeneralSecurityException, IOException {
testKeyStoreRoundTrip(() -> KeyStoreUtils.getKeyStore(KeystoreType.JKS.toString().toLowerCase()));
public void testKeystoreTypesPrivateKeyEntry() throws GeneralSecurityException, IOException {
for (final KeystoreType keystoreType : KeystoreType.values()) {
final KeyStore sourceKeyStore = KeyStoreUtils.getKeyStore(keystoreType.getType());
final KeyStore destinationKeyStore = KeyStoreUtils.getKeyStore(keystoreType.getType());
assertKeyEntryStoredLoaded(sourceKeyStore, destinationKeyStore);
}
}
@Test
public void testPkcs12KeyStoreBcRoundTrip() throws GeneralSecurityException, IOException {
testKeyStoreRoundTrip(() -> KeyStoreUtils.getKeyStore(KeystoreType.PKCS12.toString().toLowerCase()));
public void testKeystoreTypesCertificateEntry() throws GeneralSecurityException, IOException {
for (final KeystoreType keystoreType : KeystoreType.values()) {
final KeyStore sourceKeyStore = KeyStoreUtils.getKeyStore(keystoreType.getType());
final KeyStore destinationKeyStore = KeyStoreUtils.getKeyStore(keystoreType.getType());
assertCertificateEntryStoredLoaded(sourceKeyStore, destinationKeyStore);
}
}
@Test
public void testPkcs12KeyStoreRoundTripBcReload() throws GeneralSecurityException, IOException {
// Pkcs12 Bouncy Castle needs same key and keystore password to interoperate with Java provider
testKeyStoreRoundTrip(() -> KeyStore.getInstance(KeystoreType.PKCS12.toString().toLowerCase()),
() -> KeyStoreUtils.getKeyStore(KeystoreType.PKCS12.toString().toLowerCase()), BAD_KEY_STORE_TEST_PASSWORD_DONT_USE_THIS);
private void assertCertificateEntryStoredLoaded(final KeyStore sourceKeyStore, final KeyStore destinationKeyStore) throws GeneralSecurityException, IOException {
sourceKeyStore.load(null, null);
sourceKeyStore.setCertificateEntry(ALIAS, certificate);
final KeyStore copiedKeyStore = copyKeyStore(sourceKeyStore, destinationKeyStore);
assertEquals(String.format("[%s] Certificate not matched", sourceKeyStore.getType()), certificate, copiedKeyStore.getCertificate(ALIAS));
}
@Test
public void testBcfksTrustStoreRoundTrip() throws GeneralSecurityException, IOException {
testTrustStoreRoundTrip(() -> KeyStoreUtils.getKeyStore(KeystoreType.BCFKS.toString().toLowerCase()));
private void assertKeyEntryStoredLoaded(final KeyStore sourceKeyStore, final KeyStore destinationKeyStore) throws GeneralSecurityException, IOException {
sourceKeyStore.load(null, null);
final Certificate[] certificateChain = new Certificate[]{certificate};
sourceKeyStore.setKeyEntry(ALIAS, keyPair.getPrivate(), KEY_PASSWORD, certificateChain);
final KeyStore copiedKeyStore = copyKeyStore(sourceKeyStore, destinationKeyStore);
final KeyStore.Entry entry = copiedKeyStore.getEntry(ALIAS, new KeyStore.PasswordProtection(KEY_PASSWORD));
assertTrue(String.format("[%s] Private Key entry not found", sourceKeyStore.getType()), entry instanceof KeyStore.PrivateKeyEntry);
final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) entry;
final Certificate[] entryCertificateChain = privateKeyEntry.getCertificateChain();
assertArrayEquals(String.format("[%s] Certificate Chain not matched", sourceKeyStore.getType()), certificateChain, entryCertificateChain);
assertEquals(String.format("[%s] Private Key not matched", sourceKeyStore.getType()), keyPair.getPrivate(), privateKeyEntry.getPrivateKey());
assertEquals(String.format("[%s] Public Key not matched", sourceKeyStore.getType()), keyPair.getPublic(), entryCertificateChain[0].getPublicKey());
}
@Test
public void testJksTrustStoreRoundTrip() throws GeneralSecurityException, IOException {
testTrustStoreRoundTrip(() -> KeyStoreUtils.getKeyStore(KeystoreType.JKS.toString().toLowerCase()));
}
private KeyStore copyKeyStore(final KeyStore sourceKeyStore, final KeyStore destinationKeyStore) throws GeneralSecurityException, IOException {
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
sourceKeyStore.store(byteArrayOutputStream, STORE_PASSWORD);
@Test
public void testPkcs12TrustStoreBcRoundTrip() throws GeneralSecurityException, IOException {
testTrustStoreRoundTrip(() -> KeyStoreUtils.getKeyStore(KeystoreType.PKCS12.toString().toLowerCase()));
}
@Test
public void testPkcs12TrustStoreRoundTripBcReload() throws GeneralSecurityException, IOException {
testTrustStoreRoundTrip(() -> KeyStore.getInstance(KeystoreType.PKCS12.toString().toLowerCase()), () -> KeyStoreUtils.getKeyStore(KeystoreType.PKCS12.toString().toLowerCase()));
}
private void testTrustStoreRoundTrip(KeyStoreSupplier keyStoreSupplier) throws GeneralSecurityException, IOException {
testTrustStoreRoundTrip(keyStoreSupplier, keyStoreSupplier);
}
private void testTrustStoreRoundTrip(KeyStoreSupplier initialKeyStoreSupplier, KeyStoreSupplier reloadKeyStoreSupplier) throws GeneralSecurityException, IOException {
KeyStore trustStore = initialKeyStoreSupplier.get();
trustStore.load(null, null);
trustStore.setCertificateEntry(ALIAS, caCertificate);
KeyStore roundTrip = roundTrip(trustStore, reloadKeyStoreSupplier);
assertEquals(caCertificate, roundTrip.getCertificate(ALIAS));
}
private void testKeyStoreRoundTrip(KeyStoreSupplier keyStoreSupplier) throws GeneralSecurityException, IOException {
testKeyStoreRoundTrip(keyStoreSupplier, keyStoreSupplier, BAD_TEST_PASSWORD_DONT_USE_THIS);
}
private void testKeyStoreRoundTrip(KeyStoreSupplier initialKeyStoreSupplier, KeyStoreSupplier reloadKeyStoreSupplier, char[] keyPassword) throws GeneralSecurityException, IOException {
KeyStore keyStore = initialKeyStoreSupplier.get();
keyStore.load(null, null);
keyStore.setKeyEntry(ALIAS, issuedCertificateKeyPair.getPrivate(), keyPassword, new Certificate[]{issuedCertificate, caCertificate});
KeyStore roundTrip = roundTrip(keyStore, reloadKeyStoreSupplier);
KeyStore.Entry entry = roundTrip.getEntry(ALIAS, new KeyStore.PasswordProtection(keyPassword));
assertTrue(entry instanceof KeyStore.PrivateKeyEntry);
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) entry;
Certificate[] certificateChain = privateKeyEntry.getCertificateChain();
assertArrayEquals(new Certificate[]{issuedCertificate, caCertificate}, certificateChain);
assertEquals(issuedCertificateKeyPair.getPrivate(), privateKeyEntry.getPrivateKey());
assertEquals(issuedCertificateKeyPair.getPublic(), certificateChain[0].getPublicKey());
}
private KeyStore roundTrip(KeyStore keyStore, KeyStoreSupplier keyStoreSupplier) throws GeneralSecurityException, IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
keyStore.store(byteArrayOutputStream, BAD_KEY_STORE_TEST_PASSWORD_DONT_USE_THIS);
KeyStore result = keyStoreSupplier.get();
result.load(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()), BAD_KEY_STORE_TEST_PASSWORD_DONT_USE_THIS);
return result;
}
private interface KeyStoreSupplier {
KeyStore get() throws GeneralSecurityException;
destinationKeyStore.load(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()), STORE_PASSWORD);
return destinationKeyStore;
}
}