mirror of https://github.com/apache/nifi.git
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:
parent
c22181e79f
commit
9f2ad260bd
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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();
|
||||||
|
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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[]{};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue