diff --git a/nifi-assembly/pom.xml b/nifi-assembly/pom.xml
index 3b932c882e..5e559a5935 100644
--- a/nifi-assembly/pom.xml
+++ b/nifi-assembly/pom.xml
@@ -227,6 +227,12 @@ language governing permissions and limitations under the License. -->
1.19.0-SNAPSHOT
nar
+
+ org.apache.nifi
+ nifi-key-service-nar
+ 1.19.0-SNAPSHOT
+ nar
+
org.apache.nifi
nifi-distributed-cache-services-nar
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-key-service-api/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-api/pom.xml
new file mode 100644
index 0000000000..5dca67fe03
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-api/pom.xml
@@ -0,0 +1,29 @@
+
+
+ 4.0.0
+
+ org.apache.nifi
+ nifi-standard-services
+ 1.19.0-SNAPSHOT
+
+ nifi-key-service-api
+
+
+ org.apache.nifi
+ nifi-api
+
+
+
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-key-service-api/src/main/java/org/apache/nifi/key/service/api/PrivateKeyService.java b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-api/src/main/java/org/apache/nifi/key/service/api/PrivateKeyService.java
new file mode 100644
index 0000000000..756a573b4d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-api/src/main/java/org/apache/nifi/key/service/api/PrivateKeyService.java
@@ -0,0 +1,33 @@
+/*
+ * 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.key.service.api;
+
+import org.apache.nifi.controller.ControllerService;
+
+import java.security.PrivateKey;
+
+/**
+ * Controller Service abstracting access to Private Keys
+ */
+public interface PrivateKeyService extends ControllerService {
+ /**
+ * Get Private Key
+ *
+ * @return Private Key
+ */
+ PrivateKey getPrivateKey();
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service-nar/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service-nar/pom.xml
new file mode 100644
index 0000000000..40b83b478f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service-nar/pom.xml
@@ -0,0 +1,37 @@
+
+
+ 4.0.0
+
+ org.apache.nifi
+ nifi-key-service-bundle
+ 1.19.0-SNAPSHOT
+
+ nifi-key-service-nar
+ nar
+
+
+ org.apache.nifi
+ nifi-standard-services-api-nar
+ 1.19.0-SNAPSHOT
+ nar
+
+
+ org.apache.nifi
+ nifi-key-service
+ 1.19.0-SNAPSHOT
+
+
+
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/pom.xml
new file mode 100644
index 0000000000..7da2104334
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/pom.xml
@@ -0,0 +1,51 @@
+
+
+ 4.0.0
+
+ org.apache.nifi
+ nifi-key-service-bundle
+ 1.19.0-SNAPSHOT
+
+ nifi-key-service
+ jar
+
+
+ org.apache.nifi
+ nifi-key-service-api
+ 1.19.0-SNAPSHOT
+ provided
+
+
+ org.apache.nifi
+ nifi-api
+
+
+ org.apache.nifi
+ nifi-utils
+ 1.19.0-SNAPSHOT
+
+
+ org.bouncycastle
+ bcpkix-jdk15on
+
+
+ org.apache.nifi
+ nifi-mock
+ 1.19.0-SNAPSHOT
+ test
+
+
+
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/StandardPrivateKeyService.java b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/StandardPrivateKeyService.java
new file mode 100644
index 0000000000..b1a1ead53a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/StandardPrivateKeyService.java
@@ -0,0 +1,217 @@
+/*
+ * 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.key.service;
+
+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.PropertyDescriptor;
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.components.ValidationContext;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.resource.ResourceCardinality;
+import org.apache.nifi.components.resource.ResourceType;
+import org.apache.nifi.context.PropertyContext;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.key.service.api.PrivateKeyService;
+import org.apache.nifi.key.service.reader.BouncyCastlePrivateKeyReader;
+import org.apache.nifi.key.service.reader.PrivateKeyReader;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.reporting.InitializationException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.PrivateKey;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Standard implementation of Private Key Service supporting encrypted or unencrypted sources
+ */
+@Tags({"PEM", "PKCS8"})
+@CapabilityDescription("Private Key Service provides access to a Private Key loaded from configured sources")
+public class StandardPrivateKeyService extends AbstractControllerService implements PrivateKeyService {
+ public static final PropertyDescriptor KEY_FILE = new PropertyDescriptor.Builder()
+ .name("key-file")
+ .displayName("Key File")
+ .description("File path to Private Key structured using PKCS8 and encoded as PEM")
+ .required(false)
+ .identifiesExternalResource(ResourceCardinality.SINGLE, ResourceType.FILE)
+ .build();
+
+ public static final PropertyDescriptor KEY = new PropertyDescriptor.Builder()
+ .name("key")
+ .displayName("Key")
+ .description("Private Key structured using PKCS8 and encoded as PEM")
+ .required(false)
+ .sensitive(true)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .build();
+
+ public static final PropertyDescriptor KEY_PASSWORD = new PropertyDescriptor.Builder()
+ .name("key-password")
+ .displayName("Key Password")
+ .description("Password used for decrypting Private Keys")
+ .required(false)
+ .sensitive(true)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .build();
+
+ private static final List DESCRIPTORS = Arrays.asList(
+ KEY_FILE,
+ KEY,
+ KEY_PASSWORD
+ );
+
+ private static final Charset KEY_CHARACTER_SET = StandardCharsets.US_ASCII;
+
+ private static final PrivateKeyReader PRIVATE_KEY_READER = new BouncyCastlePrivateKeyReader();
+
+ private final AtomicReference keyReference = new AtomicReference<>();
+
+ @Override
+ public PrivateKey getPrivateKey() {
+ return keyReference.get();
+ }
+
+ /**
+ * On Property Modified clears the current Private Key reference to require a new read for validation
+ *
+ * @param propertyDescriptor the descriptor for the property being modified
+ * @param oldValue the value that was previously set, or null if no value
+ * was previously set for this property
+ * @param newValue the new property value or if null indicates the property
+ * was removed
+ */
+ @Override
+ public void onPropertyModified(final PropertyDescriptor propertyDescriptor, final String oldValue, final String newValue) {
+ keyReference.set(null);
+ }
+
+ /**
+ * On Enabled reads Private Keys using configured properties
+ *
+ * @param context Configuration Context with properties
+ * @throws InitializationException Thrown when unable to load
+ */
+ @OnEnabled
+ public void onEnabled(final ConfigurationContext context) throws InitializationException {
+ try {
+ final PrivateKey readKey = readKey(context);
+ keyReference.set(readKey);
+ } catch (final RuntimeException e) {
+ throw new InitializationException("Reading Private Key Failed", e);
+ }
+ }
+
+ /**
+ * On Disabled clears Private Keys
+ */
+ @OnDisabled
+ public void onDisabled() {
+ keyReference.set(null);
+ }
+
+ /**
+ * Get Supported Property Descriptors
+ *
+ * @return Supported Property Descriptors
+ */
+ @Override
+ protected List getSupportedPropertyDescriptors() {
+ return DESCRIPTORS;
+ }
+
+ /**
+ * Custom Validate reads key using configured password for encrypted keys
+ *
+ * @param context Validation Context
+ * @return Validation Results
+ */
+ @Override
+ protected Collection customValidate(final ValidationContext context) {
+ final Collection results = new ArrayList<>();
+
+ final PropertyValue keyFileProperty = context.getProperty(KEY_FILE);
+ final PropertyValue keyProperty = context.getProperty(KEY);
+ if (keyFileProperty.isSet() && keyProperty.isSet()) {
+ final String explanation = String.format("Both [%s] and [%s] properties configured", KEY_FILE.getDisplayName(), KEY.getDisplayName());
+ final ValidationResult result = new ValidationResult.Builder()
+ .valid(false)
+ .subject(KEY.getDisplayName())
+ .explanation(explanation)
+ .build();
+ results.add(result);
+ } else if (keyReference.get() == null) {
+ try {
+ final PrivateKey readKey = readKey(context);
+ keyReference.set(readKey);
+ } catch (final RuntimeException e) {
+ final ValidationResult result = new ValidationResult.Builder()
+ .valid(false)
+ .subject(KEY.getDisplayName())
+ .explanation(e.getMessage())
+ .build();
+ results.add(result);
+ }
+ }
+
+ return results;
+ }
+
+ private PrivateKey readKey(final PropertyContext context) {
+ final PrivateKey readKey;
+
+ final char[] keyPassword = getKeyPassword(context);
+
+ final PropertyValue keyFileProperty = context.getProperty(KEY_FILE);
+ final PropertyValue keyProperty = context.getProperty(KEY);
+
+ if (keyFileProperty.isSet()) {
+ try (final InputStream inputStream = keyFileProperty.asResource().read()) {
+ readKey = PRIVATE_KEY_READER.readPrivateKey(inputStream, keyPassword);
+ } catch (final IOException e) {
+ throw new UncheckedIOException("Read Private Key File failed", e);
+ }
+ } else if (keyProperty.isSet()) {
+ final byte[] key = keyProperty.getValue().getBytes(KEY_CHARACTER_SET);
+ try (final InputStream inputStream = new ByteArrayInputStream(key)) {
+ readKey = PRIVATE_KEY_READER.readPrivateKey(inputStream, keyPassword);
+ } catch (final IOException e) {
+ throw new UncheckedIOException("Read Private Key failed", e);
+ }
+ } else {
+ throw new IllegalStateException("Private Key not configured");
+ }
+
+ return readKey;
+ }
+
+ private char[] getKeyPassword(final PropertyContext context) {
+ final PropertyValue keyPasswordProperty = context.getProperty(KEY_PASSWORD);
+ return keyPasswordProperty.isSet() ? keyPasswordProperty.getValue().toCharArray() : new char[]{};
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/BouncyCastlePrivateKeyReader.java b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/BouncyCastlePrivateKeyReader.java
new file mode 100644
index 0000000000..ff8746215d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/BouncyCastlePrivateKeyReader.java
@@ -0,0 +1,111 @@
+/*
+ * 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.key.service.reader;
+
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.openssl.PEMDecryptorProvider;
+import org.bouncycastle.openssl.PEMEncryptedKeyPair;
+import org.bouncycastle.openssl.PEMException;
+import org.bouncycastle.openssl.PEMKeyPair;
+import org.bouncycastle.openssl.PEMParser;
+import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
+import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
+import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
+import org.bouncycastle.operator.InputDecryptorProvider;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
+import org.bouncycastle.pkcs.PKCSException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UncheckedIOException;
+import java.security.PrivateKey;
+
+/**
+ * Bouncy Castle implementation of Private Key Reader supporting PEM files
+ */
+public class BouncyCastlePrivateKeyReader implements PrivateKeyReader {
+ private static final String INVALID_PEM = "Invalid PEM";
+
+ /**
+ * Read Private Key using Bouncy Castle PEM Parser
+ *
+ * @param inputStream Key stream
+ * @param keyPassword Password
+ * @return Private Key
+ */
+ @Override
+ public PrivateKey readPrivateKey(final InputStream inputStream, final char[] keyPassword) {
+ try (final PEMParser parser = new PEMParser(new InputStreamReader(inputStream))) {
+ final Object object = parser.readObject();
+
+ final PrivateKeyInfo privateKeyInfo;
+
+ if (object instanceof PrivateKeyInfo) {
+ privateKeyInfo = (PrivateKeyInfo) object;
+ } else if (object instanceof PKCS8EncryptedPrivateKeyInfo) {
+ final PKCS8EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = (PKCS8EncryptedPrivateKeyInfo) object;
+ privateKeyInfo = readEncryptedPrivateKey(encryptedPrivateKeyInfo, keyPassword);
+ } else if (object instanceof PEMKeyPair) {
+ final PEMKeyPair pemKeyPair = (PEMKeyPair) object;
+ privateKeyInfo = pemKeyPair.getPrivateKeyInfo();
+ } else if (object instanceof PEMEncryptedKeyPair) {
+ final PEMEncryptedKeyPair encryptedKeyPair = (PEMEncryptedKeyPair) object;
+ privateKeyInfo = readEncryptedPrivateKey(encryptedKeyPair, keyPassword);
+ } else {
+ final String objectType = object == null ? INVALID_PEM : object.getClass().getName();
+ final String message = String.format("Private Key [%s] not supported", objectType);
+ throw new IllegalArgumentException(message);
+ }
+
+ return convertPrivateKey(privateKeyInfo);
+ } catch (final IOException e) {
+ throw new UncheckedIOException("Read Private Key stream failed", e);
+ }
+ }
+
+ private PrivateKeyInfo readEncryptedPrivateKey(final PKCS8EncryptedPrivateKeyInfo encryptedPrivateKeyInfo, final char[] keyPassword) {
+ try {
+ final InputDecryptorProvider provider = new JceOpenSSLPKCS8DecryptorProviderBuilder().build(keyPassword);
+ return encryptedPrivateKeyInfo.decryptPrivateKeyInfo(provider);
+ } catch (final OperatorCreationException e) {
+ throw new PrivateKeyException("Preparing Private Key Decryption failed", e);
+ } catch (final PKCSException e) {
+ throw new PrivateKeyException("Decrypting Private Key failed", e);
+ }
+ }
+
+ private PrivateKeyInfo readEncryptedPrivateKey(final PEMEncryptedKeyPair encryptedKeyPair, final char[] keyPassword) {
+ final PEMDecryptorProvider provider = new JcePEMDecryptorProviderBuilder().build(keyPassword);
+ try {
+ final PEMKeyPair pemKeyPair = encryptedKeyPair.decryptKeyPair(provider);
+ return pemKeyPair.getPrivateKeyInfo();
+ } catch (final IOException e) {
+ throw new PrivateKeyException("Decrypting Private Key Pair failed", e);
+ }
+ }
+
+ private PrivateKey convertPrivateKey(final PrivateKeyInfo privateKeyInfo) {
+ final JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
+ try {
+ return converter.getPrivateKey(privateKeyInfo);
+ } catch (final PEMException e) {
+ throw new PrivateKeyException("Convert Private Key failed", e);
+ }
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/PrivateKeyException.java b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/PrivateKeyException.java
new file mode 100644
index 0000000000..01d6964d43
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/PrivateKeyException.java
@@ -0,0 +1,28 @@
+/*
+ * 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.key.service.reader;
+
+/**
+ * Private Key Exception
+ */
+public class PrivateKeyException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public PrivateKeyException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/PrivateKeyReader.java b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/PrivateKeyReader.java
new file mode 100644
index 0000000000..c59afb75e8
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/PrivateKeyReader.java
@@ -0,0 +1,34 @@
+/*
+ * 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.key.service.reader;
+
+import java.io.InputStream;
+import java.security.PrivateKey;
+
+/**
+ * Private Key Reader
+ */
+public interface PrivateKeyReader {
+ /**
+ * Read Private Key from stream with optional password for encrypted keys
+ *
+ * @param inputStream Key stream
+ * @param keyPassword Password
+ * @return Private Key
+ */
+ PrivateKey readPrivateKey(InputStream inputStream, char[] keyPassword);
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
new file mode 100644
index 0000000000..6e0289b706
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
@@ -0,0 +1,15 @@
+# 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.
+org.apache.nifi.key.service.StandardPrivateKeyService
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/test/java/org/apache/nifi/key/service/StandardPrivateKeyServiceTest.java b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/test/java/org/apache/nifi/key/service/StandardPrivateKeyServiceTest.java
new file mode 100644
index 0000000000..a00ca4c540
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/test/java/org/apache/nifi/key/service/StandardPrivateKeyServiceTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.key.service;
+
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.util.NoOpProcessor;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+
+import org.bouncycastle.openssl.PKCS8Generator;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
+import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.OutputEncryptor;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.StringWriter;
+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.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class StandardPrivateKeyServiceTest {
+ private static final String SERVICE_ID = StandardPrivateKeyServiceTest.class.getSimpleName();
+
+ private static final String PATH_NOT_FOUND = "/path/not/found";
+
+ private static final String KEY_NOT_VALID = "-----BEGIN KEY NOT VALID-----";
+
+ private static final String RSA_ALGORITHM = "RSA";
+
+ private static final OutputEncryptor DISABLED_ENCRYPTOR = null;
+
+ private static PrivateKey generatedPrivateKey;
+
+ StandardPrivateKeyService service;
+
+ TestRunner runner;
+
+ @BeforeAll
+ static void setPrivateKey() throws NoSuchAlgorithmException {
+ final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM);
+ final KeyPair keyPair = keyPairGenerator.generateKeyPair();
+ generatedPrivateKey = keyPair.getPrivate();
+ }
+
+ @BeforeEach
+ void setService() throws InitializationException {
+ runner = TestRunners.newTestRunner(NoOpProcessor.class);
+ service = new StandardPrivateKeyService();
+ runner.addControllerService(SERVICE_ID, service);
+ }
+
+ @Test
+ void testMissingRequiredProperties() {
+ runner.assertNotValid(service);
+ }
+
+ @Test
+ void testKeyFileNotFound() {
+ runner.setProperty(StandardPrivateKeyService.KEY_FILE, PATH_NOT_FOUND);
+ runner.assertNotValid();
+ }
+
+ @Test
+ void testKeyNotValid() {
+ runner.setProperty(StandardPrivateKeyService.KEY, KEY_NOT_VALID);
+ runner.assertNotValid();
+ }
+
+ @Test
+ void testGetPrivateKeyEncodedKey() throws Exception {
+ final String encodedPrivateKey = getEncodedPrivateKey(generatedPrivateKey, DISABLED_ENCRYPTOR);
+
+ runner.setProperty(service, StandardPrivateKeyService.KEY, encodedPrivateKey);
+ runner.enableControllerService(service);
+
+ final PrivateKey privateKey = service.getPrivateKey();
+ assertEquals(generatedPrivateKey, privateKey);
+ }
+
+ @Test
+ void testGetPrivateKeyEncryptedKey() throws Exception {
+ final String password = UUID.randomUUID().toString();
+ final OutputEncryptor outputEncryptor = getOutputEncryptor(password);
+ final String encryptedPrivateKey = getEncodedPrivateKey(generatedPrivateKey, outputEncryptor);
+ final Path keyPath = writeKey(encryptedPrivateKey);
+
+ runner.setProperty(service, StandardPrivateKeyService.KEY_FILE, keyPath.toString());
+ runner.setProperty(service, StandardPrivateKeyService.KEY_PASSWORD, password);
+ runner.enableControllerService(service);
+
+ final PrivateKey privateKey = service.getPrivateKey();
+ assertEquals(generatedPrivateKey, privateKey);
+ }
+
+ private Path writeKey(final String encodedPrivateKey) throws IOException {
+ final Path keyPath = Files.createTempFile(StandardPrivateKeyServiceTest.class.getSimpleName(), RSA_ALGORITHM);
+ keyPath.toFile().deleteOnExit();
+
+ final byte[] keyBytes = encodedPrivateKey.getBytes(StandardCharsets.UTF_8);
+
+ Files.write(keyPath, keyBytes);
+ return keyPath;
+ }
+
+ private String getEncodedPrivateKey(final PrivateKey privateKey, final OutputEncryptor outputEncryptor) throws Exception {
+ final StringWriter stringWriter = new StringWriter();
+ try (final JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) {
+ final JcaPKCS8Generator generator = new JcaPKCS8Generator(privateKey, outputEncryptor);
+ pemWriter.writeObject(generator);
+ }
+ return stringWriter.toString();
+ }
+
+ private OutputEncryptor getOutputEncryptor(final String password) throws OperatorCreationException {
+ return new JceOpenSSLPKCS8EncryptorBuilder(PKCS8Generator.PBE_SHA1_3DES)
+ .setPassword(password.toCharArray())
+ .build();
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/pom.xml
new file mode 100644
index 0000000000..0bf550abe5
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-key-service-bundle/pom.xml
@@ -0,0 +1,28 @@
+
+
+ 4.0.0
+
+ org.apache.nifi
+ nifi-standard-services
+ 1.19.0-SNAPSHOT
+
+ nifi-key-service-bundle
+ pom
+
+ nifi-key-service
+ nifi-key-service-nar
+
+
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml
index 2aa5d6d2b1..0f63b8f056 100644
--- a/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml
@@ -133,5 +133,11 @@
1.19.0-SNAPSHOT
compile
+
+ org.apache.nifi
+ nifi-key-service-api
+ 1.19.0-SNAPSHOT
+ compile
+
diff --git a/nifi-nar-bundles/nifi-standard-services/pom.xml b/nifi-nar-bundles/nifi-standard-services/pom.xml
index d5100cef9e..6c8b4d47d1 100644
--- a/nifi-nar-bundles/nifi-standard-services/pom.xml
+++ b/nifi-nar-bundles/nifi-standard-services/pom.xml
@@ -48,6 +48,8 @@
nifi-kerberos-credentials-service-bundle
nifi-proxy-configuration-api
nifi-proxy-configuration-bundle
+ nifi-key-service-api
+ nifi-key-service-bundle
nifi-rules-engine-service-api
nifi-record-sink-api
nifi-record-sink-service-bundle