blob: 5e4fa145dac8c76e4235d86ba22ec740bf5f47a2 [file] [log] [blame]
/*
* 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 java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A factory for creating SSL contexts using the application's security properties. By requiring callers to bundle
* the properties in a {@link TlsConfiguration} container object, much better validation and property matching can
* occur. The {@code public} methods are designed for easy use, while the {@code protected} methods provide more
* granular (but less common) access to intermediate objects if required.
*/
public final class SslContextFactory {
private static final Logger logger = LoggerFactory.getLogger(SslContextFactory.class);
// TODO: Move to nifi-security-utils-core
/**
* Create and initialize a {@link SSLContext} from the provided TLS configuration.
*
* @param tlsConfiguration the TLS configuration container object
* @return {@link SSLContext} initialized from TLS Configuration or null when TLS Configuration is empty
* @throws TlsException if there is a problem configuring the SSLContext
*/
public static SSLContext createSslContext(final TlsConfiguration tlsConfiguration) throws TlsException {
if (TlsConfiguration.isEmpty(tlsConfiguration)) {
logger.debug("Cannot create SSLContext from empty TLS configuration; returning null");
return null;
}
// If the keystore properties are present, truststore properties are required to be present as well
if (tlsConfiguration.isKeystorePopulated() && !tlsConfiguration.isTruststorePopulated()) {
logger.error("The TLS config keystore properties were populated but the truststore properties were not");
if (logger.isDebugEnabled()) {
logger.debug("Provided TLS config: {}", tlsConfiguration);
}
throw new TlsException("Truststore properties are required if keystore properties are present");
}
final TrustManager[] trustManagers = getTrustManagers(tlsConfiguration);
return createSslContext(tlsConfiguration, trustManagers);
}
/**
* Create and initialize a {@link SSLContext} from the provided TLS configuration and Trust Managers.
*
* @param tlsConfiguration the TLS configuration container object
* @param trustManagers Trust Managers can be null to use platform default Trust Managers
* @return {@link SSLContext} initialized from TLS Configuration or null when TLS Configuration is empty
* @throws TlsException if there is a problem configuring the SSLContext
*/
public static SSLContext createSslContext(final TlsConfiguration tlsConfiguration, final TrustManager[] trustManagers) throws TlsException {
if (TlsConfiguration.isEmpty(tlsConfiguration)) {
logger.debug("Cannot create SSLContext from empty TLS configuration; returning null");
return null;
}
final KeyManager[] keyManagers = getKeyManagers(tlsConfiguration);
return initializeSSLContext(tlsConfiguration, keyManagers, trustManagers);
}
/**
* Returns a configured {@link X509TrustManager} for the provided configuration. Useful for
* constructing HTTP clients which require their own trust management rather than an
* {@link SSLContext}. Filters and removes any trust managers that are not
* {@link javax.net.ssl.X509TrustManager} implementations, and returns the <em>first</em>
* X.509 trust manager.
*
* @param tlsConfiguration the TLS configuration container object
* @return an X.509 TrustManager (can be {@code null})
* @throws TlsException if there is a problem reading the truststore to create the trust managers
*/
public static X509TrustManager getX509TrustManager(TlsConfiguration tlsConfiguration) throws TlsException {
TrustManager[] trustManagers = getTrustManagers(tlsConfiguration);
if (trustManagers == null) {
return null;
}
Optional<X509TrustManager> x509TrustManager = Arrays.stream(trustManagers)
.filter(tm -> tm instanceof X509TrustManager)
.map(tm -> (X509TrustManager) tm)
.findFirst();
return x509TrustManager.orElse(null);
}
/**
* Convenience method to return the {@link SSLSocketFactory} from the created {@link SSLContext}
*
* @param tlsConfiguration the TLS configuration container object
* @return the configured SSLSocketFactory (can be {@code null})
* @throws TlsException if there is a problem creating the SSLContext or SSLSocketFactory
*/
public static SSLSocketFactory createSSLSocketFactory(final TlsConfiguration tlsConfiguration) throws TlsException {
SSLContext sslContext = createSslContext(tlsConfiguration);
if (sslContext == null) {
// Only display an error in the log if the provided config wasn't empty
if (!TlsConfiguration.isEmpty(tlsConfiguration)) {
logger.error("The SSLContext could not be formed from the provided TLS configuration. Check the provided keystore and truststore properties");
}
return null;
}
return sslContext.getSocketFactory();
}
/**
* Returns an array of {@link KeyManager}s for the provided configuration. Useful for constructing
* HTTP clients which require their own key management rather than an {@link SSLContext}. The result can be
* {@code null} or empty. If an empty configuration is provided, {@code null} is returned. However, if a partially-populated
* but invalid configuration is provided, a {@link TlsException} is thrown.
*
* @param tlsConfiguration the TLS configuration container object with keystore properties
* @return an array of KeyManagers (can be {@code null})
* @throws TlsException if there is a problem reading the keystore to create the key managers
*/
protected static KeyManager[] getKeyManagers(TlsConfiguration tlsConfiguration) throws TlsException {
KeyManager[] keyManagers = null;
if (tlsConfiguration.isKeystoreValid()) {
KeyManagerFactory keyManagerFactory = KeyStoreUtils.loadKeyManagerFactory(tlsConfiguration);
keyManagers = keyManagerFactory.getKeyManagers();
} else {
// If some keystore properties were populated but the key managers are empty, throw an exception to inform the caller
if (tlsConfiguration.isAnyKeystorePopulated()) {
logger.warn("Some keystore properties are populated ({}, {}, {}, {}) but not valid", (Object[]) tlsConfiguration.getKeystorePropertiesForLogging());
throw new TlsException("The keystore properties are not valid");
} else {
// If they are empty, the caller was not expecting a valid response
logger.debug("The keystore properties are not populated");
}
}
return keyManagers;
}
/**
* Returns an array of {@link TrustManager} implementations based on the provided truststore configurations. The result can be
* {@code null} or empty. If an empty configuration is provided, {@code null} is returned. However, if a partially-populated
* but invalid configuration is provided, a {@link TlsException} is thrown.
* <p>
* Most callers do not need the full array and can use {@link #getX509TrustManager(TlsConfiguration)} directly.
*
* @param tlsConfiguration the TLS configuration container object with truststore properties
* @return the loaded trust managers
* @throws TlsException if there is a problem reading from the truststore
*/
public static TrustManager[] getTrustManagers(final TlsConfiguration tlsConfiguration) throws TlsException {
Objects.requireNonNull(tlsConfiguration, "TLS Configuration required");
TrustManager[] trustManagers = null;
if (tlsConfiguration.isTruststoreValid()) {
TrustManagerFactory trustManagerFactory = KeyStoreUtils.loadTrustManagerFactory(tlsConfiguration);
trustManagers = trustManagerFactory.getTrustManagers();
} else {
// If some truststore properties were populated but the trust managers are empty, throw an exception to inform the caller
if (tlsConfiguration.isAnyTruststorePopulated()) {
logger.warn("Some truststore properties are populated ({}, {}, {}) but not valid", (Object[]) tlsConfiguration.getTruststorePropertiesForLogging());
throw new TlsException("The truststore properties are not valid");
} else {
// If they are empty, the caller was not expecting a valid response
logger.debug("The truststore properties are not populated");
}
}
return trustManagers;
}
private static SSLContext initializeSSLContext(final TlsConfiguration tlsConfiguration, final KeyManager[] keyManagers, final TrustManager[] trustManagers) throws TlsException {
try {
final SSLContext sslContext = SSLContext.getInstance(tlsConfiguration.getProtocol());
sslContext.init(keyManagers, trustManagers, new SecureRandom());
return sslContext;
} catch (final NoSuchAlgorithmException | KeyManagementException e) {
logger.error("Encountered an error creating SSLContext from TLS configuration ({}): {}", tlsConfiguration, e.getLocalizedMessage());
throw new TlsException("Error creating SSL context", e);
}
}
}