/* * 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.ssl; import org.apache.nifi.annotation.documentation.CapabilityDescription; import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.annotation.lifecycle.OnEnabled; import org.apache.nifi.components.AllowableValue; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.components.PropertyValue; import org.apache.nifi.components.ValidationContext; import org.apache.nifi.components.ValidationResult; import org.apache.nifi.components.Validator; import org.apache.nifi.controller.AbstractControllerService; 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.KeystoreType; import org.apache.nifi.security.util.SslContextFactory; import javax.net.ssl.SSLContext; import java.io.File; import java.net.MalformedURLException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @Tags({"ssl", "secure", "certificate", "keystore", "truststore", "jks", "p12", "pkcs12", "pkcs"}) @CapabilityDescription("Standard implementation of the SSLContextService. Provides the ability to configure " + "keystore and/or truststore properties once and reuse that configuration throughout the application") public class StandardSSLContextService extends AbstractControllerService implements SSLContextService { public static final String STORE_TYPE_JKS = "JKS"; public static final String STORE_TYPE_PKCS12 = "PKCS12"; public static final PropertyDescriptor TRUSTSTORE = new PropertyDescriptor.Builder() .name("Truststore Filename") .description("The fully-qualified filename of the Truststore") .defaultValue(null) .addValidator(createFileExistsAndReadableValidator()) .sensitive(false) .build(); public static final PropertyDescriptor TRUSTSTORE_TYPE = new PropertyDescriptor.Builder() .name("Truststore Type") .description("The Type of the Truststore. Either JKS or PKCS12") .allowableValues(STORE_TYPE_JKS, STORE_TYPE_PKCS12) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .sensitive(false) .build(); public static final PropertyDescriptor TRUSTSTORE_PASSWORD = new PropertyDescriptor.Builder() .name("Truststore Password") .description("The password for the Truststore") .defaultValue(null) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .sensitive(true) .build(); public static final PropertyDescriptor KEYSTORE = new PropertyDescriptor.Builder() .name("Keystore Filename") .description("The fully-qualified filename of the Keystore") .defaultValue(null) .addValidator(createFileExistsAndReadableValidator()) .sensitive(false) .build(); public static final PropertyDescriptor KEYSTORE_TYPE = new PropertyDescriptor.Builder() .name("Keystore Type") .description("The Type of the Keystore") .allowableValues(STORE_TYPE_JKS, STORE_TYPE_PKCS12) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .sensitive(false) .build(); public static final PropertyDescriptor KEYSTORE_PASSWORD = new PropertyDescriptor.Builder() .name("Keystore Password") .defaultValue(null) .description("The password for the Keystore") .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .sensitive(true) .build(); static final PropertyDescriptor KEY_PASSWORD = new PropertyDescriptor.Builder() .name("key-password") .displayName("Key Password") .description("The password for the key. If this is not specified, but the Keystore Filename, Password, and Type are specified, " + "then the Keystore Password will be assumed to be the same as the Key Password.") .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .sensitive(true) .required(false) .build(); public static final PropertyDescriptor SSL_ALGORITHM = new PropertyDescriptor.Builder() .name("SSL Protocol") .defaultValue("TLS") .required(false) .allowableValues(buildAlgorithmAllowableValues()) .description("The algorithm to use for this SSL context") .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .sensitive(false) .build(); private static final List<PropertyDescriptor> properties; private ConfigurationContext configContext; private boolean isValidated; // TODO: This can be made configurable if necessary private static final int VALIDATION_CACHE_EXPIRATION = 5; private int validationCacheCount = 0; static { List<PropertyDescriptor> props = new ArrayList<>(); props.add(KEYSTORE); props.add(KEYSTORE_PASSWORD); props.add(KEY_PASSWORD); props.add(KEYSTORE_TYPE); props.add(TRUSTSTORE); props.add(TRUSTSTORE_PASSWORD); props.add(TRUSTSTORE_TYPE); props.add(SSL_ALGORITHM); properties = Collections.unmodifiableList(props); } @OnEnabled public void onConfigured(final ConfigurationContext context) throws InitializationException { configContext = context; final Collection<ValidationResult> results = new ArrayList<>(); results.addAll(validateStore(context.getProperties(), KeystoreValidationGroup.KEYSTORE)); results.addAll(validateStore(context.getProperties(), KeystoreValidationGroup.TRUSTSTORE)); if (!results.isEmpty()) { final StringBuilder sb = new StringBuilder(this + " is not valid due to:"); for (final ValidationResult result : results) { sb.append("\n").append(result.toString()); } 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 public void onPropertyModified(PropertyDescriptor descriptor, String oldValue, String newValue) { super.onPropertyModified(descriptor, oldValue, newValue); resetValidationCache(); } 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 String substituted; try { substituted = context.newPropertyValue(input).evaluateAttributeExpressions().getValue(); } catch (final Exception e) { return new ValidationResult.Builder() .subject(subject) .input(input) .valid(false) .explanation("Not a valid Expression Language value: " + e.getMessage()) .build(); } final File file = new File(substituted); 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(); } }; } @Override protected List<PropertyDescriptor> getSupportedPropertyDescriptors() { return properties; } @Override protected Collection<ValidationResult> customValidate(ValidationContext validationContext) { final Collection<ValidationResult> results = new ArrayList<>(); if (isValidated) { validationCacheCount++; if (validationCacheCount > VALIDATION_CACHE_EXPIRATION) { resetValidationCache(); } else { return results; } } 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; } private void resetValidationCache() { validationCacheCount = 0; isValidated = false; } protected int getValidationCacheExpiration() { return VALIDATION_CACHE_EXPIRATION; } private void verifySslConfig(final ValidationContext validationContext) throws ProcessException { final String protocol = validationContext.getProperty(SSL_ALGORITHM).getValue(); 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 = configContext.getProperty(SSL_ALGORITHM).getValue(); try { final PropertyValue keyPasswdProp = configContext.getProperty(KEY_PASSWORD); final char[] keyPassword = keyPasswdProp.isSet() ? keyPasswdProp.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(), configContext.getProperty(TRUSTSTORE_TYPE).getValue(), protocol); } final String truststoreFile = configContext.getProperty(TRUSTSTORE).getValue(); if (truststoreFile == null) { // If truststore not specified, create SSL Context based only on key store. return SslContextFactory.createSslContext( configContext.getProperty(KEYSTORE).getValue(), configContext.getProperty(KEYSTORE_PASSWORD).getValue().toCharArray(), keyPassword, configContext.getProperty(KEYSTORE_TYPE).getValue(), protocol); } return SslContextFactory.createSslContext( configContext.getProperty(KEYSTORE).getValue(), configContext.getProperty(KEYSTORE_PASSWORD).getValue().toCharArray(), keyPassword, configContext.getProperty(KEYSTORE_TYPE).getValue(), configContext.getProperty(TRUSTSTORE).getValue(), configContext.getProperty(TRUSTSTORE_PASSWORD).getValue().toCharArray(), configContext.getProperty(TRUSTSTORE_TYPE).getValue(), org.apache.nifi.security.util.SslContextFactory.ClientAuth.valueOf(clientAuth.name()), protocol); } catch (final Exception e) { throw new ProcessException(e); } } @Override public String getTrustStoreFile() { return configContext.getProperty(TRUSTSTORE).getValue(); } @Override public String getTrustStoreType() { return configContext.getProperty(TRUSTSTORE_TYPE).getValue(); } @Override public String getTrustStorePassword() { return configContext.getProperty(TRUSTSTORE_PASSWORD).getValue(); } @Override public boolean isTrustStoreConfigured() { return getTrustStoreFile() != null && getTrustStorePassword() != null && getTrustStoreType() != null; } @Override public String getKeyStoreFile() { return configContext.getProperty(KEYSTORE).getValue(); } @Override public String getKeyStoreType() { return configContext.getProperty(KEYSTORE_TYPE).getValue(); } @Override public String getKeyStorePassword() { return configContext.getProperty(KEYSTORE_PASSWORD).getValue(); } @Override public String getKeyPassword() { return configContext.getProperty(KEY_PASSWORD).getValue(); } @Override public boolean isKeyStoreConfigured() { return getKeyStoreFile() != null && getKeyStorePassword() != null && getKeyStoreType() != null; } @Override public String getSslAlgorithm() { return configContext.getProperty(SSL_ALGORITHM).getValue(); } 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; if (keyStoreOrTrustStore == KeystoreValidationGroup.KEYSTORE) { filename = properties.get(KEYSTORE); password = properties.get(KEYSTORE_PASSWORD); type = properties.get(KEYSTORE_TYPE); } else { filename = properties.get(TRUSTSTORE); password = properties.get(TRUSTSTORE_PASSWORD); type = properties.get(TRUSTSTORE_TYPE); } 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()); } } } return results; } private static int countNulls(Object... objects) { int count = 0; for (final Object x : objects) { if (x == null) { count++; } } return count; } public enum KeystoreValidationGroup { KEYSTORE, TRUSTSTORE } private static AllowableValue[] buildAlgorithmAllowableValues() { final Set<String> supportedProtocols = new HashSet<>(); /* * Prepopulate protocols with generic instance types commonly used * see: http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SSLContext */ supportedProtocols.add("SSL"); supportedProtocols.add("TLS"); // Determine those provided by the JVM on the system try { supportedProtocols.addAll(Arrays.asList(SSLContext.getDefault().createSSLEngine().getSupportedProtocols())); } catch (NoSuchAlgorithmException e) { // ignored as default is used } final int numProtocols = supportedProtocols.size(); // Sort for consistent presentation in configuration views final List<String> supportedProtocolList = new ArrayList<>(supportedProtocols); Collections.sort(supportedProtocolList); final List<AllowableValue> protocolAllowableValues = new ArrayList<>(); for (final String protocol : supportedProtocolList) { protocolAllowableValues.add(new AllowableValue(protocol)); } return protocolAllowableValues.toArray(new AllowableValue[numProtocols]); } @Override public String toString() { return "SSLContextService[id=" + getIdentifier() + "]"; } }