This is an automated email from the ASF dual-hosted git repository.
pvillard31 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 f3a4fe33039 NIFI-16212 Switch MQTT Processors to SSLContextProvider
(#11553)
f3a4fe33039 is described below
commit f3a4fe330394a4af2c5d5d54f7910744e1d4fcd4
Author: David Handermann <[email protected]>
AuthorDate: Sat Aug 15 15:26:59 2026 -0500
NIFI-16212 Switch MQTT Processors to SSLContextProvider (#11553)
---
.../nifi-mqtt-bundle/nifi-mqtt-processors/pom.xml | 12 +
.../mqtt/adapters/HiveMqV5ClientAdapter.java | 61 ++---
.../mqtt/adapters/PahoMqttClientAdapter.java | 35 +--
.../mqtt/adapters/PredefinedKeyManagerFactory.java | 60 +++++
.../mqtt/adapters/PredefinedSecurityProvider.java | 37 +++
.../adapters/PredefinedTrustManagerFactory.java | 61 +++++
.../mqtt/common/AbstractMQTTProcessor.java | 12 +-
.../mqtt/common/MqttClientProperties.java | 12 +-
.../nifi/processors/mqtt/ConsumeMQTTTLSTest.java | 275 +++++++++++++++++++++
.../nifi/processors/mqtt/TestConsumeMQTT.java | 12 +-
10 files changed, 484 insertions(+), 93 deletions(-)
diff --git
a/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/pom.xml
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/pom.xml
index 521ba43abfe..ca4a6d9ae90 100644
--- a/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/pom.xml
+++ b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/pom.xml
@@ -89,5 +89,17 @@
<artifactId>jackson-databind</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.hivemq</groupId>
+ <artifactId>hivemq-community-edition-embedded</artifactId>
+ <version>2026.5</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-security-cert-builder</artifactId>
+ <version>2.12.0-SNAPSHOT</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git
a/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/adapters/HiveMqV5ClientAdapter.java
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/adapters/HiveMqV5ClientAdapter.java
index 475ffd9839f..9aa927f282d 100644
---
a/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/adapters/HiveMqV5ClientAdapter.java
+++
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/adapters/HiveMqV5ClientAdapter.java
@@ -31,23 +31,19 @@ import
org.apache.nifi.processors.mqtt.common.MqttProtocolScheme;
import org.apache.nifi.processors.mqtt.common.ReceivedMqttMessage;
import org.apache.nifi.processors.mqtt.common.ReceivedMqttMessageHandler;
import org.apache.nifi.processors.mqtt.common.StandardMqttMessage;
-import org.apache.nifi.security.ssl.StandardKeyManagerFactoryBuilder;
-import org.apache.nifi.security.ssl.StandardKeyStoreBuilder;
-import org.apache.nifi.security.ssl.StandardTrustManagerFactoryBuilder;
-import org.apache.nifi.security.util.TlsConfiguration;
import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.ssl.SSLContextProvider;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
-import java.security.KeyStore;
import java.util.Objects;
+import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509TrustManager;
import static org.apache.nifi.processors.mqtt.common.MqttProtocolScheme.SSL;
import static org.apache.nifi.processors.mqtt.common.MqttProtocolScheme.WS;
@@ -176,43 +172,22 @@ public class HiveMqV5ClientAdapter implements MqttClient {
}
if (SSL.equals(scheme) || WSS.equals(scheme)) {
- final TlsConfiguration tlsConfiguration =
clientProperties.getTlsConfiguration();
-
- if (tlsConfiguration.getTruststorePath() != null) {
- final KeyStore trustStore;
- try (InputStream inputStream = new
FileInputStream(tlsConfiguration.getTruststorePath())) {
- trustStore = new StandardKeyStoreBuilder()
-
.type(tlsConfiguration.getTruststoreType().getType())
-
.password(tlsConfiguration.getTruststorePassword().toCharArray())
- .inputStream(inputStream)
- .build();
- } catch (final IOException e) {
- throw new TlsException("Trust Store loading failed", e);
- }
-
- final TrustManagerFactory trustManagerFactory = new
StandardTrustManagerFactoryBuilder().trustStore(trustStore).build();
- mqtt5ClientBuilder
- .sslConfig()
- .trustManagerFactory(trustManagerFactory)
- .applySslConfig();
+ final SSLContextProvider sslContextProvider =
clientProperties.getSslContextProvider();
+
+ if (sslContextProvider == null) {
+ throw new TlsException("SSL Context Provider not configured
for Broker URI scheme requiring TLS communication: " + scheme);
}
- if (tlsConfiguration.getKeystorePath() != null) {
- final KeyStore keyStore;
- try (InputStream inputStream = new
FileInputStream(tlsConfiguration.getKeystorePath())) {
- keyStore = new StandardKeyStoreBuilder()
- .type(tlsConfiguration.getKeystoreType().getType())
-
.password(tlsConfiguration.getKeystorePassword().toCharArray())
- .inputStream(inputStream)
- .build();
- } catch (final IOException e) {
- throw new TlsException("Key Store loading failed", e);
- }
-
- final KeyManagerFactory keyManagerFactory = new
StandardKeyManagerFactoryBuilder()
- .keyStore(keyStore)
-
.keyPassword(tlsConfiguration.getFunctionalKeyPassword().toCharArray())
- .build();
+ final X509TrustManager trustManager =
sslContextProvider.createTrustManager();
+ final TrustManagerFactory trustManagerFactory = new
PredefinedTrustManagerFactory(trustManager);
+ mqtt5ClientBuilder
+ .sslConfig()
+ .trustManagerFactory(trustManagerFactory)
+ .applySslConfig();
+
+ final Optional<X509ExtendedKeyManager> keyManagerFound =
sslContextProvider.createKeyManager();
+ if (keyManagerFound.isPresent()) {
+ final KeyManagerFactory keyManagerFactory = new
PredefinedKeyManagerFactory(keyManagerFound.get());
mqtt5ClientBuilder
.sslConfig()
.keyManagerFactory(keyManagerFactory)
diff --git
a/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/adapters/PahoMqttClientAdapter.java
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/adapters/PahoMqttClientAdapter.java
index 9d13d206a30..ddef128eb94 100644
---
a/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/adapters/PahoMqttClientAdapter.java
+++
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/adapters/PahoMqttClientAdapter.java
@@ -23,7 +23,7 @@ import org.apache.nifi.processors.mqtt.common.MqttException;
import org.apache.nifi.processors.mqtt.common.ReceivedMqttMessage;
import org.apache.nifi.processors.mqtt.common.ReceivedMqttMessageHandler;
import org.apache.nifi.processors.mqtt.common.StandardMqttMessage;
-import org.apache.nifi.security.util.TlsConfiguration;
+import org.apache.nifi.ssl.SSLContextProvider;
import org.eclipse.paho.client.mqttv3.IMqttClient;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
@@ -33,7 +33,6 @@ import
org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import java.net.URI;
import java.util.Arrays;
-import java.util.Properties;
public class PahoMqttClientAdapter implements MqttClient {
@@ -67,9 +66,9 @@ public class PahoMqttClientAdapter implements MqttClient {
connectOptions.setMqttVersion(clientProperties.getMqttVersion().getVersionCode());
connectOptions.setConnectionTimeout(clientProperties.getConnectionTimeout());
- final TlsConfiguration tlsConfiguration =
clientProperties.getTlsConfiguration();
- if (tlsConfiguration != null) {
-
connectOptions.setSSLProperties(transformSSLContextService(tlsConfiguration));
+ final SSLContextProvider sslContextProvider =
clientProperties.getSslContextProvider();
+ if (sslContextProvider != null) {
+
connectOptions.setSocketFactory(sslContextProvider.createContext().getSocketFactory());
}
final String lastWillTopic = clientProperties.getLastWillTopic();
@@ -136,32 +135,6 @@ public class PahoMqttClientAdapter implements MqttClient {
}
}
- public static Properties transformSSLContextService(TlsConfiguration
tlsConfiguration) {
- final Properties properties = new Properties();
- if (tlsConfiguration.getProtocol() != null) {
- properties.setProperty("com.ibm.ssl.protocol",
tlsConfiguration.getProtocol());
- }
- if (tlsConfiguration.getKeystorePath() != null) {
- properties.setProperty("com.ibm.ssl.keyStore",
tlsConfiguration.getKeystorePath());
- }
- if (tlsConfiguration.getKeystorePassword() != null) {
- properties.setProperty("com.ibm.ssl.keyStorePassword",
tlsConfiguration.getKeystorePassword());
- }
- if (tlsConfiguration.getKeystoreType() != null) {
- properties.setProperty("com.ibm.ssl.keyStoreType",
tlsConfiguration.getKeystoreType().getType());
- }
- if (tlsConfiguration.getTruststorePath() != null) {
- properties.setProperty("com.ibm.ssl.trustStore",
tlsConfiguration.getTruststorePath());
- }
- if (tlsConfiguration.getTruststorePassword() != null) {
- properties.setProperty("com.ibm.ssl.trustStorePassword",
tlsConfiguration.getTruststorePassword());
- }
- if (tlsConfiguration.getTruststoreType() != null) {
- properties.setProperty("com.ibm.ssl.trustStoreType",
tlsConfiguration.getTruststoreType().getType());
- }
- return properties;
- }
-
private static org.eclipse.paho.client.mqttv3.MqttClient createClient(URI
brokerUri, MqttClientProperties clientProperties, ComponentLog logger) {
logger.debug("Creating Mqtt v3 client");
diff --git
a/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/adapters/PredefinedKeyManagerFactory.java
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/adapters/PredefinedKeyManagerFactory.java
new file mode 100644
index 00000000000..16baf194d63
--- /dev/null
+++
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/adapters/PredefinedKeyManagerFactory.java
@@ -0,0 +1,60 @@
+/*
+ * 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.processors.mqtt.adapters;
+
+import java.security.KeyStore;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.KeyManagerFactorySpi;
+import javax.net.ssl.ManagerFactoryParameters;
+import javax.net.ssl.X509ExtendedKeyManager;
+
+/**
+ * Key Manager Factory that returns a single Key Manager provided at
construction time, instead of loading
+ * a Key Store. This bridges client libraries whose SSL configuration requires
a KeyManagerFactory with
+ * SSLContextProvider implementations, which expose an already-initialized
X.509 Key Manager directly.
+ */
+public class PredefinedKeyManagerFactory extends KeyManagerFactory {
+
+ private static final String ALGORITHM = "PredefinedKeyManager";
+
+ public PredefinedKeyManagerFactory(final X509ExtendedKeyManager
keyManager) {
+ super(new PredefinedKeyManagerFactorySpi(keyManager), new
PredefinedSecurityProvider(), ALGORITHM);
+ }
+
+ private static class PredefinedKeyManagerFactorySpi extends
KeyManagerFactorySpi {
+
+ private final KeyManager[] keyManagers;
+
+ private PredefinedKeyManagerFactorySpi(final X509ExtendedKeyManager
keyManager) {
+ this.keyManagers = new KeyManager[]{keyManager};
+ }
+
+ @Override
+ protected void engineInit(final KeyStore keyStore, final char[]
password) {
+ }
+
+ @Override
+ protected void engineInit(final ManagerFactoryParameters
managerFactoryParameters) {
+ }
+
+ @Override
+ protected KeyManager[] engineGetKeyManagers() {
+ return keyManagers;
+ }
+ }
+}
diff --git
a/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/adapters/PredefinedSecurityProvider.java
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/adapters/PredefinedSecurityProvider.java
new file mode 100644
index 00000000000..8e97d7b3f0f
--- /dev/null
+++
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/adapters/PredefinedSecurityProvider.java
@@ -0,0 +1,37 @@
+/*
+ * 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.processors.mqtt.adapters;
+
+import java.security.Provider;
+
+/**
+ * Placeholder security Provider required by the Key Manager Factory and Trust
Manager Factory constructors.
+ * PredefinedKeyManagerFactory and PredefinedTrustManagerFactory return Key
Managers and Trust Managers that
+ * were already built elsewhere, so no actual provider-specific algorithm
implementation is registered here.
+ */
+class PredefinedSecurityProvider extends Provider {
+
+ private static final String NAME = "PredefinedSecurityProvider";
+
+ private static final String VERSION = "1.0";
+
+ private static final String INFO = "Provider for Key Manager Factory and
Trust Manager Factory implementations backed by predefined Managers";
+
+ PredefinedSecurityProvider() {
+ super(NAME, VERSION, INFO);
+ }
+}
diff --git
a/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/adapters/PredefinedTrustManagerFactory.java
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/adapters/PredefinedTrustManagerFactory.java
new file mode 100644
index 00000000000..7ddbebbc8ad
--- /dev/null
+++
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/adapters/PredefinedTrustManagerFactory.java
@@ -0,0 +1,61 @@
+/*
+ * 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.processors.mqtt.adapters;
+
+import java.security.KeyStore;
+import javax.net.ssl.ManagerFactoryParameters;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.TrustManagerFactorySpi;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * Trust Manager Factory that returns a single Trust Manager provided at
construction time, instead of
+ * loading a Key Store. This bridges client libraries whose SSL configuration
requires a
+ * TrustManagerFactory with SSLContextProvider implementations, which expose
an already-initialized
+ * X.509 Trust Manager directly.
+ */
+public class PredefinedTrustManagerFactory extends TrustManagerFactory {
+
+ private static final String ALGORITHM = "PredefinedTrustManager";
+
+ public PredefinedTrustManagerFactory(final X509TrustManager trustManager) {
+ super(new PredefinedTrustManagerFactorySpi(trustManager), new
PredefinedSecurityProvider(), ALGORITHM);
+ }
+
+ private static class PredefinedTrustManagerFactorySpi extends
TrustManagerFactorySpi {
+
+ private final TrustManager[] trustManagers;
+
+ private PredefinedTrustManagerFactorySpi(final X509TrustManager
trustManager) {
+ this.trustManagers = new TrustManager[]{trustManager};
+ }
+
+ @Override
+ protected void engineInit(final KeyStore keyStore) {
+ }
+
+ @Override
+ protected void engineInit(final ManagerFactoryParameters
managerFactoryParameters) {
+ }
+
+ @Override
+ protected TrustManager[] engineGetTrustManagers() {
+ return trustManagers;
+ }
+ }
+}
diff --git
a/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/common/AbstractMQTTProcessor.java
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/common/AbstractMQTTProcessor.java
index b820fe262a9..b8991f8e665 100644
---
a/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/common/AbstractMQTTProcessor.java
+++
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/common/AbstractMQTTProcessor.java
@@ -35,7 +35,7 @@ import org.apache.nifi.processor.util.StandardValidators;
import org.apache.nifi.security.util.TlsException;
import org.apache.nifi.serialization.RecordReaderFactory;
import org.apache.nifi.serialization.RecordSetWriterFactory;
-import org.apache.nifi.ssl.SSLContextService;
+import org.apache.nifi.ssl.SSLContextProvider;
import java.net.URI;
import java.net.URISyntaxException;
@@ -143,9 +143,9 @@ public abstract class AbstractMQTTProcessor extends
AbstractSessionFactoryProces
public static final PropertyDescriptor PROP_SSL_CONTEXT_SERVICE = new
PropertyDescriptor.Builder()
.name("SSL Context Service")
- .description("The SSL Context Service used to provide client
certificate information for TLS/SSL connections.")
+ .description("The SSL Context Provider used to provide client
certificate information for TLS connections.")
.required(false)
- .identifiesControllerService(SSLContextService.class)
+ .identifiesControllerService(SSLContextProvider.class)
.build();
public static final PropertyDescriptor PROP_LAST_WILL_MESSAGE = new
PropertyDescriptor.Builder()
@@ -387,10 +387,8 @@ public abstract class AbstractMQTTProcessor extends
AbstractSessionFactoryProces
clientProperties.setKeepAliveInterval(context.getProperty(PROP_KEEP_ALIVE_INTERVAL).asInteger());
clientProperties.setConnectionTimeout(context.getProperty(PROP_CONN_TIMEOUT).asInteger());
- final SSLContextService sslContextService =
context.getProperty(PROP_SSL_CONTEXT_SERVICE).asControllerService(SSLContextService.class);
- if (sslContextService != null) {
-
clientProperties.setTlsConfiguration(sslContextService.createTlsConfiguration());
- }
+ final SSLContextProvider sslContextProvider =
context.getProperty(PROP_SSL_CONTEXT_SERVICE).asControllerService(SSLContextProvider.class);
+ clientProperties.setSslContextProvider(sslContextProvider);
if (context.getProperty(PROP_LAST_WILL_MESSAGE).isSet()) {
clientProperties.setLastWillMessage(context.getProperty(PROP_LAST_WILL_MESSAGE).getValue());
diff --git
a/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/common/MqttClientProperties.java
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/common/MqttClientProperties.java
index 5710c9c9a76..5bfe465b3b1 100644
---
a/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/common/MqttClientProperties.java
+++
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/main/java/org/apache/nifi/processors/mqtt/common/MqttClientProperties.java
@@ -16,7 +16,7 @@
*/
package org.apache.nifi.processors.mqtt.common;
-import org.apache.nifi.security.util.TlsConfiguration;
+import org.apache.nifi.ssl.SSLContextProvider;
import java.net.URI;
import java.util.List;
@@ -35,7 +35,7 @@ public class MqttClientProperties {
private boolean cleanSession;
private Long sessionExpiryInterval;
- private TlsConfiguration tlsConfiguration;
+ private SSLContextProvider sslContextProvider;
private String lastWillTopic;
private String lastWillMessage;
@@ -109,12 +109,12 @@ public class MqttClientProperties {
this.sessionExpiryInterval = sessionExpiryInterval;
}
- public TlsConfiguration getTlsConfiguration() {
- return tlsConfiguration;
+ public SSLContextProvider getSslContextProvider() {
+ return sslContextProvider;
}
- public void setTlsConfiguration(TlsConfiguration tlsConfiguration) {
- this.tlsConfiguration = tlsConfiguration;
+ public void setSslContextProvider(SSLContextProvider sslContextProvider) {
+ this.sslContextProvider = sslContextProvider;
}
public String getLastWillTopic() {
diff --git
a/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/test/java/org/apache/nifi/processors/mqtt/ConsumeMQTTTLSTest.java
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/test/java/org/apache/nifi/processors/mqtt/ConsumeMQTTTLSTest.java
new file mode 100644
index 00000000000..094d658d50b
--- /dev/null
+++
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/test/java/org/apache/nifi/processors/mqtt/ConsumeMQTTTLSTest.java
@@ -0,0 +1,275 @@
+/*
+ * 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.processors.mqtt;
+
+import com.hivemq.embedded.EmbeddedHiveMQ;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.security.cert.builder.StandardCertificateBuilder;
+import org.apache.nifi.security.ssl.EphemeralKeyStoreBuilder;
+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.ssl.SSLContextProvider;
+import org.apache.nifi.util.MockFlowFile;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.eclipse.paho.client.mqttv3.MqttClient;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509TrustManager;
+import javax.security.auth.x500.X500Principal;
+
+import static
org.apache.nifi.processors.mqtt.common.MqttConstants.ALLOWABLE_VALUE_MQTT_VERSION_311;
+import static
org.apache.nifi.processors.mqtt.common.MqttConstants.ALLOWABLE_VALUE_MQTT_VERSION_500;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class ConsumeMQTTTLSTest {
+
+ private static final String LOCALHOST = "localhost";
+
+ private static final String KEY_ALGORITHM = "RSA";
+
+ private static final X500Principal CERTIFICATE_PRINCIPAL = new
X500Principal("CN=%s".formatted(LOCALHOST));
+
+ private static final char[] BROKER_KEY_STORE_PASSWORD =
UUID.randomUUID().toString().toCharArray();
+
+ private static final char[] CLIENT_KEY_PASSWORD =
UUID.randomUUID().toString().toCharArray();
+
+ private static final Duration CERTIFICATE_VALIDITY = Duration.ofHours(1);
+
+ private static final long BROKER_LIFECYCLE_TIMEOUT_SECONDS = 30;
+
+ private static final long MESSAGE_TIMEOUT_SECONDS = 15;
+
+ private static final long POLL_INTERVAL_MILLIS = 100;
+
+ private static final String INTERNAL_QUEUE_SIZE = "100";
+
+ private static final String CONNECTION_TIMEOUT_SECONDS = "10";
+
+ private static final String TOPIC_MQTT_3 = "mqtt-3";
+
+ private static final String TOPIC_MQTT_5 = "mqtt-5";
+
+ @TempDir
+ private static Path tempDir;
+
+ private static EmbeddedHiveMQ embeddedHiveMQ;
+
+ private static int tlsPort;
+
+ private static SSLContext clientSslContext;
+
+ private static X509TrustManager clientTrustManager;
+
+ private static X509ExtendedKeyManager clientKeyManager;
+
+ @BeforeAll
+ static void startBroker() throws Exception {
+ tlsPort = getAvailablePort();
+
+ final KeyPair keyPair =
KeyPairGenerator.getInstance(KEY_ALGORITHM).generateKeyPair();
+ final X509Certificate certificate = new
StandardCertificateBuilder(keyPair, CERTIFICATE_PRINCIPAL,
CERTIFICATE_VALIDITY).build();
+
+ final KeyStore ephemeralKeyStore = new EphemeralKeyStoreBuilder()
+ .addPrivateKeyEntry(new
KeyStore.PrivateKeyEntry(keyPair.getPrivate(), new Certificate[]{certificate}))
+ .keyPassword(CLIENT_KEY_PASSWORD)
+ .build();
+ clientSslContext = new StandardSslContextBuilder()
+ .trustStore(ephemeralKeyStore)
+ .keyStore(ephemeralKeyStore)
+ .keyPassword(CLIENT_KEY_PASSWORD)
+ .build();
+ clientTrustManager = new
StandardTrustManagerBuilder().trustStore(ephemeralKeyStore).build();
+ clientKeyManager = new
StandardKeyManagerBuilder().keyStore(ephemeralKeyStore).keyPassword(CLIENT_KEY_PASSWORD).build();
+
+ final Path brokerKeyStorePath = writeBrokerKeyStore(keyPair,
certificate);
+ final Path configFolder =
Files.createDirectories(tempDir.resolve("conf"));
+ final Path dataFolder =
Files.createDirectories(tempDir.resolve("data"));
+ final Path extensionsFolder =
Files.createDirectories(tempDir.resolve("extensions"));
+ Files.writeString(configFolder.resolve("config.xml"),
getBrokerConfiguration(brokerKeyStorePath));
+
+ embeddedHiveMQ = EmbeddedHiveMQ.builder()
+ .withConfigurationFolder(configFolder)
+ .withDataFolder(dataFolder)
+ .withExtensionsFolder(extensionsFolder)
+ .withoutLoggingBootstrap()
+ .build();
+ embeddedHiveMQ.start().get(BROKER_LIFECYCLE_TIMEOUT_SECONDS,
TimeUnit.SECONDS);
+ }
+
+ @AfterAll
+ static void stopBroker() throws Exception {
+ if (embeddedHiveMQ != null) {
+ embeddedHiveMQ.stop().get(BROKER_LIFECYCLE_TIMEOUT_SECONDS,
TimeUnit.SECONDS);
+ embeddedHiveMQ.close();
+ }
+ }
+
+ @Test
+ void testConsumeOverTlsUsingMqttVersion3() throws Exception {
+ final TestRunner testRunner = createTestRunner(TOPIC_MQTT_3,
ALLOWABLE_VALUE_MQTT_VERSION_311.getValue(), "ConsumeMQTTTLSTest-3");
+
+ assertConsumesPublishedMessageOverTls(testRunner, TOPIC_MQTT_3);
+ }
+
+ @Test
+ void testConsumeOverTlsUsingMqttVersion5() throws Exception {
+ final TestRunner testRunner = createTestRunner(TOPIC_MQTT_5,
ALLOWABLE_VALUE_MQTT_VERSION_500.getValue(), "ConsumeMQTTTLSTest-5");
+
+ assertConsumesPublishedMessageOverTls(testRunner, TOPIC_MQTT_5);
+ }
+
+ private void assertConsumesPublishedMessageOverTls(final TestRunner
testRunner, final String topic) throws Exception {
+ // Establish the TLS connection and subscription
+ testRunner.run(1, false, true);
+
+ final String payload = "message-%s".formatted(topic);
+ publishMessageOverTls(topic, payload);
+
+ final List<MockFlowFile> flowFiles = awaitFlowFiles(testRunner);
+ assertEquals(1, flowFiles.size());
+ flowFiles.getFirst().assertContentEquals(payload);
+ }
+
+ private List<MockFlowFile> awaitFlowFiles(final TestRunner testRunner)
throws InterruptedException {
+ final long deadline = System.currentTimeMillis() +
TimeUnit.SECONDS.toMillis(MESSAGE_TIMEOUT_SECONDS);
+ List<MockFlowFile> flowFiles;
+ do {
+ testRunner.run(1, false, false);
+ flowFiles =
testRunner.getFlowFilesForRelationship(ConsumeMQTT.REL_MESSAGE);
+ if (!flowFiles.isEmpty()) {
+ return flowFiles;
+ }
+
+ TimeUnit.MILLISECONDS.sleep(POLL_INTERVAL_MILLIS);
+ } while (System.currentTimeMillis() < deadline);
+ return flowFiles;
+ }
+
+ private TestRunner createTestRunner(final String topic, final String
mqttVersion, final String clientId) throws InitializationException {
+ final TestRunner testRunner =
TestRunners.newTestRunner(ConsumeMQTT.class);
+ testRunner.setProperty(ConsumeMQTT.PROP_BROKER_URI,
"ssl://%s:%d".formatted(LOCALHOST, tlsPort));
+ testRunner.setProperty(ConsumeMQTT.PROP_CLIENTID, clientId);
+ testRunner.setProperty(ConsumeMQTT.PROP_TOPIC_FILTER, topic);
+ testRunner.setProperty(ConsumeMQTT.PROP_MAX_QUEUE_SIZE,
INTERNAL_QUEUE_SIZE);
+ testRunner.setProperty(ConsumeMQTT.PROP_CONN_TIMEOUT,
CONNECTION_TIMEOUT_SECONDS);
+ testRunner.setProperty(ConsumeMQTT.PROP_MQTT_VERSION, mqttVersion);
+
+ final String sslContextServiceIdentifier =
"ssl-context-provider-%s".formatted(clientId);
+ final SSLContextProvider sslContextProvider =
mock(SSLContextProvider.class);
+
when(sslContextProvider.getIdentifier()).thenReturn(sslContextServiceIdentifier);
+ when(sslContextProvider.createContext()).thenReturn(clientSslContext);
+
when(sslContextProvider.createTrustManager()).thenReturn(clientTrustManager);
+
when(sslContextProvider.createKeyManager()).thenReturn(Optional.of(clientKeyManager));
+
+ testRunner.addControllerService(sslContextServiceIdentifier,
sslContextProvider);
+ testRunner.enableControllerService(sslContextProvider);
+ testRunner.setProperty(ConsumeMQTT.PROP_SSL_CONTEXT_SERVICE,
sslContextServiceIdentifier);
+
+ testRunner.assertValid();
+ return testRunner;
+ }
+
+ private void publishMessageOverTls(final String topic, final String
payload) throws Exception {
+ final MqttConnectOptions connectOptions = new MqttConnectOptions();
+ connectOptions.setSocketFactory(clientSslContext.getSocketFactory());
+
+ final MqttClient publisher = new MqttClient(
+ "ssl://%s:%d".formatted(LOCALHOST, tlsPort),
"ConsumeMQTTTLSTest-Publisher-%s".formatted(topic), new MemoryPersistence());
+ try {
+ publisher.connect(connectOptions);
+ final MqttMessage message = new
MqttMessage(payload.getBytes(StandardCharsets.UTF_8));
+ message.setQos(1);
+ publisher.publish(topic, message);
+ } finally {
+ publisher.disconnect();
+ publisher.close();
+ }
+ }
+
+ private static Path writeBrokerKeyStore(final KeyPair keyPair, final
X509Certificate certificate) throws Exception {
+ final KeyStore brokerKeyStore =
KeyStore.getInstance(KeyStore.getDefaultType());
+ brokerKeyStore.load(null);
+ brokerKeyStore.setKeyEntry("broker", keyPair.getPrivate(),
BROKER_KEY_STORE_PASSWORD, new Certificate[]{certificate});
+
+ final Path keyStorePath = tempDir.resolve("broker-keystore");
+ try (OutputStream outputStream = Files.newOutputStream(keyStorePath)) {
+ brokerKeyStore.store(outputStream, BROKER_KEY_STORE_PASSWORD);
+ }
+
+ return keyStorePath;
+ }
+
+ private static String getBrokerConfiguration(final Path keyStorePath) {
+ final String keyStorePassword = new String(BROKER_KEY_STORE_PASSWORD);
+ return """
+ <?xml version="1.0"?>
+ <hivemq>
+ <listeners>
+ <tls-tcp-listener>
+ <port>%d</port>
+ <bind-address>0.0.0.0</bind-address>
+ <tls>
+ <keystore>
+ <path>%s</path>
+ <password>%s</password>
+
<private-key-password>%s</private-key-password>
+ </keystore>
+ <truststore>
+ <path>%s</path>
+ <password>%s</password>
+ </truststore>
+
<client-authentication-mode>REQUIRED</client-authentication-mode>
+ </tls>
+ </tls-tcp-listener>
+ </listeners>
+ </hivemq>
+ """.formatted(tlsPort, keyStorePath, keyStorePassword,
keyStorePassword, keyStorePath, keyStorePassword);
+ }
+
+ private static int getAvailablePort() throws Exception {
+ try (ServerSocket serverSocket = new ServerSocket(0)) {
+ return serverSocket.getLocalPort();
+ }
+ }
+}
diff --git
a/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/test/java/org/apache/nifi/processors/mqtt/TestConsumeMQTT.java
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/test/java/org/apache/nifi/processors/mqtt/TestConsumeMQTT.java
index 0654cc69c02..ce3eed059e0 100644
---
a/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/test/java/org/apache/nifi/processors/mqtt/TestConsumeMQTT.java
+++
b/nifi-extension-bundles/nifi-mqtt-bundle/nifi-mqtt-processors/src/test/java/org/apache/nifi/processors/mqtt/TestConsumeMQTT.java
@@ -27,7 +27,7 @@ import
org.apache.nifi.processors.mqtt.common.StandardMqttMessage;
import org.apache.nifi.provenance.ProvenanceEventRecord;
import org.apache.nifi.provenance.ProvenanceEventType;
import org.apache.nifi.reporting.InitializationException;
-import org.apache.nifi.ssl.SSLContextService;
+import org.apache.nifi.ssl.SSLContextProvider;
import org.apache.nifi.util.MockFlowFile;
import org.apache.nifi.util.TestRunner;
import org.apache.nifi.util.TestRunners;
@@ -831,12 +831,12 @@ public class TestConsumeMQTT {
}
private static String addSSLContextService(TestRunner testRunner) throws
InitializationException {
- final SSLContextService sslContextService =
mock(SSLContextService.class);
- final String identifier = SSLContextService.class.getSimpleName();
- when(sslContextService.getIdentifier()).thenReturn(identifier);
+ final SSLContextProvider sslContextProvider =
mock(SSLContextProvider.class);
+ final String identifier = SSLContextProvider.class.getSimpleName();
+ when(sslContextProvider.getIdentifier()).thenReturn(identifier);
- testRunner.addControllerService(identifier, sslContextService);
- testRunner.enableControllerService(sslContextService);
+ testRunner.addControllerService(identifier, sslContextProvider);
+ testRunner.enableControllerService(sslContextProvider);
return identifier;
}
}