From 04eb792ee726dbefc0f589e47c479542d33b388d Mon Sep 17 00:00:00 2001 From: Joe Gresock Date: Wed, 21 Apr 2021 15:09:39 -0400 Subject: [PATCH 1/2] NIFI-8403: Implementing auto-generated certificates for secure startup --- .../org/apache/nifi/bootstrap/RunNiFi.java | 20 +- .../bootstrap/util/SecureNiFiConfigUtil.java | 197 ++++++++++++++++ .../util/TestSecureNiFiConfigUtil.java | 220 ++++++++++++++++++ .../test/resources/nifi.properties.dns-sans | 43 ++++ .../test/resources/nifi.properties.ip-sans | 43 ++++ .../test/resources/nifi.properties.no-https | 39 ++++ .../nifi.properties.no-keystore-types | 39 ++++ .../resources/nifi.properties.no-keystores | 39 ++++ .../resources/nifi.properties.only-keystore | 39 ++++ .../resources/nifi.properties.only-truststore | 39 ++++ .../resources/nifi.properties.stores-exist | 39 ++++ .../test/resources/nifi.properties.success | 39 ++++ .../nifi/security/util/CertificateUtils.java | 31 ++- .../nifi/security/util/KeyStoreUtils.java | 38 ++- .../util/StandardTlsConfiguration.java | 41 +++- .../manager/StandardStateManagerProvider.java | 3 +- .../nifi-framework/nifi-resources/pom.xml | 6 + 17 files changed, 897 insertions(+), 18 deletions(-) create mode 100644 nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/util/SecureNiFiConfigUtil.java create mode 100644 nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/util/TestSecureNiFiConfigUtil.java create mode 100644 nifi-bootstrap/src/test/resources/nifi.properties.dns-sans create mode 100644 nifi-bootstrap/src/test/resources/nifi.properties.ip-sans create mode 100644 nifi-bootstrap/src/test/resources/nifi.properties.no-https create mode 100644 nifi-bootstrap/src/test/resources/nifi.properties.no-keystore-types create mode 100644 nifi-bootstrap/src/test/resources/nifi.properties.no-keystores create mode 100644 nifi-bootstrap/src/test/resources/nifi.properties.only-keystore create mode 100644 nifi-bootstrap/src/test/resources/nifi.properties.only-truststore create mode 100644 nifi-bootstrap/src/test/resources/nifi.properties.stores-exist create mode 100644 nifi-bootstrap/src/test/resources/nifi.properties.success diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/RunNiFi.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/RunNiFi.java index e6310f200514..dc213b5e025d 100644 --- a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/RunNiFi.java +++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/RunNiFi.java @@ -16,6 +16,14 @@ */ package org.apache.nifi.bootstrap; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.bootstrap.notification.NotificationType; +import org.apache.nifi.bootstrap.util.OSUtils; +import org.apache.nifi.bootstrap.util.SecureNiFiConfigUtil; +import org.apache.nifi.util.file.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; @@ -60,12 +68,6 @@ import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import org.apache.commons.lang3.StringUtils; -import org.apache.nifi.bootstrap.notification.NotificationType; -import org.apache.nifi.bootstrap.util.OSUtils; -import org.apache.nifi.util.file.FileUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** *

@@ -1110,6 +1112,12 @@ public boolean accept(final File dir, final String filename) { } } + try { + SecureNiFiConfigUtil.configureSecureNiFiProperties(nifiPropsFilename, cmdLogger); + } catch (IOException | RuntimeException e) { + cmdLogger.error("Self-Signed Certificate Generation Failed", e); + } + final NiFiListener listener = new NiFiListener(); final int listenPort = listener.start(this); diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/util/SecureNiFiConfigUtil.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/util/SecureNiFiConfigUtil.java new file mode 100644 index 000000000000..b1bb4f25d5de --- /dev/null +++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/util/SecureNiFiConfigUtil.java @@ -0,0 +1,197 @@ +/* + * 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.bootstrap.util; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.security.util.KeyStoreUtils; +import org.apache.nifi.security.util.StandardTlsConfiguration; +import org.apache.nifi.security.util.TlsConfiguration; +import org.apache.nifi.util.NiFiProperties; +import org.bouncycastle.util.IPAddress; +import org.slf4j.Logger; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.security.GeneralSecurityException; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; + +public class SecureNiFiConfigUtil { + + private static final int CERT_DURATION_DAYS = 60; + private static final String LOCALHOST_NAME = "localhost"; + private static final String PROPERTY_VALUE_PATTERN = "%s=%s"; + + private SecureNiFiConfigUtil() { + + } + + private static boolean fileExists(String filename) { + return StringUtils.isNotEmpty(filename) && Paths.get(filename).toFile().exists(); + } + + private static boolean isHttpsSecurityConfigured(final Properties nifiProperties) { + if (StringUtils.isEmpty(nifiProperties.getProperty(NiFiProperties.WEB_HTTPS_PORT, StringUtils.EMPTY))) { + return false; + } + + String keystorePath = nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE, StringUtils.EMPTY); + String truststorePath = nifiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE, StringUtils.EMPTY); + if (StringUtils.isEmpty(keystorePath) || StringUtils.isEmpty(truststorePath)) { + return false; + } + + return true; + } + + /** + * If HTTPS is enabled (nifi.web.https.port is set), but the keystore file specified in nifi.security.keystore + * does not exist, this will generate a key pair and self-signed certificate, generate the associated keystore + * and truststore and write them to disk under the configured filepaths, generate a secure random keystore password + * and truststore password, and write these to the nifi.properties file. + * @param nifiPropertiesFilename The filename of the nifi.properties file + * @param cmdLogger The bootstrap logger + * @throws IOException can be thrown when writing keystores to disk + * @throws RuntimeException indicates a security exception while generating keystores + */ + public static void configureSecureNiFiProperties(String nifiPropertiesFilename, Logger cmdLogger) throws IOException, RuntimeException { + final File propertiesFile = new File(nifiPropertiesFilename); + final Properties nifiProperties = loadProperties(propertiesFile); + + if (!isHttpsSecurityConfigured(nifiProperties)) { + cmdLogger.debug("No HTTPS configuration detected: skipping Apache Nifi certificate generation."); + return; + } + + String keystorePath = nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE, StringUtils.EMPTY); + String truststorePath = nifiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE, StringUtils.EMPTY); + boolean keystoreExists = fileExists(keystorePath); + boolean truststoreExists = fileExists(truststorePath); + + if (!keystoreExists && !truststoreExists) { + TlsConfiguration tlsConfiguration = null; + cmdLogger.info("Generating Self-Signed Certificate: Expires on {}", LocalDate.now().plus(CERT_DURATION_DAYS, ChronoUnit.DAYS)); + try { + String[] subjectAlternativeNames = getSubjectAlternativeNames(nifiProperties, cmdLogger); + tlsConfiguration = KeyStoreUtils.createTlsConfigAndNewKeystoreTruststore(StandardTlsConfiguration + .fromNiFiProperties(nifiProperties), CERT_DURATION_DAYS, subjectAlternativeNames); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + + // Move over the new stores from temp dir + Files.move(Paths.get(tlsConfiguration.getKeystorePath()), Paths.get(keystorePath), + StandardCopyOption.REPLACE_EXISTING); + Files.move(Paths.get(tlsConfiguration.getTruststorePath()), Paths.get(truststorePath), + StandardCopyOption.REPLACE_EXISTING); + + updateProperties(propertiesFile, tlsConfiguration); + + cmdLogger.debug("Generated Keystore [{}] Truststore [{}]", keystorePath, truststorePath); + } else if (!keystoreExists && truststoreExists) { + cmdLogger.warn("Truststore file {} already exists. Apache NiFi will not generate keystore and truststore separately.", + truststorePath); + } else if (keystoreExists && !truststoreExists) { + cmdLogger.warn("Keystore file {} already exists. Apache NiFi will not generate keystore and truststore separately.", + keystorePath); + } + } + + /** + * Attempts to add some reasonable guesses at desired SAN values that can be added to the generated + * certificate. + * @param nifiProperties The nifi.properties + * @return A Pair with IP SANs on the left and DNS SANs on the right + */ + private static String[] getSubjectAlternativeNames(Properties nifiProperties, Logger cmdLogger) { + Set dnsSubjectAlternativeNames = new HashSet<>(); + + try { + dnsSubjectAlternativeNames.add(InetAddress.getLocalHost().getHostName()); + } catch (UnknownHostException e) { + cmdLogger.debug("Could not add localhost hostname as certificate SAN", e); + } + addSubjectAlternativeName(nifiProperties, NiFiProperties.REMOTE_INPUT_HOST, dnsSubjectAlternativeNames); + addSubjectAlternativeName(nifiProperties, NiFiProperties.WEB_HTTPS_HOST, dnsSubjectAlternativeNames); + addSubjectAlternativeName(nifiProperties, NiFiProperties.WEB_PROXY_HOST, dnsSubjectAlternativeNames); + addSubjectAlternativeName(nifiProperties, NiFiProperties.LOAD_BALANCE_ADDRESS, dnsSubjectAlternativeNames); + + // Not necessary to add as a SAN + dnsSubjectAlternativeNames.remove(LOCALHOST_NAME); + + return dnsSubjectAlternativeNames.toArray(new String[dnsSubjectAlternativeNames.size()]); + } + + private static void addSubjectAlternativeName(Properties nifiProperties, String propertyName, + Set dnsSubjectAlternativeNames) { + String hostValue = nifiProperties.getProperty(propertyName, StringUtils.EMPTY); + if (!hostValue.isEmpty()) { + if (!IPAddress.isValid(hostValue)) { + dnsSubjectAlternativeNames.add(hostValue); + } + } + } + + private static String getPropertyLine(String name, String value) { + return String.format(PROPERTY_VALUE_PATTERN, name, value); + } + + private static void updateProperties(final File propertiesFile, final TlsConfiguration tlsConfiguration) throws IOException { + final Path propertiesFilePath = propertiesFile.toPath(); + final List lines = Files.readAllLines(propertiesFilePath); + final List updatedLines = lines.stream().map(line -> { + if (line.startsWith(NiFiProperties.SECURITY_KEYSTORE_PASSWD)) { + return getPropertyLine(NiFiProperties.SECURITY_KEYSTORE_PASSWD, tlsConfiguration.getKeystorePassword()); + } else if (line.startsWith(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD)) { + return getPropertyLine(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD, tlsConfiguration.getTruststorePassword()); + } else if (line.startsWith(NiFiProperties.SECURITY_KEY_PASSWD)) { + return getPropertyLine(NiFiProperties.SECURITY_KEY_PASSWD, tlsConfiguration.getKeystorePassword()); + } else if (line.startsWith(NiFiProperties.SECURITY_KEYSTORE_TYPE)) { + return getPropertyLine(NiFiProperties.SECURITY_KEYSTORE_TYPE, tlsConfiguration.getKeystoreType().getType()); + } else if (line.startsWith(NiFiProperties.SECURITY_TRUSTSTORE_TYPE)) { + return getPropertyLine(NiFiProperties.SECURITY_TRUSTSTORE_TYPE, tlsConfiguration.getTruststoreType().getType()); + } else { + return line; + } + }).collect(Collectors.toList()); + Files.write(propertiesFilePath, updatedLines); + } + + private static Properties loadProperties(final File propertiesFile) { + final Properties properties = new Properties(); + try (final FileReader reader = new FileReader(propertiesFile)) { + properties.load(reader); + } catch (final IOException e) { + final String message = String.format("Failed to read NiFi Properties [%s]", propertiesFile); + throw new UncheckedIOException(message, e); + } + return properties; + } +} diff --git a/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/util/TestSecureNiFiConfigUtil.java b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/util/TestSecureNiFiConfigUtil.java new file mode 100644 index 000000000000..313b7a2c7d14 --- /dev/null +++ b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/util/TestSecureNiFiConfigUtil.java @@ -0,0 +1,220 @@ +/* + * 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.bootstrap.util; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.nifi.properties.NiFiPropertiesLoader; +import org.apache.nifi.security.util.KeyStoreUtils; +import org.apache.nifi.security.util.StandardTlsConfiguration; +import org.apache.nifi.security.util.TlsConfiguration; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.util.StringUtils; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.util.IPAddress; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class TestSecureNiFiConfigUtil { + public static final String TEST_RESOURCE_DIR = "src/test/resources/"; + private Logger logger = LoggerFactory.getLogger("org.apache.nifi.bootstrap.util.TestSecureNiFiConfigUtil"); + + private static final String PROPERTIES_PREFIX = "nifi-properties"; + private static final boolean EXPECT_STORES_TO_EXIST = true; + + private Path nifiPropertiesFile; + private Path keystorePath; + private Path truststorePath; + private Path existingKeystorePath = getTestFilePath("existing-keystore.p12"); + private Path existingTruststorePath = getTestFilePath("existing-truststore.p12"); + + private NiFiProperties configureSecureNiFiProperties(Path testPropertiesFile) throws IOException { + Files.copy(testPropertiesFile, nifiPropertiesFile, StandardCopyOption.REPLACE_EXISTING); + SecureNiFiConfigUtil.configureSecureNiFiProperties(nifiPropertiesFile.toString(), logger); + + return new NiFiPropertiesLoader().load(nifiPropertiesFile.toFile()); + } + + private static Path getPathFromClasspath(String filename) { + try { + return Paths.get(TestSecureNiFiConfigUtil.class.getClassLoader().getResource(filename).toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + private static Path getTestFilePath(String filename) { + return Paths.get(TEST_RESOURCE_DIR + filename); + } + + private static String getFileHash(Path filepath) throws IOException { + return DigestUtils.sha256Hex(Files.readAllBytes(filepath)); + } + + @Before + public void init() throws IOException, GeneralSecurityException { + nifiPropertiesFile = Files.createTempFile(PROPERTIES_PREFIX, ".properties"); + TlsConfiguration tlsConfig = KeyStoreUtils.createTlsConfigAndNewKeystoreTruststore(new StandardTlsConfiguration()); + Files.move(Paths.get(tlsConfig.getKeystorePath()), existingKeystorePath, StandardCopyOption.REPLACE_EXISTING); + Files.move(Paths.get(tlsConfig.getTruststorePath()), existingTruststorePath, StandardCopyOption.REPLACE_EXISTING); + } + + @After + public void cleanUp() throws IOException { + deleteIfExists(nifiPropertiesFile); + + deleteIfExists(keystorePath); + deleteIfExists(truststorePath); + deleteIfExists(existingKeystorePath); + deleteIfExists(existingTruststorePath); + } + + private static void deleteIfExists(Path path) throws IOException { + if (path != null && StringUtils.isNotEmpty(path.toString())) { + Files.deleteIfExists(path); + } + } + + private void runTestWithExpectedSuccess(String testPropertiesFile, List expectedSANs) throws IOException, GeneralSecurityException { + Path testPropertiesFilePath = getPathFromClasspath(testPropertiesFile); + NiFiProperties niFiProperties = this.configureSecureNiFiProperties(testPropertiesFilePath); + + keystorePath = Paths.get(niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE)); + Assert.assertTrue(keystorePath.toFile().exists()); + Assert.assertFalse(niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD).isEmpty()); + Assert.assertEquals("PKCS12", niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE)); + + char[] keyPassword = niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD).toCharArray(); + KeyStore keyStore = KeyStoreUtils.loadKeyStore(keystorePath.toString(), + niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD).toCharArray(), + niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE)); + String alias = keyStore.aliases().nextElement(); + Assert.assertTrue(keyStore.isKeyEntry(alias)); + Key key = keyStore.getKey(alias, keyPassword); + Assert.assertNotNull(key); + + truststorePath = Paths.get(niFiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE)); + Assert.assertTrue(truststorePath.toFile().exists()); + Assert.assertFalse(niFiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD).isEmpty()); + Assert.assertEquals("PKCS12", niFiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE)); + + KeyStore trustStore = KeyStoreUtils.loadKeyStore(truststorePath.toString(), + niFiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD).toCharArray(), + niFiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE)); + String trustAlias = trustStore.aliases().nextElement(); + Assert.assertTrue(trustStore.isCertificateEntry(trustAlias)); + Certificate certificate = trustStore.getCertificate(trustAlias); + certificate.verify(certificate.getPublicKey()); + + if (!expectedSANs.isEmpty()) { + Collection> sans = ((X509Certificate)certificate).getSubjectAlternativeNames(); + Set foundSands = new HashSet<>(); + for(List list : sans) { + String san = (String) list.get(1); + if (IPAddress.isValid(san)) { + Assert.assertEquals(GeneralName.iPAddress, list.get(0)); + } else { + Assert.assertEquals(GeneralName.dNSName, list.get(0)); + } + foundSands.add((String) list.get(1)); + } + for(String expectedSAN : expectedSANs) { + Assert.assertTrue(foundSands.contains(expectedSAN)); + } + } + } + + private void runTestWithNoExpectedUpdates(String testPropertiesFile, boolean expectBothStoresToExist) throws IOException, GeneralSecurityException { + this.runTestWithNoExpectedUpdates(testPropertiesFile, expectBothStoresToExist, expectBothStoresToExist); + } + + private void runTestWithNoExpectedUpdates(String testPropertiesFile, boolean expectKeystoreToExist, boolean expectTruststoreToExist) + throws IOException, GeneralSecurityException { + Path testPropertiesFilePath = getPathFromClasspath(testPropertiesFile); + NiFiProperties niFiProperties = this.configureSecureNiFiProperties(testPropertiesFilePath); + keystorePath = Paths.get(niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE)); + truststorePath = Paths.get(niFiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE)); + + Assert.assertEquals(expectKeystoreToExist, Paths.get(niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE)).toFile().exists()); + Assert.assertEquals(expectTruststoreToExist, Paths.get(niFiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE)).toFile().exists()); + + // Show that nifi.properties was not updated + Assert.assertEquals(getFileHash(nifiPropertiesFile), getFileHash(testPropertiesFilePath)); + } + + @Test + public void testSuccessfulConfiguration() throws IOException, GeneralSecurityException { + runTestWithExpectedSuccess("nifi.properties.success", Collections.EMPTY_LIST); + } + + @Test + public void testSuccessfulDNSSANs() throws IOException, GeneralSecurityException { + runTestWithExpectedSuccess("nifi.properties.dns-sans", + Arrays.asList(new String[] {"test-host", "remote-host", "proxy-host", "cluster-host"})); + } + + @Test + public void testNoHttps() throws IOException, GeneralSecurityException { + runTestWithNoExpectedUpdates("nifi.properties.no-https", !EXPECT_STORES_TO_EXIST); + } + + @Test + public void testNoKeystores() throws IOException, GeneralSecurityException { + runTestWithNoExpectedUpdates("nifi.properties.no-keystores", !EXPECT_STORES_TO_EXIST); + } + + @Test + public void test_keystoreAndTruststoreAlreadyExist() throws IOException, GeneralSecurityException { + runTestWithNoExpectedUpdates("nifi.properties.stores-exist", EXPECT_STORES_TO_EXIST); + } + + @Test + public void testNoKeystoresTypes() throws IOException, GeneralSecurityException { + runTestWithExpectedSuccess("nifi.properties.no-keystore-types", Collections.EMPTY_LIST); + } + + @Test + public void testFailure_onlyTruststoreExists() throws IOException, GeneralSecurityException { + runTestWithNoExpectedUpdates("nifi.properties.only-truststore", !EXPECT_STORES_TO_EXIST, EXPECT_STORES_TO_EXIST); + } + + @Test + public void testFailure_onlyKeystoreExists() throws IOException, GeneralSecurityException { + runTestWithNoExpectedUpdates("nifi.properties.only-keystore", EXPECT_STORES_TO_EXIST, !EXPECT_STORES_TO_EXIST); + } +} diff --git a/nifi-bootstrap/src/test/resources/nifi.properties.dns-sans b/nifi-bootstrap/src/test/resources/nifi.properties.dns-sans new file mode 100644 index 000000000000..3ad1c9fb1b3f --- /dev/null +++ b/nifi-bootstrap/src/test/resources/nifi.properties.dns-sans @@ -0,0 +1,43 @@ +# 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. + +# Core Properties # +nifi.flow.configuration.file=./target/flow.xml.gz + +# Removing most properties for testing... + +nifi.remote.input.host=remote-host +nifi.web.proxy.host=proxy-host +nifi.cluster.load.balance.address=cluster-host + +# web properties # +nifi.web.http.host= +nifi.web.http.port= +nifi.web.https.host=test-host +nifi.web.https.port=8443 + +# security properties # +nifi.sensitive.props.key=key +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC + +nifi.security.keystore=./src/test/resources/keystore.p12 +nifi.security.keystoreType=PKCS12 +nifi.security.keystorePasswd= +nifi.security.keyPasswd= +nifi.security.truststore=./src/test/resources/truststore.p12 +nifi.security.truststoreType=PKCS12 +nifi.security.truststorePasswd= +nifi.security.user.authorizer= \ No newline at end of file diff --git a/nifi-bootstrap/src/test/resources/nifi.properties.ip-sans b/nifi-bootstrap/src/test/resources/nifi.properties.ip-sans new file mode 100644 index 000000000000..8a759e52e109 --- /dev/null +++ b/nifi-bootstrap/src/test/resources/nifi.properties.ip-sans @@ -0,0 +1,43 @@ +# 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. + +# Core Properties # +nifi.flow.configuration.file=./target/flow.xml.gz + +# Removing most properties for testing... + +nifi.remote.input.host=1.2.3.4 +nifi.web.proxy.host=1.2.3.5 +nifi.cluster.load.balance.address=1.2.3.6 + +# web properties # +nifi.web.http.host= +nifi.web.http.port= +nifi.web.https.host=1.2.3.4 +nifi.web.https.port=8443 + +# security properties # +nifi.sensitive.props.key=key +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC + +nifi.security.keystore=./src/test/resources/keystore.p12 +nifi.security.keystoreType=PKCS12 +nifi.security.keystorePasswd= +nifi.security.keyPasswd= +nifi.security.truststore=./src/test/resources/truststore.p12 +nifi.security.truststoreType=PKCS12 +nifi.security.truststorePasswd= +nifi.security.user.authorizer= \ No newline at end of file diff --git a/nifi-bootstrap/src/test/resources/nifi.properties.no-https b/nifi-bootstrap/src/test/resources/nifi.properties.no-https new file mode 100644 index 000000000000..0faf0b84f226 --- /dev/null +++ b/nifi-bootstrap/src/test/resources/nifi.properties.no-https @@ -0,0 +1,39 @@ +# 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. + +# Core Properties # +nifi.flow.configuration.file=./target/flow.xml.gz + +# Removing most properties for testing... + +# web properties # +nifi.web.http.host= +nifi.web.http.port=8080 +nifi.web.https.host= +nifi.web.https.port= + +# security properties # +nifi.sensitive.props.key=key +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC + +nifi.security.keystore=./src/test/resources/keystore.p12 +nifi.security.keystoreType=PKCS12 +nifi.security.keystorePasswd= +nifi.security.keyPasswd= +nifi.security.truststore=./src/test/resources/truststore.p12 +nifi.security.truststoreType=PKCS12 +nifi.security.truststorePasswd= +nifi.security.user.authorizer= \ No newline at end of file diff --git a/nifi-bootstrap/src/test/resources/nifi.properties.no-keystore-types b/nifi-bootstrap/src/test/resources/nifi.properties.no-keystore-types new file mode 100644 index 000000000000..fe2d13bf8b78 --- /dev/null +++ b/nifi-bootstrap/src/test/resources/nifi.properties.no-keystore-types @@ -0,0 +1,39 @@ +# 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. + +# Core Properties # +nifi.flow.configuration.file=./target/flow.xml.gz + +# Removing most properties for testing... + +# web properties # +nifi.web.http.host= +nifi.web.http.port= +nifi.web.https.host= +nifi.web.https.port=8443 + +# security properties # +nifi.sensitive.props.key=key +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC + +nifi.security.keystore=./src/test/resources/keystore.p12 +nifi.security.keystoreType= +nifi.security.keystorePasswd= +nifi.security.keyPasswd= +nifi.security.truststore=./src/test/resources/truststore.p12 +nifi.security.truststoreType= +nifi.security.truststorePasswd= +nifi.security.user.authorizer= \ No newline at end of file diff --git a/nifi-bootstrap/src/test/resources/nifi.properties.no-keystores b/nifi-bootstrap/src/test/resources/nifi.properties.no-keystores new file mode 100644 index 000000000000..87d608a5dd32 --- /dev/null +++ b/nifi-bootstrap/src/test/resources/nifi.properties.no-keystores @@ -0,0 +1,39 @@ +# 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. + +# Core Properties # +nifi.flow.configuration.file=./target/flow.xml.gz + +# Removing most properties for testing... + +# web properties # +nifi.web.http.host= +nifi.web.http.port= +nifi.web.https.host= +nifi.web.https.port=8443 + +# security properties # +nifi.sensitive.props.key=key +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC + +nifi.security.keystore= +nifi.security.keystoreType=PKCS12 +nifi.security.keystorePasswd= +nifi.security.keyPasswd= +nifi.security.truststore= +nifi.security.truststoreType=PKCS12 +nifi.security.truststorePasswd= +nifi.security.user.authorizer= \ No newline at end of file diff --git a/nifi-bootstrap/src/test/resources/nifi.properties.only-keystore b/nifi-bootstrap/src/test/resources/nifi.properties.only-keystore new file mode 100644 index 000000000000..e4028872f744 --- /dev/null +++ b/nifi-bootstrap/src/test/resources/nifi.properties.only-keystore @@ -0,0 +1,39 @@ +# 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. + +# Core Properties # +nifi.flow.configuration.file=./target/flow.xml.gz + +# Removing most properties for testing... + +# web properties # +nifi.web.http.host= +nifi.web.http.port= +nifi.web.https.host= +nifi.web.https.port=8443 + +# security properties # +nifi.sensitive.props.key=key +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC + +nifi.security.keystore=./src/test/resources/existing-keystore.p12 +nifi.security.keystoreType=PKCS12 +nifi.security.keystorePasswd= +nifi.security.keyPasswd= +nifi.security.truststore=./src/test/resources/truststore.p12 +nifi.security.truststoreType=PKCS12 +nifi.security.truststorePasswd= +nifi.security.user.authorizer= \ No newline at end of file diff --git a/nifi-bootstrap/src/test/resources/nifi.properties.only-truststore b/nifi-bootstrap/src/test/resources/nifi.properties.only-truststore new file mode 100644 index 000000000000..2867c8842bb0 --- /dev/null +++ b/nifi-bootstrap/src/test/resources/nifi.properties.only-truststore @@ -0,0 +1,39 @@ +# 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. + +# Core Properties # +nifi.flow.configuration.file=./target/flow.xml.gz + +# Removing most properties for testing... + +# web properties # +nifi.web.http.host= +nifi.web.http.port= +nifi.web.https.host= +nifi.web.https.port=8443 + +# security properties # +nifi.sensitive.props.key=key +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC + +nifi.security.keystore=./src/test/resources/keystore.p12 +nifi.security.keystoreType=PKCS12 +nifi.security.keystorePasswd= +nifi.security.keyPasswd= +nifi.security.truststore=./src/test/resources/existing-truststore.p12 +nifi.security.truststoreType=PKCS12 +nifi.security.truststorePasswd= +nifi.security.user.authorizer= \ No newline at end of file diff --git a/nifi-bootstrap/src/test/resources/nifi.properties.stores-exist b/nifi-bootstrap/src/test/resources/nifi.properties.stores-exist new file mode 100644 index 000000000000..f653cd7b995d --- /dev/null +++ b/nifi-bootstrap/src/test/resources/nifi.properties.stores-exist @@ -0,0 +1,39 @@ +# 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. + +# Core Properties # +nifi.flow.configuration.file=./target/flow.xml.gz + +# Removing most properties for testing... + +# web properties # +nifi.web.http.host= +nifi.web.http.port= +nifi.web.https.host= +nifi.web.https.port=8443 + +# security properties # +nifi.sensitive.props.key=key +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC + +nifi.security.keystore=./src/test/resources/existing-keystore.p12 +nifi.security.keystoreType=PKCS12 +nifi.security.keystorePasswd= +nifi.security.keyPasswd= +nifi.security.truststore=./src/test/resources/existing-truststore.p12 +nifi.security.truststoreType=PKCS12 +nifi.security.truststorePasswd= +nifi.security.user.authorizer= \ No newline at end of file diff --git a/nifi-bootstrap/src/test/resources/nifi.properties.success b/nifi-bootstrap/src/test/resources/nifi.properties.success new file mode 100644 index 000000000000..159007c97ff8 --- /dev/null +++ b/nifi-bootstrap/src/test/resources/nifi.properties.success @@ -0,0 +1,39 @@ +# 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. + +# Core Properties # +nifi.flow.configuration.file=./target/flow.xml.gz + +# Removing most properties for testing... + +# web properties # +nifi.web.http.host= +nifi.web.http.port= +nifi.web.https.host= +nifi.web.https.port=8443 + +# security properties # +nifi.sensitive.props.key=key +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC + +nifi.security.keystore=./src/test/resources/keystore.p12 +nifi.security.keystoreType=PKCS12 +nifi.security.keystorePasswd= +nifi.security.keyPasswd= +nifi.security.truststore=./src/test/resources/truststore.p12 +nifi.security.truststoreType=PKCS12 +nifi.security.truststorePasswd= +nifi.security.user.authorizer= \ No newline at end of file diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java index 2f2ec87d6b81..d386e0f0fce2 100644 --- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java +++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java @@ -466,6 +466,23 @@ protected static synchronized BigInteger getUniqueSerialNumber() { */ public static X509Certificate generateSelfSignedX509Certificate(KeyPair keyPair, String dn, String signingAlgorithm, int certificateDurationDays) throws CertificateException { + return generateSelfSignedX509Certificate(keyPair, dn, signingAlgorithm, certificateDurationDays, null); + } + + /** + * Generates a self-signed {@link X509Certificate} suitable for use as a Certificate Authority. + * + * @param keyPair the {@link KeyPair} to generate the {@link X509Certificate} for + * @param dn the distinguished name to user for the {@link X509Certificate} + * @param signingAlgorithm the signing algorithm to use for the {@link X509Certificate} + * @param certificateDurationDays the duration in days for which the {@link X509Certificate} should be valid + * @param dnsSubjectAlternativeNames An optional array of dnsName SANs + * @return a self-signed {@link X509Certificate} suitable for use as a Certificate Authority + * @throws CertificateException if there is an generating the new certificate + */ + public static X509Certificate generateSelfSignedX509Certificate(KeyPair keyPair, String dn, String signingAlgorithm, int certificateDurationDays, + String[] dnsSubjectAlternativeNames) + throws CertificateException { try { ContentSigner sigGen = new JcaContentSignerBuilder(signingAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate()); SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()); @@ -495,8 +512,20 @@ public static X509Certificate generateSelfSignedX509Certificate(KeyPair keyPair, // (3) subjectAlternativeName extension. Include CN as a SAN entry if it exists. final String cn = getCommonName(dn); + List generalNames = new ArrayList<>(); if (StringUtils.isNotBlank(cn)) { - certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(new GeneralName(GeneralName.dNSName, cn))); + generalNames.add(new GeneralName(GeneralName.dNSName, cn)); + } + if (dnsSubjectAlternativeNames != null) { + for (String subjectAlternativeName : dnsSubjectAlternativeNames) { + if (StringUtils.isNotBlank(subjectAlternativeName)) { + generalNames.add(new GeneralName(GeneralName.dNSName, subjectAlternativeName)); + } + } + } + if (!generalNames.isEmpty()) { + certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(generalNames.toArray( + new GeneralName[generalNames.size()]))); } // Sign the certificate diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/KeyStoreUtils.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/KeyStoreUtils.java index 22f53651a498..ddee951610a6 100644 --- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/KeyStoreUtils.java +++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/KeyStoreUtils.java @@ -154,11 +154,25 @@ public static TlsConfiguration createTlsConfigAndNewKeystoreTruststore() throws /** * Creates a temporary Keystore and Truststore and returns it wrapped in a new TLS configuration with the given values. + * Specifies an expiration duration of 365 days. * * @param tlsConfiguration a {@link org.apache.nifi.security.util.TlsConfiguration} * @return a {@link org.apache.nifi.security.util.TlsConfiguration} */ public static TlsConfiguration createTlsConfigAndNewKeystoreTruststore(final TlsConfiguration tlsConfiguration) throws IOException, GeneralSecurityException { + return createTlsConfigAndNewKeystoreTruststore(tlsConfiguration, CERT_DURATION_DAYS, null); + } + + /** + * Creates a temporary Keystore and Truststore and returns it wrapped in a new TLS configuration with the given values. + * + * @param tlsConfiguration a {@link org.apache.nifi.security.util.TlsConfiguration} + * @param certDurationDays The number of days the cert should be valid + * @param dnsSubjectAlternativeNames An optional array of dnsName SANs + * @return a {@link org.apache.nifi.security.util.TlsConfiguration} + */ + public static TlsConfiguration createTlsConfigAndNewKeystoreTruststore(final TlsConfiguration tlsConfiguration, int certDurationDays, + String[] dnsSubjectAlternativeNames) throws IOException, GeneralSecurityException { final Path keyStorePath; final String keystorePassword = StringUtils.isNotBlank(tlsConfiguration.getKeystorePassword()) ? tlsConfiguration.getKeystorePassword() : generatePassword(); final KeystoreType keystoreType = tlsConfiguration.getKeystoreType() != null ? tlsConfiguration.getKeystoreType() : KeystoreType.PKCS12; @@ -184,7 +198,8 @@ public static TlsConfiguration createTlsConfigAndNewKeystoreTruststore(final Tls } // Create X509 Certificate - final X509Certificate clientCert = createKeyStoreAndGetX509Certificate(KEY_ALIAS, keystorePassword, keyPassword, keyStorePath.toString(), keystoreType); + final X509Certificate clientCert = createKeyStoreAndGetX509Certificate(KEY_ALIAS, keystorePassword, keyPassword, + keyStorePath.toString(), keystoreType, certDurationDays, dnsSubjectAlternativeNames); // Create Truststore createTrustStore(clientCert, CERT_ALIAS, truststorePassword, trustStorePath.toString(), truststoreType); @@ -459,12 +474,31 @@ public static String sslServerSocketToString(SSLServerSocket sslServerSocket) { private static X509Certificate createKeyStoreAndGetX509Certificate( final String alias, final String keyStorePassword, final String keyPassword, final String keyStorePath, final KeystoreType keyStoreType) throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException { + return createKeyStoreAndGetX509Certificate(alias, keyStorePassword, keyPassword, keyStorePath, keyStoreType, CERT_DURATION_DAYS, + null); + } + /** + * Loads the Keystore and returns a X509 Certificate with the given values. + * + * @param alias the certificate alias + * @param keyStorePassword the keystore password + * @param keyPassword the key password + * @param keyStorePath the keystore path + * @param keyStoreType the keystore type + * @param dnsSubjectAlternativeNames An optional array of dnsName SANs + * @param certDurationDays the duration of the validity of the certificate, in days + * @return a {@link X509Certificate} + */ + private static X509Certificate createKeyStoreAndGetX509Certificate( + final String alias, final String keyStorePassword, final String keyPassword, final String keyStorePath, + final KeystoreType keyStoreType, int certDurationDays, String[] dnsSubjectAlternativeNames) + throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException { try (final FileOutputStream outputStream = new FileOutputStream(keyStorePath)) { final KeyPair keyPair = KeyPairGenerator.getInstance(KEY_ALGORITHM).generateKeyPair(); final X509Certificate selfSignedCert = CertificateUtils.generateSelfSignedX509Certificate( - keyPair, CERT_DN, SIGNING_ALGORITHM, CERT_DURATION_DAYS + keyPair, CERT_DN, SIGNING_ALGORITHM, certDurationDays, dnsSubjectAlternativeNames ); final KeyStore keyStore = loadEmptyKeyStore(keyStoreType); diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/StandardTlsConfiguration.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/StandardTlsConfiguration.java index 3fc386dec6ec..4b731d4bf505 100644 --- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/StandardTlsConfiguration.java +++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/StandardTlsConfiguration.java @@ -16,16 +16,18 @@ */ package org.apache.nifi.security.util; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.File; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; -import org.apache.nifi.util.NiFiProperties; -import org.apache.nifi.util.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.util.Properties; /** @@ -177,10 +179,8 @@ public StandardTlsConfiguration(TlsConfiguration other) { * @param niFiProperties the NiFi properties * @return a populated TlsConfiguration container object */ - public static StandardTlsConfiguration fromNiFiProperties(NiFiProperties niFiProperties) { - if (niFiProperties == null) { - throw new IllegalArgumentException("The NiFi properties cannot be null"); - } + public static TlsConfiguration fromNiFiProperties(NiFiProperties niFiProperties) { + Objects.requireNonNull("The NiFi properties cannot be null"); String keystorePath = niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE); String keystorePassword = niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD); @@ -203,6 +203,31 @@ public static StandardTlsConfiguration fromNiFiProperties(NiFiProperties niFiPro return tlsConfiguration; } + /** + * Returns a {@link org.apache.nifi.security.util.TlsConfiguration} instantiated from the relevant {@link NiFiProperties} properties. + * + * @param niFiProperties the NiFi properties, as a simple java.util.Properties object + * @return a populated TlsConfiguration container object + */ + public static TlsConfiguration fromNiFiProperties(Properties niFiProperties) { + Objects.requireNonNull("The NiFi properties cannot be null"); + + String keystorePath = niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE); + String keystorePassword = niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD); + String keyPassword = niFiProperties.getProperty(NiFiProperties.SECURITY_KEY_PASSWD); + String keystoreType = niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE); + String truststorePath = niFiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE); + String truststorePassword = niFiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD); + String truststoreType = niFiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE); + String protocol = TLS_PROTOCOL_VERSION; + + final StandardTlsConfiguration tlsConfiguration = new StandardTlsConfiguration(keystorePath, keystorePassword, keyPassword, + keystoreType, truststorePath, truststorePassword, + truststoreType, protocol); + + return tlsConfiguration; + } + /** * Returns a {@link org.apache.nifi.security.util.TlsConfiguration} instantiated * from the relevant {@link NiFiProperties} properties for the truststore diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/state/manager/StandardStateManagerProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/state/manager/StandardStateManagerProvider.java index e7e95f8eb8d4..dd16686d05c3 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/state/manager/StandardStateManagerProvider.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/state/manager/StandardStateManagerProvider.java @@ -53,6 +53,7 @@ import org.apache.nifi.registry.VariableRegistry; import org.apache.nifi.security.util.SslContextFactory; import org.apache.nifi.security.util.StandardTlsConfiguration; +import org.apache.nifi.security.util.TlsConfiguration; import org.apache.nifi.security.util.TlsException; import org.apache.nifi.util.NiFiProperties; import org.slf4j.Logger; @@ -211,7 +212,7 @@ private static StateProvider createStateProvider(final File configFile, final Sc } final SSLContext sslContext; - StandardTlsConfiguration standardTlsConfiguration = StandardTlsConfiguration.fromNiFiProperties(properties); + TlsConfiguration standardTlsConfiguration = StandardTlsConfiguration.fromNiFiProperties(properties); try { sslContext = SslContextFactory.createSslContext(standardTlsConfiguration); } catch (TlsException e) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml index 0dc8dad02c05..43eed6158ecb 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml @@ -148,10 +148,16 @@ true + + From 5ee5f9e76d08114473eb25be748bd60d30556b8b Mon Sep 17 00:00:00 2001 From: Joe Gresock Date: Fri, 23 Apr 2021 13:54:41 -0400 Subject: [PATCH 2/2] Adding check for passwords in SecureNiFiConfigUtil --- .../bootstrap/util/SecureNiFiConfigUtil.java | 21 ++++++++-- .../util/TestSecureNiFiConfigUtil.java | 10 +++++ .../nifi.properties.keystore-password | 39 +++++++++++++++++++ .../nifi.properties.truststore-password | 39 +++++++++++++++++++ 4 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 nifi-bootstrap/src/test/resources/nifi.properties.keystore-password create mode 100644 nifi-bootstrap/src/test/resources/nifi.properties.truststore-password diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/util/SecureNiFiConfigUtil.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/util/SecureNiFiConfigUtil.java index b1bb4f25d5de..10d825f97e31 100644 --- a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/util/SecureNiFiConfigUtil.java +++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/util/SecureNiFiConfigUtil.java @@ -57,7 +57,16 @@ private static boolean fileExists(String filename) { return StringUtils.isNotEmpty(filename) && Paths.get(filename).toFile().exists(); } - private static boolean isHttpsSecurityConfigured(final Properties nifiProperties) { + /** + * Returns true only if nifi.web.https.port is set, nifi.security.keystore and nifi.security.truststore + * are both set, and both nifi.security.keystorePasswd and nifi.security.truststorePassword are NOT set. + * + * This would indicate that the user intends to auto-generate a keystore and truststore, rather than + * using their existing kestore and truststore. + * @param nifiProperties The nifi properties + * @return + */ + private static boolean isHttpsSecurityConfiguredWithEmptyPasswords(final Properties nifiProperties) { if (StringUtils.isEmpty(nifiProperties.getProperty(NiFiProperties.WEB_HTTPS_PORT, StringUtils.EMPTY))) { return false; } @@ -68,6 +77,12 @@ private static boolean isHttpsSecurityConfigured(final Properties nifiProperties return false; } + String keystorePassword = nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD, StringUtils.EMPTY); + String truststorePassword = nifiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD, StringUtils.EMPTY); + if (StringUtils.isNotEmpty(keystorePassword) || StringUtils.isNotEmpty(truststorePassword)) { + return false; + } + return true; } @@ -85,8 +100,8 @@ public static void configureSecureNiFiProperties(String nifiPropertiesFilename, final File propertiesFile = new File(nifiPropertiesFilename); final Properties nifiProperties = loadProperties(propertiesFile); - if (!isHttpsSecurityConfigured(nifiProperties)) { - cmdLogger.debug("No HTTPS configuration detected: skipping Apache Nifi certificate generation."); + if (!isHttpsSecurityConfiguredWithEmptyPasswords(nifiProperties)) { + cmdLogger.debug("Skipping Apache Nifi certificate generation."); return; } diff --git a/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/util/TestSecureNiFiConfigUtil.java b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/util/TestSecureNiFiConfigUtil.java index 313b7a2c7d14..c11c293a2bd1 100644 --- a/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/util/TestSecureNiFiConfigUtil.java +++ b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/util/TestSecureNiFiConfigUtil.java @@ -198,6 +198,16 @@ public void testNoKeystores() throws IOException, GeneralSecurityException { runTestWithNoExpectedUpdates("nifi.properties.no-keystores", !EXPECT_STORES_TO_EXIST); } + @Test + public void testTruststorePasswordSet() throws IOException, GeneralSecurityException { + runTestWithNoExpectedUpdates("nifi.properties.truststore-password", !EXPECT_STORES_TO_EXIST); + } + + @Test + public void testKeystorePasswordSet() throws IOException, GeneralSecurityException { + runTestWithNoExpectedUpdates("nifi.properties.keystore-password", !EXPECT_STORES_TO_EXIST); + } + @Test public void test_keystoreAndTruststoreAlreadyExist() throws IOException, GeneralSecurityException { runTestWithNoExpectedUpdates("nifi.properties.stores-exist", EXPECT_STORES_TO_EXIST); diff --git a/nifi-bootstrap/src/test/resources/nifi.properties.keystore-password b/nifi-bootstrap/src/test/resources/nifi.properties.keystore-password new file mode 100644 index 000000000000..e6db337d6bf9 --- /dev/null +++ b/nifi-bootstrap/src/test/resources/nifi.properties.keystore-password @@ -0,0 +1,39 @@ +# 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. + +# Core Properties # +nifi.flow.configuration.file=./target/flow.xml.gz + +# Removing most properties for testing... + +# web properties # +nifi.web.http.host= +nifi.web.http.port= +nifi.web.https.host= +nifi.web.https.port=8443 + +# security properties # +nifi.sensitive.props.key=key +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC + +nifi.security.keystore=./src/test/resources/keystore.p12 +nifi.security.keystoreType=PKCS12 +nifi.security.keystorePasswd=12345 +nifi.security.keyPasswd= +nifi.security.truststore=./src/test/resources/truststore.p12 +nifi.security.truststoreType=PKCS12 +nifi.security.truststorePasswd= +nifi.security.user.authorizer= \ No newline at end of file diff --git a/nifi-bootstrap/src/test/resources/nifi.properties.truststore-password b/nifi-bootstrap/src/test/resources/nifi.properties.truststore-password new file mode 100644 index 000000000000..8deecc973ebf --- /dev/null +++ b/nifi-bootstrap/src/test/resources/nifi.properties.truststore-password @@ -0,0 +1,39 @@ +# 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. + +# Core Properties # +nifi.flow.configuration.file=./target/flow.xml.gz + +# Removing most properties for testing... + +# web properties # +nifi.web.http.host= +nifi.web.http.port= +nifi.web.https.host= +nifi.web.https.port=8443 + +# security properties # +nifi.sensitive.props.key=key +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC + +nifi.security.keystore=./src/test/resources/keystore.p12 +nifi.security.keystoreType=PKCS12 +nifi.security.keystorePasswd= +nifi.security.keyPasswd= +nifi.security.truststore=./src/test/resources/truststore.p12 +nifi.security.truststoreType=PKCS12 +nifi.security.truststorePasswd=12345 +nifi.security.user.authorizer= \ No newline at end of file