/* * Copyright 2017 StreamSets Inc. * * Licensed under 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 com.streamsets.pipeline.lib.tls; import com.google.common.base.Strings; import com.streamsets.pipeline.api.ConfigDef; import com.streamsets.pipeline.api.Stage; import com.streamsets.pipeline.api.ValueChooserModel; import com.streamsets.pipeline.lib.el.VaultEL; import org.apache.log4j.Level; import org.apache.log4j.Logger; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.TrustManagerFactory; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; 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.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; public class TlsConfigBean { public static final String DEFAULT_KEY_MANAGER_ALGORITHM = "SunX509"; private static final String[] MODERN_PROTOCOLS = {"TLSv1.2"}; private static final String[] MODERN_CIPHER_SUITES = { "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384" }; private static final Logger LOGGER = Logger.getLogger(TlsConfigBean.class); public TlsConfigBean() { this(TlsConnectionType.NEITHER); } public TlsConfigBean(TlsConnectionType connectionType) { switch (connectionType) { case NEITHER: this.hasKeyStore = false; this.hasTrustStore = false; break; case CLIENT: this.hasKeyStore = false; this.hasTrustStore = true; break; case SERVER: this.hasKeyStore = true; this.hasTrustStore = false; break; case BOTH: this.hasKeyStore = true; this.hasTrustStore = true; break; } } @ConfigDef( required = true, type = ConfigDef.Type.BOOLEAN, defaultValue = "false", label = "Use Key Store", description = "Use a key store to manage server-side private keys.", displayPosition = 10, group = "#0", dependsOn = "tlsEnabled^", triggeredByValue = "true" ) public boolean hasKeyStore = false; @ConfigDef( required = true, type = ConfigDef.Type.MODEL, defaultValue = "JKS", label = "Key Store Type", description = "The type of certificate/key scheme to use for the key store.", displayPosition = 20, group = "#0", dependsOn = "hasKeyStore", triggeredByValue = "true" ) @ValueChooserModel(KeyStoreTypeChooserValues.class) public KeyStoreType keyStoreType = KeyStoreType.JKS; @ConfigDef( required = true, type = ConfigDef.Type.STRING, description = "The path to the key store file. Absolute path, or relative to the Data Collector resources " + "directory.", label = "Key Store File", displayPosition = 50, group = "#0", dependsOn = "hasKeyStore", triggeredByValue = "true" ) public String keyStoreFilePath; @ConfigDef( required = false, type = ConfigDef.Type.STRING, description = "The password to the key store file, if applicable. Using a password is highly recommended for" + "security reasons.", label = "Key Store Password", displayPosition = 70, elDefs = VaultEL.class, group = "#0", dependsOn = "hasKeyStore", triggeredByValue = "true" ) public String keyStorePassword; @ConfigDef( required = true, type = ConfigDef.Type.STRING, label = "Key Store Key Algorithm", description = "The key manager algorithm to use with the key store.", defaultValue = DEFAULT_KEY_MANAGER_ALGORITHM, displayPosition = 80, group = "#0", dependsOn = "hasKeyStore", triggeredByValue = "true" ) public String keyStoreAlgorithm = DEFAULT_KEY_MANAGER_ALGORITHM; @ConfigDef( required = true, type = ConfigDef.Type.BOOLEAN, defaultValue = "false", label = "Use Trust Store", description = "Use a trust store to manage client-side certificates.", displayPosition = 100, group = "#0", dependsOn = "tlsEnabled^", triggeredByValue = "true" ) public boolean hasTrustStore = false; @ConfigDef( required = true, type = ConfigDef.Type.MODEL, defaultValue = "JKS", label = "Trust Store Type", description = "The type of certificate/key scheme to use for the trust store.", displayPosition = 120, group = "#0", dependsOn = "hasTrustStore", triggeredByValue = "true" ) @ValueChooserModel(KeyStoreTypeChooserValues.class) public KeyStoreType trustStoreType = KeyStoreType.JKS; @ConfigDef( required = true, type = ConfigDef.Type.STRING, description = "The path to the trust store file. Absolute path, or relative to the Data Collector resources " + "directory.", label = "Trust Store File", displayPosition = 150, group = "#0", dependsOn = "hasTrustStore", triggeredByValue = "true" ) public String trustStoreFilePath; @ConfigDef( required = false, type = ConfigDef.Type.STRING, description = "The password to the trust store file, if applicable. Using a password is highly recommended for" + "security reasons.", label = "Trust Store Password", displayPosition = 170, elDefs = VaultEL.class, group = "#0", dependsOn = "hasTrustStore", triggeredByValue = "true" ) public String trustStorePassword; @ConfigDef( required = true, type = ConfigDef.Type.STRING, label = "Trust Store Trust Algorithm", description = "The key manager algorithm to use with the trust store.", defaultValue = DEFAULT_KEY_MANAGER_ALGORITHM, displayPosition = 180, group = "#0", dependsOn = "hasTrustStore", triggeredByValue = "true" ) public String trustStoreAlgorithm = DEFAULT_KEY_MANAGER_ALGORITHM; @ConfigDef( required = true, type = ConfigDef.Type.BOOLEAN, label = "Use Default (Modern) Protocols", description = "Use only modern TLS protocols. This is highly recommended for security reasons, but can be" + "overridden if special circumstances require it.", defaultValue = "true", displayPosition = 300, group = "#0", dependsOn = "tlsEnabled^", triggeredByValue = "true" ) public boolean useDefaultProtocols = true; @ConfigDef( required = true, type = ConfigDef.Type.LIST, label = "Transport Protocols", description = "The transport protocols to enable for connections (ex: TLSv1.2, TLSv1.1, etc.).", displayPosition = 310, group = "#0", dependsOn = "useDefaultProtocols", triggeredByValue = "false" ) public List<String> protocols = new LinkedList<>(); @ConfigDef( required = true, type = ConfigDef.Type.BOOLEAN, label = "Use Default (Modern) Cipher Suites", description = "Use only modern cipher suites. This is highly recommended for security reasons, but can be" + "overridden if special circumstances require it.", defaultValue = "true", displayPosition = 350, group = "#0", dependsOn = "tlsEnabled^", triggeredByValue = "true" ) public boolean useDefaultCiperSuites = true; @ConfigDef( required = true, type = ConfigDef.Type.LIST, label = "Cipher Suites", description = "The cipher suites for connections (ex: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, etc.).", displayPosition = 360, group = "#0", dependsOn = "useDefaultProtocols", triggeredByValue = "false" ) public List<String> cipherSuites = new LinkedList<>(); public boolean pathRelativeToResourcesDir = true; private SSLEngine sslEngine; private SSLContext sslContext; private KeyStore keyStore; private KeyStore trustStore; private Set<String> getSupportedValuesFromSpecified( Collection<String> supportedValues, Collection<String> specifiedValues, String type ) { Set<String> returnSet = new HashSet<>(); final Set<String> supportedSet = new HashSet<>(supportedValues); for (String specified : specifiedValues) { if (supportedSet.contains(specified)) { returnSet.add(specified); } else { if (LOGGER.isEnabledFor(Level.WARN)) { LOGGER.warn(String.format( "%s %s was specified, but is not supported within the JVM; disabling", type, specified )); } } } return returnSet; } private static Path getFilePath(String resourcesDir, String path, boolean pathRelativeToResourcesDir) { if (Strings.isNullOrEmpty(path)) { return null; } final Path p = Paths.get(path); if (p.isAbsolute() || !pathRelativeToResourcesDir) { return p; } else { return Paths.get(resourcesDir, path); } } public boolean isEitherStoreEnabled() { return hasKeyStore || hasTrustStore; } public boolean init( Stage.Context context, String groupName, String configPrefix, List<Stage.ConfigIssue> issues ) { KeyManagerFactory keyStoreFactory = null; TrustManagerFactory trustStoreFactory = null; if (!isEitherStoreEnabled()) { issues.add(context.createConfigIssue( groupName, configPrefix + "hasKeyStore", TlsConfigErrors.TLS_05 )); return false; } if (hasKeyStore) { keyStoreFactory = initializeKeyStore(context, groupName, configPrefix, issues); if (keyStoreFactory == null) { return false; } } if (hasTrustStore) { trustStoreFactory = initializeTrustStore(context, groupName, configPrefix, issues); if (trustStoreFactory == null) { return false; } } try { sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyStoreFactory != null ? keyStoreFactory.getKeyManagers() : null, trustStoreFactory != null ? trustStoreFactory.getTrustManagers() : null, null ); } catch (KeyManagementException | NoSuchAlgorithmException e) { issues.add(context.createConfigIssue( groupName, "trustStoreFilePath", TlsConfigErrors.TLS_51, e.getMessage(), e )); return false; } sslEngine = sslContext.createSSLEngine(); sslEngine.setUseClientMode(false); sslEngine.setNeedClientAuth(false); Collection<String> filteredProtocols; if (useDefaultProtocols) { filteredProtocols = getSupportedValuesFromSpecified(Arrays.asList(sslEngine.getSupportedProtocols()), Arrays.asList(MODERN_PROTOCOLS), "Protocol" ); } else { filteredProtocols = getSupportedValuesFromSpecified(Arrays.asList(sslEngine.getSupportedProtocols()), protocols, "Protocol" ); } sslEngine.setEnabledProtocols(filteredProtocols.toArray(new String[0])); Collection<String> filteredCipherSuites; if (useDefaultCiperSuites) { filteredCipherSuites = getSupportedValuesFromSpecified(Arrays.asList(sslEngine.getSupportedCipherSuites()), Arrays.asList(MODERN_CIPHER_SUITES), "Cipher suite" ); } else { filteredCipherSuites = getSupportedValuesFromSpecified(Arrays.asList(sslEngine.getSupportedCipherSuites()), cipherSuites, "Cipher suite" ); } sslEngine.setEnabledCipherSuites(filteredCipherSuites.toArray(new String[0])); sslEngine.setEnableSessionCreation(true); sslEngine.setUseClientMode(isClientMode()); return true; } private KeyManagerFactory initializeKeyStore( Stage.Context context, String groupName, String configPrefix, List<Stage.ConfigIssue> issues ) { final Path keyStorePath = getFilePath( context.getResourcesDirectory(), keyStoreFilePath, pathRelativeToResourcesDir ); if (keyStorePath == null) { issues.add(context.createConfigIssue( groupName, configPrefix + "keyStoreFilePath", TlsConfigErrors.TLS_02, "Key" )); return null; } keyStore = initializeKeyStoreFromConfig(context, groupName, configPrefix, issues, keyStorePath, keyStorePassword, keyStoreType, "Key" ); if (keyStore == null) { return null; } KeyManagerFactory kmf; try { kmf = KeyManagerFactory.getInstance(keyStoreAlgorithm); } catch (NoSuchAlgorithmException e) { issues.add(context.createConfigIssue( groupName, configPrefix + "keyStoreAlgorithm", TlsConfigErrors.TLS_22, keyStoreAlgorithm, e.getMessage(), e )); return null; } try { kmf.init(keyStore, getPasswordChars(keyStorePassword)); } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) { issues.add(context.createConfigIssue( groupName, configPrefix + "keyStoreFilePath", TlsConfigErrors.TLS_23, e.getMessage(), e )); return null; } return kmf; } private TrustManagerFactory initializeTrustStore( Stage.Context context, String groupName, String configPrefix, List<Stage.ConfigIssue> issues ) { final Path trustStorePath = getFilePath( context.getResourcesDirectory(), trustStoreFilePath, pathRelativeToResourcesDir ); if (trustStorePath == null) { issues.add(context.createConfigIssue( groupName, configPrefix + "trustStoreFilePath", TlsConfigErrors.TLS_02, "Trust" )); return null; } trustStore = initializeKeyStoreFromConfig( context, groupName, configPrefix, issues, trustStorePath, trustStorePassword, trustStoreType, "Trust" ); if (trustStore == null) { return null; } TrustManagerFactory tmf; try { tmf = TrustManagerFactory.getInstance(trustStoreAlgorithm); } catch (NoSuchAlgorithmException e) { issues.add(context.createConfigIssue( groupName, configPrefix + "trustStoreAlgorithm", TlsConfigErrors.TLS_50, trustStoreAlgorithm, e.getMessage(), e )); return null; } try { tmf.init(trustStore); } catch (KeyStoreException e) { issues.add(context.createConfigIssue( groupName, configPrefix + "trustStoreFilePath", TlsConfigErrors.TLS_51, e.getMessage(), e )); return null; } return tmf; } public boolean isClientMode() { return hasTrustStore && !hasKeyStore; } public KeyStore getKeyStore() { return keyStore; } public KeyStore getTrustStore() { return trustStore; } public SSLContext getSslContext() { return sslContext; } public SSLEngine getSslEngine() { return sslEngine; } private static KeyStore initializeKeyStoreFromConfig( Stage.Context context, String groupName, String configPrefix, List<Stage.ConfigIssue> issues, Path keyStorePath, String password, KeyStoreType type, String storeCategory ) { if (!keyStorePath.toFile().exists()) { issues.add(context.createConfigIssue( groupName, configPrefix + storeCategory.toLowerCase() + "StoreFilePath", TlsConfigErrors.TLS_01, storeCategory, keyStorePath )); return null; } KeyStore ks; try { ks = KeyStore.getInstance(type.getJavaValue()); } catch (KeyStoreException e) { issues.add(context.createConfigIssue( groupName, configPrefix + storeCategory.toLowerCase() + "StoreType", TlsConfigErrors.TLS_20, storeCategory, e.getMessage(), e )); return null; } try (final InputStream keyStoreIs = Files.newInputStream(keyStorePath)) { ks.load(keyStoreIs, getPasswordChars(password)); } catch (IOException | NoSuchAlgorithmException | CertificateException e) { issues.add(context.createConfigIssue( groupName, configPrefix + storeCategory.toLowerCase() + "StoreFilePath", TlsConfigErrors.TLS_21, storeCategory, keyStorePath, e.getMessage(), e )); return null; } return ks; } private static char[] getPasswordChars(String password) { if (Strings.isNullOrEmpty(password)) { return new char[0]; } else { return password.toCharArray(); } } }