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

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


The following commit(s) were added to refs/heads/master by this push:
     new 4ec9155  NIFI-6770 - Set validator to Validator.VALID to allow empty 
password for truststores. Added no-password keystore for tests System NiFi 
truststore now allows a passwordless truststore. Added a unit test to prove 
this. Forgot no-password-truststore.jks file for the unit test. Refactored 
utility method from CertificateUtils to KeyStoreUtils. Added utility methods to 
verify keystore and key passwords. Added unit tests. Implemented different 
keystore and truststore validation [...]
4ec9155 is described below

commit 4ec9155cbc1796c0fc31893cdd91733b7f9cec45
Author: Nathan Gough <[email protected]>
AuthorDate: Thu Oct 17 13:09:08 2019 -0400

    NIFI-6770 - Set validator to Validator.VALID to allow empty password for 
truststores.
    Added no-password keystore for tests
    System NiFi truststore now allows a passwordless truststore. Added a unit 
test to prove this.
    Forgot no-password-truststore.jks file for the unit test.
    Refactored utility method from CertificateUtils to KeyStoreUtils.
    Added utility methods to verify keystore and key passwords.
    Added unit tests.
    Implemented different keystore and truststore validation logic.
    Refactored internal custom validation in StandardSSLContextService.
    Added unit test resource for keystore with different key and keystore 
passwords.
    Added unit test to generate passwordless truststore for 
https://nifi.apache.org for live testing.
    Resolved NPE in SSLContext generation in StandardSSLContextService
    Added unit test to generate passwordless truststore for localhost for 
InvokeHTTP testing.
    Resolved TrustManagerFactoryImpl initialization error.
    Fixed unit test without proper cleanup which caused RAT failures.
    
    Co-authored-by: Andy LoPresto <[email protected]>
    
    This closes #3823.
    
    Signed-off-by: Andy LoPresto <[email protected]>
---
 .../nifi/security/util/CertificateUtils.java       | 223 +++++--------
 .../apache/nifi/security/util/KeyStoreUtils.java   | 105 +++++-
 .../security/util/KeyStoreUtilsGroovyTest.groovy   | 144 ++++++++
 .../framework/security/util/SslContextFactory.java |   1 -
 .../security/util/SslContextFactoryTest.java       |  22 +-
 .../src/test/resources/no-password-truststore.jks  | Bin 0 -> 911 bytes
 .../nifi/processors/standard/InvokeHTTP.java       |  95 +++---
 .../processors/standard/TestInvokeHttpSSL.java     |  16 +-
 .../nifi/processors/standard/TestListenHTTP.java   |   3 +-
 .../src/test/resources/truststore.no-password.jks  | Bin 0 -> 917 bytes
 .../ssl/StandardRestrictedSSLContextService.java   |   6 -
 .../apache/nifi/ssl/StandardSSLContextService.java | 362 +++++++++++++--------
 .../nifi/ssl/StandardSSLContextServiceTest.groovy  | 105 +++++-
 .../org/apache/nifi/ssl/SSLContextServiceTest.java |  36 +-
 .../test/resources/keystore-with-key-password.jks  | Bin 0 -> 2012 bytes
 .../src/test/resources/no-password-truststore.jks  | Bin 0 -> 911 bytes
 16 files changed, 748 insertions(+), 370 deletions(-)

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 4d80336..d51e951 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
@@ -16,6 +16,33 @@
  */
 package org.apache.nifi.security.util;
 
+import java.io.ByteArrayInputStream;
+import java.math.BigInteger;
+import java.net.Socket;
+import java.security.KeyPair;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Security;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSocket;
 import org.apache.commons.lang3.StringUtils;
 import org.bouncycastle.asn1.ASN1Encodable;
 import org.bouncycastle.asn1.ASN1ObjectIdentifier;
@@ -47,38 +74,6 @@ import 
org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.naming.InvalidNameException;
-import javax.naming.ldap.LdapName;
-import javax.naming.ldap.Rdn;
-import javax.net.ssl.SSLPeerUnverifiedException;
-import javax.net.ssl.SSLSocket;
-import java.io.BufferedInputStream;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.math.BigInteger;
-import java.net.Socket;
-import java.net.URL;
-import java.security.KeyPair;
-import java.security.KeyStore;
-import java.security.NoSuchAlgorithmException;
-import java.security.PublicKey;
-import java.security.Security;
-import java.security.cert.Certificate;
-import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
-import java.security.cert.CertificateParsingException;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-
 public final class CertificateUtils {
     private static final Logger logger = 
LoggerFactory.getLogger(CertificateUtils.class);
     private static final String PEER_NOT_AUTHENTICATED_MSG = "peer not 
authenticated";
@@ -138,48 +133,6 @@ public final class CertificateUtils {
     }
 
     /**
-     * Returns true if the given keystore can be loaded using the given 
keystore type and password. Returns false otherwise.
-     *
-     * @param keystore     the keystore to validate
-     * @param keystoreType the type of the keystore
-     * @param password     the password to access the keystore
-     * @return true if valid; false otherwise
-     */
-    public static boolean isStoreValid(final URL keystore, final KeystoreType 
keystoreType, final char[] password) {
-
-        if (keystore == null) {
-            throw new IllegalArgumentException("keystore may not be null");
-        } else if (keystoreType == null) {
-            throw new IllegalArgumentException("keystore type may not be 
null");
-        } else if (password == null) {
-            throw new IllegalArgumentException("password may not be null");
-        }
-
-        BufferedInputStream bis = null;
-        final KeyStore ks;
-        try {
-
-            // load the keystore
-            bis = new BufferedInputStream(keystore.openStream());
-            ks = KeyStoreUtils.getKeyStore(keystoreType.name());
-            ks.load(bis, password);
-
-            return true;
-
-        } catch (Exception e) {
-            return false;
-        } finally {
-            if (bis != null) {
-                try {
-                    bis.close();
-                } catch (final IOException ioe) {
-                    logger.warn("Failed to close input stream", ioe);
-                }
-            }
-        }
-    }
-
-    /**
      * Extracts the username from the specified DN. If the username cannot be 
extracted because the CN is in an unrecognized format, the entire CN is 
returned. If the CN cannot be extracted because
      * the DN is in an unrecognized format, the entire DN is returned.
      *
@@ -243,7 +196,7 @@ public final class CertificateUtils {
 
     /**
      * Returns the DN extracted from the peer certificate (the server DN if 
run on the client; the client DN (if available) if run on the server).
-     *
+     * <p>
      * If the client auth setting is WANT or NONE and a client certificate is 
not present, this method will return {@code null}.
      * If the client auth is NEED, it will throw a {@link 
CertificateException}.
      *
@@ -263,10 +216,10 @@ public final class CertificateUtils {
 
             if (clientMode) {
                 logger.debug("This socket is in client mode, so attempting to 
extract certificate from remote 'server' socket");
-               dn = extractPeerDNFromServerSSLSocket(sslSocket);
+                dn = extractPeerDNFromServerSSLSocket(sslSocket);
             } else {
                 logger.debug("This socket is in server mode, so attempting to 
extract certificate from remote 'client' socket");
-               dn = extractPeerDNFromClientSSLSocket(sslSocket);
+                dn = extractPeerDNFromClientSSLSocket(sslSocket);
             }
         }
 
@@ -275,7 +228,7 @@ public final class CertificateUtils {
 
     /**
      * Returns the DN extracted from the client certificate.
-     *
+     * <p>
      * If the client auth setting is WANT or NONE and a certificate is not 
present (and {@code respectClientAuth} is {@code true}), this method will 
return {@code null}.
      * If the client auth is NEED, it will throw a {@link 
CertificateException}.
      *
@@ -286,34 +239,34 @@ public final class CertificateUtils {
     private static String extractPeerDNFromClientSSLSocket(SSLSocket 
sslSocket) throws CertificateException {
         String dn = null;
 
-            /** The clientAuth value can be "need", "want", or "none"
-             * A client must send client certificates for need, should for 
want, and will not for none.
-             * This method should throw an exception if none are provided for 
need, return null if none are provided for want, and return null (without 
checking) for none.
-             */
-
-            ClientAuth clientAuth = getClientAuthStatus(sslSocket);
-            logger.debug("SSL Socket client auth status: {}", clientAuth);
-
-            if (clientAuth != ClientAuth.NONE) {
-                try {
-                    final Certificate[] certChains = 
sslSocket.getSession().getPeerCertificates();
-                    if (certChains != null && certChains.length > 0) {
-                        X509Certificate x509Certificate = 
convertAbstractX509Certificate(certChains[0]);
-                        dn = x509Certificate.getSubjectDN().getName().trim();
-                        logger.debug("Extracted DN={} from client 
certificate", dn);
-                    }
-                } catch (SSLPeerUnverifiedException e) {
-                    if (e.getMessage().equals(PEER_NOT_AUTHENTICATED_MSG)) {
-                        logger.error("The incoming request did not contain 
client certificates and thus the DN cannot" +
-                                " be extracted. Check that the other endpoint 
is providing a complete client certificate chain");
-                    }
-                    if (clientAuth == ClientAuth.WANT) {
-                        logger.warn("Suppressing missing client certificate 
exception because client auth is set to 'want'");
-                        return dn;
-                    }
-                    throw new CertificateException(e);
+        /** The clientAuth value can be "need", "want", or "none"
+         * A client must send client certificates for need, should for want, 
and will not for none.
+         * This method should throw an exception if none are provided for 
need, return null if none are provided for want, and return null (without 
checking) for none.
+         */
+
+        ClientAuth clientAuth = getClientAuthStatus(sslSocket);
+        logger.debug("SSL Socket client auth status: {}", clientAuth);
+
+        if (clientAuth != ClientAuth.NONE) {
+            try {
+                final Certificate[] certChains = 
sslSocket.getSession().getPeerCertificates();
+                if (certChains != null && certChains.length > 0) {
+                    X509Certificate x509Certificate = 
convertAbstractX509Certificate(certChains[0]);
+                    dn = x509Certificate.getSubjectDN().getName().trim();
+                    logger.debug("Extracted DN={} from client certificate", 
dn);
                 }
+            } catch (SSLPeerUnverifiedException e) {
+                if (e.getMessage().equals(PEER_NOT_AUTHENTICATED_MSG)) {
+                    logger.error("The incoming request did not contain client 
certificates and thus the DN cannot" +
+                            " be extracted. Check that the other endpoint is 
providing a complete client certificate chain");
+                }
+                if (clientAuth == ClientAuth.WANT) {
+                    logger.warn("Suppressing missing client certificate 
exception because client auth is set to 'want'");
+                    return dn;
+                }
+                throw new CertificateException(e);
             }
+        }
         return dn;
     }
 
@@ -328,20 +281,20 @@ public final class CertificateUtils {
         String dn = null;
         if (socket instanceof SSLSocket) {
             final SSLSocket sslSocket = (SSLSocket) socket;
-                try {
-                    final Certificate[] certChains = 
sslSocket.getSession().getPeerCertificates();
-                    if (certChains != null && certChains.length > 0) {
-                        X509Certificate x509Certificate = 
convertAbstractX509Certificate(certChains[0]);
-                        dn = x509Certificate.getSubjectDN().getName().trim();
-                        logger.debug("Extracted DN={} from server 
certificate", dn);
-                    }
-                } catch (SSLPeerUnverifiedException e) {
-                    if (e.getMessage().equals(PEER_NOT_AUTHENTICATED_MSG)) {
-                        logger.error("The server did not present a certificate 
and thus the DN cannot" +
-                                " be extracted. Check that the other endpoint 
is providing a complete certificate chain");
-                    }
-                    throw new CertificateException(e);
+            try {
+                final Certificate[] certChains = 
sslSocket.getSession().getPeerCertificates();
+                if (certChains != null && certChains.length > 0) {
+                    X509Certificate x509Certificate = 
convertAbstractX509Certificate(certChains[0]);
+                    dn = x509Certificate.getSubjectDN().getName().trim();
+                    logger.debug("Extracted DN={} from server certificate", 
dn);
+                }
+            } catch (SSLPeerUnverifiedException e) {
+                if (e.getMessage().equals(PEER_NOT_AUTHENTICATED_MSG)) {
+                    logger.error("The server did not present a certificate and 
thus the DN cannot" +
+                            " be extracted. Check that the other endpoint is 
providing a complete certificate chain");
                 }
+                throw new CertificateException(e);
+            }
         }
         return dn;
     }
@@ -403,9 +356,9 @@ public final class CertificateUtils {
 
     /**
      * Reorders DN to the order the elements appear in the RFC 2253 table
-     *
+     * <p>
      * https://www.ietf.org/rfc/rfc2253.txt
-     *
+     * <p>
      * String  X.500 AttributeType
      * ------------------------------
      * CN      commonName
@@ -496,7 +449,7 @@ public final class CertificateUtils {
      * @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
      * @return a self-signed {@link X509Certificate} suitable for use as a 
Certificate Authority
-     * @throws CertificateException      if there is an generating the new 
certificate
+     * @throws CertificateException if there is an generating the new 
certificate
      */
     public static X509Certificate generateSelfSignedX509Certificate(KeyPair 
keyPair, String dn, String signingAlgorithm, int certificateDurationDays)
             throws CertificateException {
@@ -538,12 +491,12 @@ public final class CertificateUtils {
     /**
      * Generates an issued {@link X509Certificate} from the given issuer 
certificate and {@link KeyPair}
      *
-     * @param dn the distinguished name to use
-     * @param publicKey the public key to issue the certificate to
-     * @param issuer the issuer's certificate
-     * @param issuerKeyPair the issuer's keypair
+     * @param dn               the distinguished name to use
+     * @param publicKey        the public key to issue the certificate to
+     * @param issuer           the issuer's certificate
+     * @param issuerKeyPair    the issuer's keypair
      * @param signingAlgorithm the signing algorithm to use
-     * @param days the number of days it should be valid for
+     * @param days             the number of days it should be valid for
      * @return an issued {@link X509Certificate} from the given issuer 
certificate and {@link KeyPair}
      * @throws CertificateException if there is an error issuing the 
certificate
      */
@@ -555,13 +508,13 @@ public final class CertificateUtils {
     /**
      * Generates an issued {@link X509Certificate} from the given issuer 
certificate and {@link KeyPair}
      *
-     * @param dn the distinguished name to use
-     * @param publicKey the public key to issue the certificate to
-     * @param extensions extensions extracted from the CSR
-     * @param issuer the issuer's certificate
-     * @param issuerKeyPair the issuer's keypair
+     * @param dn               the distinguished name to use
+     * @param publicKey        the public key to issue the certificate to
+     * @param extensions       extensions extracted from the CSR
+     * @param issuer           the issuer's certificate
+     * @param issuerKeyPair    the issuer's keypair
      * @param signingAlgorithm the signing algorithm to use
-     * @param days the number of days it should be valid for
+     * @param days             the number of days it should be valid for
      * @return an issued {@link X509Certificate} from the given issuer 
certificate and {@link KeyPair}
      * @throws CertificateException if there is an error issuing the 
certificate
      */
@@ -594,7 +547,7 @@ public final class CertificateUtils {
             certBuilder.addExtension(Extension.extendedKeyUsage, false, new 
ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, 
KeyPurposeId.id_kp_serverAuth}));
 
             // (3) subjectAlternativeName
-            if(extensions != null && 
extensions.getExtension(Extension.subjectAlternativeName) != null) {
+            if (extensions != null && 
extensions.getExtension(Extension.subjectAlternativeName) != null) {
                 certBuilder.addExtension(Extension.subjectAlternativeName, 
false, extensions.getExtensionParsedValue(Extension.subjectAlternativeName));
             }
 
@@ -607,15 +560,15 @@ public final class CertificateUtils {
 
     /**
      * Returns true if the two provided DNs are equivalent, regardless of the 
order of the elements. Returns false if one or both are invalid DNs.
-     *
+     * <p>
      * Example:
-     *
+     * <p>
      * CN=test1, O=testOrg, C=US compared to CN=test1, O=testOrg, C=US -> true
      * CN=test1, O=testOrg, C=US compared to O=testOrg, CN=test1, C=US -> true
      * CN=test1, O=testOrg, C=US compared to CN=test2, O=testOrg, C=US -> false
      * CN=test1, O=testOrg, C=US compared to O=testOrg, CN=test2, C=US -> false
      * CN=test1, O=testOrg, C=US compared to                           -> false
-     *                           compared to                           -> true
+     * compared to                           -> true
      *
      * @param dn1 the first DN to compare
      * @param dn2 the second DN to compare
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 6b52009..d763b09 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
@@ -17,15 +17,19 @@
 
 package org.apache.nifi.security.util;
 
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.Security;
+import java.security.UnrecoverableKeyException;
 import org.apache.commons.lang3.StringUtils;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.Security;
-
 public class KeyStoreUtils {
     private static final Logger logger = 
LoggerFactory.getLogger(KeyStoreUtils.class);
 
@@ -83,4 +87,97 @@ public class KeyStoreUtils {
         }
         return getKeyStore(trustStoreType);
     }
+
+    /**
+     * Returns true if the given keystore can be loaded using the given 
keystore type and password. Returns false otherwise.
+     *
+     * @param keystore     the keystore to validate
+     * @param keystoreType the type of the keystore
+     * @param password     the password to access the keystore
+     * @return true if valid; false otherwise
+     */
+    public static boolean isStoreValid(final URL keystore, final KeystoreType 
keystoreType, final char[] password) {
+
+        if (keystore == null) {
+            throw new IllegalArgumentException("Keystore may not be null");
+        } else if (keystoreType == null) {
+            throw new IllegalArgumentException("Keystore type may not be 
null");
+        } else if (password == null) {
+            throw new IllegalArgumentException("Password may not be null");
+        }
+
+        BufferedInputStream bis = null;
+        final KeyStore ks;
+        try {
+
+            // Load the keystore
+            bis = new BufferedInputStream(keystore.openStream());
+            ks = KeyStoreUtils.getKeyStore(keystoreType.name());
+            ks.load(bis, password);
+
+            return true;
+
+        } catch (Exception e) {
+            return false;
+        } finally {
+            if (bis != null) {
+                try {
+                    bis.close();
+                } catch (final IOException ioe) {
+                    logger.warn("Failed to close input stream", ioe);
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns true if the given keystore can be loaded using the given 
keystore type and password and the default
+     * (first) alias can be retrieved with the key-specific password. Returns 
false otherwise.
+     *
+     * @param keystore     the keystore to validate
+     * @param keystoreType the type of the keystore
+     * @param password     the password to access the keystore
+     * @param keyPassword  the password to access the specific key
+     * @return true if valid; false otherwise
+     */
+    public static boolean isKeyPasswordCorrect(final URL keystore, final 
KeystoreType keystoreType, final char[] password, final char[] keyPassword) {
+
+        if (keystore == null) {
+            throw new IllegalArgumentException("Keystore may not be null");
+        } else if (keystoreType == null) {
+            throw new IllegalArgumentException("Keystore type may not be 
null");
+        } else if (password == null) {
+            throw new IllegalArgumentException("Password may not be null");
+        }
+
+        BufferedInputStream bis = null;
+        final KeyStore ks;
+        try {
+
+            // Load the keystore
+            bis = new BufferedInputStream(keystore.openStream());
+            ks = KeyStoreUtils.getKeyStore(keystoreType.name());
+            ks.load(bis, password);
+
+            // Determine the default alias
+            String alias = ks.aliases().nextElement();
+            try {
+                Key privateKeyEntry = ks.getKey(alias, keyPassword);
+                return true;
+            } catch (UnrecoverableKeyException e) {
+                logger.warn("Tried to access a key in keystore " + keystore + 
" with a key password that failed");
+                return false;
+            }
+        } catch (Exception e) {
+            return false;
+        } finally {
+            if (bis != null) {
+                try {
+                    bis.close();
+                } catch (final IOException ioe) {
+                    logger.warn("Failed to close input stream", ioe);
+                }
+            }
+        }
+    }
 }
diff --git 
a/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/KeyStoreUtilsGroovyTest.groovy
 
b/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/KeyStoreUtilsGroovyTest.groovy
new file mode 100644
index 0000000..7a76348
--- /dev/null
+++ 
b/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/KeyStoreUtilsGroovyTest.groovy
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.util
+
+import org.junit.After
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.net.ssl.HttpsURLConnection
+import javax.net.ssl.SSLSocket
+import javax.net.ssl.SSLSocketFactory
+import java.security.KeyStore
+import java.security.cert.Certificate
+
+@RunWith(JUnit4.class)
+class KeyStoreUtilsGroovyTest extends GroovyTestCase {
+    private static final Logger logger = 
LoggerFactory.getLogger(KeyStoreUtilsGroovyTest.class)
+
+    private static final File KEYSTORE_FILE = new 
File("src/test/resources/keystore.jks")
+    private static final String KEYSTORE_PASSWORD = "passwordpassword"
+    private static final String KEY_PASSWORD = "keypassword"
+    private static final KeystoreType KEYSTORE_TYPE = KeystoreType.JKS
+
+    @BeforeClass
+    static void setUpOnce() {
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    @Before
+    void setUp() {
+
+    }
+
+    @After
+    void tearDown() {
+
+    }
+
+    @Test
+    void testShouldVerifyKeystoreIsValid() {
+        // Arrange
+
+        // Act
+        boolean keystoreIsValid = 
KeyStoreUtils.isStoreValid(KEYSTORE_FILE.toURI().toURL(), KEYSTORE_TYPE, 
KEYSTORE_PASSWORD.toCharArray())
+
+        // Assert
+        assert keystoreIsValid
+    }
+
+    @Test
+    void testShouldVerifyKeystoreIsNotValid() {
+        // Arrange
+
+        // Act
+        boolean keystoreIsValid = 
KeyStoreUtils.isStoreValid(KEYSTORE_FILE.toURI().toURL(), KEYSTORE_TYPE, 
KEYSTORE_PASSWORD.reverse().toCharArray())
+
+        // Assert
+        assert !keystoreIsValid
+    }
+
+    @Test
+    void testShouldVerifyKeyPasswordIsValid() {
+        // Arrange
+
+        // Act
+        boolean keyPasswordIsValid = 
KeyStoreUtils.isKeyPasswordCorrect(KEYSTORE_FILE.toURI().toURL(), 
KEYSTORE_TYPE, KEYSTORE_PASSWORD.toCharArray(), KEYSTORE_PASSWORD.toCharArray())
+
+        // Assert
+        assert keyPasswordIsValid
+    }
+
+    @Test
+    void testShouldVerifyKeyPasswordIsNotValid() {
+        // Arrange
+
+        // Act
+        boolean keyPasswordIsValid = 
KeyStoreUtils.isKeyPasswordCorrect(KEYSTORE_FILE.toURI().toURL(), 
KEYSTORE_TYPE, KEYSTORE_PASSWORD.toCharArray(), 
KEYSTORE_PASSWORD.reverse().toCharArray())
+
+        // Assert
+        assert !keyPasswordIsValid
+    }
+
+    @Test
+    @Ignore("Used to create passwordless truststore file for testing 
NIFI-6770")
+    void createPasswordlessTruststore() {
+        // Retrieve the public certificate from https://nifi.apache.org
+        String hostname = "nifi.apache.org"
+        SSLSocketFactory factory = 
HttpsURLConnection.getDefaultSSLSocketFactory()
+        SSLSocket socket = (SSLSocket) factory.createSocket(hostname, 443)
+        socket.startHandshake()
+        List<Certificate> certs = socket.session.peerCertificateChain as 
List<Certificate>
+        Certificate nodeCert = 
CertificateUtils.formX509Certificate(certs.first().encoded)
+
+        // Create a JKS truststore containing that cert as a trustedCertEntry 
and do not put a password on the truststore
+        KeyStore truststore = KeyStore.getInstance("JKS")
+        // Explicitly set the second parameter to empty to avoid a password
+        truststore.load(null, "".chars)
+        truststore.setCertificateEntry("nifi.apache.org", nodeCert)
+
+        // Save the truststore to disk
+        FileOutputStream fos = new 
FileOutputStream("target/nifi.apache.org.ts.jks")
+        truststore.store(fos, "".chars)
+    }
+
+    @Test
+    @Ignore("Used to create passwordless truststore file for testing 
NIFI-6770")
+    void createLocalPasswordlessTruststore() {
+        KeyStore truststoreWithPassword = KeyStore.getInstance("JKS")
+        truststoreWithPassword.load(new 
FileInputStream("/Users/alopresto/Workspace/nifi/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/truststore.jks"),
 "passwordpassword".chars)
+        Certificate nodeCert = 
truststoreWithPassword.getCertificate("nifi-cert")
+
+        // Create a JKS truststore containing that cert as a trustedCertEntry 
and do not put a password on the truststore
+        KeyStore truststore = KeyStore.getInstance("JKS")
+        // Explicitly set the second parameter to empty to avoid a password
+        truststore.load(null, "".chars)
+        truststore.setCertificateEntry("nifi.apache.org", nodeCert)
+
+        // Save the truststore to disk
+        FileOutputStream fos = new 
FileOutputStream("/Users/alopresto/Workspace/nifi/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/truststore.no-password.jks")
+        truststore.store(fos, "".chars)
+    }
+}
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-security/src/main/java/org/apache/nifi/framework/security/util/SslContextFactory.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-security/src/main/java/org/apache/nifi/framework/security/util/SslContextFactory.java
index 8f43339..6159264 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-security/src/main/java/org/apache/nifi/framework/security/util/SslContextFactory.java
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-security/src/main/java/org/apache/nifi/framework/security/util/SslContextFactory.java
@@ -98,7 +98,6 @@ public final class SslContextFactory {
 
     private static boolean hasTruststoreProperties(final NiFiProperties props) 
{
         return 
(StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE))
-                && 
StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD))
                 && 
StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE)));
     }
 
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-security/src/test/java/org/apache/nifi/framework/security/util/SslContextFactoryTest.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-security/src/test/java/org/apache/nifi/framework/security/util/SslContextFactoryTest.java
index 93b2c8f..6bcaa9f 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-security/src/test/java/org/apache/nifi/framework/security/util/SslContextFactoryTest.java
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-security/src/test/java/org/apache/nifi/framework/security/util/SslContextFactoryTest.java
@@ -16,29 +16,30 @@
  */
 package org.apache.nifi.framework.security.util;
 
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
 import org.apache.nifi.security.util.KeystoreType;
 import org.apache.nifi.util.NiFiProperties;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 
-import java.io.File;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
 /**
  */
 public class SslContextFactoryTest {
 
     private NiFiProperties mutualAuthProps;
     private NiFiProperties authProps;
+    private NiFiProperties noPasswordTruststore;
 
     @Before
     public void setUp() throws Exception {
 
         final File ksFile = new 
File(SslContextFactoryTest.class.getResource("/keystore.jks").toURI());
         final File trustFile = new 
File(SslContextFactoryTest.class.getResource("/truststore.jks").toURI());
+        final File noPasswordTrustFile = new 
File(SslContextFactoryTest.class.getResource("/no-password-truststore.jks").toURI());
 
         authProps = mock(NiFiProperties.class);
         
when(authProps.getProperty(NiFiProperties.SECURITY_KEYSTORE)).thenReturn(ksFile.getAbsolutePath());
@@ -53,6 +54,13 @@ public class SslContextFactoryTest {
         
when(mutualAuthProps.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE)).thenReturn(KeystoreType.JKS.toString());
         
when(mutualAuthProps.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD)).thenReturn("passwordpassword");
 
+        noPasswordTruststore = mock(NiFiProperties.class);
+        
when(noPasswordTruststore.getProperty(NiFiProperties.SECURITY_KEYSTORE)).thenReturn(ksFile.getAbsolutePath());
+        
when(noPasswordTruststore.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE)).thenReturn(KeystoreType.JKS.toString());
+        
when(noPasswordTruststore.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD)).thenReturn("passwordpassword");
+        
when(noPasswordTruststore.getProperty(NiFiProperties.SECURITY_TRUSTSTORE)).thenReturn(noPasswordTrustFile.getAbsolutePath());
+        
when(noPasswordTruststore.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE)).thenReturn(KeystoreType.JKS.toString());
+        
when(noPasswordTruststore.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD)).thenReturn("");
     }
 
     @Test
@@ -65,4 +73,8 @@ public class SslContextFactoryTest {
         SslContextFactory.createSslContext(authProps);
     }
 
+    @Test
+    public void testCreateSslContextWithNoPasswordTruststore() {
+        
Assert.assertNotNull(SslContextFactory.createSslContext(noPasswordTruststore));
+    }
 }
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-security/src/test/resources/no-password-truststore.jks
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-security/src/test/resources/no-password-truststore.jks
new file mode 100644
index 0000000..aa1ce5d
Binary files /dev/null and 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-security/src/test/resources/no-password-truststore.jks
 differ
diff --git 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/InvokeHTTP.java
 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/InvokeHTTP.java
index 0fe5dab..c344410 100644
--- 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/InvokeHTTP.java
+++ 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/InvokeHTTP.java
@@ -16,12 +16,54 @@
  */
 package org.apache.nifi.processors.standard;
 
+import static org.apache.commons.lang3.StringUtils.trimToEmpty;
+
 import com.burgstaller.okhttp.AuthenticationCacheInterceptor;
 import com.burgstaller.okhttp.CachingAuthenticatorDecorator;
 import com.burgstaller.okhttp.digest.CachingAuthenticator;
 import com.burgstaller.okhttp.digest.DigestAuthenticator;
 import com.google.common.io.Files;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Proxy;
+import java.net.Proxy.Type;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
 import java.security.Principal;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
 import okhttp3.Cache;
 import okhttp3.Credentials;
 import okhttp3.MediaType;
@@ -53,8 +95,8 @@ import org.apache.nifi.logging.ComponentLog;
 import org.apache.nifi.processor.AbstractProcessor;
 import org.apache.nifi.processor.DataUnit;
 import org.apache.nifi.processor.ProcessContext;
-import org.apache.nifi.processor.ProcessorInitializationContext;
 import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.ProcessorInitializationContext;
 import org.apache.nifi.processor.Relationship;
 import org.apache.nifi.processor.exception.ProcessException;
 import org.apache.nifi.processor.util.StandardValidators;
@@ -68,50 +110,6 @@ import org.apache.nifi.stream.io.StreamUtils;
 import org.joda.time.format.DateTimeFormat;
 import org.joda.time.format.DateTimeFormatter;
 
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.KeyManager;
-import javax.net.ssl.KeyManagerFactory;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSession;
-import javax.net.ssl.SSLSocketFactory;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.TrustManagerFactory;
-import javax.net.ssl.X509TrustManager;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.Proxy;
-import java.net.Proxy.Type;
-import java.net.URL;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.security.KeyManagementException;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.UnrecoverableKeyException;
-import java.security.cert.CertificateException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import static org.apache.commons.lang3.StringUtils.trimToEmpty;
-
 @SupportsBatching
 @Tags({"http", "https", "rest", "client"})
 @InputRequirement(Requirement.INPUT_ALLOWED)
@@ -686,8 +684,13 @@ public final class InvokeHTTP extends AbstractProcessor {
             final String truststorePass = sslService.getTrustStorePassword();
             final String truststoreType = sslService.getTrustStoreType();
 
+            char[] truststorePasswordChars = new char[0];
+            if (StringUtils.isNotBlank(truststorePass)) {
+                truststorePasswordChars = truststorePass.toCharArray();
+            }
+
             KeyStore truststore = KeyStore.getInstance(truststoreType);
-            truststore.load(new FileInputStream(truststoreLocation), 
truststorePass.toCharArray());
+            truststore.load(new FileInputStream(truststoreLocation), 
truststorePasswordChars);
             trustManagerFactory.init(truststore);
         }
 
diff --git 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestInvokeHttpSSL.java
 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestInvokeHttpSSL.java
index 5ef2576..0dce0f0 100644
--- 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestInvokeHttpSSL.java
+++ 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestInvokeHttpSSL.java
@@ -17,19 +17,18 @@
 
 package org.apache.nifi.processors.standard;
 
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
 import org.apache.nifi.processors.standard.util.TestInvokeHttpCommon;
-import org.apache.nifi.web.util.TestServer;
 import org.apache.nifi.ssl.StandardSSLContextService;
 import org.apache.nifi.util.TestRunners;
+import org.apache.nifi.web.util.TestServer;
 import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
 
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-
 /**
  * Executes the same tests as TestInvokeHttp but with one-way SSL enabled.  
The Jetty server created for these tests
  * will not require client certificates and the client will not use keystore 
properties in the SSLContextService.
@@ -46,7 +45,7 @@ public class TestInvokeHttpSSL extends TestInvokeHttpCommon {
         // don't commit this with this property enabled, or any 'mvn test' 
will be really verbose
         // 
System.setProperty("org.slf4j.simpleLogger.log.nifi.processors.standard", 
"debug");
 
-        // create the SSL properties, which basically store keystore / 
trustore information
+        // create the SSL properties, which basically store keystore / 
truststore information
         // this is used by the StandardSSLContextService and the Jetty Server
         serverSslProperties = createServerSslProperties(false);
         sslProperties = createClientSslProperties(false);
@@ -138,8 +137,9 @@ public class TestInvokeHttpSSL extends TestInvokeHttpCommon 
{
 
     private static Map<String, String> getTruststoreProperties() {
         final Map<String, String> map = new HashMap<>();
-        map.put(StandardSSLContextService.TRUSTSTORE.getName(), 
"src/test/resources/truststore.jks");
-        map.put(StandardSSLContextService.TRUSTSTORE_PASSWORD.getName(), 
"passwordpassword");
+        map.put(StandardSSLContextService.TRUSTSTORE.getName(), 
"src/test/resources/truststore.no-password.jks");
+        // Commented this line to test passwordless truststores for NIFI-6770
+        // map.put(StandardSSLContextService.TRUSTSTORE_PASSWORD.getName(), 
"passwordpassword");
         map.put(StandardSSLContextService.TRUSTSTORE_TYPE.getName(), "JKS");
         return map;
     }
diff --git 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestListenHTTP.java
 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestListenHTTP.java
index b45a0c0..0dee76c 100644
--- 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestListenHTTP.java
+++ 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestListenHTTP.java
@@ -92,6 +92,7 @@ public class TestListenHTTP {
     @After
     public void teardown() {
         proc.shutdownHttpServer();
+        new 
File("/Users/alopresto/Workspace/nifi/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/my-file-text.txt").delete();
     }
 
     @Test
@@ -482,7 +483,7 @@ public class TestListenHTTP {
       return bytes;
     }
      private File createTextFile(String fileName, String... lines) throws 
IOException {
-      File file = new File(fileName);
+      File file = new File("target/" + fileName);
       file.deleteOnExit();
       for (String string : lines) {
         Files.append(string, file, Charsets.UTF_8);
diff --git 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/truststore.no-password.jks
 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/truststore.no-password.jks
new file mode 100644
index 0000000..51f8f61
Binary files /dev/null and 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/truststore.no-password.jks
 differ
diff --git 
a/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/main/java/org/apache/nifi/ssl/StandardRestrictedSSLContextService.java
 
b/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/main/java/org/apache/nifi/ssl/StandardRestrictedSSLContextService.java
index 4ad7bbe..f17c9bf 100644
--- 
a/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/main/java/org/apache/nifi/ssl/StandardRestrictedSSLContextService.java
+++ 
b/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/main/java/org/apache/nifi/ssl/StandardRestrictedSSLContextService.java
@@ -22,7 +22,6 @@ import java.util.List;
 import org.apache.nifi.annotation.documentation.CapabilityDescription;
 import org.apache.nifi.annotation.documentation.Tags;
 import org.apache.nifi.components.PropertyDescriptor;
-import org.apache.nifi.components.ValidationContext;
 import org.apache.nifi.processor.util.StandardValidators;
 
 /**
@@ -73,9 +72,4 @@ public class StandardRestrictedSSLContextService extends 
StandardSSLContextServi
     public String getSslAlgorithm() {
         return configContext.getProperty(RESTRICTED_SSL_ALGORITHM).getValue();
     }
-
-    @Override
-    protected String getSSLProtocolForValidation(final ValidationContext 
validationContext) {
-        return 
validationContext.getProperty(RESTRICTED_SSL_ALGORITHM).getValue();
-    }
 }
diff --git 
a/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/main/java/org/apache/nifi/ssl/StandardSSLContextService.java
 
b/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/main/java/org/apache/nifi/ssl/StandardSSLContextService.java
index 6b267ac..3f6be26 100644
--- 
a/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/main/java/org/apache/nifi/ssl/StandardSSLContextService.java
+++ 
b/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/main/java/org/apache/nifi/ssl/StandardSSLContextService.java
@@ -37,9 +37,10 @@ import org.apache.nifi.controller.ConfigurationContext;
 import org.apache.nifi.processor.exception.ProcessException;
 import org.apache.nifi.processor.util.StandardValidators;
 import org.apache.nifi.reporting.InitializationException;
-import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
 import org.apache.nifi.security.util.KeystoreType;
 import org.apache.nifi.security.util.SslContextFactory;
+import org.apache.nifi.util.StringUtils;
 
 @Tags({"ssl", "secure", "certificate", "keystore", "truststore", "jks", "p12", 
"pkcs12", "pkcs", "tls"})
 @CapabilityDescription("Standard implementation of the SSLContextService. 
Provides the ability to configure "
@@ -70,7 +71,8 @@ public class StandardSSLContextService extends 
AbstractControllerService impleme
             .name("Truststore Password")
             .description("The password for the Truststore")
             .defaultValue(null)
-            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .addValidator(Validator.VALID)
+            .required(false)
             .sensitive(true)
             .build();
     public static final PropertyDescriptor KEYSTORE = new 
PropertyDescriptor.Builder()
@@ -150,18 +152,6 @@ public class StandardSSLContextService extends 
AbstractControllerService impleme
             }
             throw new InitializationException(sb.toString());
         }
-
-        if (countNulls(context.getProperty(KEYSTORE).getValue(),
-                context.getProperty(KEYSTORE_PASSWORD).getValue(),
-                context.getProperty(KEYSTORE_TYPE).getValue(),
-                context.getProperty(TRUSTSTORE).getValue(),
-                context.getProperty(TRUSTSTORE_PASSWORD).getValue(),
-                context.getProperty(TRUSTSTORE_TYPE).getValue()) >= 4) {
-            throw new InitializationException(this + " does not have the 
KeyStore or the TrustStore populated");
-        }
-
-        // verify that the filename, password, and type match
-        createSSLContext(ClientAuth.REQUIRED);
     }
 
     @Override
@@ -171,21 +161,17 @@ public class StandardSSLContextService extends 
AbstractControllerService impleme
     }
 
     private static Validator createFileExistsAndReadableValidator() {
-        return new Validator() {
-            // Not using the FILE_EXISTS_VALIDATOR because the default is to
-            // allow expression language
-            @Override
-            public ValidationResult validate(String subject, String input, 
ValidationContext context) {
-                final File file = new File(input);
-                final boolean valid = file.exists() && file.canRead();
-                final String explanation = valid ? null : "File " + file + " 
does not exist or cannot be read";
-                return new ValidationResult.Builder()
-                        .subject(subject)
-                        .input(input)
-                        .valid(valid)
-                        .explanation(explanation)
-                        .build();
-            }
+        // Not using the FILE_EXISTS_VALIDATOR because the default is to allow 
expression language
+        return (subject, input, context) -> {
+            final File file = new File(input);
+            final boolean valid = file.exists() && file.canRead();
+            final String explanation = valid ? null : "File " + file + " does 
not exist or cannot be read";
+            return new ValidationResult.Builder()
+                    .subject(subject)
+                    .input(input)
+                    .valid(valid)
+                    .explanation(explanation)
+                    .build();
         };
     }
 
@@ -210,32 +196,6 @@ public class StandardSSLContextService extends 
AbstractControllerService impleme
         results.addAll(validateStore(validationContext.getProperties(), 
KeystoreValidationGroup.KEYSTORE));
         results.addAll(validateStore(validationContext.getProperties(), 
KeystoreValidationGroup.TRUSTSTORE));
 
-        if (countNulls(validationContext.getProperty(KEYSTORE).getValue(),
-                validationContext.getProperty(KEYSTORE_PASSWORD).getValue(),
-                validationContext.getProperty(KEYSTORE_TYPE).getValue(),
-                validationContext.getProperty(TRUSTSTORE).getValue(),
-                validationContext.getProperty(TRUSTSTORE_PASSWORD).getValue(),
-                validationContext.getProperty(TRUSTSTORE_TYPE).getValue())
-                >= 4) {
-            results.add(new ValidationResult.Builder()
-                    .subject(this.getClass().getSimpleName() + " : " + 
getIdentifier())
-                    .valid(false)
-                    .explanation("Does not have the KeyStore or the TrustStore 
populated")
-                    .build());
-        }
-        if (results.isEmpty()) {
-            // verify that the filename, password, and type match
-            try {
-                verifySslConfig(validationContext);
-            } catch (ProcessException e) {
-                results.add(new ValidationResult.Builder()
-                        .subject(getClass().getSimpleName() + " : " + 
getIdentifier())
-                        .valid(false)
-                        .explanation(e.getMessage())
-                        .build());
-            }
-        }
-
         isValidated = results.isEmpty();
 
         return results;
@@ -250,64 +210,21 @@ public class StandardSSLContextService extends 
AbstractControllerService impleme
         return VALIDATION_CACHE_EXPIRATION;
     }
 
-    protected String getSSLProtocolForValidation(final ValidationContext 
validationContext) {
-        return validationContext.getProperty(SSL_ALGORITHM).getValue();
-    }
-
-    private void verifySslConfig(final ValidationContext validationContext) 
throws ProcessException {
-        final String protocol = getSSLProtocolForValidation(validationContext);
-        try {
-            final PropertyValue keyPasswdProp = 
validationContext.getProperty(KEY_PASSWORD);
-            final char[] keyPassword = keyPasswdProp.isSet() ? 
keyPasswdProp.getValue().toCharArray() : null;
-
-            final String keystoreFile = 
validationContext.getProperty(KEYSTORE).getValue();
-            if (keystoreFile == null) {
-                SslContextFactory.createTrustSslContext(
-                        validationContext.getProperty(TRUSTSTORE).getValue(),
-                        
validationContext.getProperty(TRUSTSTORE_PASSWORD).getValue().toCharArray(),
-                        
validationContext.getProperty(TRUSTSTORE_TYPE).getValue(),
-                        protocol);
-                return;
-            }
-            final String truststoreFile = 
validationContext.getProperty(TRUSTSTORE).getValue();
-            if (truststoreFile == null) {
-                SslContextFactory.createSslContext(
-                        validationContext.getProperty(KEYSTORE).getValue(),
-                        
validationContext.getProperty(KEYSTORE_PASSWORD).getValue().toCharArray(),
-                        keyPassword,
-                        
validationContext.getProperty(KEYSTORE_TYPE).getValue(),
-                        protocol);
-                return;
-            }
-
-            SslContextFactory.createSslContext(
-                    validationContext.getProperty(KEYSTORE).getValue(),
-                    
validationContext.getProperty(KEYSTORE_PASSWORD).getValue().toCharArray(),
-                    keyPassword,
-                    validationContext.getProperty(KEYSTORE_TYPE).getValue(),
-                    validationContext.getProperty(TRUSTSTORE).getValue(),
-                    
validationContext.getProperty(TRUSTSTORE_PASSWORD).getValue().toCharArray(),
-                    validationContext.getProperty(TRUSTSTORE_TYPE).getValue(),
-                    
org.apache.nifi.security.util.SslContextFactory.ClientAuth.REQUIRED,
-                    protocol);
-        } catch (final Exception e) {
-            throw new ProcessException(e);
-        }
-    }
-
     @Override
     public SSLContext createSSLContext(final ClientAuth clientAuth) throws 
ProcessException {
         final String protocol = getSslAlgorithm();
         try {
             final PropertyValue keyPasswdProp = 
configContext.getProperty(KEY_PASSWORD);
+            final PropertyValue truststorePasswordProp = 
configContext.getProperty(TRUSTSTORE_PASSWORD);
             final char[] keyPassword = keyPasswdProp.isSet() ? 
keyPasswdProp.getValue().toCharArray() : null;
+            final char[] truststorePassword = truststorePasswordProp.isSet() ? 
truststorePasswordProp.getValue().toCharArray() : null;
 
             final String keystoreFile = 
configContext.getProperty(KEYSTORE).getValue();
             if (keystoreFile == null) {
                 // If keystore not specified, create SSL Context based only on 
trust store.
                 return SslContextFactory.createTrustSslContext(
                         configContext.getProperty(TRUSTSTORE).getValue(),
-                        
configContext.getProperty(TRUSTSTORE_PASSWORD).getValue().toCharArray(),
+                        truststorePassword,
                         configContext.getProperty(TRUSTSTORE_TYPE).getValue(),
                         protocol);
             }
@@ -329,7 +246,7 @@ public class StandardSSLContextService extends 
AbstractControllerService impleme
                     keyPassword,
                     configContext.getProperty(KEYSTORE_TYPE).getValue(),
                     configContext.getProperty(TRUSTSTORE).getValue(),
-                    
configContext.getProperty(TRUSTSTORE_PASSWORD).getValue().toCharArray(),
+                    truststorePassword,
                     configContext.getProperty(TRUSTSTORE_TYPE).getValue(),
                     
org.apache.nifi.security.util.SslContextFactory.ClientAuth.valueOf(clientAuth.name()),
                     protocol);
@@ -350,12 +267,13 @@ public class StandardSSLContextService extends 
AbstractControllerService impleme
 
     @Override
     public String getTrustStorePassword() {
-        return configContext.getProperty(TRUSTSTORE_PASSWORD).getValue();
+        PropertyValue truststorePassword = 
configContext.getProperty(TRUSTSTORE_PASSWORD);
+        return truststorePassword.isSet() ? truststorePassword.getValue() : "";
     }
 
     @Override
     public boolean isTrustStoreConfigured() {
-        return getTrustStoreFile() != null && getTrustStorePassword() != null 
&& getTrustStoreType() != null;
+        return getTrustStoreFile() != null && getTrustStoreType() != null;
     }
 
     @Override
@@ -388,71 +306,227 @@ public class StandardSSLContextService extends 
AbstractControllerService impleme
         return configContext.getProperty(SSL_ALGORITHM).getValue();
     }
 
+    /**
+     * Returns a list of {@link ValidationResult}s for the provided
+     * keystore/truststore properties. Called during
+     * {@link #customValidate(ValidationContext)}.
+     *
+     * @param properties           the map of component properties
+     * @param keyStoreOrTrustStore an enum {@link KeystoreValidationGroup} 
indicating keystore or truststore because logic is different
+     * @return the list of validation results (empty means valid)
+     */
     private static Collection<ValidationResult> validateStore(final 
Map<PropertyDescriptor, String> properties,
                                                               final 
KeystoreValidationGroup keyStoreOrTrustStore) {
-        final Collection<ValidationResult> results = new ArrayList<>();
-
-        final String filename;
-        final String password;
-        final String type;
+        List<ValidationResult> results;
 
         if (keyStoreOrTrustStore == KeystoreValidationGroup.KEYSTORE) {
-            filename = properties.get(KEYSTORE);
-            password = properties.get(KEYSTORE_PASSWORD);
-            type = properties.get(KEYSTORE_TYPE);
+            results = validateKeystore(properties);
         } else {
-            filename = properties.get(TRUSTSTORE);
-            password = properties.get(TRUSTSTORE_PASSWORD);
-            type = properties.get(TRUSTSTORE_TYPE);
+            results = validateTruststore(properties);
+        }
+
+        if (keystorePropertiesEmpty(properties) && 
truststorePropertiesEmpty(properties)) {
+            results.add(new 
ValidationResult.Builder().valid(false).explanation("Either the keystore and/or 
truststore must be populated").subject("Keystore/truststore 
properties").build());
         }
 
-        final String keystoreDesc = (keyStoreOrTrustStore == 
KeystoreValidationGroup.KEYSTORE) ? "Keystore" : "Truststore";
+        return results;
+    }
+
+    private static boolean keystorePropertiesEmpty(Map<PropertyDescriptor, 
String> properties) {
+        return StringUtils.isBlank(properties.get(KEYSTORE)) && 
StringUtils.isBlank(properties.get(KEYSTORE_PASSWORD)) && 
StringUtils.isBlank(properties.get(KEYSTORE_TYPE));
+    }
+
+    private static boolean truststorePropertiesEmpty(Map<PropertyDescriptor, 
String> properties) {
+        return StringUtils.isBlank(properties.get(TRUSTSTORE)) && 
StringUtils.isBlank(properties.get(TRUSTSTORE_PASSWORD)) && 
StringUtils.isBlank(properties.get(TRUSTSTORE_TYPE));
+    }
+
+    /**
+     * Returns the count of {@code null} objects in the parameters. Used for 
keystore/truststore validation.
+     *
+     * @param objects a variable array of objects, some of which can be null
+     * @return the count of provided objects which were null
+     */
+    private static int countNulls(Object... objects) {
+        int count = 0;
+        for (final Object x : objects) {
+            if (x == null) {
+                count++;
+            }
+        }
+
+        return count;
+    }
+
+    /**
+     * Returns a list of {@link ValidationResult}s for keystore validity 
checking. Ensures none or all of the properties
+     * are populated; if populated, validates the keystore file on disk and 
password as well.
+     *
+     * @param properties the component properties
+     * @return the list of validation results (empty is valid)
+     */
+    private static List<ValidationResult> validateKeystore(final 
Map<PropertyDescriptor, String> properties) {
+        final List<ValidationResult> results = new ArrayList<>();
+
+        final String filename = properties.get(KEYSTORE);
+        final String password = properties.get(KEYSTORE_PASSWORD);
+        final String keyPassword = properties.get(KEY_PASSWORD);
+        final String type = properties.get(KEYSTORE_TYPE);
 
         final int nulls = countNulls(filename, password, type);
         if (nulls != 3 && nulls != 0) {
-            results.add(new 
ValidationResult.Builder().valid(false).explanation("Must set either 0 or 3 
properties for " + keystoreDesc)
-                    .subject(keystoreDesc + " Properties").build());
+            results.add(new 
ValidationResult.Builder().valid(false).explanation("Must set either 0 or 3 
properties for Keystore")
+                    .subject("Keystore Properties").build());
         } else if (nulls == 0) {
             // all properties were filled in.
-            final File file = new File(filename);
-            if (!file.exists() || !file.canRead()) {
-                results.add(new ValidationResult.Builder()
-                        .valid(false)
-                        .subject(keystoreDesc + " Properties")
-                        .explanation("Cannot access file " + 
file.getAbsolutePath())
-                        .build());
-            } else {
-                try {
-                    final boolean storeValid = 
CertificateUtils.isStoreValid(file.toURI().toURL(), KeystoreType.valueOf(type), 
password.toCharArray());
-                    if (!storeValid) {
-                        results.add(new ValidationResult.Builder()
-                                .subject(keystoreDesc + " Properties")
-                                .valid(false)
-                                .explanation("Invalid KeyStore Password or 
Type specified for file " + filename)
-                                .build());
-                    }
-                } catch (MalformedURLException e) {
+            List<ValidationResult> fileValidationResults = 
validateKeystoreFile(filename, password, keyPassword, type);
+            results.addAll(fileValidationResults);
+        }
+
+        // If nulls == 3, no values were populated, so just return
+
+        return results;
+    }
+
+
+    /**
+     * Returns a list of {@link ValidationResult}s for truststore validity 
checking. Ensures none of the properties
+     * are populated or at least filename and type are populated; if 
populated, validates the truststore file on disk
+     * and password as well.
+     *
+     * @param properties the component properties
+     * @return the list of validation results (empty is valid)
+     */
+    private static List<ValidationResult> validateTruststore(final 
Map<PropertyDescriptor, String> properties) {
+        String filename = properties.get(TRUSTSTORE);
+        String password = properties.get(TRUSTSTORE_PASSWORD);
+        String type = properties.get(TRUSTSTORE_TYPE);
+
+        List<ValidationResult> results = new ArrayList<>();
+
+        if (!StringUtils.isBlank(filename) && !StringUtils.isBlank(type)) {
+            // In this case both the filename and type are populated, which is 
sufficient
+            results.addAll(validateTruststoreFile(filename, password, type));
+        } else {
+            // The filename or type are blank; all values must be unpopulated 
for this to be valid
+            if (!StringUtils.isBlank(filename) || !StringUtils.isBlank(type)) {
+                results.add(new 
ValidationResult.Builder().valid(false).explanation("If the truststore filename 
or type are set, both must be populated").subject("Truststore 
Properties").build());
+            }
+        }
+
+        return results;
+    }
+
+    /**
+     * Returns a list of {@link ValidationResult}s when validating an actual 
JKS or PKCS12 file on disk. Verifies the
+     * file permissions and existence, and attempts to open the file given the 
provided password.
+     *
+     * @param filename     the path of the file on disk
+     * @param password     the file password
+     * @param type         the type (JKS or PKCS12)
+     * @return the list of validation results (empty is valid)
+     */
+    private static List<ValidationResult> validateTruststoreFile(String 
filename, String password, String type) {
+        List<ValidationResult> results = new ArrayList<>();
+
+        final File file = new File(filename);
+        if (!file.exists() || !file.canRead()) {
+            results.add(new ValidationResult.Builder()
+                    .valid(false)
+                    .subject("Truststore Properties")
+                    .explanation("Cannot access file " + 
file.getAbsolutePath())
+                    .build());
+        } else {
+            char[] passwordChars = new char[0];
+            if (!StringUtils.isBlank(password)) {
+                passwordChars = password.toCharArray();
+            }
+            try {
+                final boolean storeValid = 
KeyStoreUtils.isStoreValid(file.toURI().toURL(), KeystoreType.valueOf(type), 
passwordChars);
+                if (!storeValid) {
                     results.add(new ValidationResult.Builder()
-                            .subject(keystoreDesc + " Properties")
+                            .subject("Truststore Properties")
                             .valid(false)
-                            .explanation("Malformed URL from file: " + e)
+                            .explanation("Invalid truststore password or type 
specified for file " + filename)
                             .build());
                 }
+
+            } catch (MalformedURLException e) {
+                results.add(new ValidationResult.Builder()
+                        .subject("Truststore Properties")
+                        .valid(false)
+                        .explanation("Malformed URL from file: " + e)
+                        .build());
             }
         }
 
         return results;
     }
 
-    private static int countNulls(Object... objects) {
-        int count = 0;
-        for (final Object x : objects) {
-            if (x == null) {
-                count++;
+    /**
+     * Returns a list of {@link ValidationResult}s when validating an actual 
JKS or PKCS12 file on disk. Verifies the
+     * file permissions and existence, and attempts to open the file given the 
provided (keystore or key) password.
+     *
+     * @param filename     the path of the file on disk
+     * @param password     the file password
+     * @param keyPassword  the (optional) key-specific password
+     * @param type         the type (JKS or PKCS12)
+     * @return the list of validation results (empty is valid)
+     */
+    private static List<ValidationResult> validateKeystoreFile(String 
filename, String password, String keyPassword, String type) {
+        List<ValidationResult> results = new ArrayList<>();
+
+        final File file = new File(filename);
+        if (!file.exists() || !file.canRead()) {
+            results.add(new ValidationResult.Builder()
+                    .valid(false)
+                    .subject("Keystore Properties")
+                    .explanation("Cannot access file " + 
file.getAbsolutePath())
+                    .build());
+        } else {
+            char[] passwordChars = new char[0];
+            if (!StringUtils.isBlank(password)) {
+                passwordChars = password.toCharArray();
+            }
+            try {
+                final boolean storeValid = 
KeyStoreUtils.isStoreValid(file.toURI().toURL(), KeystoreType.valueOf(type), 
passwordChars);
+                if (!storeValid) {
+                    results.add(new ValidationResult.Builder()
+                            .subject("Keystore Properties")
+                            .valid(false)
+                            .explanation("Invalid keystore password or type 
specified for file " + filename)
+                            .build());
+                }
+
+                // The key password can be explicitly set (and can be the same 
as the
+                // keystore password or different), or it can be left blank. 
In the event
+                // it's blank, the keystore password will be used
+                char[] keyPasswordChars = new char[0];
+                if (StringUtils.isBlank(keyPassword) || 
keyPassword.equals(password)) {
+                    keyPasswordChars = passwordChars;
+                }
+                if (!StringUtils.isBlank(keyPassword)) {
+                    keyPasswordChars = keyPassword.toCharArray();
+                }
+
+                boolean keyPasswordValid = 
KeyStoreUtils.isKeyPasswordCorrect(file.toURI().toURL(), 
KeystoreType.valueOf(type), passwordChars, keyPasswordChars);
+                if (!keyPasswordValid) {
+                    results.add(new ValidationResult.Builder()
+                            .subject("Keystore Properties")
+                            .valid(false)
+                            .explanation("Invalid key password specified for 
file " + filename)
+                            .build());
+                }
+
+            } catch (MalformedURLException e) {
+                results.add(new ValidationResult.Builder()
+                        .subject("Keystore Properties")
+                        .valid(false)
+                        .explanation("Malformed URL from file: " + e)
+                        .build());
             }
         }
 
-        return count;
+        return results;
     }
 
     public enum KeystoreValidationGroup {
diff --git 
a/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/test/groovy/org/apache/nifi/ssl/StandardSSLContextServiceTest.groovy
 
b/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/test/groovy/org/apache/nifi/ssl/StandardSSLContextServiceTest.groovy
index 19c3568..06d5709 100644
--- 
a/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/test/groovy/org/apache/nifi/ssl/StandardSSLContextServiceTest.groovy
+++ 
b/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/test/groovy/org/apache/nifi/ssl/StandardSSLContextServiceTest.groovy
@@ -38,6 +38,7 @@ import org.junit.runners.JUnit4
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 
+import javax.net.ssl.SSLContext
 import java.security.Security
 
 import static groovy.test.GroovyAssert.shouldFail
@@ -48,10 +49,12 @@ class StandardSSLContextServiceTest {
 
     private static final String KEYSTORE_PATH = 
"src/test/resources/keystore.jks"
     private static final String TRUSTSTORE_PATH = 
"src/test/resources/truststore.jks"
+    private static final String NO_PASSWORD_TRUSTSTORE_PATH = 
"src/test/resources/no-password-truststore.jks"
     private static final String TRUSTSTORE_PATH_WITH_EL = 
"\${someAttribute}/truststore.jks"
 
     private static final String KEYSTORE_PASSWORD = "passwordpassword"
     private static final String TRUSTSTORE_PASSWORD = "passwordpassword"
+    private static final String TRUSTSTORE_NO_PASSWORD = ""
 
     private static final String KEYSTORE_TYPE = "JKS"
     private static final String TRUSTSTORE_TYPE = "JKS"
@@ -101,6 +104,106 @@ class StandardSSLContextServiceTest {
     }
 
     @Test
+    void testTruststoreWithNoPasswordIsValid() {
+        // Arrange
+        TestRunner runner = TestRunners.newTestRunner(TestProcessor.class)
+        String controllerServiceId = "ssl-context"
+        final SSLContextService sslContextService = new 
StandardSSLContextService()
+        runner.addControllerService(controllerServiceId, sslContextService)
+        runner.setProperty(sslContextService, 
StandardSSLContextService.TRUSTSTORE, NO_PASSWORD_TRUSTSTORE_PATH)
+        runner.setProperty(sslContextService, 
StandardSSLContextService.TRUSTSTORE_PASSWORD, TRUSTSTORE_NO_PASSWORD)
+        runner.setProperty(sslContextService, 
StandardSSLContextService.TRUSTSTORE_TYPE, TRUSTSTORE_TYPE)
+        runner.enableControllerService(sslContextService)
+
+        // Act
+        runner.assertValid(sslContextService)
+
+        // Assert
+        final MockProcessContext processContext = (MockProcessContext) 
runner.getProcessContext()
+        assert 
processContext.getControllerServiceProperties(sslContextService).get(StandardSSLContextService.TRUSTSTORE,
 "") == NO_PASSWORD_TRUSTSTORE_PATH
+    }
+
+    @Test
+    void testTruststoreWithNullPasswordIsValid() {
+        // Arrange
+        TestRunner runner = TestRunners.newTestRunner(TestProcessor.class)
+        String controllerServiceId = "ssl-context"
+        final SSLContextService sslContextService = new 
StandardSSLContextService()
+        runner.addControllerService(controllerServiceId, sslContextService)
+        runner.setProperty(sslContextService, 
StandardSSLContextService.TRUSTSTORE, NO_PASSWORD_TRUSTSTORE_PATH)
+        runner.setProperty(sslContextService, 
StandardSSLContextService.TRUSTSTORE_PASSWORD, null as String)
+        runner.setProperty(sslContextService, 
StandardSSLContextService.TRUSTSTORE_TYPE, TRUSTSTORE_TYPE)
+        runner.enableControllerService(sslContextService)
+
+        // Act
+        runner.assertValid(sslContextService)
+
+        // Assert
+        final MockProcessContext processContext = (MockProcessContext) 
runner.getProcessContext()
+        assert 
processContext.getControllerServiceProperties(sslContextService).get(StandardSSLContextService.TRUSTSTORE,
 "") == NO_PASSWORD_TRUSTSTORE_PATH
+    }
+
+    @Test
+    void testTruststoreWithMissingPasswordIsValid() {
+        // Arrange
+        TestRunner runner = TestRunners.newTestRunner(TestProcessor.class)
+        String controllerServiceId = "ssl-context"
+        final SSLContextService sslContextService = new 
StandardSSLContextService()
+        runner.addControllerService(controllerServiceId, sslContextService)
+        runner.setProperty(sslContextService, 
StandardSSLContextService.TRUSTSTORE, NO_PASSWORD_TRUSTSTORE_PATH)
+        runner.setProperty(sslContextService, 
StandardSSLContextService.TRUSTSTORE_TYPE, TRUSTSTORE_TYPE)
+        runner.enableControllerService(sslContextService)
+
+        // Act
+        runner.assertValid(sslContextService)
+
+        // Assert
+        final MockProcessContext processContext = (MockProcessContext) 
runner.getProcessContext()
+        assert 
processContext.getControllerServiceProperties(sslContextService).get(StandardSSLContextService.TRUSTSTORE,
 "") == NO_PASSWORD_TRUSTSTORE_PATH
+    }
+
+    @Test
+    void testShouldConnectWithPasswordlessTruststore() {
+        // Arrange
+        TestRunner runner = TestRunners.newTestRunner(TestProcessor.class)
+        String controllerServiceId = "ssl-context"
+        final SSLContextService sslContextService = new 
StandardSSLContextService()
+        runner.addControllerService(controllerServiceId, sslContextService)
+        runner.setProperty(sslContextService, 
StandardSSLContextService.TRUSTSTORE, NO_PASSWORD_TRUSTSTORE_PATH)
+        runner.setProperty(sslContextService, 
StandardSSLContextService.TRUSTSTORE_TYPE, TRUSTSTORE_TYPE)
+        runner.enableControllerService(sslContextService)
+        runner.assertValid(sslContextService)
+
+        // Act
+        SSLContext sslContext = 
sslContextService.createSSLContext(SSLContextService.ClientAuth.NONE)
+
+        // Assert
+        assert sslContext
+    }
+
+    @Test
+    void testShouldConnectWithPasswordlessTruststoreWhenKeystorePresent() {
+        // Arrange
+        TestRunner runner = TestRunners.newTestRunner(TestProcessor.class)
+        String controllerServiceId = "ssl-context"
+        final SSLContextService sslContextService = new 
StandardSSLContextService()
+        runner.addControllerService(controllerServiceId, sslContextService)
+        runner.setProperty(sslContextService, 
StandardSSLContextService.KEYSTORE, KEYSTORE_PATH)
+        runner.setProperty(sslContextService, 
StandardSSLContextService.KEYSTORE_PASSWORD, KEYSTORE_PASSWORD)
+        runner.setProperty(sslContextService, 
StandardSSLContextService.KEYSTORE_TYPE, KEYSTORE_TYPE)
+        runner.setProperty(sslContextService, 
StandardSSLContextService.TRUSTSTORE, NO_PASSWORD_TRUSTSTORE_PATH)
+        runner.setProperty(sslContextService, 
StandardSSLContextService.TRUSTSTORE_TYPE, TRUSTSTORE_TYPE)
+        runner.enableControllerService(sslContextService)
+        runner.assertValid(sslContextService)
+
+        // Act
+        SSLContext sslContext = 
sslContextService.createSSLContext(SSLContextService.ClientAuth.NONE)
+
+        // Assert
+        assert sslContext
+    }
+
+    @Test
     void testShouldNotValidateExpressionLanguageInFileValidator() {
         // Arrange
         TestRunner runner = TestRunners.newTestRunner(TestProcessor.class)
@@ -117,7 +220,7 @@ class StandardSSLContextServiceTest {
         }
 
         // Assert
-        assert msg =~ "Cannot enable Controller Service SSLContextService.* 
because it is in an invalid state: 'Truststore Filename'.* is invalid because 
File.* does not exist or cannot be read";
+        assert msg =~ "Cannot enable Controller Service SSLContextService.* 
because it is in an invalid state: 'Truststore Filename'.* is invalid because 
File.* does not exist or cannot be read"
         runner.assertNotValid(sslContextService)
     }
 
diff --git 
a/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/test/java/org/apache/nifi/ssl/SSLContextServiceTest.java
 
b/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/test/java/org/apache/nifi/ssl/SSLContextServiceTest.java
index b98824c..7b304aa 100644
--- 
a/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/test/java/org/apache/nifi/ssl/SSLContextServiceTest.java
+++ 
b/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/test/java/org/apache/nifi/ssl/SSLContextServiceTest.java
@@ -33,9 +33,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
-
 import javax.net.ssl.SSLContext;
-
 import org.apache.nifi.components.AllowableValue;
 import org.apache.nifi.components.ValidationContext;
 import org.apache.nifi.components.ValidationResult;
@@ -60,32 +58,34 @@ public class SSLContextServiceTest {
     private final String TRUSTSTORE_PATH = "src/test/resources/truststore.jks";
     private final String DIFFERENT_PASS_KEYSTORE_PATH = 
"src/test/resources/keystore-different-password.jks";
     private final String DIFFERENT_KEYSTORE_PASSWORD = "differentpassword";
+    private static final String KEYSTORE_WITH_KEY_PASSWORD_PATH = 
"src/test/resources/keystore-with-key-password.jks";
+
 
     @Rule
     public TemporaryFolder tmp = new TemporaryFolder(new 
File("src/test/resources"));
 
     @Test
-    public void testBad1() throws InitializationException {
+    public void testShouldFailToAddControllerServiceWithNoProperties() throws 
InitializationException {
         final TestRunner runner = 
TestRunners.newTestRunner(TestProcessor.class);
         final SSLContextService service = new StandardSSLContextService();
         final Map<String, String> properties = new HashMap<>();
-        runner.addControllerService("test-bad1", service, properties);
+        runner.addControllerService("test-no-properties", service, properties);
         runner.assertNotValid(service);
     }
 
     @Test
-    public void testBad2() throws InitializationException {
+    public void testShouldFailToAddControllerServiceWithoutKeystoreType() 
throws InitializationException {
         final TestRunner runner = 
TestRunners.newTestRunner(TestProcessor.class);
         final SSLContextService service = new StandardSSLContextService();
         final Map<String, String> properties = new HashMap<>();
         properties.put(StandardSSLContextService.KEYSTORE.getName(), 
KEYSTORE_PATH);
         properties.put(StandardSSLContextService.KEYSTORE_PASSWORD.getName(), 
KEYSTORE_AND_TRUSTSTORE_PASSWORD);
-        runner.addControllerService("test-bad2", service, properties);
+        runner.addControllerService("test-no-keystore-type", service, 
properties);
         runner.assertNotValid(service);
     }
 
     @Test
-    public void testBad3() throws InitializationException {
+    public void testShouldFailToAddControllerServiceWithOnlyTruststorePath() 
throws InitializationException {
         final TestRunner runner = 
TestRunners.newTestRunner(TestProcessor.class);
         final SSLContextService service = new StandardSSLContextService();
         final Map<String, String> properties = new HashMap<>();
@@ -93,12 +93,12 @@ public class SSLContextServiceTest {
         properties.put(StandardSSLContextService.KEYSTORE_PASSWORD.getName(), 
KEYSTORE_AND_TRUSTSTORE_PASSWORD);
         properties.put(StandardSSLContextService.KEYSTORE_TYPE.getName(), 
JKS_TYPE);
         properties.put(StandardSSLContextService.TRUSTSTORE.getName(), 
TRUSTSTORE_PATH);
-        runner.addControllerService("test-bad3", service, properties);
+        runner.addControllerService("test-no-truststore-password-or-type", 
service, properties);
         runner.assertNotValid(service);
     }
 
     @Test
-    public void testBad4() throws InitializationException {
+    public void testShouldFailToAddControllerServiceWithWrongPasswords() 
throws InitializationException {
         final TestRunner runner = 
TestRunners.newTestRunner(TestProcessor.class);
         final SSLContextService service = new StandardSSLContextService();
         final Map<String, String> properties = new HashMap<>();
@@ -108,13 +108,13 @@ public class SSLContextServiceTest {
         properties.put(StandardSSLContextService.TRUSTSTORE.getName(), 
TRUSTSTORE_PATH);
         
properties.put(StandardSSLContextService.TRUSTSTORE_PASSWORD.getName(), 
"wrongpassword");
         properties.put(StandardSSLContextService.TRUSTSTORE_TYPE.getName(), 
JKS_TYPE);
-        runner.addControllerService("test-bad4", service, properties);
+        runner.addControllerService("test-wrong-passwords", service, 
properties);
 
         runner.assertNotValid(service);
     }
 
     @Test
-    public void testBad5() throws InitializationException {
+    public void testShouldFailToAddControllerServiceWithNonExistentFiles() 
throws InitializationException {
         final TestRunner runner = 
TestRunners.newTestRunner(TestProcessor.class);
         final SSLContextService service = new StandardSSLContextService();
         final Map<String, String> properties = new HashMap<>();
@@ -124,7 +124,7 @@ public class SSLContextServiceTest {
         properties.put(StandardSSLContextService.TRUSTSTORE.getName(), 
TRUSTSTORE_PATH);
         
properties.put(StandardSSLContextService.TRUSTSTORE_PASSWORD.getName(), 
KEYSTORE_AND_TRUSTSTORE_PASSWORD);
         properties.put(StandardSSLContextService.TRUSTSTORE_TYPE.getName(), 
JKS_TYPE);
-        runner.addControllerService("test-bad5", service, properties);
+        runner.addControllerService("test-keystore-file-does-not-exist", 
service, properties);
         runner.assertNotValid(service);
     }
 
@@ -297,8 +297,8 @@ public class SSLContextServiceTest {
             final TestRunner runner = 
TestRunners.newTestRunner(TestProcessor.class);
             final SSLContextService service = new StandardSSLContextService();
             final Map<String, String> properties = new HashMap<>();
-            properties.put(StandardSSLContextService.KEYSTORE.getName(), 
DIFFERENT_PASS_KEYSTORE_PATH);
-            
properties.put(StandardSSLContextService.KEYSTORE_PASSWORD.getName(), 
DIFFERENT_KEYSTORE_PASSWORD);
+            properties.put(StandardSSLContextService.KEYSTORE.getName(), 
KEYSTORE_WITH_KEY_PASSWORD_PATH);
+            
properties.put(StandardSSLContextService.KEYSTORE_PASSWORD.getName(), 
KEYSTORE_AND_TRUSTSTORE_PASSWORD);
             properties.put(StandardSSLContextService.KEY_PASSWORD.getName(), 
"keypassword");
             properties.put(StandardSSLContextService.KEYSTORE_TYPE.getName(), 
JKS_TYPE);
             runner.addControllerService("test-diff-keys", service, properties);
@@ -307,9 +307,7 @@ public class SSLContextServiceTest {
             runner.setProperty("SSL Context Svc ID", "test-diff-keys");
             runner.assertValid();
             Assert.assertNotNull(service);
-            assertTrue(service instanceof StandardSSLContextService);
-            SSLContextService sslService = service;
-            sslService.createSSLContext(ClientAuth.NONE);
+            service.createSSLContext(ClientAuth.NONE);
         } catch (Exception e) {
             System.out.println(e);
             Assert.fail("Should not have thrown a exception " + 
e.getMessage());
@@ -327,8 +325,8 @@ public class SSLContextServiceTest {
             final TestRunner runner = 
TestRunners.newTestRunner(TestProcessor.class);
             final SSLContextService service = new StandardSSLContextService();
             final Map<String, String> properties = new HashMap<>();
-            properties.put(StandardSSLContextService.KEYSTORE.getName(), 
DIFFERENT_PASS_KEYSTORE_PATH);
-            
properties.put(StandardSSLContextService.KEYSTORE_PASSWORD.getName(), 
DIFFERENT_KEYSTORE_PASSWORD);
+            properties.put(StandardSSLContextService.KEYSTORE.getName(), 
KEYSTORE_WITH_KEY_PASSWORD_PATH);
+            
properties.put(StandardSSLContextService.KEYSTORE_PASSWORD.getName(), 
KEYSTORE_AND_TRUSTSTORE_PASSWORD);
             properties.put(StandardSSLContextService.KEYSTORE_TYPE.getName(), 
JKS_TYPE);
             runner.addControllerService("test-diff-keys", service, properties);
 
diff --git 
a/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/test/resources/keystore-with-key-password.jks
 
b/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/test/resources/keystore-with-key-password.jks
new file mode 100644
index 0000000..56216d9
Binary files /dev/null and 
b/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/test/resources/keystore-with-key-password.jks
 differ
diff --git 
a/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/test/resources/no-password-truststore.jks
 
b/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/test/resources/no-password-truststore.jks
new file mode 100644
index 0000000..aa1ce5d
Binary files /dev/null and 
b/nifi-nar-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/test/resources/no-password-truststore.jks
 differ

Reply via email to