This is an automated email from the ASF dual-hosted git repository.
ottersbach pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new 3214e9163c NIFI-14049 Added PEM-Encoded SSLContextProvider (#9555)
3214e9163c is described below
commit 3214e9163c59e2f1c9d21b275dda8f1b8c9e8268
Author: David Handermann <[email protected]>
AuthorDate: Sun Dec 1 04:20:26 2024 -0600
NIFI-14049 Added PEM-Encoded SSLContextProvider (#9555)
Signed-off-by: Lucas Ottersbach <[email protected]>
---
.../nifi/ssl/PEMEncodedSSLContextProvider.java | 389 +++++++++++++++++++++
.../org.apache.nifi.controller.ControllerService | 4 +-
.../nifi/ssl/PEMEncodedSSLContextProviderTest.java | 372 ++++++++++++++++++++
3 files changed, 764 insertions(+), 1 deletion(-)
diff --git
a/nifi-extension-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/main/java/org/apache/nifi/ssl/PEMEncodedSSLContextProvider.java
b/nifi-extension-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/main/java/org/apache/nifi/ssl/PEMEncodedSSLContextProvider.java
new file mode 100644
index 0000000000..16fe1cdd59
--- /dev/null
+++
b/nifi-extension-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/main/java/org/apache/nifi/ssl/PEMEncodedSSLContextProvider.java
@@ -0,0 +1,389 @@
+/*
+ * 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.OnDisabled;
+import org.apache.nifi.annotation.lifecycle.OnEnabled;
+import org.apache.nifi.components.AllowableValue;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.DescribedValue;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.resource.ResourceCardinality;
+import org.apache.nifi.components.resource.ResourceReference;
+import org.apache.nifi.components.resource.ResourceType;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.controller.VerifiableControllerService;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.security.ssl.BuilderConfigurationException;
+import org.apache.nifi.security.ssl.PemCertificateKeyStoreBuilder;
+import org.apache.nifi.security.ssl.PemPrivateKeyCertificateKeyStoreBuilder;
+import org.apache.nifi.security.ssl.StandardKeyManagerBuilder;
+import org.apache.nifi.security.ssl.StandardSslContextBuilder;
+import org.apache.nifi.security.ssl.StandardTrustManagerBuilder;
+import org.apache.nifi.security.util.TlsPlatform;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509ExtendedTrustManager;
+import javax.net.ssl.X509TrustManager;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+@Tags({"PEM", "SSL", "TLS", "Key", "Certificate", "PKCS1", "PKCS8", "X.509",
"ECDSA", "Ed25519", "RSA"})
+@CapabilityDescription("""
+ SSLContext Provider configurable using PEM Private Key and Certificate
files.
+ Supports PKCS1 and PKCS8 encoding for Private Keys as well as X.509
encoding for Certificates.
+""")
+public class PEMEncodedSSLContextProvider extends AbstractControllerService
implements SSLContextProvider, VerifiableControllerService {
+ static final String DEFAULT_PROTOCOL = "TLS";
+
+ static final PropertyDescriptor TLS_PROTOCOL = new
PropertyDescriptor.Builder()
+ .name("TLS Protocol")
+ .description("TLS protocol version required for negotiating
encrypted communications.")
+ .required(true)
+ .sensitive(false)
+ .defaultValue(DEFAULT_PROTOCOL)
+ .allowableValues(getProtocolAllowableValues())
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .build();
+
+ static final PropertyDescriptor PRIVATE_KEY_SOURCE = new
PropertyDescriptor.Builder()
+ .name("Private Key Source")
+ .description("Source of information for loading Private Key and
Certificate Chain")
+ .required(true)
+ .defaultValue(PrivateKeySource.PROPERTIES)
+ .allowableValues(PrivateKeySource.class)
+ .build();
+
+ static final PropertyDescriptor PRIVATE_KEY = new
PropertyDescriptor.Builder()
+ .name("Private Key")
+ .description("PEM Private Key encoded using either PKCS1 or PKCS8.
Supported algorithms include ECDSA, Ed25519, and RSA")
+ .required(true)
+ .sensitive(true)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .identifiesExternalResource(ResourceCardinality.SINGLE,
ResourceType.TEXT)
+ .dependsOn(PRIVATE_KEY_SOURCE, PrivateKeySource.PROPERTIES)
+ .build();
+
+ static final PropertyDescriptor PRIVATE_KEY_LOCATION = new
PropertyDescriptor.Builder()
+ .name("Private Key Location")
+ .description("PEM Private Key file location encoded using either
PKCS1 or PKCS8. Supported algorithms include ECDSA, Ed25519, and RSA")
+ .required(true)
+ .sensitive(false)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .identifiesExternalResource(ResourceCardinality.SINGLE,
ResourceType.FILE)
+ .dependsOn(PRIVATE_KEY_SOURCE, PrivateKeySource.FILES)
+ .build();
+
+ static final PropertyDescriptor CERTIFICATE_CHAIN = new
PropertyDescriptor.Builder()
+ .name("Certificate Chain")
+ .description("PEM X.509 Certificate Chain associated with Private
Key starting with standard BEGIN CERTIFICATE header")
+ .required(true)
+ .sensitive(false)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .identifiesExternalResource(ResourceCardinality.SINGLE,
ResourceType.TEXT)
+ .dependsOn(PRIVATE_KEY_SOURCE, PrivateKeySource.PROPERTIES)
+ .build();
+
+ static final PropertyDescriptor CERTIFICATE_CHAIN_LOCATION = new
PropertyDescriptor.Builder()
+ .name("Certificate Chain Location")
+ .description("PEM X.509 Certificate Chain file location associated
with Private Key starting with standard BEGIN CERTIFICATE header")
+ .required(true)
+ .sensitive(false)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .identifiesExternalResource(ResourceCardinality.SINGLE,
ResourceType.FILE)
+ .dependsOn(PRIVATE_KEY_SOURCE, PrivateKeySource.FILES)
+ .build();
+
+ static final PropertyDescriptor CERTIFICATE_AUTHORITIES_SOURCE = new
PropertyDescriptor.Builder()
+ .name("Certificate Authorities Source")
+ .description("Source of information for loading trusted
Certificate Authorities")
+ .required(true)
+ .defaultValue(CertificateAuthoritiesSource.PROPERTIES)
+ .allowableValues(CertificateAuthoritiesSource.class)
+ .build();
+
+ static final PropertyDescriptor CERTIFICATE_AUTHORITIES = new
PropertyDescriptor.Builder()
+ .name("Certificate Authorities")
+ .description("PEM X.509 Certificate Authorities trusted for
verifying peers in TLS communications containing one or more standard
certificates")
+ .required(true)
+ .sensitive(false)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .identifiesExternalResource(ResourceCardinality.SINGLE,
ResourceType.FILE, ResourceType.TEXT)
+ .dependsOn(CERTIFICATE_AUTHORITIES_SOURCE,
CertificateAuthoritiesSource.PROPERTIES)
+ .build();
+
+ private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS =
List.of(
+ TLS_PROTOCOL,
+ PRIVATE_KEY_SOURCE,
+ PRIVATE_KEY,
+ PRIVATE_KEY_LOCATION,
+ CERTIFICATE_CHAIN,
+ CERTIFICATE_CHAIN_LOCATION,
+ CERTIFICATE_AUTHORITIES_SOURCE,
+ CERTIFICATE_AUTHORITIES
+ );
+
+ private static final char[] EMPTY_PROTECTION_PARAMETER = new char[]{};
+
+ private String protocol = DEFAULT_PROTOCOL;
+
+ private KeyStore keyStore;
+
+ private KeyStore trustStore;
+
+ @Override
+ protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+ return PROPERTY_DESCRIPTORS;
+ }
+
+ @Override
+ public List<ConfigVerificationResult> verify(final ConfigurationContext
context, final ComponentLog verificationLogger, final Map<String, String>
variables) {
+ final List<ConfigVerificationResult> results = new ArrayList<>();
+
+ final ConfigVerificationResult.Builder privateKeyBuilder = new
ConfigVerificationResult.Builder().verificationStepName("Load Private Key and
Certificate Chain");
+ final PrivateKeySource privateKeySource =
context.getProperty(PRIVATE_KEY_SOURCE).asAllowableValue(PrivateKeySource.class);
+ if (privateKeySource == PrivateKeySource.UNDEFINED) {
+ privateKeyBuilder.outcome(Outcome.SKIPPED).explanation("Private
Key and Certificate Chain properties not required");
+ } else {
+ try {
+ loadKeyStore(context);
+ privateKeyBuilder.outcome(Outcome.SUCCESSFUL);
+ } catch (final Exception e) {
+
privateKeyBuilder.outcome(Outcome.FAILED).explanation(e.getMessage());
+ }
+ }
+ results.add(privateKeyBuilder.build());
+
+ final ConfigVerificationResult.Builder authoritiesBuilder = new
ConfigVerificationResult.Builder().verificationStepName("Load Certificate
Authorities");
+ try {
+ loadTrustStore(context);
+ authoritiesBuilder.outcome(Outcome.SUCCESSFUL);
+ } catch (final Exception e) {
+
authoritiesBuilder.outcome(Outcome.FAILED).explanation(e.getMessage());
+ }
+ results.add(authoritiesBuilder.build());
+
+ return results;
+ }
+
+ @OnEnabled
+ public void onEnabled(final ConfigurationContext context) throws
InitializationException {
+ protocol = context.getProperty(TLS_PROTOCOL).getValue();
+ loadKeyStore(context);
+ loadTrustStore(context);
+ }
+
+ @OnDisabled
+ public void onDisabled() {
+ keyStore = null;
+ trustStore = null;
+ }
+
+ @Override
+ public SSLContext createContext() {
+ final StandardSslContextBuilder sslContextBuilder = new
StandardSslContextBuilder();
+ sslContextBuilder.protocol(protocol);
+
+ final X509TrustManager trustManager = createTrustManager();
+ sslContextBuilder.trustManager(trustManager);
+
+ final Optional<X509ExtendedKeyManager> keyManagerCreated =
createKeyManager();
+ if (keyManagerCreated.isPresent()) {
+ final X509ExtendedKeyManager keyManager = keyManagerCreated.get();
+ sslContextBuilder.keyManager(keyManager);
+ }
+
+ return sslContextBuilder.build();
+ }
+
+ @Override
+ public Optional<X509ExtendedKeyManager> createKeyManager() {
+ final Optional<X509ExtendedKeyManager> keyManagerCreated;
+
+ if (keyStore == null) {
+ keyManagerCreated = Optional.empty();
+ } else {
+ final X509ExtendedKeyManager keyManager = new
StandardKeyManagerBuilder()
+ .keyStore(keyStore)
+ .keyPassword(EMPTY_PROTECTION_PARAMETER)
+ .build();
+ keyManagerCreated = Optional.of(keyManager);
+ }
+
+ return keyManagerCreated;
+ }
+
+ @Override
+ public X509TrustManager createTrustManager() {
+ final X509ExtendedTrustManager trustManager;
+
+ if (trustStore == null) {
+ try {
+ final String algorithm =
TrustManagerFactory.getDefaultAlgorithm();
+ final TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(algorithm);
+ trustManagerFactory.init(trustStore);
+
+ final TrustManager[] trustManagers =
trustManagerFactory.getTrustManagers();
+ final Optional<X509ExtendedTrustManager>
configuredTrustManager = Arrays.stream(trustManagers)
+ .filter(manager -> manager instanceof
X509ExtendedTrustManager)
+ .map(manager -> (X509ExtendedTrustManager) manager)
+ .findFirst();
+ trustManager = configuredTrustManager.orElseThrow(() -> new
BuilderConfigurationException("X.509 Trust Manager not configured"));
+ } catch (final GeneralSecurityException e) {
+ throw new BuilderConfigurationException("Trust Manager
creation failed for System Certificate Authorities", e);
+ }
+ } else {
+ trustManager = new
StandardTrustManagerBuilder().trustStore(trustStore).build();
+ }
+
+ return trustManager;
+ }
+
+ private void loadKeyStore(final ConfigurationContext context) throws
InitializationException {
+ final PrivateKeySource privateKeySource =
context.getProperty(PRIVATE_KEY_SOURCE).asAllowableValue(PrivateKeySource.class);
+ if (privateKeySource == PrivateKeySource.UNDEFINED) {
+ getLogger().debug("Private Key and Certificate Chain not
configured");
+ } else {
+ final PropertyDescriptor privateKeyProperty;
+ final PropertyDescriptor certificateChainProperty;
+
+ if (privateKeySource == PrivateKeySource.FILES) {
+ privateKeyProperty = PRIVATE_KEY_LOCATION;
+ certificateChainProperty = CERTIFICATE_CHAIN_LOCATION;
+ } else {
+ privateKeyProperty = PRIVATE_KEY;
+ certificateChainProperty = CERTIFICATE_CHAIN;
+ }
+
+ final ResourceReference privateKeyReference =
context.getProperty(privateKeyProperty).asResource();
+ final ResourceReference certificateChainReference =
context.getProperty(certificateChainProperty).asResource();
+ final PemPrivateKeyCertificateKeyStoreBuilder keyStoreBuilder =
new PemPrivateKeyCertificateKeyStoreBuilder();
+ try (
+ InputStream privateKeyInputStream =
privateKeyReference.read();
+ InputStream certificateInputStream =
certificateChainReference.read()
+ ) {
+ keyStore = keyStoreBuilder
+ .privateKeyInputStream(privateKeyInputStream)
+ .certificateInputStream(certificateInputStream)
+ .build();
+ } catch (final Exception e) {
+ throw new InitializationException("Failed to load Private Key
or Certificate Chain from configured properties", e);
+ }
+ }
+ }
+
+ private void loadTrustStore(final ConfigurationContext context) throws
InitializationException {
+ final CertificateAuthoritiesSource certificateAuthoritiesSource =
context.getProperty(CERTIFICATE_AUTHORITIES_SOURCE).asAllowableValue(CertificateAuthoritiesSource.class);
+ if (certificateAuthoritiesSource ==
CertificateAuthoritiesSource.SYSTEM) {
+ trustStore = null;
+ } else if (certificateAuthoritiesSource ==
CertificateAuthoritiesSource.PROPERTIES) {
+ final ResourceReference certificateAuthoritiesReference =
context.getProperty(CERTIFICATE_AUTHORITIES).asResource();
+ try (InputStream certificateAuthoritiesStream =
certificateAuthoritiesReference.read()) {
+ trustStore = new
PemCertificateKeyStoreBuilder().inputStream(certificateAuthoritiesStream).build();
+ } catch (final Exception e) {
+ throw new InitializationException("Failed to load Certificate
Authorities from configured properties", e);
+ }
+ }
+ }
+
+ private static AllowableValue[] getProtocolAllowableValues() {
+ final List<AllowableValue> allowableValues = new ArrayList<>();
+
+ allowableValues.add(new AllowableValue(DEFAULT_PROTOCOL,
DEFAULT_PROTOCOL, "Negotiate latest TLS protocol version based on platform
supported versions"));
+
+ for (final String supportedProtocol :
TlsPlatform.getPreferredProtocols()) {
+ final String description = String.format("Require %s protocol
version", supportedProtocol);
+ allowableValues.add(new AllowableValue(supportedProtocol,
supportedProtocol, description));
+ }
+
+ return allowableValues.toArray(new AllowableValue[0]);
+ }
+
+ enum PrivateKeySource implements DescribedValue {
+ UNDEFINED("Undefined", "Avoid configuring Private Key and Certificate
Chain properties"),
+ PROPERTIES("Properties", "Load Private Key and Certificate Chain from
configured properties"),
+ FILES("Files", "Load Private Key and Certificate Chain from configured
files");
+
+ private final String displayName;
+
+ private final String description;
+
+ PrivateKeySource(final String displayName, final String description) {
+ this.displayName = displayName;
+ this.description = description;
+ }
+
+ @Override
+ public String getValue() {
+ return name();
+ }
+
+ @Override
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+ }
+
+ enum CertificateAuthoritiesSource implements DescribedValue {
+ PROPERTIES("Properties", "Load trusted Certificate Authorities from
configured properties"),
+ SYSTEM("System", "Load trusted Certificate Authorities from the
default system location");
+
+ private final String displayName;
+
+ private final String description;
+
+ CertificateAuthoritiesSource(final String displayName, final String
description) {
+ this.displayName = displayName;
+ this.description = description;
+ }
+
+ @Override
+ public String getValue() {
+ return name();
+ }
+
+ @Override
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+ }
+}
diff --git
a/nifi-extension-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
b/nifi-extension-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
index 115fe0defa..c287526b71 100644
---
a/nifi-extension-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
+++
b/nifi-extension-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
@@ -12,5 +12,7 @@
# 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.
+
+org.apache.nifi.ssl.PEMEncodedSSLContextProvider
org.apache.nifi.ssl.StandardSSLContextService
-org.apache.nifi.ssl.StandardRestrictedSSLContextService
\ No newline at end of file
+org.apache.nifi.ssl.StandardRestrictedSSLContextService
diff --git
a/nifi-extension-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/test/java/org/apache/nifi/ssl/PEMEncodedSSLContextProviderTest.java
b/nifi-extension-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/test/java/org/apache/nifi/ssl/PEMEncodedSSLContextProviderTest.java
new file mode 100644
index 0000000000..50dc1f4257
--- /dev/null
+++
b/nifi-extension-bundles/nifi-standard-services/nifi-ssl-context-bundle/nifi-ssl-context-service/src/test/java/org/apache/nifi/ssl/PEMEncodedSSLContextProviderTest.java
@@ -0,0 +1,372 @@
+/*
+ * 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.components.ConfigVerificationResult;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.controller.ControllerServiceLookup;
+import org.apache.nifi.ssl.PEMEncodedSSLContextProvider.PrivateKeySource;
+import
org.apache.nifi.ssl.PEMEncodedSSLContextProvider.CertificateAuthoritiesSource;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.security.cert.builder.StandardCertificateBuilder;
+import org.apache.nifi.security.util.TlsPlatform;
+import org.apache.nifi.util.MockConfigurationContext;
+import org.apache.nifi.util.NoOpProcessor;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509TrustManager;
+import javax.security.auth.x500.X500Principal;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class PEMEncodedSSLContextProviderTest {
+ private static final String RSA_ALGORITHM = "RSA";
+
+ private static final int RSA_KEY_SIZE = 3072;
+
+ private static final String SERVICE_ID =
PEMEncodedSSLContextProviderTest.class.getSimpleName();
+
+ private static final String PRIVATE_KEY_HEADER = "-----BEGIN PRIVATE
KEY-----";
+
+ private static final String PRIVATE_KEY_FOOTER = "-----END PRIVATE
KEY-----";
+
+ private static final String CERTIFICATE_HEADER = "-----BEGIN
CERTIFICATE-----";
+
+ private static final String CERTIFICATE_FOOTER = "-----END
CERTIFICATE-----";
+
+ private static final String CERTIFICATE_INVALID =
"%s%nINVALID".formatted(CERTIFICATE_HEADER);
+
+ private static final String PRIVATE_KEY_FILE_PREFIX = "private-key";
+
+ private static final String CERTIFICATE_CHAIN_FILE_PREFIX =
"certificate-chain";
+
+ private static final String PEM_EXTENSION = ".pem";
+
+ private static final String LINE_LENGTH_PATTERN = "(?<=\\G.{64})";
+
+ private static final char LINE_FEED = 10;
+
+ private static final Base64.Encoder encoder = Base64.getEncoder();
+
+ private static String privateKeyPemEncoded;
+
+ private static String certificatePemEncoded;
+
+ private static X509Certificate certificate;
+
+ private TestRunner runner;
+
+ private PEMEncodedSSLContextProvider provider;
+
+ @BeforeAll
+ static void setKeyCertificate() throws Exception {
+ final KeyPairGenerator rsaKeyPairGenerator =
KeyPairGenerator.getInstance(RSA_ALGORITHM);
+ rsaKeyPairGenerator.initialize(RSA_KEY_SIZE);
+ final KeyPair rsaKeyPair = rsaKeyPairGenerator.generateKeyPair();
+ final PrivateKey rsaPrivateKey = rsaKeyPair.getPrivate();
+ final byte[] rsaPrivateKeyEncoded = rsaPrivateKey.getEncoded();
+ privateKeyPemEncoded = getPrivateKeyPemEncoded(rsaPrivateKeyEncoded);
+
+ certificate = new StandardCertificateBuilder(rsaKeyPair, new
X500Principal("CN=localhost"), Duration.ofHours(1)).build();
+ final byte[] certificateEncoded = certificate.getEncoded();
+ certificatePemEncoded = getCertificatePemEncoded(certificateEncoded);
+ }
+
+ @BeforeEach
+ void setRunner() throws InitializationException {
+ runner = TestRunners.newTestRunner(NoOpProcessor.class);
+ provider = new PEMEncodedSSLContextProvider();
+ runner.addControllerService(SERVICE_ID, provider);
+ }
+
+ @Test
+ void testVerifySystemCertificateAuthorities() {
+ final Map<PropertyDescriptor, String> properties = new HashMap<>();
+ properties.put(PEMEncodedSSLContextProvider.PRIVATE_KEY_SOURCE,
PrivateKeySource.UNDEFINED.getValue());
+
properties.put(PEMEncodedSSLContextProvider.CERTIFICATE_AUTHORITIES_SOURCE,
CertificateAuthoritiesSource.SYSTEM.getValue());
+
+ final ControllerServiceLookup serviceLookup =
runner.getProcessContext().getControllerServiceLookup();
+ final ConfigurationContext context = new
MockConfigurationContext(provider, properties, serviceLookup,
Collections.emptyMap());
+
+ final List<ConfigVerificationResult> results =
provider.verify(context, runner.getLogger(), Map.of());
+
+ assertNotNull(results);
+ assertFalse(results.isEmpty());
+
+ for (final ConfigVerificationResult result : results) {
+ assertNotEquals(ConfigVerificationResult.Outcome.FAILED,
result.getOutcome(), result.getExplanation());
+ }
+ }
+
+ @Test
+ void testVerifyPrivateKeyCertificateChain() {
+ final Map<PropertyDescriptor, String> properties = new HashMap<>();
+ properties.put(PEMEncodedSSLContextProvider.PRIVATE_KEY_SOURCE,
PrivateKeySource.PROPERTIES.getValue());
+ properties.put(PEMEncodedSSLContextProvider.PRIVATE_KEY,
privateKeyPemEncoded);
+ properties.put(PEMEncodedSSLContextProvider.CERTIFICATE_CHAIN,
certificatePemEncoded);
+
properties.put(PEMEncodedSSLContextProvider.CERTIFICATE_AUTHORITIES_SOURCE,
CertificateAuthoritiesSource.SYSTEM.getValue());
+
+ final ControllerServiceLookup serviceLookup =
runner.getProcessContext().getControllerServiceLookup();
+ final ConfigurationContext context = new
MockConfigurationContext(provider, properties, serviceLookup,
Collections.emptyMap());
+
+ final List<ConfigVerificationResult> results =
provider.verify(context, runner.getLogger(), Map.of());
+
+ assertNotNull(results);
+ assertFalse(results.isEmpty());
+
+ for (final ConfigVerificationResult result : results) {
+ assertEquals(ConfigVerificationResult.Outcome.SUCCESSFUL,
result.getOutcome(), result.getExplanation());
+ }
+ }
+
+ @Test
+ void testCreateContextSystemCertificateAuthorities() {
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.PRIVATE_KEY_SOURCE, PrivateKeySource.UNDEFINED);
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.CERTIFICATE_AUTHORITIES_SOURCE,
CertificateAuthoritiesSource.SYSTEM);
+
+ runner.enableControllerService(provider);
+
+ final SSLContext sslContext = provider.createContext();
+
+ assertNotNull(sslContext);
+
+ runner.disableControllerService(provider);
+ }
+
+ @Test
+ void testCreateContextProtocolConfigured() {
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.PRIVATE_KEY_SOURCE, PrivateKeySource.UNDEFINED);
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.CERTIFICATE_AUTHORITIES_SOURCE,
CertificateAuthoritiesSource.SYSTEM);
+
+ final String protocol = TlsPlatform.getLatestProtocol();
+
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.TLS_PROTOCOL, protocol);
+ runner.enableControllerService(provider);
+
+ final SSLContext sslContext = provider.createContext();
+
+ assertNotNull(sslContext);
+ assertEquals(protocol, sslContext.getProtocol());
+ }
+
+ @Test
+ void testCreateContextPrivateKeyCertificateConfigured() {
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.CERTIFICATE_AUTHORITIES_SOURCE,
CertificateAuthoritiesSource.SYSTEM);
+
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.PRIVATE_KEY_SOURCE, PrivateKeySource.PROPERTIES);
+ runner.setProperty(provider, PEMEncodedSSLContextProvider.PRIVATE_KEY,
privateKeyPemEncoded);
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.CERTIFICATE_CHAIN, certificatePemEncoded);
+ runner.enableControllerService(provider);
+
+ final SSLContext sslContext = provider.createContext();
+
+ assertNotNull(sslContext);
+ }
+
+ @Test
+ void testCreateContextPrivateKeyCertificateLocationsConfigured(@TempDir
final Path tempDir) throws IOException {
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.CERTIFICATE_AUTHORITIES_SOURCE,
CertificateAuthoritiesSource.SYSTEM);
+
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.PRIVATE_KEY_SOURCE, PrivateKeySource.FILES);
+
+ final Path privateKeyPath = Files.createTempFile(tempDir,
PRIVATE_KEY_FILE_PREFIX, PEM_EXTENSION);
+ Files.writeString(privateKeyPath, privateKeyPemEncoded);
+
+ final Path certificateChainPath = Files.createTempFile(tempDir,
CERTIFICATE_CHAIN_FILE_PREFIX, PEM_EXTENSION);
+ Files.writeString(certificateChainPath, certificatePemEncoded);
+
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.PRIVATE_KEY_LOCATION, privateKeyPath.toString());
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.CERTIFICATE_CHAIN_LOCATION,
certificateChainPath.toString());
+ runner.enableControllerService(provider);
+
+ final SSLContext sslContext = provider.createContext();
+
+ assertNotNull(sslContext);
+ }
+
+ @Test
+ void testCreateKeyManagerSourceUndefined() {
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.PRIVATE_KEY_SOURCE, PrivateKeySource.UNDEFINED);
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.CERTIFICATE_AUTHORITIES_SOURCE,
CertificateAuthoritiesSource.SYSTEM);
+
+ runner.enableControllerService(provider);
+
+ final Optional<X509ExtendedKeyManager> keyManagerCreated =
provider.createKeyManager();
+
+ assertTrue(keyManagerCreated.isEmpty());
+ }
+
+ @Test
+ void testCreateKeyManager() {
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.CERTIFICATE_AUTHORITIES_SOURCE,
CertificateAuthoritiesSource.SYSTEM);
+
+ runner.setProperty(provider, PEMEncodedSSLContextProvider.PRIVATE_KEY,
privateKeyPemEncoded);
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.CERTIFICATE_CHAIN, certificatePemEncoded);
+
+ runner.enableControllerService(provider);
+
+ final Optional<X509ExtendedKeyManager> keyManagerCreated =
provider.createKeyManager();
+ assertTrue(keyManagerCreated.isPresent());
+
+ final X509ExtendedKeyManager keyManager = keyManagerCreated.get();
+
+ final String[] serverAliases =
keyManager.getServerAliases(RSA_ALGORITHM, null);
+ assertNotNull(serverAliases);
+
+ final String[] clientAliases =
keyManager.getClientAliases(RSA_ALGORITHM, null);
+ assertNotNull(clientAliases);
+ }
+
+ @Test
+ void testCreateKeyManagerPrivateKeyInvalid() {
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.CERTIFICATE_AUTHORITIES_SOURCE,
CertificateAuthoritiesSource.SYSTEM);
+
+ runner.setProperty(provider, PEMEncodedSSLContextProvider.PRIVATE_KEY,
certificatePemEncoded);
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.CERTIFICATE_CHAIN, certificatePemEncoded);
+
+ assertThrows(AssertionError.class, () ->
runner.enableControllerService(provider));
+ }
+
+ @Test
+ void testCreateTrustManagerSystem() {
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.PRIVATE_KEY_SOURCE, PrivateKeySource.UNDEFINED);
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.CERTIFICATE_AUTHORITIES_SOURCE,
CertificateAuthoritiesSource.SYSTEM);
+
+ runner.enableControllerService(provider);
+
+ final X509TrustManager trustManager = provider.createTrustManager();
+
+ assertNotNull(trustManager);
+
+ final List<X509Certificate> acceptedIssuers =
Arrays.asList(trustManager.getAcceptedIssuers());
+ final boolean issuerFound =
acceptedIssuers.stream().anyMatch(certificate::equals);
+ assertFalse(issuerFound);
+ }
+
+ @Test
+ void testCreateTrustManagerInvalid() {
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.PRIVATE_KEY_SOURCE, PrivateKeySource.UNDEFINED);
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.CERTIFICATE_AUTHORITIES_SOURCE,
CertificateAuthoritiesSource.PROPERTIES);
+
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.CERTIFICATE_AUTHORITIES, CERTIFICATE_INVALID);
+
+ assertThrows(AssertionError.class, () ->
runner.enableControllerService(provider));
+ }
+
+ @Test
+ void testCreateTrustManagerContentEncoded() {
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.PRIVATE_KEY_SOURCE, PrivateKeySource.UNDEFINED);
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.CERTIFICATE_AUTHORITIES_SOURCE,
CertificateAuthoritiesSource.PROPERTIES);
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.CERTIFICATE_AUTHORITIES, certificatePemEncoded);
+
+ runner.enableControllerService(provider);
+
+ final X509TrustManager trustManager = provider.createTrustManager();
+
+ assertNotNull(trustManager);
+
+ final List<X509Certificate> acceptedIssuers =
Arrays.asList(trustManager.getAcceptedIssuers());
+ final boolean issuerFound =
acceptedIssuers.stream().anyMatch(certificate::equals);
+ assertTrue(issuerFound);
+ }
+
+ @Test
+ void testCreateTrustManagerFilePath(@TempDir final Path tempDir) throws
IOException {
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.PRIVATE_KEY_SOURCE, PrivateKeySource.UNDEFINED);
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.CERTIFICATE_AUTHORITIES_SOURCE,
CertificateAuthoritiesSource.PROPERTIES);
+
+ final Path certificateFile = Files.createTempFile(tempDir,
PEMEncodedSSLContextProviderTest.class.getSimpleName(), PEM_EXTENSION);
+ Files.writeString(certificateFile, certificatePemEncoded);
+
+ runner.setProperty(provider,
PEMEncodedSSLContextProvider.CERTIFICATE_AUTHORITIES,
certificateFile.toString());
+
+ runner.enableControllerService(provider);
+
+ final X509TrustManager trustManager = provider.createTrustManager();
+
+ assertNotNull(trustManager);
+
+ final List<X509Certificate> acceptedIssuers =
Arrays.asList(trustManager.getAcceptedIssuers());
+ final boolean issuerFound =
acceptedIssuers.stream().anyMatch(certificate::equals);
+ assertTrue(issuerFound);
+ }
+
+ private static String getPrivateKeyPemEncoded(final byte[]
privateKeyEncoded) {
+ final StringBuilder builder = new StringBuilder();
+ builder.append(PRIVATE_KEY_HEADER);
+ builder.append(LINE_FEED);
+
+ appendLines(privateKeyEncoded, builder);
+
+ builder.append(PRIVATE_KEY_FOOTER);
+ builder.append(LINE_FEED);
+
+ return builder.toString();
+ }
+
+ private static String getCertificatePemEncoded(final byte[]
certificateEncoded) {
+ final StringBuilder builder = new StringBuilder();
+ builder.append(CERTIFICATE_HEADER);
+ builder.append(LINE_FEED);
+
+ appendLines(certificateEncoded, builder);
+
+ builder.append(CERTIFICATE_FOOTER);
+ builder.append(LINE_FEED);
+
+ return builder.toString();
+ }
+
+ private static void appendLines(final byte[] encoded, final StringBuilder
builder) {
+ final String formatted = encoder.encodeToString(encoded);
+ final String[] lines = formatted.split(LINE_LENGTH_PATTERN);
+
+ for (final String line : lines) {
+ builder.append(line);
+ builder.append(LINE_FEED);
+ }
+ }
+}