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