diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml
index 231e8eb968d9..e583d6895a12 100644
--- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml
@@ -62,6 +62,11 @@
org.apache.nifi
nifi-lookup-service-api
+
+ org.apache.nifi
+ nifi-pki-service-api
+ 1.13.0-SNAPSHOT
+
org.apache.nifi
nifi-flowfile-packager
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/AbstractCryptographicProcessor.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/AbstractCryptographicProcessor.java
new file mode 100644
index 000000000000..d753c9d749eb
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/AbstractCryptographicProcessor.java
@@ -0,0 +1,65 @@
+/*
+ * 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.standard.crypto;
+
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processors.standard.crypto.algorithm.CryptographicAlgorithm;
+import org.apache.nifi.processors.standard.crypto.attributes.CryptographicAttributeKey;
+import org.apache.nifi.processors.standard.crypto.attributes.CryptographicMethod;
+import org.apache.nifi.security.util.crypto.CipherUtility;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+import java.security.Security;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Abstract Cryptographic Processor for methods shared across implementations
+ */
+public abstract class AbstractCryptographicProcessor extends AbstractProcessor {
+ static {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ /**
+ * Get Cryptographic Flow File Attributes based on resolved Cryptographic Algorithm
+ *
+ * @param algorithm Cryptographic Algorithm
+ * @return Flow File Attributes
+ */
+ protected Map getCryptographicAttributes(final CryptographicAlgorithm algorithm) {
+ final Map attributes = new HashMap<>();
+
+ attributes.put(CryptographicAttributeKey.ALGORITHM.key(), algorithm.toString());
+ attributes.put(CryptographicAttributeKey.ALGORITHM_CIPHER.key(), algorithm.getCipher().getLabel());
+ attributes.put(CryptographicAttributeKey.ALGORITHM_KEY_SIZE.key(), Integer.toString(algorithm.getKeySize()));
+ attributes.put(CryptographicAttributeKey.ALGORITHM_BLOCK_CIPHER_MODE.key(), algorithm.getBlockCipherMode().getLabel());
+ attributes.put(CryptographicAttributeKey.ALGORITHM_OBJECT_IDENTIFIER.key(), algorithm.getObjectIdentifier());
+
+ attributes.put(CryptographicAttributeKey.METHOD.key(), getCryptographicMethod().toString());
+ attributes.put(CryptographicAttributeKey.PROCESSING_COMPLETED.key(), CipherUtility.getTimestampString());
+ return attributes;
+ }
+
+ /**
+ * Get Cryptographic Method definition for implementing Processors
+ *
+ * @return Cryptographic Method
+ */
+ protected abstract CryptographicMethod getCryptographicMethod();
+}
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/algorithm/BlockCipherMode.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/algorithm/BlockCipherMode.java
new file mode 100644
index 000000000000..e59428d78d02
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/algorithm/BlockCipherMode.java
@@ -0,0 +1,51 @@
+/*
+ * 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.standard.crypto.algorithm;
+
+/**
+ * Cryptographic Block Cipher Mode Cipher enumeration with acronym and description
+ */
+public enum BlockCipherMode {
+ CBC("CBC", "Cipher Block Chaining"),
+
+ CCM("CCM", "Counter with Cipher Block Chaining-Message Authentication Code"),
+
+ CFB("CFB", "Cipher Feedback"),
+
+ ECB("ECB", "Electronic Codebook"),
+
+ GCM("GCM", "Galois Counter Mode"),
+
+ OFB("OFB", "Output Feedback");
+
+ private String label;
+
+ private String description;
+
+ BlockCipherMode(final String label, final String description) {
+ this.label = label;
+ this.description = description;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/algorithm/CryptographicAlgorithm.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/algorithm/CryptographicAlgorithm.java
new file mode 100644
index 000000000000..8b5c2f40477c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/algorithm/CryptographicAlgorithm.java
@@ -0,0 +1,95 @@
+/*
+ * 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.standard.crypto.algorithm;
+
+import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
+import org.bouncycastle.asn1.ntt.NTTObjectIdentifiers;
+import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+
+/**
+ * Cryptographic Algorithm enumerates identified ciphers and modes
+ */
+public enum CryptographicAlgorithm {
+ AES_128_CBC(CryptographicCipher.AES, 128, BlockCipherMode.CBC, NISTObjectIdentifiers.id_aes128_CBC.getId()),
+
+ AES_128_CCM(CryptographicCipher.AES, 128, BlockCipherMode.CCM, NISTObjectIdentifiers.id_aes128_CCM.getId()),
+
+ AES_128_GCM(CryptographicCipher.AES, 128, BlockCipherMode.GCM, NISTObjectIdentifiers.id_aes128_GCM.getId()),
+
+ AES_192_CBC(CryptographicCipher.AES, 192, BlockCipherMode.CBC, NISTObjectIdentifiers.id_aes192_CBC.getId()),
+
+ AES_192_CCM(CryptographicCipher.AES, 192, BlockCipherMode.CCM, NISTObjectIdentifiers.id_aes192_CCM.getId()),
+
+ AES_192_GCM(CryptographicCipher.AES, 192, BlockCipherMode.GCM, NISTObjectIdentifiers.id_aes192_GCM.getId()),
+
+ AES_256_CBC(CryptographicCipher.AES, 256, BlockCipherMode.CBC, NISTObjectIdentifiers.id_aes256_CBC.getId()),
+
+ AES_256_CCM(CryptographicCipher.AES, 256, BlockCipherMode.CCM, NISTObjectIdentifiers.id_aes256_CCM.getId()),
+
+ AES_256_GCM(CryptographicCipher.AES, 256, BlockCipherMode.GCM, NISTObjectIdentifiers.id_aes256_GCM.getId()),
+
+ CAMELLIA_128_CBC(CryptographicCipher.CAMELLIA, 128, BlockCipherMode.CBC, NTTObjectIdentifiers.id_camellia128_cbc.getId()),
+
+ CAMELLIA_192_CBC(CryptographicCipher.CAMELLIA, 192, BlockCipherMode.CBC, NTTObjectIdentifiers.id_camellia192_cbc.getId()),
+
+ CAMELLIA_256_CBC(CryptographicCipher.CAMELLIA, 256, BlockCipherMode.CBC, NTTObjectIdentifiers.id_camellia256_cbc.getId()),
+
+ DES_56_CBC(CryptographicCipher.DES, 56, BlockCipherMode.CBC, OIWObjectIdentifiers.desCBC.getId()),
+
+ RC2_40_CBC(CryptographicCipher.RC2, 40, BlockCipherMode.CBC, PKCSObjectIdentifiers.RC2_CBC.getId()),
+
+ TDEA_168_CBC(CryptographicCipher.TDEA, 168, BlockCipherMode.CBC, PKCSObjectIdentifiers.des_EDE3_CBC.getId());
+
+ private static final String FORMAT = "%s-%d-%s";
+
+ private CryptographicCipher cipher;
+
+ private int keySize;
+
+ private BlockCipherMode blockCipherMode;
+
+ private String objectIdentifier;
+
+ CryptographicAlgorithm(final CryptographicCipher cipher, final int keySize, final BlockCipherMode blockCipherMode, final String objectIdentifier) {
+ this.cipher = cipher;
+ this.keySize = keySize;
+ this.blockCipherMode = blockCipherMode;
+ this.objectIdentifier = objectIdentifier;
+ }
+
+ public CryptographicCipher getCipher() {
+ return cipher;
+ }
+
+ public int getKeySize() {
+ return keySize;
+ }
+
+ public BlockCipherMode getBlockCipherMode() {
+ return blockCipherMode;
+ }
+
+ public String getObjectIdentifier() {
+ return objectIdentifier;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(FORMAT, cipher.getLabel(), keySize, blockCipherMode.getLabel());
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/algorithm/CryptographicAlgorithmResolver.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/algorithm/CryptographicAlgorithmResolver.java
new file mode 100644
index 000000000000..eadb755f41f7
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/algorithm/CryptographicAlgorithmResolver.java
@@ -0,0 +1,32 @@
+/*
+ * 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.standard.crypto.algorithm;
+
+import java.util.Optional;
+
+/**
+ * Cryptographic Algorithm Resolver
+ */
+public interface CryptographicAlgorithmResolver {
+ /**
+ * Find Cryptographic Algorithm based on Object Identifier
+ *
+ * @param objectIdentifier ASN.1 Object Identifier
+ * @return Optional Cryptographic Algorithm
+ */
+ Optional findCryptographicAlgorithm(final String objectIdentifier);
+}
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/algorithm/CryptographicCipher.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/algorithm/CryptographicCipher.java
new file mode 100644
index 000000000000..66d0eabd3e1d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/algorithm/CryptographicCipher.java
@@ -0,0 +1,49 @@
+/*
+ * 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.standard.crypto.algorithm;
+
+/**
+ * Cryptographic Cipher enumeration with acronym and description
+ */
+public enum CryptographicCipher {
+ AES("AES", "Advanced Encryption Standard"),
+
+ CAMELLIA("CAMELLIA", "Camellia"),
+
+ DES("DES", "Data Encryption Standard"),
+
+ RC2("RC2", "Rivest Cipher 2"),
+
+ TDEA("TDEA", "Triple Data Encryption Algorithm");
+
+ private String label;
+
+ private String description;
+
+ CryptographicCipher(final String label, final String description) {
+ this.label = label;
+ this.description = description;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/algorithm/DefaultCryptographicAlgorithmResolver.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/algorithm/DefaultCryptographicAlgorithmResolver.java
new file mode 100644
index 000000000000..d682af5c2164
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/algorithm/DefaultCryptographicAlgorithmResolver.java
@@ -0,0 +1,38 @@
+/*
+ * 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.standard.crypto.algorithm;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+/**
+ * Default implementation of Cryptographic Algorithm Resolver
+ */
+public class DefaultCryptographicAlgorithmResolver implements CryptographicAlgorithmResolver {
+ /**
+ * Find Cryptographic Algorithm based on Object Identifier using Cryptographic Algorithm enumeration
+ *
+ * @param objectIdentifier ASN.1 Object Identifier
+ * @return Cryptographic Algorithm or empty when not found
+ */
+ @Override
+ public Optional findCryptographicAlgorithm(final String objectIdentifier) {
+ return Arrays.stream(CryptographicAlgorithm.values()).filter(
+ method -> method.getObjectIdentifier().equals(objectIdentifier)
+ ).findFirst();
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/attributes/CryptographicAttribute.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/attributes/CryptographicAttribute.java
new file mode 100644
index 000000000000..3366d8d04a74
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/attributes/CryptographicAttribute.java
@@ -0,0 +1,36 @@
+/*
+ * 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.standard.crypto.attributes;
+
+/**
+ * Cryptographic Flow File Attribute for reference in Processor documentation
+ */
+public interface CryptographicAttribute {
+ String ALGORITHM = "cryptographic.algorithm";
+
+ String ALGORITHM_CIPHER = "cryptographic.algorithm.cipher";
+
+ String ALGORITHM_KEY_SIZE = "cryptographic.algorithm.key.size";
+
+ String ALGORITHM_BLOCK_CIPHER_MODE = "cryptographic.algorithm.block.cipher.mode";
+
+ String ALGORITHM_OBJECT_IDENTIFIER = "cryptographic.algorithm.object.identifier";
+
+ String METHOD = "cryptographic.method";
+
+ String PROCESSING_COMPLETED = "cryptographic.processing.completed";
+}
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/attributes/CryptographicAttributeKey.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/attributes/CryptographicAttributeKey.java
new file mode 100644
index 000000000000..d5787236cdb7
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/attributes/CryptographicAttributeKey.java
@@ -0,0 +1,49 @@
+/*
+ * 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.standard.crypto.attributes;
+
+import org.apache.nifi.flowfile.attributes.FlowFileAttributeKey;
+
+/**
+ * Cryptographic Flow File Attribute Keys used across encryption and decryption processors
+ */
+public enum CryptographicAttributeKey implements FlowFileAttributeKey {
+ ALGORITHM(CryptographicAttribute.ALGORITHM),
+
+ ALGORITHM_CIPHER(CryptographicAttribute.ALGORITHM_CIPHER),
+
+ ALGORITHM_KEY_SIZE(CryptographicAttribute.ALGORITHM_KEY_SIZE),
+
+ ALGORITHM_BLOCK_CIPHER_MODE(CryptographicAttribute.ALGORITHM_BLOCK_CIPHER_MODE),
+
+ ALGORITHM_OBJECT_IDENTIFIER(CryptographicAttribute.ALGORITHM_OBJECT_IDENTIFIER),
+
+ METHOD(CryptographicAttribute.METHOD),
+
+ PROCESSING_COMPLETED(CryptographicAttribute.PROCESSING_COMPLETED);
+
+ private final String key;
+
+ CryptographicAttributeKey(final String key) {
+ this.key = key;
+ }
+
+ @Override
+ public String key() {
+ return key;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/attributes/CryptographicMethod.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/attributes/CryptographicMethod.java
new file mode 100644
index 000000000000..0e234be0e280
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/attributes/CryptographicMethod.java
@@ -0,0 +1,24 @@
+/*
+ * 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.standard.crypto.attributes;
+
+/**
+ * Cryptographic Method enumeration referenced in Processors and Flow File attributes
+ */
+public enum CryptographicMethod {
+ CMS
+}
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/cms/AbstractCMSCryptographicProcessor.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/cms/AbstractCMSCryptographicProcessor.java
new file mode 100644
index 000000000000..d2d7dc4fe026
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/cms/AbstractCMSCryptographicProcessor.java
@@ -0,0 +1,35 @@
+/*
+ * 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.standard.crypto.cms;
+
+import org.apache.nifi.processors.standard.crypto.AbstractCryptographicProcessor;
+import org.apache.nifi.processors.standard.crypto.attributes.CryptographicMethod;
+
+/**
+ * Abstract CMS Cryptographic Processor
+ */
+public abstract class AbstractCMSCryptographicProcessor extends AbstractCryptographicProcessor {
+ /**
+ * Get Cryptographic Method
+ *
+ * @return CMS Cryptographic Method
+ */
+ @Override
+ protected CryptographicMethod getCryptographicMethod() {
+ return CryptographicMethod.CMS;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/cms/DecryptCMS.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/cms/DecryptCMS.java
new file mode 100644
index 000000000000..67075f702fbb
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/cms/DecryptCMS.java
@@ -0,0 +1,256 @@
+/*
+ * 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.standard.crypto.cms;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.SideEffectFree;
+import org.apache.nifi.annotation.behavior.WritesAttribute;
+import org.apache.nifi.annotation.behavior.WritesAttributes;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.pki.PrivateKeyService;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.ProcessorInitializationContext;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+
+import org.apache.nifi.processors.standard.crypto.algorithm.CryptographicAlgorithm;
+import org.apache.nifi.processors.standard.crypto.algorithm.CryptographicAlgorithmResolver;
+import org.apache.nifi.processors.standard.crypto.algorithm.DefaultCryptographicAlgorithmResolver;
+import org.apache.nifi.processors.standard.crypto.attributes.CryptographicAttribute;
+import org.apache.nifi.processors.standard.crypto.attributes.CryptographicAttributeKey;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.cms.CMSEnvelopedDataParser;
+import org.bouncycastle.cms.CMSException;
+import org.bouncycastle.cms.CMSTypedStream;
+import org.bouncycastle.cms.KeyTransRecipient;
+import org.bouncycastle.cms.KeyTransRecipientId;
+import org.bouncycastle.cms.RecipientId;
+import org.bouncycastle.cms.RecipientInformation;
+import org.bouncycastle.cms.RecipientInformationStore;
+import org.bouncycastle.cms.jcajce.JceKeyTransEnvelopedRecipient;
+
+import javax.security.auth.x500.X500Principal;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.security.PrivateKey;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Decrypt CMS Processor reads and decrypts contents using private keys matching CMS Recipients
+ */
+@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED)
+@Tags({"Cryptography", "CMS", "PKCS7", "RFC 5652", "AES"})
+@CapabilityDescription("Decrypt content using Cryptographic Message Syntax")
+@SideEffectFree
+@SeeAlso({EncryptCMS.class})
+@WritesAttributes({
+ @WritesAttribute(attribute = CryptographicAttribute.ALGORITHM, description = "Cryptographic Algorithm"),
+ @WritesAttribute(attribute = CryptographicAttribute.ALGORITHM_BLOCK_CIPHER_MODE, description = "Cryptographic Algorithm Block Cipher Mode"),
+ @WritesAttribute(attribute = CryptographicAttribute.ALGORITHM_CIPHER, description = "Cryptographic Algorithm Cipher"),
+ @WritesAttribute(attribute = CryptographicAttribute.ALGORITHM_KEY_SIZE, description = "Cryptographic Algorithm Key Size"),
+ @WritesAttribute(attribute = CryptographicAttribute.ALGORITHM_OBJECT_IDENTIFIER, description = "Cryptographic Algorithm Object Identifier"),
+ @WritesAttribute(attribute = CryptographicAttribute.METHOD, description = "Cryptographic Method is CMS"),
+ @WritesAttribute(attribute = CryptographicAttribute.PROCESSING_COMPLETED, description = "Cryptographic Processing Completed")
+})
+public class DecryptCMS extends AbstractCMSCryptographicProcessor {
+ public static final PropertyDescriptor PRIVATE_KEY_SERVICE = new PropertyDescriptor.Builder()
+ .name("Private Key Service")
+ .displayName("Private Key Service")
+ .description("Private Key Service provides Private Keys for Recipients")
+ .required(true)
+ .identifiesControllerService(PrivateKeyService.class)
+ .build();
+
+ public static final Relationship SUCCESS = new Relationship.Builder()
+ .name("success")
+ .description("Decryption Succeeded")
+ .build();
+
+ public static final Relationship FAILURE = new Relationship.Builder()
+ .name("failure")
+ .description("Decryption Failed")
+ .build();
+
+ private static final String CMS_PARSING_FAILED = "CMS Parsing Failed: %s";
+
+ private static final String PROCESSING_FAILED = "CMS Processing Failed {}";
+
+ private static final String RECIPIENT_NOT_FOUND = "Recipient Private Key not found for Serial Numbers and Issuers";
+
+ private static final String RECIPIENT_PARSED = "Parsed Recipient Serial Number [{}] Issuer [{}]";
+
+ private static final String KEY_FOUND = "Recipient Private Key Found Serial Number [{}] Issuer [{}]";
+
+ private static final String ALGORITHM_NOT_RESOLVED = "Cryptographic Algorithm not resolved [{}]";
+
+ private static final CryptographicAlgorithmResolver RESOLVER = new DefaultCryptographicAlgorithmResolver();
+
+ private List descriptors;
+
+ private Set relationships;
+
+ /**
+ * Get Relationships
+ *
+ * @return Relations configured during initialization
+ */
+ @Override
+ public Set getRelationships() {
+ return relationships;
+ }
+
+ /**
+ * Get Supported Property Descriptors
+ *
+ * @return Supported Property Descriptors configured during initialization
+ */
+ @Override
+ public final List getSupportedPropertyDescriptors() {
+ return descriptors;
+ }
+
+ /**
+ * Initialize Processor Properties and Relationships
+ *
+ * @param context Processor Initialization Context is not used
+ */
+ @Override
+ protected void init(final ProcessorInitializationContext context) {
+ final List descriptors = new ArrayList<>();
+ descriptors.add(PRIVATE_KEY_SERVICE);
+ this.descriptors = Collections.unmodifiableList(descriptors);
+
+ final Set relationships = new HashSet<>();
+ relationships.add(SUCCESS);
+ relationships.add(FAILURE);
+ this.relationships = Collections.unmodifiableSet(relationships);
+ }
+
+ /**
+ * On Trigger encrypts Flow File contents using configured algorithm and Certificate Service properties
+ *
+ * @param context Process Context properties configured properties
+ * @param session Process Session for handling Flow Files
+ */
+ @Override
+ public void onTrigger(final ProcessContext context, final ProcessSession session) {
+ FlowFile flowFile = session.get();
+ if (flowFile == null) {
+ return;
+ }
+
+ final PrivateKeyService privateKeyService = context.getProperty(PRIVATE_KEY_SERVICE).asControllerService(PrivateKeyService.class);
+ try {
+ flowFile = processFlowFile(flowFile, session, privateKeyService);
+ session.transfer(flowFile, SUCCESS);
+ } catch (final ProcessException e) {
+ getLogger().error(PROCESSING_FAILED, new Object[]{flowFile}, e);
+ session.transfer(flowFile, FAILURE);
+ }
+ }
+
+ /**
+ * Process Flow File and write decrypted contents when matching Private Key found
+ *
+ * @param flowFile Flow File to be processed
+ * @param session Process Session
+ * @param privateKeyService Private Key Service for finding matching recipient private keys
+ * @return Updated Flow File
+ */
+ private FlowFile processFlowFile(final FlowFile flowFile, final ProcessSession session, final PrivateKeyService privateKeyService) {
+ final Map flowFileAttributes = new HashMap<>();
+
+ final FlowFile decryptedFlowFile = session.write(flowFile, (inputStream, outputStream) -> {
+ final AtomicReference recipientIdFound = new AtomicReference<>();
+ try {
+ final CMSEnvelopedDataParser parser = new CMSEnvelopedDataParser(inputStream);
+ final RecipientInformationStore recipientStore = parser.getRecipientInfos();
+ final Collection recipients = recipientStore.getRecipients();
+ for (final RecipientInformation recipientInformation : recipients) {
+ final RecipientId recipientId = recipientInformation.getRID();
+ if (recipientId instanceof KeyTransRecipientId) {
+ final KeyTransRecipientId keyTransRecipientId = (KeyTransRecipientId) recipientId;
+ final BigInteger serialNumber = keyTransRecipientId.getSerialNumber();
+ final X500Name issuerName = keyTransRecipientId.getIssuer();
+ getLogger().debug(RECIPIENT_PARSED, new Object[]{serialNumber, issuerName});
+
+ final X500Principal issuer = new X500Principal(issuerName.toString());
+ final Optional privateKey = privateKeyService.findPrivateKey(serialNumber, issuer);
+ if (privateKey.isPresent()) {
+ getLogger().info(KEY_FOUND, new Object[]{serialNumber, issuer});
+ final InputStream contentStream = getContentStream(privateKey.get(), recipientInformation);
+ IOUtils.copy(contentStream, outputStream);
+ recipientIdFound.set(keyTransRecipientId);
+ break;
+ }
+ }
+ }
+
+ final String objectIdentifier = parser.getEncryptionAlgOID();
+ final Optional optionalAlgorithm = RESOLVER.findCryptographicAlgorithm(objectIdentifier);
+ if (optionalAlgorithm.isPresent()) {
+ final CryptographicAlgorithm cryptographicAlgorithm = optionalAlgorithm.get();
+ final Map cryptographicAttributes = getCryptographicAttributes(cryptographicAlgorithm);
+ flowFileAttributes.putAll(cryptographicAttributes);
+ } else {
+ flowFileAttributes.put(CryptographicAttributeKey.ALGORITHM_OBJECT_IDENTIFIER.key(), objectIdentifier);
+ getLogger().warn(ALGORITHM_NOT_RESOLVED, new Object[]{objectIdentifier});
+ }
+ } catch (final CMSException e) {
+ final String message = String.format(CMS_PARSING_FAILED, e.getMessage());
+ throw new IOException(message, e);
+ }
+
+ if (recipientIdFound.get() == null) {
+ throw new IOException(RECIPIENT_NOT_FOUND);
+ }
+ });
+
+ return session.putAllAttributes(decryptedFlowFile, flowFileAttributes);
+ }
+
+ /**
+ * Get Decrypted Content Stream
+ *
+ * @param key Private Key matched from Private Key Service
+ * @param recipientInformation Recipient Information matching Private Key
+ * @return Decrypted Input Stream
+ * @throws CMSException Thrown on RecipientInformation.getContentStream()
+ * @throws IOException Thrown on RecipientInformation.getContentStream()
+ */
+ private InputStream getContentStream(final PrivateKey key, final RecipientInformation recipientInformation) throws CMSException, IOException {
+ final KeyTransRecipient keyTransRecipient = new JceKeyTransEnvelopedRecipient(key);
+ final CMSTypedStream cmsTypedStream = recipientInformation.getContentStream(keyTransRecipient);
+ return cmsTypedStream.getContentStream();
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/cms/EncryptCMS.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/cms/EncryptCMS.java
new file mode 100644
index 000000000000..604be94db2af
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/crypto/cms/EncryptCMS.java
@@ -0,0 +1,307 @@
+/*
+ * 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.standard.crypto.cms;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.SideEffectFree;
+import org.apache.nifi.annotation.behavior.WritesAttribute;
+import org.apache.nifi.annotation.behavior.WritesAttributes;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.pki.CertificateService;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.ProcessorInitializationContext;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.processors.standard.crypto.algorithm.CryptographicAlgorithm;
+import org.apache.nifi.processors.standard.crypto.attributes.CryptographicAttribute;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.cms.CMSEnvelopedDataStreamGenerator;
+import org.bouncycastle.cms.CMSException;
+import org.bouncycastle.cms.RecipientInfoGenerator;
+import org.bouncycastle.cms.bc.BcCMSContentEncryptorBuilder;
+import org.bouncycastle.cms.jcajce.JceKeyTransRecipientInfoGenerator;
+import org.bouncycastle.operator.OutputEncryptor;
+
+import javax.security.auth.x500.X500Principal;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Encrypt CMS Processor writes and encrypts content using provided certificates and algorithm specified
+ */
+@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED)
+@Tags({"Cryptography", "CMS", "PKCS7", "RFC 5652", "AES"})
+@CapabilityDescription("Encrypt content using Cryptographic Message Syntax")
+@SideEffectFree
+@SeeAlso({DecryptCMS.class})
+@WritesAttributes({
+ @WritesAttribute(attribute = CryptographicAttribute.ALGORITHM, description = "Cryptographic Algorithm"),
+ @WritesAttribute(attribute = CryptographicAttribute.ALGORITHM_BLOCK_CIPHER_MODE, description = "Cryptographic Algorithm Block Cipher Mode"),
+ @WritesAttribute(attribute = CryptographicAttribute.ALGORITHM_CIPHER, description = "Cryptographic Algorithm Cipher"),
+ @WritesAttribute(attribute = CryptographicAttribute.ALGORITHM_KEY_SIZE, description = "Cryptographic Algorithm Key Size"),
+ @WritesAttribute(attribute = CryptographicAttribute.ALGORITHM_OBJECT_IDENTIFIER, description = "Cryptographic Algorithm Object Identifier"),
+ @WritesAttribute(attribute = CryptographicAttribute.METHOD, description = "Cryptographic Method is CMS"),
+ @WritesAttribute(attribute = CryptographicAttribute.PROCESSING_COMPLETED, description = "Cryptographic Processing Completed"),
+ @WritesAttribute(attribute = "mime.type", description = "MIME Type of encrypted contents set to application/pkcs7-mime")
+})
+public class EncryptCMS extends AbstractCMSCryptographicProcessor {
+ public static final PropertyDescriptor CRYPTOGRAPHIC_ALGORITHM = new PropertyDescriptor.Builder()
+ .name("Cryptographic Algorithm")
+ .displayName("Cryptographic Algorithm")
+ .description("Cryptographic Algorithm supports various ciphers and key sizes compatible with CMS")
+ .defaultValue(CryptographicAlgorithm.AES_256_GCM.toString())
+ .allowableValues(
+ CryptographicAlgorithm.AES_128_CBC.toString(),
+ CryptographicAlgorithm.AES_128_CCM.toString(),
+ CryptographicAlgorithm.AES_128_GCM.toString(),
+ CryptographicAlgorithm.AES_192_CBC.toString(),
+ CryptographicAlgorithm.AES_192_CCM.toString(),
+ CryptographicAlgorithm.AES_192_GCM.toString(),
+ CryptographicAlgorithm.AES_256_CBC.toString(),
+ CryptographicAlgorithm.AES_256_CCM.toString(),
+ CryptographicAlgorithm.AES_256_GCM.toString(),
+ CryptographicAlgorithm.TDEA_168_CBC.toString(),
+ CryptographicAlgorithm.RC2_40_CBC.toString()
+ )
+ .required(true)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR).build();
+
+ public static final PropertyDescriptor CERTIFICATE_SERVICE = new PropertyDescriptor.Builder()
+ .name("Certificate Service")
+ .displayName("Certificate Service")
+ .description("Certificate Service provides X.509 Certificates for Recipients")
+ .required(true)
+ .identifiesControllerService(CertificateService.class)
+ .build();
+
+ public static final PropertyDescriptor CERTIFICATE_SEARCH = new PropertyDescriptor.Builder()
+ .name("Certificate Search")
+ .displayName("Certificate Search")
+ .description("Certificate Search pattern defined according to configured Certificate Service")
+ .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .build();
+
+ public static final Relationship SUCCESS = new Relationship.Builder()
+ .name("success")
+ .description("Encryption Succeeded")
+ .build();
+
+ public static final Relationship FAILURE = new Relationship.Builder()
+ .name("failure")
+ .description("Encryption Failed")
+ .build();
+
+ public static final String MIME_TYPE = "application/pkcs7-mime; smime-type=enveloped-data";
+
+ private static final String CONTENT_ENCRYPTOR_BUILDER_FAILED = "Building Output Encryptor Failed for Algorithm Identifier [%s]";
+
+ private static final String ENCRYPTED_OUTPUT_STREAM_FAILED = "Opening Encrypted Output Stream Failed";
+
+ private static final String SECURE_RANDOM_FAILED = "Building SecureRandom Failed";
+
+ private static final String PROCESSING_FAILED = "CMS Processing Failed {}: {}";
+
+ private static final String CERTIFICATE_FAILED = "Recipient Information Certificate Encoding Failed [%s]: %s";
+
+ private SecureRandom secureRandom;
+
+ private List descriptors;
+
+ private Set relationships;
+
+ /**
+ * Get Relationships
+ *
+ * @return Relations configured during initialization
+ */
+ @Override
+ public Set getRelationships() {
+ return relationships;
+ }
+
+ /**
+ * Get Supported Property Descriptors
+ *
+ * @return Supported Property Descriptors configured during initialization
+ */
+ @Override
+ public final List getSupportedPropertyDescriptors() {
+ return descriptors;
+ }
+
+ /**
+ * On Trigger encrypts Flow File contents using configured algorithm and Certificate Service properties
+ *
+ * @param context Process Context properties configured properties
+ * @param session Process Session for handling Flow Files
+ */
+ @Override
+ public void onTrigger(final ProcessContext context, final ProcessSession session) {
+ FlowFile flowFile = session.get();
+ if (flowFile == null) {
+ return;
+ }
+
+ try {
+ final String method = context.getProperty(CRYPTOGRAPHIC_ALGORITHM).getValue();
+ final CryptographicAlgorithm cryptographicAlgorithm = getCryptographicAlgorithm(method);
+
+ final CertificateService certificateService = context.getProperty(CERTIFICATE_SERVICE).asControllerService(CertificateService.class);
+ final String search = context.getProperty(CERTIFICATE_SEARCH).evaluateAttributeExpressions(flowFile).getValue();
+
+ flowFile = session.write(flowFile, (inputStream, outputStream) -> {
+ final ASN1ObjectIdentifier algorithmIdentifier = new ASN1ObjectIdentifier(cryptographicAlgorithm.getObjectIdentifier());
+ final OutputEncryptor outputEncryptor = getOutputEncryptor(algorithmIdentifier);
+ final List certificates = certificateService.findCertificates(search);
+ final List recipients = getRecipients(certificates);
+
+ final OutputStream encryptedOutputStream = getCmsOutputStream(outputStream, outputEncryptor, recipients);
+ IOUtils.copy(inputStream, encryptedOutputStream);
+ encryptedOutputStream.close();
+ });
+
+ flowFile = session.putAttribute(flowFile, CoreAttributes.MIME_TYPE.key(), MIME_TYPE);
+ final Map cryptographicAttributes = getCryptographicAttributes(cryptographicAlgorithm);
+ flowFile = session.putAllAttributes(flowFile, cryptographicAttributes);
+ session.transfer(flowFile, SUCCESS);
+ } catch (final ProcessException e) {
+ getLogger().error(PROCESSING_FAILED, new Object[]{flowFile, e.getMessage()}, e);
+ session.transfer(flowFile, FAILURE);
+ }
+ }
+
+ /**
+ * Initialize Processor Properties and Relationships
+ *
+ * @param context Processor Initialization Context is not used
+ */
+ @Override
+ protected void init(final ProcessorInitializationContext context) {
+ final List descriptors = new ArrayList<>();
+ descriptors.add(CRYPTOGRAPHIC_ALGORITHM);
+ descriptors.add(CERTIFICATE_SERVICE);
+ descriptors.add(CERTIFICATE_SEARCH);
+ this.descriptors = Collections.unmodifiableList(descriptors);
+
+ final Set relationships = new HashSet<>();
+ relationships.add(SUCCESS);
+ relationships.add(FAILURE);
+ this.relationships = Collections.unmodifiableSet(relationships);
+
+ try {
+ secureRandom = SecureRandom.getInstanceStrong();
+ } catch (final NoSuchAlgorithmException e) {
+ throw new ProcessException(SECURE_RANDOM_FAILED, e);
+ }
+ }
+
+ /**
+ * Get Output Encryptor
+ *
+ * @param objectIdentifier Algorithm Object Identifier
+ * @return Output Encryptor
+ */
+ private OutputEncryptor getOutputEncryptor(final ASN1ObjectIdentifier objectIdentifier) {
+ final BcCMSContentEncryptorBuilder builder = new BcCMSContentEncryptorBuilder(objectIdentifier);
+ builder.setSecureRandom(secureRandom);
+ try {
+ return builder.build();
+ } catch (final CMSException e) {
+ final String message = String.format(CONTENT_ENCRYPTOR_BUILDER_FAILED, objectIdentifier);
+ throw new ProcessException(message, e);
+ }
+ }
+
+ /**
+ * Get Recipient Information Generators for provided X.509 Certificates
+ *
+ * @param certificates X.509 Certificates
+ * @return Recipient Information Generators
+ */
+ private List getRecipients(final List certificates) {
+ return certificates.stream().map(certificate -> {
+ try {
+ return new JceKeyTransRecipientInfoGenerator(certificate);
+ } catch (final CertificateEncodingException e) {
+ final X500Principal subjectPrincipal = certificate.getSubjectX500Principal();
+ final String message = String.format(CERTIFICATE_FAILED, subjectPrincipal, e.getMessage());
+ throw new ProcessException(message, e);
+ }
+ }).collect(Collectors.toList());
+ }
+
+ /**
+ * Get CMS Encrypted Output Stream wrapping provided Output Stream
+ *
+ * @param outputStream Output Stream
+ * @param outputEncryptor Output Encryptor
+ * @param recipients Recipient Information Generator
+ * @return Encrypted Output Stream
+ * @throws IOException Thrown on CMSEnvelopedDataStreamGenerator.open()
+ */
+ private OutputStream getCmsOutputStream(final OutputStream outputStream, final OutputEncryptor outputEncryptor, final List recipients) throws IOException {
+ final CMSEnvelopedDataStreamGenerator dataStreamGenerator = new CMSEnvelopedDataStreamGenerator();
+ recipients.forEach(dataStreamGenerator::addRecipientInfoGenerator);
+ try {
+ return dataStreamGenerator.open(outputStream, outputEncryptor);
+ } catch (final CMSException e) {
+ throw new ProcessException(ENCRYPTED_OUTPUT_STREAM_FAILED, e);
+ }
+ }
+
+ /**
+ * Get Cryptographic Algorithm from method property matching CryptographicAlgorithm.toString()
+ *
+ * @param algorithm Algorithm Property
+ * @return Cryptographic Algorithm
+ */
+ private CryptographicAlgorithm getCryptographicAlgorithm(final String algorithm) {
+ final Stream algorithms = Arrays.stream(CryptographicAlgorithm.values());
+ final Optional foundAlgorithm = algorithms.filter(cryptographicAlgorithm ->
+ cryptographicAlgorithm.toString().equals(algorithm)
+ ).findFirst();
+ if (foundAlgorithm.isPresent()) {
+ return foundAlgorithm.get();
+ } else {
+ throw new IllegalArgumentException(algorithm);
+ }
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
index 6a767c15ff82..30bcf7d8d30c 100644
--- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
@@ -125,3 +125,5 @@ org.apache.nifi.processors.standard.ValidateCsv
org.apache.nifi.processors.standard.ValidateRecord
org.apache.nifi.processors.standard.ValidateXml
org.apache.nifi.processors.standard.Wait
+org.apache.nifi.processors.standard.crypto.cms.EncryptCMS
+org.apache.nifi.processors.standard.crypto.cms.DecryptCMS
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/crypto/cms/DecryptCMSTest.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/crypto/cms/DecryptCMSTest.java
new file mode 100644
index 000000000000..6a67870c5088
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/crypto/cms/DecryptCMSTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.standard.crypto.cms;
+
+import org.apache.nifi.pki.PrivateKeyService;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.util.MockFlowFile;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.cms.CMSAlgorithm;
+import org.bouncycastle.cms.CMSEnvelopedDataStreamGenerator;
+import org.bouncycastle.cms.jcajce.JceCMSContentEncryptorBuilder;
+import org.bouncycastle.cms.jcajce.JceKeyTransRecipientInfoGenerator;
+import org.bouncycastle.operator.OutputEncryptor;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+public class DecryptCMSTest {
+ private static final String CONTENT = String.class.getSimpleName();
+
+ private static final ASN1ObjectIdentifier DEFAULT_ALGORITHM = CMSAlgorithm.AES128_GCM;
+
+ private static final String DECRYPTED_CONTENT_NOT_MATCHED = "Decrypted Content not matched";
+
+ private static final String KEY_SERVICE_ID = UUID.randomUUID().toString();
+
+ private static final String KEY_ALGORITHM = "RSA";
+
+ private static final String SUBJECT_DN = "CN=subject";
+
+ private static final String SIGNING_ALGORITHM = "SHA256withRSA";
+
+ private static final int ONE_DAY = 1;
+
+ private KeyPair keyPair;
+
+ private X509Certificate certificate;
+
+ private TestRunner testRunner;
+
+ private PrivateKeyService privateKeyService;
+
+ @Before
+ public void init() throws Exception {
+ testRunner = TestRunners.newTestRunner(DecryptCMS.class);
+
+ privateKeyService = Mockito.mock(PrivateKeyService.class);
+ when(privateKeyService.getIdentifier()).thenReturn(KEY_SERVICE_ID);
+ testRunner.addControllerService(KEY_SERVICE_ID, privateKeyService);
+ testRunner.enableControllerService(privateKeyService);
+ testRunner.assertValid(privateKeyService);
+
+ testRunner.setProperty(DecryptCMS.PRIVATE_KEY_SERVICE, KEY_SERVICE_ID);
+
+ keyPair = KeyPairGenerator.getInstance(KEY_ALGORITHM).generateKeyPair();
+ certificate = CertificateUtils.generateSelfSignedX509Certificate(keyPair, SUBJECT_DN, SIGNING_ALGORITHM, ONE_DAY);
+ }
+
+ @Test
+ public void testFlowFileNotFound() {
+ testRunner.run();
+ testRunner.assertQueueEmpty();
+ }
+
+ @Test
+ public void testSuccess() throws Exception {
+ final Optional privateKey = Optional.of(keyPair.getPrivate());
+ when(privateKeyService.findPrivateKey(any(), any())).thenReturn(privateKey);
+
+ final byte[] encryptedContent = getEncryptedContent(certificate);
+ final byte[] decryptedContent = assertFlowFileSuccess(encryptedContent);
+ final String decryptedString = new String(decryptedContent);
+ assertEquals(DECRYPTED_CONTENT_NOT_MATCHED, CONTENT, decryptedString);
+ }
+
+ @Test
+ public void testFailure() throws Exception {
+ final byte[] encryptedContent = getEncryptedContent(certificate);
+
+ testRunner.enqueue(encryptedContent);
+ testRunner.run();
+ testRunner.assertAllFlowFilesTransferred(DecryptCMS.FAILURE);
+ }
+
+ private byte[] getEncryptedContent(final X509Certificate certificate) throws Exception {
+ final JceCMSContentEncryptorBuilder builder = new JceCMSContentEncryptorBuilder(DEFAULT_ALGORITHM);
+ builder.setSecureRandom(SecureRandom.getInstanceStrong());
+ final OutputEncryptor outputEncryptor = builder.build();
+
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ final CMSEnvelopedDataStreamGenerator dataStreamGenerator = new CMSEnvelopedDataStreamGenerator();
+ final JceKeyTransRecipientInfoGenerator recipientInfo = new JceKeyTransRecipientInfoGenerator(certificate);
+ dataStreamGenerator.addRecipientInfoGenerator(recipientInfo);
+ final OutputStream encryptedOutputStream = dataStreamGenerator.open(outputStream, outputEncryptor);
+
+ encryptedOutputStream.write(CONTENT.getBytes());
+ encryptedOutputStream.close();
+ return outputStream.toByteArray();
+ }
+
+ private byte[] assertFlowFileSuccess(final byte[] bytes) {
+ testRunner.enqueue(bytes);
+ testRunner.run();
+ testRunner.assertAllFlowFilesTransferred(DecryptCMS.SUCCESS);
+ final List flowFiles = testRunner.getFlowFilesForRelationship(EncryptCMS.SUCCESS);
+ final MockFlowFile flowFile = flowFiles.iterator().next();
+ return testRunner.getContentAsByteArray(flowFile);
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/crypto/cms/EncryptCMSTest.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/crypto/cms/EncryptCMSTest.java
new file mode 100644
index 000000000000..3feda324d89f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/crypto/cms/EncryptCMSTest.java
@@ -0,0 +1,205 @@
+/*
+ * 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.standard.crypto.cms;
+
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.pki.CertificateService;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processors.standard.crypto.algorithm.CryptographicAlgorithm;
+import org.apache.nifi.processors.standard.crypto.algorithm.CryptographicAlgorithmResolver;
+import org.apache.nifi.processors.standard.crypto.algorithm.DefaultCryptographicAlgorithmResolver;
+import org.apache.nifi.processors.standard.crypto.attributes.CryptographicAttributeKey;
+import org.apache.nifi.processors.standard.crypto.attributes.CryptographicMethod;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.util.LogMessage;
+import org.apache.nifi.util.MockFlowFile;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.bouncycastle.cms.CMSEnvelopedDataParser;
+import org.bouncycastle.cms.CMSException;
+import org.bouncycastle.cms.KeyTransRecipientId;
+import org.bouncycastle.cms.RecipientId;
+import org.bouncycastle.cms.RecipientInformation;
+import org.bouncycastle.cms.RecipientInformationStore;
+import org.bouncycastle.cms.jcajce.JceKeyTransEnvelopedRecipient;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.X509Certificate;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+public class EncryptCMSTest {
+ private static final String CONTENT = String.class.getSimpleName();
+
+ private static final String ERRORS_NOT_FOUND = "Error Log Messages not found";
+
+ private static final String ALGORITHM_NOT_MATCHED = "Cryptographic Algorithm not matched";
+
+ private static final String RECIPIENTS_NOT_FOUND = "CMS Recipients not found";
+
+ private static final String RECIPIENT_ID_NOT_MATCHED = "CMS Recipient ID Class not matched";
+
+ private static final String SERIAL_NUMBER_NOT_MATCHED = "Recipient ID Serial Number not matched";
+
+ private static final String ISSUER_NOT_MATCHED = "Recipient Issuer not matched";
+
+ private static final String DECRYPTED_NOT_MATCHED = "Decrypted Content not matched";
+
+ private static final String OBJECT_IDENTIFIER_NOT_FOUND = "Cryptographic Object Identifier not found";
+
+ private static final String CERTIFICATE_SERVICE_ID = UUID.randomUUID().toString();
+
+ private static final String KEY_ALGORITHM = "RSA";
+
+ private static final String SUBJECT_DN = "CN=subject";
+
+ private static final String SIGNING_ALGORITHM = "SHA256withRSA";
+
+ private static final int ONE_DAY = 1;
+
+ private KeyPair keyPair;
+
+ private TestRunner testRunner;
+
+ private CertificateService certificateService;
+
+ @Before
+ public void init() throws InitializationException, NoSuchAlgorithmException {
+ testRunner = TestRunners.newTestRunner(EncryptCMS.class);
+
+ certificateService = Mockito.mock(CertificateService.class);
+ when(certificateService.getIdentifier()).thenReturn(CERTIFICATE_SERVICE_ID);
+ testRunner.addControllerService(CERTIFICATE_SERVICE_ID, certificateService);
+ testRunner.enableControllerService(certificateService);
+ testRunner.assertValid(certificateService);
+
+ testRunner.setProperty(EncryptCMS.CERTIFICATE_SERVICE, CERTIFICATE_SERVICE_ID);
+
+ keyPair = KeyPairGenerator.getInstance(KEY_ALGORITHM).generateKeyPair();
+ }
+
+ @Test
+ public void testFlowFileNotFound() {
+ testRunner.run();
+ testRunner.assertQueueEmpty();
+ }
+
+ @Test
+ public void testFlowFileProcessed() throws CMSException, IOException {
+ final List certificates = Collections.emptyList();
+ when(certificateService.findCertificates(any())).thenReturn(certificates);
+ assertFlowFileSuccess();
+ }
+
+ @Test
+ public void testRecipients() throws Exception {
+ final X509Certificate certificate = CertificateUtils.generateSelfSignedX509Certificate(keyPair, SUBJECT_DN, SIGNING_ALGORITHM, ONE_DAY);
+ final List certificates = Collections.singletonList(certificate);
+ when(certificateService.findCertificates(any())).thenReturn(certificates);
+
+ final CMSEnvelopedDataParser parser = assertFlowFileSuccess();
+
+ final RecipientInformationStore recipientInformationStore = parser.getRecipientInfos();
+ final Collection recipients = recipientInformationStore.getRecipients();
+ assertFalse(RECIPIENTS_NOT_FOUND, recipients.isEmpty());
+
+ final RecipientInformation recipient = recipients.iterator().next();
+ final RecipientId recipientId = recipient.getRID();
+ assertEquals(RECIPIENT_ID_NOT_MATCHED, KeyTransRecipientId.class, recipientId.getClass());
+ final KeyTransRecipientId keyTransRecipientId = (KeyTransRecipientId) recipientId;
+ assertEquals(SERIAL_NUMBER_NOT_MATCHED, certificate.getSerialNumber(), keyTransRecipientId.getSerialNumber());
+ assertEquals(ISSUER_NOT_MATCHED, certificate.getIssuerX500Principal().toString(), keyTransRecipientId.getIssuer().toString());
+ }
+
+ @Test
+ public void testReadEncrypted() throws Exception {
+ final X509Certificate certificate = CertificateUtils.generateSelfSignedX509Certificate(keyPair, SUBJECT_DN, SIGNING_ALGORITHM, ONE_DAY);
+ final List certificates = Collections.singletonList(certificate);
+ when(certificateService.findCertificates(any())).thenReturn(certificates);
+
+ final CMSEnvelopedDataParser parser = assertFlowFileSuccess();
+
+ final RecipientInformationStore recipientInformationStore = parser.getRecipientInfos();
+ final Collection recipients = recipientInformationStore.getRecipients();
+ assertFalse(RECIPIENTS_NOT_FOUND, recipients.isEmpty());
+
+ final RecipientInformation recipient = recipients.iterator().next();
+ final JceKeyTransEnvelopedRecipient envelopedRecipient = new JceKeyTransEnvelopedRecipient(keyPair.getPrivate());
+ final byte[] decrypted = recipient.getContent(envelopedRecipient);
+ final String decryptedContent = new String(decrypted);
+
+ assertEquals(DECRYPTED_NOT_MATCHED, CONTENT, decryptedContent);
+ }
+
+ @Test
+ public void testCertificateServiceException() {
+ when(certificateService.findCertificates(any())).thenThrow(new ProcessException(CERTIFICATE_SERVICE_ID));
+ assertFlowFileFailed();
+ }
+
+ private void assertFlowFileFailed() {
+ testRunner.enqueue(CONTENT);
+ testRunner.run();
+ testRunner.assertAllFlowFilesTransferred(EncryptCMS.FAILURE);
+ final List errorMessages = testRunner.getLogger().getErrorMessages();
+ assertFalse(ERRORS_NOT_FOUND, errorMessages.isEmpty());
+ }
+
+ private CMSEnvelopedDataParser assertFlowFileSuccess() throws CMSException, IOException {
+ testRunner.enqueue(CONTENT);
+ testRunner.run();
+ testRunner.assertAllFlowFilesTransferred(EncryptCMS.SUCCESS);
+ final List flowFiles = testRunner.getFlowFilesForRelationship(EncryptCMS.SUCCESS);
+ final MockFlowFile flowFile = flowFiles.iterator().next();
+ flowFile.assertAttributeEquals(CoreAttributes.MIME_TYPE.key(), EncryptCMS.MIME_TYPE);
+
+ final String objectIdentifier = flowFile.getAttribute(CryptographicAttributeKey.ALGORITHM_OBJECT_IDENTIFIER.key());
+ assertNotNull(OBJECT_IDENTIFIER_NOT_FOUND, objectIdentifier);
+ final CryptographicAlgorithmResolver resolver = new DefaultCryptographicAlgorithmResolver();
+ final Optional optionalAlgorithm = resolver.findCryptographicAlgorithm(objectIdentifier);
+ final CryptographicAlgorithm cryptographicAlgorithm = optionalAlgorithm.orElseThrow(IllegalArgumentException::new);
+
+ flowFile.assertAttributeEquals(CryptographicAttributeKey.ALGORITHM.key(), cryptographicAlgorithm.toString());
+ flowFile.assertAttributeEquals(CryptographicAttributeKey.ALGORITHM_BLOCK_CIPHER_MODE.key(), cryptographicAlgorithm.getBlockCipherMode().getLabel());
+ flowFile.assertAttributeEquals(CryptographicAttributeKey.ALGORITHM_CIPHER.key(), cryptographicAlgorithm.getCipher().getLabel());
+ flowFile.assertAttributeEquals(CryptographicAttributeKey.ALGORITHM_KEY_SIZE.key(), Integer.toString(cryptographicAlgorithm.getKeySize()));
+ flowFile.assertAttributeEquals(CryptographicAttributeKey.METHOD.key(), CryptographicMethod.CMS.toString());
+ flowFile.assertAttributeExists(CryptographicAttributeKey.PROCESSING_COMPLETED.key());
+
+ final byte[] content = testRunner.getContentAsByteArray(flowFile);
+ final CMSEnvelopedDataParser parser = new CMSEnvelopedDataParser(content);
+ final String encryptionObjectIdentifier = parser.getEncryptionAlgOID();
+ assertEquals(ALGORITHM_NOT_MATCHED, objectIdentifier, encryptionObjectIdentifier);
+ return parser;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-api/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-api/pom.xml
new file mode 100644
index 000000000000..ecea5dffd163
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-api/pom.xml
@@ -0,0 +1,32 @@
+
+
+
+ 4.0.0
+
+ org.apache.nifi
+ nifi-standard-services
+ 1.13.0-SNAPSHOT
+
+ nifi-pki-service-api
+ jar
+
+
+ org.apache.nifi
+ nifi-api
+
+
+
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-api/src/main/java/org/apache/nifi/pki/CertificateService.java b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-api/src/main/java/org/apache/nifi/pki/CertificateService.java
new file mode 100644
index 000000000000..f9c61195af5a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-api/src/main/java/org/apache/nifi/pki/CertificateService.java
@@ -0,0 +1,35 @@
+/*
+ * 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.pki;
+
+import org.apache.nifi.controller.ControllerService;
+
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+/**
+ * Certificate Service for finding X.509 Certificates
+ */
+public interface CertificateService extends ControllerService {
+ /**
+ * Find Certificates matching search
+ *
+ * @param search Search String
+ * @return X.509 Certificates matching search
+ */
+ List findCertificates(String search);
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-api/src/main/java/org/apache/nifi/pki/PrivateKeyService.java b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-api/src/main/java/org/apache/nifi/pki/PrivateKeyService.java
new file mode 100644
index 000000000000..2b3dcda4ebb9
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-api/src/main/java/org/apache/nifi/pki/PrivateKeyService.java
@@ -0,0 +1,38 @@
+/*
+ * 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.pki;
+
+import org.apache.nifi.controller.ControllerService;
+
+import javax.security.auth.x500.X500Principal;
+import java.math.BigInteger;
+import java.security.PrivateKey;
+import java.util.Optional;
+
+/**
+ * Private Key Service for finding Private Keys based on Certificate Parameters
+ */
+public interface PrivateKeyService extends ControllerService {
+ /**
+ * Find Private Key matching certificate serial number and issuer specified
+ *
+ * @param serialNumber Certificate Serial Number
+ * @param issuer X.500 Principal of Certificate Issuer
+ * @return Private Key
+ */
+ Optional findPrivateKey(BigInteger serialNumber, X500Principal issuer);
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service-nar/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service-nar/pom.xml
new file mode 100644
index 000000000000..5247c5e49fad
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service-nar/pom.xml
@@ -0,0 +1,41 @@
+
+
+ 4.0.0
+
+ org.apache.nifi
+ nifi-pki-service-bundle
+ 1.13.0-SNAPSHOT
+
+ nifi-pki-service-nar
+ nar
+
+ true
+ true
+
+
+
+ org.apache.nifi
+ nifi-standard-services-api-nar
+ 1.13.0-SNAPSHOT
+ nar
+
+
+ org.apache.nifi
+ nifi-pki-service
+ 1.13.0-SNAPSHOT
+
+
+
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service/pom.xml
new file mode 100644
index 000000000000..ecfbcb7dff48
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service/pom.xml
@@ -0,0 +1,54 @@
+
+
+
+ 4.0.0
+
+ org.apache.nifi
+ nifi-pki-service-bundle
+ 1.13.0-SNAPSHOT
+
+ nifi-pki-service
+ jar
+
+
+ org.apache.nifi
+ nifi-api
+ provided
+
+
+ org.apache.nifi
+ nifi-pki-service-api
+ 1.13.0-SNAPSHOT
+ provided
+
+
+ org.apache.nifi
+ nifi-utils
+ 1.13.0-SNAPSHOT
+
+
+ org.apache.nifi
+ nifi-security-utils
+ 1.13.0-SNAPSHOT
+
+
+ org.apache.nifi
+ nifi-mock
+ test
+
+
+
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service/src/main/java/org/apache/nifi/pki/KeyStorePrivateKeyService.java b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service/src/main/java/org/apache/nifi/pki/KeyStorePrivateKeyService.java
new file mode 100644
index 000000000000..9421ec3040e5
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service/src/main/java/org/apache/nifi/pki/KeyStorePrivateKeyService.java
@@ -0,0 +1,212 @@
+/*
+ * 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.pki;
+
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnEnabled;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+
+import javax.security.auth.x500.X500Principal;
+import java.math.BigInteger;
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Private Key Service implementation uses a configured Key Store to find Private Keys matching Certificate properties
+ */
+@Tags({"PKI", "X.509", "Certificates"})
+@CapabilityDescription("Private Key Service providing Private Keys matching X.509 Certificates from Key Store files")
+public class KeyStorePrivateKeyService extends AbstractControllerService implements PrivateKeyService {
+ public static final PropertyDescriptor KEY_STORE_PATH = new PropertyDescriptor.Builder()
+ .name("Key Store Path")
+ .displayName("Key Store Path")
+ .description("File path for Key Store")
+ .required(true)
+ .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .build();
+
+ public static final PropertyDescriptor KEY_STORE_TYPE = new PropertyDescriptor.Builder()
+ .name("Key Store Type")
+ .displayName("Key Store Type")
+ .description("Type of Key Store supports either JKS or PKCS12")
+ .required(true)
+ .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
+ .defaultValue("PKCS12")
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .build();
+
+ public static final PropertyDescriptor KEY_STORE_PASSWORD = new PropertyDescriptor.Builder()
+ .name("Key Store Password")
+ .displayName("Key Store Password")
+ .description("Password for Key Store")
+ .required(true)
+ .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 for Private Key Entry in Key Store")
+ .required(true)
+ .sensitive(true)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .build();
+
+ private static final String KEY_STORE_LOAD_FAILED = "Key Store [%s] loading failed";
+
+ private static final String FIND_FAILED = "Find Private Key for Serial Number [%s] Issuer [%s] failed";
+
+ private static final List DESCRIPTORS = new ArrayList<>();
+
+ static {
+ DESCRIPTORS.add(KEY_STORE_PATH);
+ DESCRIPTORS.add(KEY_STORE_TYPE);
+ DESCRIPTORS.add(KEY_STORE_PASSWORD);
+ DESCRIPTORS.add(KEY_PASSWORD);
+ }
+
+ private KeyStore keyStore;
+
+ private char[] keyPassword;
+
+ /**
+ * On Enabled configures Trust Store using Context properties
+ *
+ * @param context Configuration Context with properties
+ * @throws InitializationException Thrown when unable to load Trust Store
+ */
+ @OnEnabled
+ public void onEnabled(final ConfigurationContext context) throws InitializationException {
+ final String keyStoreType = context.getProperty(KEY_STORE_TYPE).evaluateAttributeExpressions().getValue();
+ final String keyStorePath = context.getProperty(KEY_STORE_PATH).evaluateAttributeExpressions().getValue();
+ char[] password = null;
+ final PropertyValue passwordProperty = context.getProperty(KEY_STORE_PASSWORD);
+ if (passwordProperty.isSet()) {
+ password = passwordProperty.getValue().toCharArray();
+ }
+
+ try {
+ keyStore = KeyStoreUtils.loadKeyStore(keyStorePath, password, keyStoreType);
+ } catch (final TlsException e) {
+ final String message = String.format(KEY_STORE_LOAD_FAILED, keyStorePath);
+ throw new InitializationException(message, e);
+ }
+
+ final PropertyValue keyPasswordProperty = context.getProperty(KEY_PASSWORD);
+ if (keyPasswordProperty.isSet()) {
+ keyPassword = keyPasswordProperty.getValue().toCharArray();
+ }
+ }
+
+ /**
+ * Find Private Key with X.509 Certificate matching Serial Number and Issuer
+ *
+ * @param serialNumber Certificate Serial Number
+ * @param issuer X.500 Principal of Certificate Issuer
+ * @return Private Key
+ */
+ @Override
+ public Optional findPrivateKey(final BigInteger serialNumber, final X500Principal issuer) {
+ try {
+ return findMatchingPrivateKey(serialNumber, issuer);
+ } catch (final KeyStoreException | UnrecoverableKeyException | NoSuchAlgorithmException e) {
+ final String message = String.format(FIND_FAILED, serialNumber, issuer);
+ throw new ProcessException(message, e);
+ }
+ }
+
+ /**
+ * Get Supported Property Descriptors
+ *
+ * @return Supported Property Descriptors configured during initialization
+ */
+ @Override
+ protected final List getSupportedPropertyDescriptors() {
+ return DESCRIPTORS;
+ }
+
+ /**
+ * Find Matching Private Key
+ *
+ * @param serialNumber Certificate Serial Number
+ * @param issuer Certificate Issuer
+ * @return Optional Private Key
+ * @throws KeyStoreException Thrown on KeyStore.aliases()
+ * @throws UnrecoverableKeyException Thrown on KeyStore.getKey()
+ * @throws NoSuchAlgorithmException Thrown on KeyStore.getKey()
+ */
+ private Optional findMatchingPrivateKey(final BigInteger serialNumber, final X500Principal issuer) throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException {
+ Optional matchingPrivateKey = Optional.empty();
+ final Enumeration aliases = keyStore.aliases();
+ while (aliases.hasMoreElements()) {
+ final String alias = aliases.nextElement();
+ if (isCertificateMatched(alias, serialNumber, issuer)) {
+ final Key key = keyStore.getKey(alias, keyPassword);
+ if (key instanceof PrivateKey) {
+ final PrivateKey privateKey = (PrivateKey) key;
+ matchingPrivateKey = Optional.of(privateKey);
+ }
+ }
+ }
+ return matchingPrivateKey;
+ }
+
+ /**
+ * Is Certificate Matched
+ *
+ * @param alias Certificate Alias
+ * @param serialNumber Certificate Serial Number to find
+ * @param issuer Certificate Issuer to find
+ * @return Certificate Matched status
+ * @throws KeyStoreException Thrown on KeyStore.getCertificate()
+ */
+ private boolean isCertificateMatched(final String alias, final BigInteger serialNumber, final X500Principal issuer) throws KeyStoreException {
+ boolean matched = false;
+
+ final Certificate certificateEntry = keyStore.getCertificate(alias);
+ if (certificateEntry instanceof X509Certificate) {
+ final X509Certificate certificate = (X509Certificate) certificateEntry;
+ final BigInteger certificateSerialNumber = certificate.getSerialNumber();
+ final X500Principal certificateIssuer = certificate.getIssuerX500Principal();
+ matched = certificateSerialNumber.equals(serialNumber) && certificateIssuer.equals(issuer);
+ }
+
+ return matched;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service/src/main/java/org/apache/nifi/pki/TrustStoreCertificateService.java b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service/src/main/java/org/apache/nifi/pki/TrustStoreCertificateService.java
new file mode 100644
index 000000000000..17f9430cc06d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service/src/main/java/org/apache/nifi/pki/TrustStoreCertificateService.java
@@ -0,0 +1,184 @@
+/*
+ * 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.pki;
+
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnEnabled;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+
+import javax.security.auth.x500.X500Principal;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Trust Store Certificate Service implementation finds X.509 Certificates with matching Subject Principal
+ */
+@Tags({"PKI", "X.509", "Certificates"})
+@CapabilityDescription("Certificate Service providing X.509 Certificates from Trust Store files")
+public class TrustStoreCertificateService extends AbstractControllerService implements CertificateService {
+
+ public static final PropertyDescriptor TRUST_STORE_PATH = new PropertyDescriptor.Builder()
+ .name("Trust Store Path")
+ .displayName("Trust Store Path")
+ .description("File path for Trust Store")
+ .required(true)
+ .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .build();
+
+ public static final PropertyDescriptor TRUST_STORE_TYPE = new PropertyDescriptor.Builder()
+ .name("Trust Store Type")
+ .displayName("Trust Store Type")
+ .description("Type of Trust Store supports either JKS or PKCS12")
+ .required(true)
+ .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
+ .defaultValue("PKCS12")
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .build();
+
+ public static final PropertyDescriptor TRUST_STORE_PASSWORD = new PropertyDescriptor.Builder()
+ .name("Trust Store Password")
+ .displayName("Trust Store Password")
+ .description("Password for Trust Store")
+ .required(false)
+ .sensitive(true)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .build();
+
+ private static final List DESCRIPTORS = new ArrayList<>();
+
+ private static final String DEFAULT_SEARCH = ".*";
+
+ private static final String TRUST_STORE_LOAD_FAILED = "Trust Store [%s] loading failed";
+
+ private static final String FIND_FAILED = "Find Certificates Search [%s] failed";
+
+ static {
+ DESCRIPTORS.add(TRUST_STORE_PATH);
+ DESCRIPTORS.add(TRUST_STORE_TYPE);
+ DESCRIPTORS.add(TRUST_STORE_PASSWORD);
+ }
+
+ private KeyStore trustStore;
+
+ /**
+ * On Enabled configures Trust Store using Context properties
+ *
+ * @param context Configuration Context with properties
+ * @throws InitializationException Thrown when unable to load Trust Store
+ */
+ @OnEnabled
+ public void onEnabled(final ConfigurationContext context) throws InitializationException {
+ final String trustStoreType = context.getProperty(TRUST_STORE_TYPE).evaluateAttributeExpressions().getValue();
+ final String trustStorePath = context.getProperty(TRUST_STORE_PATH).evaluateAttributeExpressions().getValue();
+ char[] password = null;
+ final PropertyValue passwordProperty = context.getProperty(TRUST_STORE_PASSWORD);
+ if (passwordProperty.isSet()) {
+ password = passwordProperty.getValue().toCharArray();
+ }
+
+ try {
+ trustStore = KeyStoreUtils.loadTrustStore(trustStorePath, password, trustStoreType);
+ } catch (final TlsException e) {
+ final String message = String.format(TRUST_STORE_LOAD_FAILED, trustStorePath);
+ throw new InitializationException(message, e);
+ }
+ }
+
+ /**
+ * Find X.509 Certificates in Trust Store where Subject Principal matches provided search pattern
+ *
+ * @param search Search String
+ * @return Matching X.509 Certificates
+ */
+ @Override
+ public List findCertificates(final String search) {
+ final Pattern searchPattern = getSearchPattern(search);
+ try {
+ return findMatchingCertificates(searchPattern);
+ } catch (final KeyStoreException e) {
+ final String message = String.format(FIND_FAILED, search);
+ throw new ProcessException(message, e);
+ }
+ }
+
+ /**
+ * Get Supported Property Descriptors
+ *
+ * @return Supported Property Descriptors configured during initialization
+ */
+ @Override
+ protected final List getSupportedPropertyDescriptors() {
+ return DESCRIPTORS;
+ }
+
+ /**
+ * Get Search Pattern converts null to wildcard pattern and sets case insensitive matching
+ *
+ * @param search Search parameter can be null
+ * @return Search Pattern
+ */
+ private Pattern getSearchPattern(final String search) {
+ final String searchExpression = search == null ? DEFAULT_SEARCH : search;
+ return Pattern.compile(searchExpression, Pattern.CASE_INSENSITIVE);
+ }
+
+ /**
+ * Find Certificates with Subject Principal matching Search Pattern
+ *
+ * @param searchPattern Search Pattern for matching against Subject Principals
+ * @return Certificates found
+ * @throws KeyStoreException Thrown on KeyStore.getCertificate()
+ */
+ private List findMatchingCertificates(final Pattern searchPattern) throws KeyStoreException {
+ final List certificates = new ArrayList<>();
+
+ final Enumeration aliases = trustStore.aliases();
+ while (aliases.hasMoreElements()) {
+ final String alias = aliases.nextElement();
+ final Certificate certificate = trustStore.getCertificate(alias);
+ if (certificate instanceof X509Certificate) {
+ final X509Certificate trustedCertificate = (X509Certificate) certificate;
+ final X500Principal subjectPrincipal = trustedCertificate.getSubjectX500Principal();
+ final String subject = subjectPrincipal.toString();
+ final Matcher subjectMatcher = searchPattern.matcher(subject);
+ if (subjectMatcher.find()) {
+ certificates.add(trustedCertificate);
+ }
+ }
+ }
+
+ return certificates;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
new file mode 100644
index 000000000000..b4b46fcb900b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
@@ -0,0 +1,16 @@
+# 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.pki.KeyStorePrivateKeyService
+org.apache.nifi.pki.TrustStoreCertificateService
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service/src/test/java/org/apache/nifi/pki/KeyStorePrivateKeyServiceTest.java b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service/src/test/java/org/apache/nifi/pki/KeyStorePrivateKeyServiceTest.java
new file mode 100644
index 000000000000..e1706bec4ac5
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service/src/test/java/org/apache/nifi/pki/KeyStorePrivateKeyServiceTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.pki;
+
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.processor.Processor;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import javax.security.auth.x500.X500Principal;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+
+public class KeyStorePrivateKeyServiceTest {
+ private static final String SERVICE_ID = UUID.randomUUID().toString();
+
+ private static final char[] PASSWORD = SERVICE_ID.toCharArray();
+
+ private static final String KEY_ALGORITHM = "RSA";
+
+ private static final String SUBJECT_DN = "CN=subject";
+
+ private static final String SIGNING_ALGORITHM = "SHA256withRSA";
+
+ private static final int ONE_DAY = 1;
+
+ private static final String P12_EXTENSION = ".p12";
+
+ private static final String PKCS12 = "PKCS12";
+
+ private static final String ALIAS = KeyStorePrivateKeyService.class.getSimpleName();
+
+ private static final String PRIVATE_KEY_NOT_FOUND = "Private Key not found";
+
+ private static final String PRIVATE_KEY_FOUND = "Private Key found";
+
+ private static final String DELETE_FAILED = "Delete Key Store [%s] failed";
+
+ private static final String UNKNOWN = "UNKNOWN";
+
+ private TestRunner testRunner;
+
+ private KeyStorePrivateKeyService service;
+
+ private File keyStoreFile;
+
+ @Before
+ public void init() throws Exception {
+ service = new KeyStorePrivateKeyService();
+ final Processor processor = Mockito.mock(Processor.class);
+ testRunner = TestRunners.newTestRunner(processor);
+
+ keyStoreFile = File.createTempFile(KeyStorePrivateKeyServiceTest.class.getSimpleName(), P12_EXTENSION);
+ keyStoreFile.deleteOnExit();
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ if (!keyStoreFile.delete()) {
+ final String message = String.format(DELETE_FAILED, keyStoreFile.getAbsolutePath());
+ throw new IOException(message);
+ }
+ }
+
+ @Test(expected = InitializationException.class)
+ public void testOnEnabledFailed() throws InitializationException {
+ final ConfigurationContext context = Mockito.mock(ConfigurationContext.class);
+ final PropertyValue typeProperty = Mockito.mock(PropertyValue.class);
+ Mockito.when(typeProperty.evaluateAttributeExpressions()).thenReturn(typeProperty);
+ Mockito.when(typeProperty.getValue()).thenReturn(UNKNOWN);
+ Mockito.when(context.getProperty(eq(KeyStorePrivateKeyService.KEY_STORE_TYPE))).thenReturn(typeProperty);
+ Mockito.when(context.getProperty(eq(KeyStorePrivateKeyService.KEY_STORE_PATH))).thenReturn(typeProperty);
+ Mockito.when(context.getProperty(eq(KeyStorePrivateKeyService.KEY_STORE_PASSWORD))).thenReturn(typeProperty);
+ Mockito.when(context.getProperty(eq(KeyStorePrivateKeyService.KEY_PASSWORD))).thenReturn(typeProperty);
+ service.onEnabled(context);
+ }
+
+ @Test
+ public void testFindPrivateKey() throws Exception {
+ final X509Certificate certificate = setServiceProperties();
+
+ final BigInteger serialNumber = certificate.getSerialNumber();
+ final X500Principal issuer = certificate.getIssuerX500Principal();
+ final Optional privateKeyFound = service.findPrivateKey(serialNumber, issuer);
+ assertTrue(PRIVATE_KEY_NOT_FOUND, privateKeyFound.isPresent());
+ }
+
+ @Test
+ public void testFindPrivateKeyUnmatched() throws Exception {
+ final X509Certificate certificate = setServiceProperties();
+
+ final X500Principal issuer = certificate.getIssuerX500Principal();
+ final Optional privateKeyFound = service.findPrivateKey(BigInteger.ZERO, issuer);
+ assertFalse(PRIVATE_KEY_FOUND, privateKeyFound.isPresent());
+ }
+
+ private X509Certificate setServiceProperties() throws Exception {
+ final KeyStore trustStore = KeyStore.getInstance(PKCS12);
+ trustStore.load(null);
+
+ final KeyPair keyPair = KeyPairGenerator.getInstance(KEY_ALGORITHM).generateKeyPair();
+ final X509Certificate certificate = CertificateUtils.generateSelfSignedX509Certificate(keyPair, SUBJECT_DN, SIGNING_ALGORITHM, ONE_DAY);
+ final X509Certificate[] certificates = new X509Certificate[]{certificate};
+ trustStore.setKeyEntry(ALIAS, keyPair.getPrivate(), PASSWORD, certificates);
+
+ try (final OutputStream outputStream = new FileOutputStream(keyStoreFile)) {
+ trustStore.store(outputStream, PASSWORD);
+ }
+
+ testRunner.addControllerService(SERVICE_ID, service);
+
+ final String keyStorePath = keyStoreFile.getAbsolutePath();
+ testRunner.setProperty(service, KeyStorePrivateKeyService.KEY_STORE_PATH, keyStorePath);
+ testRunner.setProperty(service, KeyStorePrivateKeyService.KEY_STORE_TYPE, PKCS12);
+ testRunner.setProperty(service, KeyStorePrivateKeyService.KEY_STORE_PASSWORD, SERVICE_ID);
+ testRunner.setProperty(service, KeyStorePrivateKeyService.KEY_PASSWORD, SERVICE_ID);
+
+ testRunner.assertValid(service);
+ testRunner.enableControllerService(service);
+ return certificate;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service/src/test/java/org/apache/nifi/pki/TrustStoreCertificateServiceTest.java b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service/src/test/java/org/apache/nifi/pki/TrustStoreCertificateServiceTest.java
new file mode 100644
index 000000000000..72679e982479
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/nifi-pki-service/src/test/java/org/apache/nifi/pki/TrustStoreCertificateServiceTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.pki;
+
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.processor.Processor;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.cert.X509Certificate;
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+
+public class TrustStoreCertificateServiceTest {
+ private static final String SERVICE_ID = UUID.randomUUID().toString();
+
+ private static final char[] PASSWORD = SERVICE_ID.toCharArray();
+
+ private static final String KEY_ALGORITHM = "RSA";
+
+ private static final String SUBJECT_DN = "CN=subject";
+
+ private static final String SIGNING_ALGORITHM = "SHA256withRSA";
+
+ private static final int ONE_DAY = 1;
+
+ private static final String P12_EXTENSION = ".p12";
+
+ private static final String PKCS12 = "PKCS12";
+
+ private static final String WILDCARD_SEARCH = ".*";
+
+ private static final String UNMATCHED_SEARCH = "^$";
+
+ private static final String ALIAS = TrustStoreCertificateService.class.getSimpleName();
+
+ private static final String CERTIFICATES_NOT_FOUND = "Certificates not found";
+
+ private static final String DELETE_FAILED = "Delete Trust Store [%s] failed";
+
+ private static final String UNKNOWN = "UNKNOWN";
+
+ private TestRunner testRunner;
+
+ private TrustStoreCertificateService service;
+
+ private File trustStoreFile;
+
+ @Before
+ public void init() throws Exception {
+ service = new TrustStoreCertificateService();
+ final Processor processor = Mockito.mock(Processor.class);
+ testRunner = TestRunners.newTestRunner(processor);
+
+ trustStoreFile = File.createTempFile(TrustStoreCertificateServiceTest.class.getSimpleName(), P12_EXTENSION);
+ trustStoreFile.deleteOnExit();
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ if (!trustStoreFile.delete()) {
+ final String message = String.format(DELETE_FAILED, trustStoreFile.getAbsolutePath());
+ throw new IOException(message);
+ }
+ }
+
+ @Test(expected = InitializationException.class)
+ public void testOnEnabledFailed() throws InitializationException {
+ final ConfigurationContext context = Mockito.mock(ConfigurationContext.class);
+ final PropertyValue typeProperty = Mockito.mock(PropertyValue.class);
+ Mockito.when(typeProperty.evaluateAttributeExpressions()).thenReturn(typeProperty);
+ Mockito.when(typeProperty.getValue()).thenReturn(UNKNOWN);
+ Mockito.when(context.getProperty(eq(TrustStoreCertificateService.TRUST_STORE_TYPE))).thenReturn(typeProperty);
+ Mockito.when(context.getProperty(eq(TrustStoreCertificateService.TRUST_STORE_PATH))).thenReturn(typeProperty);
+ Mockito.when(context.getProperty(eq(TrustStoreCertificateService.TRUST_STORE_PASSWORD))).thenReturn(typeProperty);
+ service.onEnabled(context);
+ }
+
+ @Test
+ public void testFindCertificates() throws Exception {
+ final X509Certificate certificate = setServiceProperties();
+
+ final List certificates = service.findCertificates(WILDCARD_SEARCH);
+ final Iterator certificatesFound = certificates.iterator();
+ assertTrue(CERTIFICATES_NOT_FOUND, certificatesFound.hasNext());
+
+ final X509Certificate certificateFound = certificatesFound.next();
+ assertEquals(certificate, certificateFound);
+ }
+
+ @Test
+ public void testFindCertificatesUnmatched() throws Exception {
+ setServiceProperties();
+
+ final List certificates = service.findCertificates(UNMATCHED_SEARCH);
+ final Iterator certificatesFound = certificates.iterator();
+ assertFalse(CERTIFICATES_NOT_FOUND, certificatesFound.hasNext());
+ }
+
+ private X509Certificate setServiceProperties() throws Exception{
+ final KeyStore trustStore = KeyStore.getInstance(PKCS12);
+ trustStore.load(null);
+
+ final KeyPair keyPair = KeyPairGenerator.getInstance(KEY_ALGORITHM).generateKeyPair();
+ final X509Certificate certificate = CertificateUtils.generateSelfSignedX509Certificate(keyPair, SUBJECT_DN, SIGNING_ALGORITHM, ONE_DAY);
+ trustStore.setCertificateEntry(ALIAS, certificate);
+
+ try (final OutputStream outputStream = new FileOutputStream(trustStoreFile)) {
+ trustStore.store(outputStream, PASSWORD);
+ }
+
+ testRunner.addControllerService(SERVICE_ID, service);
+
+ final String trustStorePath = trustStoreFile.getAbsolutePath();
+ testRunner.setProperty(service, TrustStoreCertificateService.TRUST_STORE_PATH, trustStorePath);
+ testRunner.setProperty(service, TrustStoreCertificateService.TRUST_STORE_TYPE, PKCS12);
+ testRunner.setProperty(service, TrustStoreCertificateService.TRUST_STORE_PASSWORD, SERVICE_ID);
+
+ testRunner.assertValid(service);
+ testRunner.enableControllerService(service);
+ return certificate;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/pom.xml
new file mode 100644
index 000000000000..dfe089809a18
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-pki-service-bundle/pom.xml
@@ -0,0 +1,28 @@
+
+
+ 4.0.0
+
+ org.apache.nifi
+ nifi-standard-services
+ 1.13.0-SNAPSHOT
+
+ nifi-pki-service-bundle
+ pom
+
+ nifi-pki-service
+ nifi-pki-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 eba3d4d3a63b..cbcb8f82f72e 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
@@ -117,5 +117,11 @@
1.13.0-SNAPSHOT
compile
+
+ org.apache.nifi
+ nifi-pki-service-api
+ 1.13.0-SNAPSHOT
+ compile
+
diff --git a/nifi-nar-bundles/nifi-standard-services/pom.xml b/nifi-nar-bundles/nifi-standard-services/pom.xml
index f9c6c7d997b7..2d2b439f2db4 100644
--- a/nifi-nar-bundles/nifi-standard-services/pom.xml
+++ b/nifi-nar-bundles/nifi-standard-services/pom.xml
@@ -52,5 +52,7 @@
nifi-record-sink-api
nifi-record-sink-service-bundle
nifi-hadoop-dbcp-service-bundle
+ nifi-pki-service-api
+ nifi-pki-service-bundle