NIFI-10667 Added Private Key Service interface and implementation

- Defined PrivateKeyService in nifi-key-service-api
- Implemented StandardPrivateKeyService using Bouncy Castle library

NIFI-10667 Improved custom validation to avoid repetitive reads

NIFI-10667 Added onPropertyModified() to clear Key Reference for validation

Signed-off-by: Nathan Gough <thenatog@gmail.com>

This closes #6553.
This commit is contained in:
exceptionfactory 2022-10-19 08:52:16 -05:00 committed by Nathan Gough
parent c22181e79f
commit 9f2ad260bd
14 changed files with 742 additions and 0 deletions

View File

@ -227,6 +227,12 @@ language governing permissions and limitations under the License. -->
<version>1.19.0-SNAPSHOT</version> <version>1.19.0-SNAPSHOT</version>
<type>nar</type> <type>nar</type>
</dependency> </dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-key-service-nar</artifactId>
<version>1.19.0-SNAPSHOT</version>
<type>nar</type>
</dependency>
<dependency> <dependency>
<groupId>org.apache.nifi</groupId> <groupId>org.apache.nifi</groupId>
<artifactId>nifi-distributed-cache-services-nar</artifactId> <artifactId>nifi-distributed-cache-services-nar</artifactId>

View File

@ -0,0 +1,29 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<!--
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.
-->
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-standard-services</artifactId>
<version>1.19.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-key-service-api</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -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();
}

View File

@ -0,0 +1,37 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<!--
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.
-->
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-key-service-bundle</artifactId>
<version>1.19.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-key-service-nar</artifactId>
<packaging>nar</packaging>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-standard-services-api-nar</artifactId>
<version>1.19.0-SNAPSHOT</version>
<type>nar</type>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-key-service</artifactId>
<version>1.19.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,51 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<!--
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.
-->
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-key-service-bundle</artifactId>
<version>1.19.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-key-service</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-key-service-api</artifactId>
<version>1.19.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-utils</artifactId>
<version>1.19.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-mock</artifactId>
<version>1.19.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -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<PropertyDescriptor> 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<PrivateKey> 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<PropertyDescriptor> getSupportedPropertyDescriptors() {
return DESCRIPTORS;
}
/**
* Custom Validate reads key using configured password for encrypted keys
*
* @param context Validation Context
* @return Validation Results
*/
@Override
protected Collection<ValidationResult> customValidate(final ValidationContext context) {
final Collection<ValidationResult> 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[]{};
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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

View File

@ -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();
}
}

View File

@ -0,0 +1,28 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<!--
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.
-->
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-standard-services</artifactId>
<version>1.19.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-key-service-bundle</artifactId>
<packaging>pom</packaging>
<modules>
<module>nifi-key-service</module>
<module>nifi-key-service-nar</module>
</modules>
</project>

View File

@ -133,5 +133,11 @@
<version>1.19.0-SNAPSHOT</version> <version>1.19.0-SNAPSHOT</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-key-service-api</artifactId>
<version>1.19.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -48,6 +48,8 @@
<module>nifi-kerberos-credentials-service-bundle</module> <module>nifi-kerberos-credentials-service-bundle</module>
<module>nifi-proxy-configuration-api</module> <module>nifi-proxy-configuration-api</module>
<module>nifi-proxy-configuration-bundle</module> <module>nifi-proxy-configuration-bundle</module>
<module>nifi-key-service-api</module>
<module>nifi-key-service-bundle</module>
<module>nifi-rules-engine-service-api</module> <module>nifi-rules-engine-service-api</module>
<module>nifi-record-sink-api</module> <module>nifi-record-sink-api</module>
<module>nifi-record-sink-service-bundle</module> <module>nifi-record-sink-service-bundle</module>