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


Reply via email to