You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by al...@apache.org on 2018/08/07 19:07:49 UTC

nifi git commit: NIFI-5476 Added logic to check CA certificate signature against additional certificates. Moved utility code to TlsHelper. Added unit tests. Added command-line parsing for additional CA certificate path. Added documentation on using the T

Repository: nifi
Updated Branches:
  refs/heads/master 8106af699 -> 57baae9ae


NIFI-5476 Added logic to check CA certificate signature against additional certificates.
Moved utility code to TlsHelper.
Added unit tests.
Added command-line parsing for additional CA certificate path.
Added documentation on using the TLS Toolkit to generate and sign certificates using an externally-signed CA.
Updated toolkit external CA documentation to be inline with additional context from NIFI-5473.
Cleaned up toolkit documentation.
Improved error message by changing to absolute path.
Added Javadoc to and removed unthrown exception declarations from TlsHelper#verifyCertificateSignature().
Cleaned up unit tests with utility method.
Fixed checkstyle error.
Support conversion of a PKCS#8 formatted private key automatically to avoid forcing the user to do that. Also add some log messages for debugging when the parser fails to parse the appropriate object
Incorporated Peter's contribution for PKCS #8 to PKCS #1 conversion.
Added documentation and refactored methods.
Refactored unit test.
Added RAT exclusion for test resource.

This closes #2935.

Co-authored-by: pepov <pe...@gmail.com>

Signed-off-by: Matt Gilman <mc...@apache.org>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/57baae9a
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/57baae9a
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/57baae9a

Branch: refs/heads/master
Commit: 57baae9ae28f63877e66cbc178cdd127a82d4841
Parents: 8106af6
Author: Andy LoPresto <al...@apache.org>
Authored: Tue Jul 31 21:26:16 2018 -0700
Committer: Andy LoPresto <al...@apache.org>
Committed: Tue Aug 7 12:07:35 2018 -0700

----------------------------------------------------------------------
 .../src/main/asciidoc/administration-guide.adoc |  58 +++-
 nifi-toolkit/nifi-toolkit-tls/pom.xml           |   1 +
 .../tls/configuration/StandaloneConfig.java     |   5 +-
 .../toolkit/tls/configuration/TlsConfig.java    |  33 +++
 .../tls/standalone/TlsToolkitStandalone.java    |  60 ++--
 .../TlsToolkitStandaloneCommandLine.java        |  15 +-
 .../apache/nifi/toolkit/tls/util/TlsHelper.java | 129 ++++++++-
 .../TlsToolkitStandaloneGroovyTest.groovy       | 271 +++++++++++++++++++
 .../toolkit/tls/util/TlsHelperGroovyTest.groovy | 140 ++++++++++
 .../src/test/resources/rootCert-pkcs8.key       |  28 ++
 10 files changed, 709 insertions(+), 31 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/57baae9a/nifi-docs/src/main/asciidoc/administration-guide.adoc
----------------------------------------------------------------------
diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index 1f1d64c..769b7bc 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -187,6 +187,7 @@ Standalone mode is invoked by running `./bin/tls-toolkit.sh standalone -h` which
 You can use the following command line options with the `tls-toolkit` in standalone mode:
 
 * `-a`,`--keyAlgorithm <arg>`                   Algorithm to use for generated keys (default: `RSA`)
+* `--additionalCACertificate <arg>`             Path to additional CA certificate (used to sign toolkit CA certificate) in PEM format if necessary
 * `-B`,`--clientCertPassword <arg>`             Password for client certificate. Must either be one value or one for each client DN (auto-generate if not specified)
 * `-c`,`--certificateAuthorityHostname <arg>`   Hostname of NiFi Certificate Authority (default: `localhost`)
 * `-C`,`--clientCertDn <arg>`                   Generate client certificate suitable for use in browser with specified DN (Can be specified multiple times)
@@ -231,7 +232,6 @@ Create 2 sets of keystore, truststore, nifi.properties for 10 NiFi hostnames in
 bin/tls-toolkit.sh standalone -n 'nifi[01-10].subdomain[1-4].domain(2)' -C 'CN=username,OU=NIFI'
 ----
 
-
 ==== Client/Server
 Client/Server mode relies on a long-running Certificate Authority (CA) to issue certificates. The CA can be stopped when you’re not bringing nodes online.
 
@@ -279,7 +279,7 @@ You can use the following command line options with the `tls-toolkit` in client
 
 After running the client you will have the CA’s certificate, a keystore, a truststore, and a `config.json` with information about them as well as their passwords.
 
-For a client certificate that can be easily imported into the browser, specify: `-T PKCS12`
+For a client certificate that can be easily imported into the browser, specify: `-T PKCS12`. 
 
 ==== Using An Existing Intermediate Certificate Authority (CA)
 
@@ -288,7 +288,7 @@ In some enterprise scenarios, a security/IT team may provide a signing certifica
 . Generate or obtain the signed intermediate CA keys in the following format (see additional commands below):
   * Public certificate in PEM format: `nifi-cert.pem`
   * Private key in PEM format: `nifi-key.key`
-. Place the files in the *toolkit directory*. This is the directory where the tool binary (usually called via the invoking script `tls-toolkit.sh` or `tls-toolkit.bat`) is configured to output the signed certificates. *This is not necessarily the directory where the binary is located or invoked*. 
+. Place the files in the *toolkit working directory*. This is the directory where the tool is configured to output the signed certificates. *This is not necessarily the directory where the binary is located or invoked*. 
   * For example, given the following scenario, the toolkit command can be run from its location as long as the output directory `-o` is `../hardcoded/`, and the existing `nifi-cert.pem` and `nifi-key.key` will be used.  
   ** e.g. `$ ./toolkit/bin/tls-toolkit.sh standalone -o ./hardcoded/ -n 'node4.nifi.apache.org' -P thisIsABadPassword -S thisIsABadPassword -O` will result in a new directory at `./hardcoded/node4.nifi.apache.org` with a keystore and truststore containing a certificate signed by `./hardcoded/nifi-key.key`  
   * If the `-o` argument is not provided, the default working directory (`.`) must contain `nifi-cert.pem` and `nifi-key.key`
@@ -551,7 +551,59 @@ coefficient:
 . To convert from a Java Keystore (`keystore.jks`) containing private key into PEM encoded files (`$P12_PASSWORD` is the PKCS12 keystore password, `$JKS_PASSWORD` is the Java keystore password you want to set, and `$ALIAS` can be any value -- the NiFi default is `nifi-key`):
   * `keytool -importkeystore -srckeystore keystore.jks -destkeystore keystore.p12 -srcstoretype JKS -deststoretype PKCS12 -destkeypass "$P12_PASSWORD" -deststorepass "$P12_PASSWORD" -srcstorepass "$JKS_PASSWORD" -srcalias "$ALIAS" -destalias "$ALIAS"`
   * Follow the steps above to convert from `keystore.p12` to `cert.pem` and `key.key`
+. To convert from PKCS #8 PEM format to PKCS #1 PEM format:
+  * If the private key is provided in PKCS #8 format (the file begins with `-----BEGIN PRIVATE KEY-----` rather than `-----BEGIN RSA PRIVATE KEY-----`), the following command will convert it to PKCS #1 format, move the original to `nifi-key-pkcs8.key`, and rename the PKCS #1 version as `nifi-key.key`: 
+  ** `openssl rsa -in nifi-key.key -out nifi-key-pkcs1.key && mv nifi-key.key nifi-key-pkcs8.key && mv nifi-key-pkcs1.key nifi-key.key`
+
+===== Signing with Externally-signed CA Certificates
+
+To sign generated certificates with a certificate authority (CA) generated outside of the TLS Toolkit, ensure the necessary files are in the right format and location (see above). For example, an organization *Large Organization* has an internal CA (`CN=ca.large.org, OU=Certificate Authority`). This *root CA* is offline and only used to sign other internal CAs. The Large IT team generates an *intermediate CA* (`CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority`) to be used to sign all NiFi node certificates (`CN=node1.nifi.large.org, OU=NiFi`, `CN=node2.nifi.large.org, OU=NiFi`, etc.). 
+
+To use the toolkit to generate these certificates and sign them using the *intermediate CA*, ensure that the following files are present (see <<additional-commands>> above):
+
+* `nifi-cert.pem` -- the public certificate of the *intermediate CA* in PEM format
+* `nifi-key.key` -- the Base64-encoded private key of the *intermediate CA* in PKCS #1 PEM format
 
+If the *intermediate CA* was the *root CA*, it would be *self-signed* -- the signature over the certificate would be issued from the same key. In that case (the same as a toolkit-generated CA), no additional arguments are necessary. However, because the *intermediate CA* is signed by the *root CA*, the public certificate of the *root CA* needs to be provided as well to validate the signature. The `--additionalCACertificate` parameter is used to specify the path to the signing public certificate. The value should be the absolute path to the *root CA* public certificate. 
+
+Example:
+
+```
+# Generate cert signed by intermediate CA (which is signed by root CA) -- WILL FAIL
+
+$ ./bin/tls-toolkit.sh standalone -n 'node1.nifi.apache.org' \
+-P passwordpassword \
+-S passwordpassword \
+-o /opt/certs/externalCA \
+-O
+
+2018/08/02 18:48:11 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandaloneCommandLine: No nifiPropertiesFile specified, using embedded one.
+2018/08/02 18:48:12 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Running standalone certificate generation with output directory /opt/certs/externalCA
+2018/08/02 18:48:12 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Verifying the certificate signature for CN=nifi_ca.large.org, OU=Certificate Authority
+2018/08/02 18:48:12 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Attempting to verify certificate CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority signature with CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority
+2018/08/02 18:48:12 WARN [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Certificate CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority not signed by CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority [certificate does not verify with supplied key]
+Error generating TLS configuration. (The signing certificate was not signed by any known certificates)
+
+# Provide additional CA certificate path for signature verification of intermediate CA
+
+$ ./bin/tls-toolkit.sh standalone -n 'node1.nifi.apache.org' \
+-P passwordpassword \
+-S passwordpassword \
+-o /opt/certs/externalCA \
+--additionalCACertificate /opt/certs/externalCA/root.pem \
+-O
+
+2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandaloneCommandLine: No nifiPropertiesFile specified, using embedded one.
+2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Running standalone certificate generation with output directory /opt/certs/externalCA
+2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Verifying the certificate signature for CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority
+2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Attempting to verify certificate CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority signature with CN=ca.large.org, OU=Certificate Authority
+2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Certificate was signed by CN=ca.large.org, OU=Certificate Authority
+2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Using existing CA certificate /opt/certs/externalCA/nifi-cert.pem and key /opt/certs/externalCA/nifi-key.key
+2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Writing new ssl configuration to /opt/certs/externalCA/node1.nifi.apache.org
+2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Successfully generated TLS configuration for node1.nifi.apache.org 1 in /opt/certs/externalCA/node1.nifi.apache.org
+2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: No clientCertDn specified, not generating any client certificates.
+2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: tls-toolkit standalone completed successfully
+```
 
 [[user_authentication]]
 == User Authentication

http://git-wip-us.apache.org/repos/asf/nifi/blob/57baae9a/nifi-toolkit/nifi-toolkit-tls/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-tls/pom.xml b/nifi-toolkit/nifi-toolkit-tls/pom.xml
index 3443f5a..f2b813f 100644
--- a/nifi-toolkit/nifi-toolkit-tls/pom.xml
+++ b/nifi-toolkit/nifi-toolkit-tls/pom.xml
@@ -130,6 +130,7 @@
                         <excludes combine.children="append">
                             <exclude>src/test/resources/rootCert.crt</exclude>
                             <exclude>src/test/resources/rootCert.key</exclude>
+                            <exclude>src/test/resources/rootCert-pkcs8.key</exclude>
                         </excludes>
                     </configuration>
                 </plugin>

http://git-wip-us.apache.org/repos/asf/nifi/blob/57baae9a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/StandaloneConfig.java
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/StandaloneConfig.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/StandaloneConfig.java
index 7ac44a4..d8e8634 100644
--- a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/StandaloneConfig.java
+++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/StandaloneConfig.java
@@ -17,10 +17,9 @@
 
 package org.apache.nifi.toolkit.tls.configuration;
 
-import org.apache.nifi.toolkit.tls.properties.NiFiPropertiesWriterFactory;
-
 import java.io.File;
 import java.util.List;
+import org.apache.nifi.toolkit.tls.properties.NiFiPropertiesWriterFactory;
 
 /**
  * Configuration object of the standalone service
@@ -34,6 +33,8 @@ public class StandaloneConfig extends TlsConfig {
     private boolean clientPasswordsGenerated;
     private boolean overwrite;
 
+    // TODO: A lot of these fields are null and cause NPEs in {@link TlsToolkitStandalone} when not executed with expected input
+
     public List<String> getClientDns() {
         return clientDns;
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/57baae9a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/TlsConfig.java
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/TlsConfig.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/TlsConfig.java
index 86084c0..9332022 100644
--- a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/TlsConfig.java
+++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/TlsConfig.java
@@ -52,6 +52,7 @@ public class TlsConfig {
     private String dnPrefix = DEFAULT_DN_PREFIX;
     private String dnSuffix = DEFAULT_DN_SUFFIX;
     private boolean reorderDn = DEFAULT_REORDER_DN;
+    private String additionalCACertificate = "";
 
     public String calcDefaultDn(String hostname) {
         String dn = dnPrefix + hostname + dnSuffix;
@@ -215,4 +216,36 @@ public class TlsConfig {
     public void setDomainAlternativeNames(String domainAlternativeNames) {
         this.domainAlternativeNames = domainAlternativeNames;
     }
+
+    /**
+     * Returns the path to an additional CA certificate file in PEM format which has been used to sign the CA certificate the toolkit will use.
+     *
+     * Example:
+     *
+     * nifi-cert.pem [existing PEM file for intermediate CA generated by Org's IT team and signed by org-ca.pem]
+     * org-ca.pem [PEM file for root CA owned by Org's IT team]
+     *
+     * {@code getAdditionalCACertificate() == "/path/to/org-ca.pem"}
+     *
+     * @return the path to this file
+     */
+    public String getAdditionalCACertificate() {
+        return additionalCACertificate;
+    }
+
+    /**
+     * Sets the path to an additional CA certificate file in PEM format which has been used to sign the CA certificate the toolkit will use.
+     *
+     * Example:
+     *
+     * nifi-cert.pem [existing PEM file for intermediate CA generated by Org's IT team and signed by org-ca.pem]
+     * org-ca.pem [PEM file for root CA owned by Org's IT team]
+     *
+     * {@code setAdditionalCACertificate("/path/to/org-ca.pem");}
+     *
+     * @param additionalCACertificate the path to this file
+     */
+    public void setAdditionalCACertificate(String additionalCACertificate) {
+        this.additionalCACertificate = additionalCACertificate;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/57baae9a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandalone.java
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandalone.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandalone.java
index d3da476..ffe4c5d 100644
--- a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandalone.java
+++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandalone.java
@@ -17,9 +17,23 @@
 
 package org.apache.nifi.toolkit.tls.standalone;
 
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.SignatureException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
 import org.apache.nifi.security.util.CertificateUtils;
-import org.apache.nifi.security.util.KeystoreType;
 import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.KeystoreType;
 import org.apache.nifi.toolkit.tls.configuration.InstanceDefinition;
 import org.apache.nifi.toolkit.tls.configuration.StandaloneConfig;
 import org.apache.nifi.toolkit.tls.configuration.TlsClientConfig;
@@ -29,25 +43,13 @@ import org.apache.nifi.toolkit.tls.manager.writer.NifiPropertiesTlsClientConfigW
 import org.apache.nifi.toolkit.tls.properties.NiFiPropertiesWriterFactory;
 import org.apache.nifi.toolkit.tls.util.OutputStreamFactory;
 import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.StringUtils;
 import org.bouncycastle.asn1.x509.Extensions;
 import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
 import org.bouncycastle.util.io.pem.PemWriter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.KeyStore;
-import java.security.cert.Certificate;
-import java.security.cert.X509Certificate;
-import java.util.List;
-
 public class TlsToolkitStandalone {
     public static final String NIFI_KEY = "nifi-key";
     public static final String NIFI_CERT = "nifi-cert";
@@ -65,6 +67,7 @@ public class TlsToolkitStandalone {
     }
 
     public void createNifiKeystoresAndTrustStores(StandaloneConfig standaloneConfig) throws GeneralSecurityException, IOException {
+        // TODO: This 200 line method should be refactored, as it is difficult to test the various validations separately from the filesystem interaction and generation logic
         File baseDir = standaloneConfig.getBaseDir();
         if (!baseDir.exists() && !baseDir.mkdirs()) {
             throw new IOException(baseDir + " doesn't exist and unable to create it.");
@@ -96,10 +99,35 @@ public class TlsToolkitStandalone {
                 certificate = TlsHelper.parseCertificate(pemEncodedCertificate);
             }
             try (FileReader pemEncodedKeyPair = new FileReader(nifiKey)) {
-                caKeyPair = TlsHelper.parseKeyPair(pemEncodedKeyPair);
+                caKeyPair = TlsHelper.parseKeyPairFromReader(pemEncodedKeyPair);
+            }
+
+            // TODO: Do same in client/server
+            // Load additional signing certificates from config
+            List<X509Certificate> signingCertificates = new ArrayList<>();
+
+            // Read the provided additional CA certificate if it exists and extract the certificate
+            if (!StringUtils.isBlank(standaloneConfig.getAdditionalCACertificate())) {
+                X509Certificate signingCertificate;
+                final File additionalCACertFile = new File(standaloneConfig.getAdditionalCACertificate());
+                if (!additionalCACertFile.exists()) {
+                    throw new IOException("The additional CA certificate does not exist at " + additionalCACertFile.getAbsolutePath());
+                }
+                try (FileReader pemEncodedCACertificate = new FileReader(additionalCACertFile)) {
+                    signingCertificate = TlsHelper.parseCertificate(pemEncodedCACertificate);
+                }
+                signingCertificates.add(signingCertificate);
+            }
+
+            // Support self-signed CA certificates
+            signingCertificates.add(certificate);
+
+            boolean signatureValid = TlsHelper.verifyCertificateSignature(certificate, signingCertificates);
+
+            if (!signatureValid) {
+                throw new SignatureException("The signing certificate was not signed by any known certificates");
             }
 
-            certificate.verify(caKeyPair.getPublic());
             if (!caKeyPair.getPublic().equals(certificate.getPublicKey())) {
                 throw new IOException("Expected " + nifiKey + " to correspond to CA certificate at " + nifiCert);
             }

http://git-wip-us.apache.org/repos/asf/nifi/blob/57baae9a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneCommandLine.java
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneCommandLine.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneCommandLine.java
index 2e30b48..f11ad7b 100644
--- a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneCommandLine.java
+++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneCommandLine.java
@@ -29,7 +29,6 @@ import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
-
 import org.apache.commons.cli.CommandLine;
 import org.apache.nifi.toolkit.tls.commandLine.BaseTlsToolkitCommandLine;
 import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
@@ -59,7 +58,8 @@ public class TlsToolkitStandaloneCommandLine extends BaseTlsToolkitCommandLine {
     public static final String GLOBAL_PORT_SEQUENCE_ARG = "globalPortSequence";
     public static final String NIFI_DN_PREFIX_ARG = "nifiDnPrefix";
     public static final String NIFI_DN_SUFFIX_ARG = "nifiDnSuffix";
-    public static final String SUBJECT_ALTERNATIVE_NAMES = "subjectAlternativeNames";
+    public static final String SUBJECT_ALTERNATIVE_NAMES_ARG = "subjectAlternativeNames";
+    public static final String ADDITIONAL_CA_CERTIFICATE_ARG = "additionalCACertificate";
 
     public static final String DEFAULT_OUTPUT_DIRECTORY = calculateDefaultOutputDirectory(Paths.get("."));
 
@@ -89,6 +89,7 @@ public class TlsToolkitStandaloneCommandLine extends BaseTlsToolkitCommandLine {
     private String dnPrefix;
     private String dnSuffix;
     private String domainAlternativeNames;
+    private String additionalCACertificatePath;
 
     public TlsToolkitStandaloneCommandLine() {
         this(new PasswordUtil());
@@ -107,10 +108,11 @@ public class TlsToolkitStandaloneCommandLine extends BaseTlsToolkitCommandLine {
         addOptionWithArg("B", CLIENT_CERT_PASSWORD_ARG, "Password for client certificate.  Must either be one value or one for each client DN. (autogenerate if not specified)");
         addOptionWithArg("G", GLOBAL_PORT_SEQUENCE_ARG, "Use sequential ports that are calculated for all hosts according to the provided hostname expressions. " +
                 "(Can be specified multiple times, MUST BE SAME FROM RUN TO RUN.)");
-        addOptionWithArg(null, SUBJECT_ALTERNATIVE_NAMES, "Comma-separated list of domains to use as Subject Alternative Names in the certificate");
+        addOptionWithArg(null, SUBJECT_ALTERNATIVE_NAMES_ARG, "Comma-separated list of domains to use as Subject Alternative Names in the certificate");
         addOptionWithArg(null, NIFI_DN_PREFIX_ARG, "String to prepend to hostname(s) when determining DN.", TlsConfig.DEFAULT_DN_PREFIX);
         addOptionWithArg(null, NIFI_DN_SUFFIX_ARG, "String to append to hostname(s) when determining DN.", TlsConfig.DEFAULT_DN_SUFFIX);
         addOptionNoArg("O", OVERWRITE_ARG, "Overwrite existing host output.");
+        addOptionWithArg(null, ADDITIONAL_CA_CERTIFICATE_ARG, "Path to additional CA certificate (used to sign toolkit CA certificate) in PEM format if necessary");
     }
 
     public static void main(String[] args) {
@@ -123,7 +125,7 @@ public class TlsToolkitStandaloneCommandLine extends BaseTlsToolkitCommandLine {
         try {
             new TlsToolkitStandalone().createNifiKeystoresAndTrustStores(tlsToolkitStandaloneCommandLine.createConfig());
         } catch (Exception e) {
-            tlsToolkitStandaloneCommandLine.printUsage("Error creating generating tls configuration. (" + e.getMessage() + ")");
+            tlsToolkitStandaloneCommandLine.printUsage("Error generating TLS configuration. (" + e.getMessage() + ")");
             System.exit(ExitCode.ERROR_GENERATING_CONFIG.ordinal());
         }
         System.exit(ExitCode.SUCCESS.ordinal());
@@ -137,7 +139,7 @@ public class TlsToolkitStandaloneCommandLine extends BaseTlsToolkitCommandLine {
 
         dnPrefix = commandLine.getOptionValue(NIFI_DN_PREFIX_ARG, TlsConfig.DEFAULT_DN_PREFIX);
         dnSuffix = commandLine.getOptionValue(NIFI_DN_SUFFIX_ARG, TlsConfig.DEFAULT_DN_SUFFIX);
-        domainAlternativeNames = commandLine.getOptionValue(SUBJECT_ALTERNATIVE_NAMES);
+        domainAlternativeNames = commandLine.getOptionValue(SUBJECT_ALTERNATIVE_NAMES_ARG);
 
         Stream<String> globalOrderExpressions = null;
         if (commandLine.hasOption(GLOBAL_PORT_SEQUENCE_ARG)) {
@@ -166,6 +168,8 @@ public class TlsToolkitStandaloneCommandLine extends BaseTlsToolkitCommandLine {
         clientPasswordsGenerated = commandLine.getOptionValues(CLIENT_CERT_PASSWORD_ARG) == null;
         overwrite = commandLine.hasOption(OVERWRITE_ARG);
 
+        additionalCACertificatePath = commandLine.getOptionValue(ADDITIONAL_CA_CERTIFICATE_ARG);
+
         String nifiPropertiesFile = commandLine.getOptionValue(NIFI_PROPERTIES_FILE_ARG, "");
         try {
             if (StringUtils.isEmpty(nifiPropertiesFile)) {
@@ -234,6 +238,7 @@ public class TlsToolkitStandaloneCommandLine extends BaseTlsToolkitCommandLine {
         standaloneConfig.setDnPrefix(dnPrefix);
         standaloneConfig.setDnSuffix(dnSuffix);
         standaloneConfig.setDomainAlternativeNames(domainAlternativeNames);
+        standaloneConfig.setAdditionalCACertificate(additionalCACertificatePath);
         standaloneConfig.initDefaults();
 
         return standaloneConfig;

http://git-wip-us.apache.org/repos/asf/nifi/blob/57baae9a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/TlsHelper.java
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/TlsHelper.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/TlsHelper.java
index d8a7fc0..20bc2d9 100644
--- a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/TlsHelper.java
+++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/TlsHelper.java
@@ -34,25 +34,31 @@ import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.List;
-
 import javax.crypto.Cipher;
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
-
 import org.apache.commons.lang3.StringUtils;
+import org.bouncycastle.asn1.ASN1Encodable;
+import org.bouncycastle.asn1.DERNull;
 import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.asn1.pkcs.RSAPrivateKey;
+import org.bouncycastle.asn1.pkcs.RSAPublicKey;
 import org.bouncycastle.asn1.x500.X500Name;
 import org.bouncycastle.asn1.x500.style.BCStyle;
 import org.bouncycastle.asn1.x500.style.IETFUtils;
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
 import org.bouncycastle.asn1.x509.Extension;
 import org.bouncycastle.asn1.x509.Extensions;
 import org.bouncycastle.asn1.x509.ExtensionsGenerator;
 import org.bouncycastle.asn1.x509.GeneralName;
 import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
 import org.bouncycastle.cert.X509CertificateHolder;
 import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
 import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openssl.PEMException;
 import org.bouncycastle.openssl.PEMKeyPair;
 import org.bouncycastle.openssl.PEMParser;
 import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
@@ -72,6 +78,7 @@ public class TlsHelper {
     public static final String JCE_URL = "http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html";
     public static final String ILLEGAL_KEY_SIZE = "illegal key size";
     private static boolean isUnlimitedStrengthCryptographyEnabled;
+    private static boolean isVerbose = true;
 
     // Evaluate an unlimited strength algorithm to determine if we support the capability we have on the system
     static {
@@ -185,15 +192,80 @@ public class TlsHelper {
         return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(parsePem(X509CertificateHolder.class, pemEncodedCertificate));
     }
 
-    public static KeyPair parseKeyPair(Reader pemEncodedKeyPair) throws IOException {
-        return new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getKeyPair(parsePem(PEMKeyPair.class, pemEncodedKeyPair));
+    /**
+     * Returns the parsed {@link KeyPair} from the provided {@link Reader}. The incoming format can be PKCS #8 or PKCS #1.
+     *
+     * @param pemKeyPairReader a reader with access to the serialized key pair
+     * @return the key pair
+     * @throws IOException if there is an error reading the key pair
+     */
+    public static KeyPair parseKeyPairFromReader(Reader pemKeyPairReader) throws IOException {
+        // Instantiate PEMParser from Reader
+        try (PEMParser pemParser = new PEMParser(pemKeyPairReader)) {
+            // Read the object (deserialize)
+            Object parsedObject = pemParser.readObject();
+
+            // If this is an ASN.1 private key, it's in PKCS #8 format and wraps the actual RSA private key
+            if (PrivateKeyInfo.class.isInstance(parsedObject)) {
+                if (isVerbose()) {
+                    logger.info("Provided private key is in PKCS #8 format");
+                }
+                PEMKeyPair keyPair = convertPrivateKeyFromPKCS8ToPKCS1((PrivateKeyInfo) parsedObject);
+                return getKeyPair(keyPair);
+            } else if (PEMKeyPair.class.isInstance(parsedObject)) {
+                // Already in PKCS #1 format
+                return getKeyPair((PEMKeyPair)parsedObject);
+            } else {
+                logger.warn("Expected one of %s or %s but got %s", PrivateKeyInfo.class, PEMKeyPair.class, parsedObject.getClass());
+                throw new IOException("Expected private key in PKCS #1 or PKCS #8 unencrypted format");
+            }
+        }
+    }
+
+    /**
+     * Returns a {@link KeyPair} instance containing the {@link X509Certificate} public key and the {@link java.security.spec.PKCS8EncodedKeySpec} private key from the PEM-encoded {@link PEMKeyPair}.
+     *
+     * @param keyPair the key pair in PEM format
+     * @return the key pair in a format which provides for direct access to the keys
+     * @throws PEMException if there is an error converting the key pair
+     */
+    private static KeyPair getKeyPair(PEMKeyPair keyPair) throws PEMException {
+        return new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getKeyPair(keyPair);
+    }
+
+    /**
+     * Returns a {@link PEMKeyPair} object with direct access to the public and private keys given a PKCS #8 private key.
+     *
+     * @param privateKeyInfo the PKCS #8 private key info
+     * @return the PKCS #1 public and private key pair
+     * @throws IOException if there is an error converting the key pair
+     */
+    private static PEMKeyPair convertPrivateKeyFromPKCS8ToPKCS1(PrivateKeyInfo privateKeyInfo) throws IOException {
+        // Parse the key wrapping to determine the internal key structure
+        ASN1Encodable asn1PrivateKey = privateKeyInfo.parsePrivateKey();
+
+        // Convert the parsed key to an RSA private key
+        RSAPrivateKey keyStruct = RSAPrivateKey.getInstance(asn1PrivateKey);
+
+        // Create the RSA public key from the modulus and exponent
+        RSAPublicKey pubSpec = new RSAPublicKey(
+            keyStruct.getModulus(), keyStruct.getPublicExponent());
+
+        // Create an algorithm identifier for forming the key pair
+        AlgorithmIdentifier algId = new AlgorithmIdentifier(PKCSObjectIdentifiers.rsaEncryption, DERNull.INSTANCE);
+        if (isVerbose()) {
+            logger.info("Converted private key from PKCS #8 to PKCS #1 RSA private key");
+        }
+
+        // Create the key pair container
+        return new PEMKeyPair(new SubjectPublicKeyInfo(algId, pubSpec), new PrivateKeyInfo(algId, keyStruct));
     }
 
     public static <T> T parsePem(Class<T> clazz, Reader pemReader) throws IOException {
         try (PEMParser pemParser = new PEMParser(pemReader)) {
             Object object = pemParser.readObject();
             if (!clazz.isInstance(object)) {
-                throw new IOException("Expected " + clazz);
+                throw new IOException("Expected " + clazz + " but got " + object.getClass());
             }
             return (T) object;
         }
@@ -251,4 +323,51 @@ public class TlsHelper {
         return filename.replaceAll("[^\\w\\.\\-\\=]+", "_");
     }
 
+    /**
+     * Returns true if the {@code certificate} is signed by one of the {@code signingCertificates}. The list should
+     * include the certificate itself to allow for self-signed certificates. If it does not, a self-signed certificate
+     * will return {@code false}.
+     *
+     * @param certificate the certificate containing the signature being verified
+     * @param signingCertificates a list of certificates which may have signed the certificate
+     * @return true if one of the signing certificates did sign the certificate
+     */
+    public static boolean verifyCertificateSignature(X509Certificate certificate, List<X509Certificate> signingCertificates) {
+        String certificateDisplayInfo = getCertificateDisplayInfo(certificate);
+        if (isVerbose()) {
+            logger.info("Verifying the certificate signature for " + certificateDisplayInfo);
+        }
+        boolean signatureMatches = false;
+        for (X509Certificate signingCert : signingCertificates) {
+            final String signingCertDisplayInfo = getCertificateDisplayInfo(signingCert);
+            try {
+                if (isVerbose()) {
+                    logger.info("Attempting to verify certificate " + certificateDisplayInfo + " signature with " + signingCertDisplayInfo);
+                }
+                PublicKey pub = signingCert.getPublicKey();
+                certificate.verify(pub);
+                if (isVerbose()) {
+                    logger.info("Certificate was signed by " + signingCertDisplayInfo);
+                }
+                signatureMatches = true;
+                break;
+            } catch (Exception e) {
+                // Expected if the signature does not match
+                if (isVerbose()) {
+                    logger.warn("Certificate " + certificateDisplayInfo + " not signed by " + signingCertDisplayInfo + " [" + e.getLocalizedMessage() + "]");
+                }
+            }
+        }
+        return signatureMatches;
+    }
+
+    private static String getCertificateDisplayInfo(X509Certificate certificate) {
+        return certificate.getSubjectX500Principal().getName();
+    }
+
+    private static boolean isVerbose() {
+        // TODO: When verbose mode is enabled via command-line flag, this will read the variable
+        return isVerbose;
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/57baae9a/nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneGroovyTest.groovy b/nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneGroovyTest.groovy
new file mode 100644
index 0000000..f73bc8c
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneGroovyTest.groovy
@@ -0,0 +1,271 @@
+/*
+ * 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.toolkit.tls.standalone
+
+import org.apache.nifi.security.util.CertificateUtils
+import org.apache.nifi.toolkit.tls.configuration.StandaloneConfig
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator
+import org.bouncycastle.util.io.pem.PemWriter
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import java.nio.file.Files
+import java.security.KeyPair
+import java.security.KeyPairGenerator
+import java.security.Security
+import java.security.SignatureException
+import java.security.cert.X509Certificate
+
+@RunWith(JUnit4.class)
+class TlsToolkitStandaloneGroovyTest extends GroovyTestCase {
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitStandaloneGroovyTest.class)
+
+    private final String TEST_SRC_DIR = "src/test/resources/"
+    private final String DEFAULT_KEY_PAIR_ALGORITHM = "RSA"
+    private final String DEFAULT_SIGNING_ALGORITHM = "SHA256WITHRSA"
+
+    @Rule
+    public TemporaryFolder tmpDir = new TemporaryFolder()
+
+    @BeforeClass
+    static void setUpOnce() throws Exception {
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    @Test
+    void testShouldVerifyCertificateSignatureWhenSelfSigned() {
+        // Arrange
+
+        // Create a temp directory for this test and populate it with the nifi-cert.pem and nifi-key.key files
+        File baseDir = createBaseDirAndPopulateWithCAFiles()
+
+        // Make a standalone config which doesn't trigger any keystore generation and just has a self-signed cert and key
+        StandaloneConfig standaloneConfig = new StandaloneConfig()
+        standaloneConfig.setBaseDir(baseDir)
+        standaloneConfig.setInstanceDefinitions([])
+        standaloneConfig.setClientDns([])
+        standaloneConfig.initDefaults()
+
+        TlsToolkitStandalone standalone = new TlsToolkitStandalone()
+
+        // Act
+        standalone.createNifiKeystoresAndTrustStores(standaloneConfig)
+
+        // Assert
+
+        // The test will fail with an exception if the certificate is not signed by a known certificate
+    }
+
+    /**
+     * The certificate under examination is self-signed, but there is another signing cert which will be iterated over first, fail, and then the self-signed signature will be validated.
+     */
+    @Test
+    void testShouldVerifyCertificateSignatureWithMultipleSigningCerts() {
+        // Arrange
+
+        // Create a temp directory for this test and populate it with the nifi-cert.pem and nifi-key.key files
+        File baseDir = createBaseDirAndPopulateWithCAFiles()
+
+        // Create a different cert and persist it to the base dir
+        X509Certificate otherCert = generateX509Certificate()
+        File otherCertFile = writeCertificateToPEMFile(otherCert, "${baseDir.path}/other.pem")
+        logger.info("Wrote other CA cert to ${otherCertFile.path}")
+
+        // Make a standalone config which doesn't trigger any keystore generation and just has a self-signed cert and key
+        StandaloneConfig standaloneConfig = new StandaloneConfig()
+        standaloneConfig.setBaseDir(baseDir)
+        standaloneConfig.setInstanceDefinitions([])
+        standaloneConfig.setClientDns([])
+        standaloneConfig.initDefaults()
+
+        // Inject the additional CA cert path
+        standaloneConfig.setAdditionalCACertificate(otherCertFile.path)
+
+        TlsToolkitStandalone standalone = new TlsToolkitStandalone()
+
+        // Act
+        standalone.createNifiKeystoresAndTrustStores(standaloneConfig)
+
+        // Assert
+
+        // The test will fail with an exception if the certificate is not signed by a known certificate
+    }
+
+    /**
+     * The certificate under examination is signed with the external signing cert.
+     */
+    @Test
+    void testShouldVerifyCertificateSignatureWithAdditionalSigningCert() {
+        // Arrange
+
+        // Create a temp directory for this test
+        File baseDir = createBaseDir()
+
+        // Create a root CA, create an intermediate CA, use the root to sign the intermediate and then provide the root
+        KeyPair rootKeyPair = generateKeyPair()
+        X509Certificate rootCert = generateX509Certificate("CN=Root CA", rootKeyPair)
+
+        File rootCertFile = writeCertificateToPEMFile(rootCert, "${baseDir.path}/root.pem")
+        logger.info("Wrote root CA cert to ${rootCertFile.path}")
+
+        KeyPair intermediateKeyPair = generateKeyPair()
+        X509Certificate intermediateCert = CertificateUtils.generateIssuedCertificate("CN=Intermediate CA", intermediateKeyPair.getPublic(), rootCert, rootKeyPair, DEFAULT_SIGNING_ALGORITHM, 1)
+
+        File intermediateCertFile = writeCertificateToPEMFile(intermediateCert, "${baseDir.path}/nifi-cert.pem")
+        logger.info("Wrote intermediate CA cert to ${intermediateCertFile.path}")
+
+        // Write the private key of the intermediate cert to nifi-key.key
+        File intermediateKeyFile = writePrivateKeyToFile(intermediateKeyPair, "${baseDir}/nifi-key.key")
+        logger.info("Wrote intermediate private key to ${intermediateKeyFile.path}")
+
+        // Make a standalone config which doesn't trigger any keystore generation and just has a signed cert and key
+        StandaloneConfig standaloneConfig = new StandaloneConfig()
+        standaloneConfig.setBaseDir(baseDir)
+        standaloneConfig.setInstanceDefinitions([])
+        standaloneConfig.setClientDns([])
+        standaloneConfig.initDefaults()
+
+        // Inject the additional CA cert path
+        standaloneConfig.setAdditionalCACertificate(rootCertFile.path)
+
+        TlsToolkitStandalone standalone = new TlsToolkitStandalone()
+
+        // Act
+        standalone.createNifiKeystoresAndTrustStores(standaloneConfig)
+
+        // Assert
+
+        // The test will fail with an exception if the certificate is not signed by a known certificate
+    }
+
+    @Test
+    void testShouldNotVerifyCertificateSignatureWithWrongSigningCert() {
+        // Arrange
+
+        // Create a temp directory for this test
+        File baseDir = createBaseDir()
+
+        // Create a root CA, create an intermediate CA, use the root to sign the intermediate and then do not provide the root
+        KeyPair rootKeyPair = generateKeyPair()
+        X509Certificate rootCert = generateX509Certificate("CN=Root CA", rootKeyPair)
+
+        KeyPair intermediateKeyPair = generateKeyPair()
+        X509Certificate intermediateCert = CertificateUtils.generateIssuedCertificate("CN=Intermediate CA", intermediateKeyPair.getPublic(), rootCert, rootKeyPair, DEFAULT_SIGNING_ALGORITHM, 1)
+
+        File intermediateCertFile = writeCertificateToPEMFile(intermediateCert, "${baseDir.path}/nifi-cert.pem")
+        logger.info("Wrote intermediate CA cert to ${intermediateCertFile.path}")
+
+        // Write the private key of the intermediate cert to nifi-key.key
+        File intermediateKeyFile = writePrivateKeyToFile(intermediateKeyPair, "${baseDir.path}/nifi-key.key")
+        logger.info("Wrote intermediate private key to ${intermediateKeyFile.path}")
+
+        // Make a standalone config which doesn't trigger any keystore generation and just has a signed cert and key
+        StandaloneConfig standaloneConfig = new StandaloneConfig()
+        standaloneConfig.setBaseDir(baseDir)
+        standaloneConfig.setInstanceDefinitions([])
+        standaloneConfig.setClientDns([])
+        standaloneConfig.initDefaults()
+
+        TlsToolkitStandalone standalone = new TlsToolkitStandalone()
+
+        // Act
+        def msg = shouldFail(SignatureException) {
+            standalone.createNifiKeystoresAndTrustStores(standaloneConfig)
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg =~ 'The signing certificate was not signed by any known certificates'
+    }
+
+    private static File writePrivateKeyToFile(KeyPair intermediateKeyPair, String destination) {
+        File intermediateKeyFile = new File(destination)
+        PemWriter pemWriter = new PemWriter(new FileWriter(intermediateKeyFile))
+        pemWriter.writeObject(new JcaMiscPEMGenerator(intermediateKeyPair))
+        pemWriter.close()
+        intermediateKeyFile
+    }
+
+    private File createBaseDirAndPopulateWithCAFiles() {
+        File baseDir = createBaseDir()
+
+        populateBaseDirWithCAFiles(baseDir)
+    }
+
+    private File createBaseDir() {
+        File baseDir = tmpDir.newFolder()
+        logger.info("Created base dir at ${baseDir.path}")
+        baseDir
+    }
+
+    private File populateBaseDirWithCAFiles(File baseDir) {
+        File certificateFile = new File(TEST_SRC_DIR, "rootCert.crt")
+        File keyFile = new File(TEST_SRC_DIR, "rootCert.key")
+        File destinationCertFile = new File(baseDir.path, "nifi-cert.pem")
+        Files.copy(certificateFile.toPath(), destinationCertFile.toPath())
+        logger.info("Wrote certificate to ${destinationCertFile.path}")
+        File destinationKeyFile = new File(baseDir.path, "nifi-key.key")
+        Files.copy(keyFile.toPath(), destinationKeyFile.toPath())
+        logger.info("Wrote private key to ${destinationKeyFile.path}")
+
+        baseDir
+    }
+
+    /**
+     * Returns an {@link X509Certificate} with the provided DN and default algorithms. The validity period is only 1 day.
+     *
+     * @param dn the DN (defaults to {@code CN=Test Certificate})
+     * @return the X509Certificate
+     */
+    private X509Certificate generateX509Certificate(String dn = "CN=Test Certificate", KeyPair keyPair = generateKeyPair()) {
+        CertificateUtils.generateSelfSignedX509Certificate(keyPair, CertificateUtils.reorderDn(dn), DEFAULT_SIGNING_ALGORITHM, 1)
+    }
+
+    private KeyPair generateKeyPair() {
+        KeyPairGenerator instance = KeyPairGenerator.getInstance(DEFAULT_KEY_PAIR_ALGORITHM)
+        instance.initialize(2048)
+        instance.generateKeyPair()
+    }
+
+    /**
+     * Writes the provided {@link X509Certificate} to the specified file in PEM format.
+     *
+     * @param certificate the certificate
+     * @param destination the path to write the certificate in PEM format
+     * @return the file
+     */
+    private static File writeCertificateToPEMFile(X509Certificate certificate, String destination) {
+        File certificateFile = new File(destination)
+        PemWriter pemWriter = new PemWriter(new FileWriter(certificateFile))
+        pemWriter.writeObject(new JcaMiscPEMGenerator(certificate))
+        pemWriter.close()
+
+        certificateFile
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/57baae9a/nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/util/TlsHelperGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/util/TlsHelperGroovyTest.groovy b/nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/util/TlsHelperGroovyTest.groovy
new file mode 100644
index 0000000..fa9cdae
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/util/TlsHelperGroovyTest.groovy
@@ -0,0 +1,140 @@
+/*
+ * 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.toolkit.tls.util
+
+import org.bouncycastle.crypto.params.RSAKeyParameters
+import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKey
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.security.auth.x500.X500Principal
+import java.security.KeyPair
+import java.security.PrivateKey
+import java.security.Security
+import java.security.cert.X509Certificate
+
+@RunWith(JUnit4.class)
+class TlsHelperGroovyTest extends GroovyTestCase {
+    private static final Logger logger = LoggerFactory.getLogger(TlsHelperGroovyTest.class)
+    private
+    final BCRSAPublicKey BAD_PUBLIC_KEY = new BCRSAPublicKey(new RSAKeyParameters(false, new BigInteger("1", 10), new BigInteger("1", 10)))
+
+    @BeforeClass
+    static void setUpOnce() throws Exception {
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    @Test
+    void testShouldVerifyCertificateSignatureWhenSelfSigned() {
+        // Arrange
+        File certificateFile = new File("src/test/resources/rootCert.crt")
+        FileReader certReader = new FileReader(certificateFile)
+        X509Certificate certificate = TlsHelper.parseCertificate(certReader)
+        logger.info("Read certificate ${certificate.getSubjectX500Principal().name} from ${certificateFile.path}")
+
+        // Act
+        boolean isCertificateSigned = TlsHelper.verifyCertificateSignature(certificate, [certificate])
+        logger.info("Certificate signature valid: ${isCertificateSigned}")
+
+        // Assert
+        assert isCertificateSigned
+    }
+
+    @Test
+    void testShouldVerifyCertificateSignatureWithMultipleSigningCerts() {
+        // Arrange
+        File certificateFile = new File("src/test/resources/rootCert.crt")
+        FileReader certReader = new FileReader(certificateFile)
+        X509Certificate certificate = TlsHelper.parseCertificate(certReader)
+        logger.info("Read certificate ${certificate.getSubjectX500Principal().name} from ${certificateFile.path}")
+
+        X509Certificate mockCertificate = [
+                getSubjectX500Principal: { -> new X500Principal("CN=Mock Certificate") },
+                getPublicKey           : { -> BAD_PUBLIC_KEY }
+        ] as X509Certificate
+
+        // Act
+        boolean isCertificateSigned = TlsHelper.verifyCertificateSignature(certificate, [mockCertificate, certificate])
+        logger.info("Certificate signature valid: ${isCertificateSigned}")
+
+        // Assert
+        assert isCertificateSigned
+    }
+
+    @Test
+    void testShouldNotVerifyCertificateSignatureWithNoSigningCerts() {
+        // Arrange
+        File certificateFile = new File("src/test/resources/rootCert.crt")
+        FileReader certReader = new FileReader(certificateFile)
+        X509Certificate certificate = TlsHelper.parseCertificate(certReader)
+        logger.info("Read certificate ${certificate.getSubjectX500Principal().name} from ${certificateFile.path}")
+
+        // Act
+        boolean isCertificateSigned = TlsHelper.verifyCertificateSignature(certificate, [])
+        logger.info("Certificate signature valid: ${isCertificateSigned}")
+
+        // Assert
+        assert !isCertificateSigned
+    }
+
+    @Test
+    void testShouldNotVerifyCertificateSignatureWithWrongSigningCert() {
+        // Arrange
+        File certificateFile = new File("src/test/resources/rootCert.crt")
+        FileReader certReader = new FileReader(certificateFile)
+        X509Certificate certificate = TlsHelper.parseCertificate(certReader)
+        logger.info("Read certificate ${certificate.getSubjectX500Principal().name} from ${certificateFile.path}")
+
+        X509Certificate mockCertificate = [
+                getSubjectX500Principal: { -> new X500Principal("CN=Mock Certificate") },
+                getPublicKey           : { -> BAD_PUBLIC_KEY }
+        ] as X509Certificate
+
+        // Act
+        boolean isCertificateSigned = TlsHelper.verifyCertificateSignature(certificate, [mockCertificate])
+        logger.info("Certificate signature valid: ${isCertificateSigned}")
+
+        // Assert
+        assert !isCertificateSigned
+    }
+
+    @Test
+    void testParseKeyPairFromReaderShouldHandlePKCS8PrivateKey() {
+        // Arrange
+        File keyFile = new File("src/test/resources/rootCert-pkcs8.key")
+        FileReader keyReader = new FileReader(keyFile)
+
+        final PrivateKey EXPECTED_PRIVATE_KEY = TlsHelper.parseKeyPairFromReader(new FileReader(new File ("src/test/resources/rootCert.key"))).private
+
+        // Act
+        KeyPair keyPair = TlsHelper.parseKeyPairFromReader(keyReader)
+        logger.info("Successfully read PKCS #8 unencrypted key from ${keyFile.path}")
+
+        // Assert
+        assert keyPair.private == EXPECTED_PRIVATE_KEY
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/57baae9a/nifi-toolkit/nifi-toolkit-tls/src/test/resources/rootCert-pkcs8.key
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/resources/rootCert-pkcs8.key b/nifi-toolkit/nifi-toolkit-tls/src/test/resources/rootCert-pkcs8.key
new file mode 100644
index 0000000..fc7faef
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-tls/src/test/resources/rootCert-pkcs8.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC6+OcU6ijl9hqr
+q8yI/ZyahKPenOkuO9U+xAY/g4fu/LV/cLoilaStePL+Sy2w/XXpYLg92csnQUXi
+iBH/hvlel4W4teuawaTd1ZBpzfO6dnUA68kp+VCoKI58gbz24yScl7R6cerh+GPL
+ipmvA6YyjMZrJZrmJGMZ9juZLjiW5hC+f13P0bggigZWnEmuW0cOuXR8xxWpjop/
+Fv//qO+jviZbQTzmGO7jkkex4RQwcyxlokBh8qDZ1FCvITVQhbkZqLYijXseNYip
+ZSeG3n8K8kbcwqj/dedW7DJWdue3lt7L4341VWwwpOKDebs8YrZHp/Jwq6kgwxVn
+rQVuwujvAgMBAAECggEADOpKlBBEuPXSC8+3NjNGkQneg+8U0GPDrC1APTzps+Fy
+7BWuVds+X9k998DbrCEl9vP+Zg9YUCLbH/XEQIFjUlxnGUY/uxXrPIOXESfv0Q6D
+sIeZArQ9FRCQHxubIPa5vbNg/SBHWEqfIh011ngLD+zXe+lCFOmois+OeFtP/2RQ
+wzF1as3fr+j9MvXxVkwPYTdYYzNf6Vso0uxC98AZSlt9s+2ZQ8V7NsOgvk+qSPgp
+nj/mXVF+pCJqY2KPDHGb+gZsOHwC7LG+ehHLENz05YSh6kRvvZB15zgsmdpg1dS0
+jauDVABtxfO5Eu2/yjQnSClV2qbZE7J/e0Voj4WTqQKBgQDeNkzlW1rsEcFPUeZg
+ZlA65Adb5YPsJCAt5cIIHg7jGr/uXuIbaEzem3rlMZp7vwoe0nYtyJaspG41Ou4G
+gprHowXDXpl9JBIWLQqef4RtzFyY9DaL9U7Gl05UA/0vIQooZOAfYVMTtGSIa3+g
+rR4mtgekcykCzSNHIIhgY+a3XQKBgQDXZuB+4wCGLLeS/8hi2Xdbt2gYVXO7YLn1
+VbGaSn9a8ZDKtVQJRGnk82OVvQsgzgNmcpH/MLGg/RJ/VO5WY5E7Vn5nQ0omijBj
+K95JxOef5GorDuyzH0qAZFdce5aUNwG/mUgw/m9iz/knjtqtl1uMjc1uKu+/zLiQ
+e3UuSs9YuwKBgHSpX2+eqbp8i9e/8Mo1jPOOGgr2EW+de8N894RZe4lh1tgnul+X
+P5wzVq8Tfr5vCrop1l+tCuXyoeWSXbrPQMGE5havCLg5gsFfvk5+NiGLBCZNOIH8
+NPJwJ3BWc8sVdobEhyISb5JNx+YTQfsySD0cniCJUUOmPVovS0oHyO4FAoGAEQPu
+bfeOngrAQZt0/+H/3L3jOjDd4IpmhivLyM1jvBJjBrBGQCkoWE6lqjlxvJipihk4
+0TjOf1IeePKDlU1uNorBl3SoUd0Or3bSq28jgOzxOg+GwSuSngvPHt4gafriZ3k7
+S6t9rweQvgA55AaV08eL180KfVM1rSwjeJGuSWsCgYEAuOeoNAUG5PeKjSWhxc8f
+DpRsgBsPo+bxnX4eAr+GyTJf/1uacmfejLOPHOImGnL3pGgu7SzEHGjl+McZznhC
+foJyK+7igz7iuS18AuEu4VR+J7y2vqdmWeabFHI2onEPwvlO1vrpOIL2yd/3wxgC
+qwoPTd89hQB2k+Uhuf7FLjg=
+-----END PRIVATE KEY-----