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