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 <alopresto@apache.org>

This closes #3823.

Signed-off-by: Andy LoPresto <alopresto@apache.org>
This commit is contained in:
Nathan Gough 2019-10-17 13:09:08 -04:00 committed by Andy LoPresto
parent f8057d0c2f
commit 4ec9155cbc
No known key found for this signature in database
GPG Key ID: 6EC293152D90B61D
16 changed files with 750 additions and 372 deletions

View File

@ -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";
@ -137,48 +132,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.
*/
/** 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);
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);
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

View File

@ -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);
}
}
}
}
}

View File

@ -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)
}
}

View File

@ -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)));
}

View File

@ -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));
}
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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,62 +306,46 @@ 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);
}
final String keystoreDesc = (keyStoreOrTrustStore == KeystoreValidationGroup.KEYSTORE) ? "Keystore" : "Truststore";
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());
} 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) {
results.add(new ValidationResult.Builder()
.subject(keystoreDesc + " Properties")
.valid(false)
.explanation("Malformed URL from file: " + e)
.build());
}
}
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());
}
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) {
@ -455,6 +357,178 @@ public class StandardSSLContextService extends AbstractControllerService impleme
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 Keystore")
.subject("Keystore Properties").build());
} else if (nulls == 0) {
// all properties were filled in.
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("Truststore Properties")
.valid(false)
.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;
}
/**
* 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 results;
}
public enum KeystoreValidationGroup {
KEYSTORE, TRUSTSTORE

View File

@ -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"
@ -100,6 +103,106 @@ class StandardSSLContextServiceTest {
assert processContext.getControllerServiceProperties(sslContextService).get(StandardSSLContextService.TRUSTSTORE, "") == TRUSTSTORE_PATH
}
@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
@ -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)
}

View File

@ -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);