NIFI-14001 Added Framework Support for PEM Keys and Certificates (#9517)

This commit is contained in:
David Handermann 2024-11-19 10:38:15 -06:00 committed by GitHub
parent 6f941bd622
commit e33cbe6df5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1449 additions and 54 deletions

View File

@ -141,8 +141,11 @@ public class NiFiProperties extends ApplicationProperties {
public static final String SECURITY_KEYSTORE = "nifi.security.keystore";
public static final String SECURITY_KEYSTORE_TYPE = "nifi.security.keystoreType";
public static final String SECURITY_KEYSTORE_PASSWD = "nifi.security.keystorePasswd";
public static final String SECURITY_KEYSTORE_PRIVATE_KEY = "nifi.security.keystore.privateKey";
public static final String SECURITY_KEYSTORE_CERTIFICATE = "nifi.security.keystore.certificate";
public static final String SECURITY_KEY_PASSWD = "nifi.security.keyPasswd";
public static final String SECURITY_TRUSTSTORE = "nifi.security.truststore";
public static final String SECURITY_TRUSTSTORE_CERTIFICATE = "nifi.security.truststore.certificate";
public static final String SECURITY_TRUSTSTORE_TYPE = "nifi.security.truststoreType";
public static final String SECURITY_TRUSTSTORE_PASSWD = "nifi.security.truststorePasswd";
public static final String SECURITY_AUTO_RELOAD_ENABLED = "nifi.security.autoreload.enabled";

View File

@ -0,0 +1,32 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.security.ssl;
import java.io.InputStream;
/**
* Extension of Key Store Builder supporting reading from an Input Stream
*/
public interface InputStreamKeyStoreBuilder extends KeyStoreBuilder {
/**
* Set Key Store InputStream to be loaded
*
* @param inputStream Key Store InputStream
* @return Builder
*/
KeyStoreBuilder inputStream(InputStream inputStream);
}

View File

@ -0,0 +1,92 @@
/*
* 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.security.ssl;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.util.List;
import java.util.Objects;
/**
* Key Store Builder capable of reading one or more X.509 Certificates formatted with PEM headers and footers
*/
public class PemCertificateKeyStoreBuilder implements InputStreamKeyStoreBuilder {
private static final String CERTIFICATE_ALIAS = "certificate-%d";
private InputStream inputStream;
/**
* Build Key Store using configured properties
*
* @return Key Store
*/
@Override
public KeyStore build() {
final KeyStore keyStore = getInitializedKeyStore();
if (inputStream == null) {
throw new BuilderConfigurationException("Key Store InputStream not configured");
}
loadKeyStore(keyStore);
return keyStore;
}
/**
* Set Key Store InputStream to be loaded
*
* @param inputStream Key Store InputStream
* @return Builder
*/
@Override
public PemCertificateKeyStoreBuilder inputStream(final InputStream inputStream) {
this.inputStream = Objects.requireNonNull(inputStream, "Key Store InputStream required");
return this;
}
private void loadKeyStore(final KeyStore keyStore) {
final PemCertificateReader pemCertificateReader = new StandardPemCertificateReader();
final List<Certificate> certificates = pemCertificateReader.readCertificates(inputStream);
int certificateIndex = 0;
for (final Certificate certificate : certificates) {
final String alias = CERTIFICATE_ALIAS.formatted(certificateIndex++);
try {
keyStore.setCertificateEntry(alias, certificate);
} catch (final KeyStoreException e) {
final String message = String.format("Set certificate entry [%s] failed", alias);
throw new BuilderConfigurationException(message, e);
}
}
}
private KeyStore getInitializedKeyStore() {
final String keyStoreType = KeyStore.getDefaultType();
try {
final KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null);
return keyStore;
} catch (final Exception e) {
final String message = String.format("Key Store Type [%s] initialization failed", keyStoreType);
throw new BuilderConfigurationException(message, e);
}
}
}

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.security.ssl;
import java.io.InputStream;
import java.security.cert.Certificate;
import java.util.List;
/**
* Abstraction for reading one or more Certificates from a stream containing PEM headers and footers
*/
interface PemCertificateReader {
/**
* Read Certificates from stream of PEM sections
*
* @param inputStream Input Stream required
* @return Parsed certificates or empty when none found
*/
List<Certificate> readCertificates(InputStream inputStream);
}

View File

@ -0,0 +1,108 @@
/*
* 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.security.ssl;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.util.List;
import java.util.Objects;
/**
* Key Store Builder capable of reading a Private Key and one or more X.509 Certificates formatted with PEM headers and footers
*/
public class PemPrivateKeyCertificateKeyStoreBuilder implements KeyStoreBuilder {
private static final String ALIAS = "private-key-1";
private static final char[] EMPTY_PROTECTION_PARAMETER = new char[]{};
private InputStream privateKeyInputStream;
private InputStream certificateInputStream;
/**
* Build Key Store using configured properties
*
* @return Key Store
*/
@Override
public KeyStore build() {
final KeyStore keyStore = getInitializedKeyStore();
if (certificateInputStream == null) {
throw new BuilderConfigurationException("Certificate InputStream not configured");
}
if (privateKeyInputStream == null) {
throw new BuilderConfigurationException("Private Key InputStream not configured");
}
loadKeyStore(keyStore);
return keyStore;
}
/**
* Set Certificate InputStream to be loaded
*
* @param certificateInputStream Certificate InputStream
* @return Builder
*/
public PemPrivateKeyCertificateKeyStoreBuilder certificateInputStream(final InputStream certificateInputStream) {
this.certificateInputStream = Objects.requireNonNull(certificateInputStream, "Certificate InputStream required");
return this;
}
/**
* Set Private Key InputStream to be loaded
*
* @param privateKeyInputStream Private Key InputStream
* @return Builder
*/
public PemPrivateKeyCertificateKeyStoreBuilder privateKeyInputStream(final InputStream privateKeyInputStream) {
this.privateKeyInputStream = Objects.requireNonNull(privateKeyInputStream, "Private Key InputStream required");
return this;
}
private void loadKeyStore(final KeyStore keyStore) {
final PemCertificateReader pemCertificateReader = new StandardPemCertificateReader();
final List<Certificate> certificates = pemCertificateReader.readCertificates(certificateInputStream);
final Certificate[] certificateChain = certificates.toArray(new Certificate[]{});
final PemPrivateKeyReader pemPrivateKeyReader = new StandardPemPrivateKeyReader();
final PrivateKey privateKey = pemPrivateKeyReader.readPrivateKey(privateKeyInputStream);
try {
keyStore.setKeyEntry(ALIAS, privateKey, EMPTY_PROTECTION_PARAMETER, certificateChain);
} catch (final KeyStoreException e) {
throw new BuilderConfigurationException("Set key entry failed", e);
}
}
private KeyStore getInitializedKeyStore() {
final String keyStoreType = KeyStore.getDefaultType();
try {
final KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null);
return keyStore;
} catch (final Exception e) {
final String message = String.format("Key Store Type [%s] initialization failed", keyStoreType);
throw new BuilderConfigurationException(message, e);
}
}
}

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.security.ssl;
import java.io.InputStream;
import java.security.PrivateKey;
/**
* Abstraction for reading a Private Key from a stream containing PEM headers and footers
*/
interface PemPrivateKeyReader {
/**
* Read Private Key from stream
*
* @param inputStream Stream containing PEM header and footer
* @return Private Key
*/
PrivateKey readPrivateKey(InputStream inputStream);
}

View File

@ -0,0 +1,41 @@
/*
* 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.security.ssl;
/**
* Exception indicating runtime failure to read specified entity
*/
public class ReadEntityException extends RuntimeException {
/**
* Read Entity Exception Constructor with standard properties
*
* @param message Exception Message
* @param cause Exception Cause
*/
public ReadEntityException(final String message, final Throwable cause) {
super(message, cause);
}
/**
* Read Entity Exception Constructor without Throwable cause
*
* @param message Exception Message
*/
public ReadEntityException(final String message) {
super(message);
}
}

View File

@ -28,7 +28,7 @@ import java.util.Objects;
/**
* Standard implementation of Key Store Builder
*/
public class StandardKeyStoreBuilder implements KeyStoreBuilder {
public class StandardKeyStoreBuilder implements InputStreamKeyStoreBuilder {
private Provider provider;
private String type = KeyStore.getDefaultType();
@ -98,6 +98,7 @@ public class StandardKeyStoreBuilder implements KeyStoreBuilder {
* @param inputStream Key Store InputStream
* @return Builder
*/
@Override
public StandardKeyStoreBuilder inputStream(final InputStream inputStream) {
this.inputStream = Objects.requireNonNull(inputStream, "Key Store InputStream required");
return this;

View File

@ -0,0 +1,115 @@
/*
* 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.security.ssl;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Standard implementation of PEM Certificate Reader using X.509 Certificate Factory
*/
class StandardPemCertificateReader implements PemCertificateReader {
static final String CERTIFICATE_HEADER = "-----BEGIN CERTIFICATE-----";
static final String CERTIFICATE_FOOTER = "-----END CERTIFICATE-----";
private static final String CERTIFICATE_FACTORY_TYPE = "X.509";
private static final char LINE_FEED = 10;
private static final Charset CERTIFICATE_CHARACTER_SET = StandardCharsets.US_ASCII;
/**
* Read Certificates from stream of PEM sections
*
* @param inputStream Input Stream required
* @return Parsed certificates or empty when none found
*/
@Override
public List<Certificate> readCertificates(final InputStream inputStream) {
Objects.requireNonNull(inputStream, "Input Stream required");
final CertificateFactory certificateFactory = getCertificateFactory();
final List<Certificate> certificates = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, CERTIFICATE_CHARACTER_SET))) {
String line = reader.readLine();
while (line != null) {
if (CERTIFICATE_HEADER.contentEquals(line)) {
final Certificate certificate = readCertificate(reader, certificateFactory);
certificates.add(certificate);
}
line = reader.readLine();
}
} catch (final IOException e) {
throw new ReadEntityException("Read certificates failed", e);
}
return certificates;
}
private Certificate readCertificate(final BufferedReader reader, final CertificateFactory certificateFactory) throws IOException {
final StringBuilder builder = new StringBuilder();
builder.append(CERTIFICATE_HEADER);
builder.append(LINE_FEED);
String line = reader.readLine();
while (line != null) {
if (CERTIFICATE_FOOTER.contentEquals(line)) {
builder.append(CERTIFICATE_FOOTER);
builder.append(LINE_FEED);
break;
} else {
builder.append(line);
builder.append(LINE_FEED);
}
line = reader.readLine();
}
final String certificate = builder.toString();
final byte[] certificateBytes = certificate.getBytes(CERTIFICATE_CHARACTER_SET);
final InputStream certificateStream = new ByteArrayInputStream(certificateBytes);
try {
return certificateFactory.generateCertificate(certificateStream);
} catch (final CertificateException e) {
throw new ReadEntityException("Certificate parsing failed", e);
}
}
private CertificateFactory getCertificateFactory() {
try {
return CertificateFactory.getInstance(CERTIFICATE_FACTORY_TYPE);
} catch (final CertificateException e) {
final String message = String.format("Certificate Factory Type [%s] creation failed", CERTIFICATE_FACTORY_TYPE);
throw new BuilderConfigurationException(message, e);
}
}
}

View File

@ -0,0 +1,260 @@
/*
* 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.security.ssl;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.HexFormat;
import java.util.Objects;
/**
* Standard implementation of PEM Private Key Reader supporting PKCS1 and PKCS8
*/
class StandardPemPrivateKeyReader implements PemPrivateKeyReader {
static final String RSA_PRIVATE_KEY_HEADER = "-----BEGIN RSA PRIVATE KEY-----";
static final String RSA_PRIVATE_KEY_FOOTER = "-----END RSA PRIVATE KEY-----";
static final String PRIVATE_KEY_HEADER = "-----BEGIN PRIVATE KEY-----";
static final String PRIVATE_KEY_FOOTER = "-----END PRIVATE KEY-----";
private static final Charset KEY_CHARACTER_SET = StandardCharsets.US_ASCII;
private static final PrivateKeyAlgorithmReader privateKeyAlgorithmReader = new PrivateKeyAlgorithmReader();
private static final Base64.Decoder decoder = Base64.getDecoder();
/**
* Read Private from PKCS1 or PKCS8 sources with supported algorithms including ECDSA, Ed25519, and RSA
*
* @param inputStream Stream containing PEM header and footer
* @return Parsed Private Key
*/
@Override
public PrivateKey readPrivateKey(final InputStream inputStream) {
Objects.requireNonNull(inputStream, "Input Stream required");
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, KEY_CHARACTER_SET))) {
final PrivateKey privateKey;
final String line = reader.readLine();
if (PRIVATE_KEY_HEADER.contentEquals(line)) {
final String privateKeyPayload = readPrivateKeyPayload(reader, PRIVATE_KEY_FOOTER);
privateKey = readPkcs8PrivateKey(privateKeyPayload);
} else if (RSA_PRIVATE_KEY_HEADER.contentEquals(line)) {
final String privateKeyPayload = readPrivateKeyPayload(reader, RSA_PRIVATE_KEY_FOOTER);
privateKey = readPkcs1PrivateKey(privateKeyPayload);
} else {
throw new ReadEntityException("Supported Private Key header not found");
}
return privateKey;
} catch (final IOException e) {
throw new ReadEntityException("Read Private Key failed", e);
} catch (final GeneralSecurityException e) {
throw new ReadEntityException("Parsing Private Key failed", e);
}
}
private PrivateKey readPkcs1PrivateKey(final String privateKeyPayload) throws GeneralSecurityException {
final byte[] privateKeyDecoded = decoder.decode(privateKeyPayload);
final PrivateKey encodedPrivateKey = new PKCS1EncodedPrivateKey(privateKeyDecoded);
final KeyFactory keyFactory = KeyFactory.getInstance(encodedPrivateKey.getAlgorithm());
return (PrivateKey) keyFactory.translateKey(encodedPrivateKey);
}
private PrivateKey readPkcs8PrivateKey(final String privateKeyPayload) throws GeneralSecurityException {
final byte[] privateKeyDecoded = decoder.decode(privateKeyPayload);
final ByteBuffer privateKeyBuffer = ByteBuffer.wrap(privateKeyDecoded);
final String keyAlgorithm = privateKeyAlgorithmReader.getAlgorithm(privateKeyBuffer);
final KeyFactory keyFactory = KeyFactory.getInstance(keyAlgorithm);
final PKCS8EncodedKeySpec encodedKeySpec = new PKCS8EncodedKeySpec(privateKeyDecoded);
return keyFactory.generatePrivate(encodedKeySpec);
}
private String readPrivateKeyPayload(final BufferedReader reader, final String footer) throws IOException {
final StringBuilder builder = new StringBuilder();
String line = reader.readLine();
while (line != null) {
if (footer.contentEquals(line)) {
break;
} else {
builder.append(line);
}
line = reader.readLine();
}
return builder.toString();
}
static class PrivateKeyAlgorithmReader {
private static final int DER_TAG_MASK = 0x1F;
private static final int DER_LENGTH_MASK = 0xFF;
private static final int DER_RESERVED_LENGTH_MASK = 0x7F;
private static final int DER_LENGTH_BITS = 8;
private static final int DER_INDEFINITE_LENGTH = 0x80;
private static final byte SEQUENCE_DER_TAG_TYPE = 0x10;
private static final byte INTEGER_DER_TAG_TYPE = 0x02;
enum ObjectIdentifier {
/** ECDSA Object Identifier 1.2.840.10045.2.1 */
ECDSA("2a8648ce3d0201", "EC"),
/** Ed25519 Object Identifier 1.3.101.112 */
ED25519("2b6570", "Ed25519"),
/** RSA Object Identifier 1.2.840.113549.1.1.1 */
RSA("2a864886f70d010101", "RSA");
private final String encoded;
private final String algorithm;
ObjectIdentifier(final String encoded, final String algorithm) {
this.encoded = encoded;
this.algorithm = algorithm;
}
}
private String getAlgorithm(final ByteBuffer privateKeyDecoded) throws UnrecoverableKeyException {
final String objectIdentifierEncoded = readObjectIdentifierEncoded(privateKeyDecoded);
String keyAlgorithm = null;
for (final ObjectIdentifier objectIdentifier : ObjectIdentifier.values()) {
if (objectIdentifier.encoded.contentEquals(objectIdentifierEncoded)) {
keyAlgorithm = objectIdentifier.algorithm;
break;
}
}
if (keyAlgorithm == null) {
throw new UnrecoverableKeyException("PKCS8 Algorithm Identifier not supported [%s]".formatted(objectIdentifierEncoded));
}
return keyAlgorithm;
}
private String readObjectIdentifierEncoded(final ByteBuffer buffer) throws UnrecoverableKeyException {
final byte derTagEncoded = buffer.get();
final int derTagType = derTagEncoded & DER_TAG_MASK;
final String objectIdentifier;
if (SEQUENCE_DER_TAG_TYPE == derTagType) {
final int sequenceLength = readDerLength(buffer);
if (sequenceLength == buffer.remaining()) {
final byte versionTagType = buffer.get();
if (INTEGER_DER_TAG_TYPE == versionTagType) {
// Read Private Key Information Version
buffer.get();
buffer.get();
// Read Sequence Tag Type
buffer.get();
// Read Algorithm Identifier Tag Type and Length
buffer.get();
buffer.get();
final int algorithmIdentifierLength = readDerLength(buffer);
final byte[] algorithmIdentifierEncoded = new byte[algorithmIdentifierLength];
buffer.get(algorithmIdentifierEncoded);
objectIdentifier = HexFormat.of().formatHex(algorithmIdentifierEncoded);
} else {
throw new UnrecoverableKeyException("PKCS8 DER Version Tag not found");
}
} else {
throw new UnrecoverableKeyException("PKCS8 DER Sequence Length not valid");
}
} else {
throw new UnrecoverableKeyException("PKCS8 DER Sequence Tag not found");
}
return objectIdentifier;
}
private int readDerLength(final ByteBuffer buffer) {
final int derLength;
final byte lengthEncoded = buffer.get();
final int initialByteLength = lengthEncoded & DER_INDEFINITE_LENGTH;
if (initialByteLength == 0) {
derLength = lengthEncoded & DER_RESERVED_LENGTH_MASK;
} else {
int lengthBytes = lengthEncoded & DER_RESERVED_LENGTH_MASK;
int sequenceLength = 0;
for (int i = 0; i < lengthBytes; i++) {
sequenceLength <<= DER_LENGTH_BITS;
sequenceLength |= buffer.get() & DER_LENGTH_MASK;
}
derLength = sequenceLength;
}
return derLength;
}
}
static class PKCS1EncodedPrivateKey implements PrivateKey {
private static final String PKCS1_FORMAT = "PKCS#1";
private static final String RSA_ALGORITHM = "RSA";
private final byte[] encoded;
private PKCS1EncodedPrivateKey(final byte[] encoded) {
this.encoded = encoded;
}
@Override
public String getAlgorithm() {
return RSA_ALGORITHM;
}
@Override
public String getFormat() {
return PKCS1_FORMAT;
}
@Override
public byte[] getEncoded() {
return encoded.clone();
}
}
}

View File

@ -0,0 +1,72 @@
/*
* 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.security.ssl;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.util.Enumeration;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PemCertificateKeyStoreBuilderTest {
private static Certificate certificate;
@BeforeAll
static void setTrustStore() throws Exception {
final KeyStore trustStore = StandardPemCertificateReaderTest.loadDefaultTrustStore();
final Enumeration<String> aliases = trustStore.aliases();
final String alias = aliases.nextElement();
certificate = trustStore.getCertificate(alias);
}
@Test
void testBuild() throws GeneralSecurityException {
final PemCertificateKeyStoreBuilder builder = new PemCertificateKeyStoreBuilder();
final String certificateFormatted = StandardPemCertificateReaderTest.getCertificateFormatted(certificate);
final byte[] certificatePem = certificateFormatted.getBytes(StandardCharsets.US_ASCII);
builder.inputStream(new ByteArrayInputStream(certificatePem));
final KeyStore keyStore = builder.build();
assertNotNull(keyStore);
assertCertificateEntryFound(keyStore);
}
private void assertCertificateEntryFound(final KeyStore keyStore) throws GeneralSecurityException {
final Enumeration<String> aliases = keyStore.aliases();
assertTrue(aliases.hasMoreElements());
final String alias = aliases.nextElement();
final Certificate certificateFound = keyStore.getCertificate(alias);
assertEquals(certificate, certificateFound);
assertFalse(aliases.hasMoreElements());
}
}

View File

@ -0,0 +1,87 @@
/*
* 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.security.ssl;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.util.Enumeration;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PemPrivateKeyCertificateKeyStoreBuilderTest {
private static final int RSA_KEY_SIZE = 3072;
private static final String RSA_ALGORITHM = "RSA";
private static Certificate certificate;
@BeforeAll
static void setTrustStore() throws Exception {
final KeyStore trustStore = StandardPemCertificateReaderTest.loadDefaultTrustStore();
final Enumeration<String> aliases = trustStore.aliases();
final String alias = aliases.nextElement();
certificate = trustStore.getCertificate(alias);
}
@Test
void testBuild() throws GeneralSecurityException {
final PemPrivateKeyCertificateKeyStoreBuilder builder = new PemPrivateKeyCertificateKeyStoreBuilder();
final String privateKeyEncoded = StandardPemPrivateKeyReaderTest.getRsaPrivateKeyEncoded(RSA_KEY_SIZE);
builder.privateKeyInputStream(new ByteArrayInputStream(privateKeyEncoded.getBytes(StandardCharsets.US_ASCII)));
final String certificateFormatted = StandardPemCertificateReaderTest.getCertificateFormatted(certificate);
final byte[] certificatePem = certificateFormatted.getBytes(StandardCharsets.US_ASCII);
builder.certificateInputStream(new ByteArrayInputStream(certificatePem));
final KeyStore keyStore = builder.build();
assertNotNull(keyStore);
assertKeyEntryFound(keyStore);
}
private void assertKeyEntryFound(final KeyStore keyStore) throws GeneralSecurityException {
final Enumeration<String> aliases = keyStore.aliases();
assertTrue(aliases.hasMoreElements());
final String alias = aliases.nextElement();
final Certificate[] certificateChain = keyStore.getCertificateChain(alias);
assertNotNull(certificateChain);
final Certificate certificateFound = certificateChain[0];
assertEquals(certificate, certificateFound);
final Key key = keyStore.getKey(alias, null);
assertNotNull(key);
assertEquals(RSA_ALGORITHM, key.getAlgorithm());
assertFalse(aliases.hasMoreElements());
}
}

View File

@ -0,0 +1,173 @@
/*
* 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.security.ssl;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.util.Base64;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class StandardPemCertificateReaderTest {
private static final String JAVA_HOME_PROPERTY = "java.home";
private static final String TRUST_STORE_PATH = "lib/security/cacerts";
private static final String LINE_LENGTH_PATTERN = "(?<=\\G.{64})";
private static final char LINE_FEED = 10;
private static Certificate firstCertificate;
private static byte[] firstCertificatePem;
private static Certificate secondCertificate;
private static byte[] certificatesPem;
private StandardPemCertificateReader reader;
@BeforeAll
static void setTrustStore() throws Exception {
final KeyStore trustStore = loadDefaultTrustStore();
final Enumeration<String> aliases = trustStore.aliases();
final String firstAlias = aliases.nextElement();
firstCertificate = trustStore.getCertificate(firstAlias);
final String firstCertificateFormatted = getCertificateFormatted(firstCertificate);
firstCertificatePem = firstCertificateFormatted.getBytes(StandardCharsets.US_ASCII);
final String secondAlias = aliases.nextElement();
secondCertificate = trustStore.getCertificate(secondAlias);
final String secondCertificateFormatted = getCertificateFormatted(secondCertificate);
String certificatesCombined = firstCertificateFormatted + LINE_FEED + secondCertificateFormatted;
certificatesPem = certificatesCombined.getBytes(StandardCharsets.US_ASCII);
}
@BeforeEach
void setReader() {
reader = new StandardPemCertificateReader();
}
@Test
void testReadCertificates() {
final List<Certificate> certificates = reader.readCertificates(new ByteArrayInputStream(certificatesPem));
assertNotNull(certificates);
assertFalse(certificates.isEmpty());
final Iterator<Certificate> certificatesRead = certificates.iterator();
assertTrue(certificatesRead.hasNext());
final Certificate firstCertificateRead = certificatesRead.next();
assertEquals(firstCertificate, firstCertificateRead);
assertTrue(certificatesRead.hasNext());
final Certificate secondCertificateRead = certificatesRead.next();
assertEquals(secondCertificate, secondCertificateRead);
assertFalse(certificatesRead.hasNext());
}
@Test
void testReadCertificatesOneFound() {
final List<Certificate> certificates = reader.readCertificates(new ByteArrayInputStream(firstCertificatePem));
assertNotNull(certificates);
assertFalse(certificates.isEmpty());
final Iterator<Certificate> certificatesRead = certificates.iterator();
assertTrue(certificatesRead.hasNext());
final Certificate firstCertificateRead = certificatesRead.next();
assertEquals(firstCertificate, firstCertificateRead);
assertFalse(certificatesRead.hasNext());
}
@Test
void testReadCertificatesEmpty() {
final List<Certificate> certificates = reader.readCertificates(new ByteArrayInputStream(new byte[]{}));
assertNotNull(certificates);
assertTrue(certificates.isEmpty());
}
@Test
void testReadCertificatesHeaderException() {
final byte[] encoded = StandardPemCertificateReader.CERTIFICATE_HEADER.getBytes(StandardCharsets.US_ASCII);
assertThrows(ReadEntityException.class, () -> reader.readCertificates(new ByteArrayInputStream(encoded)));
}
static String getCertificateFormatted(final Certificate certificate) throws CertificateEncodingException {
final byte[] certificateBinary = certificate.getEncoded();
final String certificateEncoded = Base64.getEncoder().encodeToString(certificateBinary);
final String[] certificateLines = certificateEncoded.split(LINE_LENGTH_PATTERN);
final StringBuilder certificateBuilder = new StringBuilder();
certificateBuilder.append(StandardPemCertificateReader.CERTIFICATE_HEADER);
certificateBuilder.append(LINE_FEED);
for (final String line : certificateLines) {
certificateBuilder.append(line);
certificateBuilder.append(LINE_FEED);
}
certificateBuilder.append(StandardPemCertificateReader.CERTIFICATE_FOOTER);
certificateBuilder.append(LINE_FEED);
return certificateBuilder.toString();
}
static KeyStore loadDefaultTrustStore() throws GeneralSecurityException, IOException {
final String javaHomeProperty = System.getProperty(JAVA_HOME_PROPERTY);
final Path javaHomeDirectory = Paths.get(javaHomeProperty);
final Path trustStorePath = javaHomeDirectory.resolve(TRUST_STORE_PATH);
final String trustStoreType = KeyStore.getDefaultType();
final KeyStore trustStore = KeyStore.getInstance(trustStoreType);
try (InputStream inputStream = Files.newInputStream(trustStorePath)) {
trustStore.load(inputStream, null);
}
return trustStore;
}
}

View File

@ -0,0 +1,213 @@
/*
* 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.security.ssl;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.spec.ECGenParameterSpec;
import java.util.Base64;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
class StandardPemPrivateKeyReaderTest {
private static final String RSA_ALGORITHM = "RSA";
private static final int RSA_KEY_SIZE = 3072;
private static final String ED25519_ALGORITHM = "Ed25519";
private static final String EDDSA_ALGORITHM = "EdDSA";
private static final String EC_ALGORITHM = "EC";
private static final String NIST_CURVE_P_256 = "secp256r1";
private static final String NIST_CURVE_P_384 = "secp384r1";
private static final String NIST_CURVE_P_512 = "secp521r1";
private static final String CERTIFICATE_HEADER = "-----BEGIN CERTIFICATE-----";
private static final String LINE_LENGTH_PATTERN = "(?<=\\G.{64})";
private static final char LINE_FEED = 10;
private static final int PKCS8_RSA_DER_HEADER_LENGTH = 26;
private static final Base64.Encoder encoder = Base64.getEncoder();
private static String PKCS1_RSA_PRIVATE_KEY_ENCODED;
private static String PKCS8_ED25519_PRIVATE_KEY_ENCODED;
private final StandardPemPrivateKeyReader reader = new StandardPemPrivateKeyReader();
@BeforeAll
static void setPrivateKey() throws Exception {
final KeyPairGenerator rsaKeyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM);
rsaKeyPairGenerator.initialize(RSA_KEY_SIZE);
final KeyPair rsaKeyPair = rsaKeyPairGenerator.generateKeyPair();
final PrivateKey rsaPrivateKey = rsaKeyPair.getPrivate();
final byte[] rsaPrivateKeyEncoded = rsaPrivateKey.getEncoded();
PKCS1_RSA_PRIVATE_KEY_ENCODED = getRsaPrivateKeyPemEncoded(rsaPrivateKeyEncoded);
final KeyPairGenerator ed25519KeyPairGenerator = KeyPairGenerator.getInstance(ED25519_ALGORITHM);
final KeyPair ed25519KeyPair = ed25519KeyPairGenerator.generateKeyPair();
final PrivateKey ed25519PrivateKey = ed25519KeyPair.getPrivate();
final byte[] ed25519PrivateKeyEncoded = ed25519PrivateKey.getEncoded();
PKCS8_ED25519_PRIVATE_KEY_ENCODED = getPrivateKeyPemEncoded(ed25519PrivateKeyEncoded);
}
@Test
void testReadPrivateKeyHeaderException() {
final InputStream inputStream = new ByteArrayInputStream(CERTIFICATE_HEADER.getBytes(StandardCharsets.US_ASCII));
assertThrows(ReadEntityException.class, () -> reader.readPrivateKey(inputStream));
}
@Test
void testReadPrivateKeyPkcs8NotSupportedException() {
final String privateKeyEncoded = getPrivateKeyPemEncoded(String.class.getName().getBytes(StandardCharsets.US_ASCII));
final InputStream inputStream = new ByteArrayInputStream(privateKeyEncoded.getBytes(StandardCharsets.US_ASCII));
final ReadEntityException exception = assertThrows(ReadEntityException.class, () -> reader.readPrivateKey(inputStream));
assertInstanceOf(UnrecoverableKeyException.class, exception.getCause());
}
@Test
void testReadPrivateKeyPkcs1Rsa() {
final InputStream inputStream = new ByteArrayInputStream(PKCS1_RSA_PRIVATE_KEY_ENCODED.getBytes(StandardCharsets.US_ASCII));
final PrivateKey privateKey = reader.readPrivateKey(inputStream);
assertNotNull(privateKey);
assertEquals(RSA_ALGORITHM, privateKey.getAlgorithm());
}
@ParameterizedTest
@ValueSource(ints = {2048, 3072})
void testReadPrivateKeyPkcs8Rsa(final int keySize) throws GeneralSecurityException {
final String privateKeyEncoded = getRsaPrivateKeyEncoded(keySize);
final InputStream inputStream = new ByteArrayInputStream(privateKeyEncoded.getBytes(StandardCharsets.US_ASCII));
final PrivateKey privateKey = reader.readPrivateKey(inputStream);
assertNotNull(privateKey);
assertEquals(RSA_ALGORITHM, privateKey.getAlgorithm());
}
@Test
void testReadPrivateKeyPkcs8Ed25519() {
final InputStream inputStream = new ByteArrayInputStream(PKCS8_ED25519_PRIVATE_KEY_ENCODED.getBytes(StandardCharsets.US_ASCII));
final PrivateKey privateKey = reader.readPrivateKey(inputStream);
assertNotNull(privateKey);
assertEquals(EDDSA_ALGORITHM, privateKey.getAlgorithm());
}
@ParameterizedTest
@ValueSource(strings = {NIST_CURVE_P_256, NIST_CURVE_P_384, NIST_CURVE_P_512})
void testReadPrivateKeyPkcs8EllipticCurve(final String curveName) throws GeneralSecurityException {
final String privateKeyEncoded = getEllipticCurvePrivateKeyEncoded(curveName);
final InputStream inputStream = new ByteArrayInputStream(privateKeyEncoded.getBytes(StandardCharsets.US_ASCII));
final PrivateKey privateKey = reader.readPrivateKey(inputStream);
assertNotNull(privateKey);
assertEquals(EC_ALGORITHM, privateKey.getAlgorithm());
}
static String getRsaPrivateKeyEncoded(final int keySize) throws GeneralSecurityException {
final KeyPairGenerator rsaKeyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM);
rsaKeyPairGenerator.initialize(keySize);
final KeyPair rsaKeyPair = rsaKeyPairGenerator.generateKeyPair();
final PrivateKey rsaPrivateKey = rsaKeyPair.getPrivate();
final byte[] rsaPrivateKeyEncoded = rsaPrivateKey.getEncoded();
return getPrivateKeyPemEncoded(rsaPrivateKeyEncoded);
}
private String getEllipticCurvePrivateKeyEncoded(final String curveName) throws GeneralSecurityException {
final KeyPairGenerator ecKeyPairGenerator = KeyPairGenerator.getInstance(EC_ALGORITHM);
ecKeyPairGenerator.initialize(new ECGenParameterSpec(curveName));
final KeyPair ecKeyPair = ecKeyPairGenerator.generateKeyPair();
final PrivateKey ecPrivateKey = ecKeyPair.getPrivate();
final byte[] ecPrivateKeyEncoded = ecPrivateKey.getEncoded();
return getPrivateKeyPemEncoded(ecPrivateKeyEncoded);
}
private static String getRsaPrivateKeyPemEncoded(final byte[] privateKeyEncoded) {
final int rsaPrivateKeyLength = privateKeyEncoded.length - PKCS8_RSA_DER_HEADER_LENGTH;
final byte[] rsaPrivateKey = new byte[rsaPrivateKeyLength];
System.arraycopy(privateKeyEncoded, PKCS8_RSA_DER_HEADER_LENGTH, rsaPrivateKey, 0, rsaPrivateKey.length);
final String formatted = encoder.encodeToString(rsaPrivateKey);
final String[] lines = formatted.split(LINE_LENGTH_PATTERN);
final StringBuilder builder = new StringBuilder();
builder.append(StandardPemPrivateKeyReader.RSA_PRIVATE_KEY_HEADER);
builder.append(LINE_FEED);
for (final String line : lines) {
builder.append(line);
builder.append(LINE_FEED);
}
builder.append(StandardPemPrivateKeyReader.RSA_PRIVATE_KEY_FOOTER);
builder.append(LINE_FEED);
return builder.toString();
}
private static String getPrivateKeyPemEncoded(final byte[] privateKeyEncoded) {
final String formatted = encoder.encodeToString(privateKeyEncoded);
final String[] lines = formatted.split(LINE_LENGTH_PATTERN);
final StringBuilder builder = new StringBuilder();
builder.append(StandardPemPrivateKeyReader.PRIVATE_KEY_HEADER);
builder.append(LINE_FEED);
for (final String line : lines) {
builder.append(line);
builder.append(LINE_FEED);
}
builder.append(StandardPemPrivateKeyReader.PRIVATE_KEY_FOOTER);
builder.append(LINE_FEED);
return builder.toString();
}
}

View File

@ -263,13 +263,23 @@ NiFi provides several different configuration options for security purposes. The
[options="header,footer"]
|==================================================================================================================================================
| Property Name | Description
|`nifi.security.keystore` | Filename of the Keystore that contains the server's private key.
|`nifi.security.keystoreType` | The type of Keystore. Must be `PKCS12` or `JKS` or `BCFKS`. PKCS12 is the preferred type, BCFKS and PKCS12 files will be loaded with BouncyCastle provider.
|`nifi.security.keystorePasswd` | The password for the Keystore.
|`nifi.security.keyPasswd` | The password for the certificate in the Keystore. If not set, the value of `nifi.security.keystorePasswd` will be used.
|`nifi.security.truststore` | Filename of the Truststore that will be used to authorize those connecting to NiFi. A secured instance with no Truststore will refuse all incoming connections.
|`nifi.security.truststoreType` | The type of the Truststore. Must be `PKCS12` or `JKS` or `BCFKS`. PKCS12 is the preferred type, BCFKS and PKCS12 files will be loaded with BouncyCastle provider.
|`nifi.security.truststorePasswd` | The password for the Truststore.
|`nifi.security.keystore` | File path to the key store containing the server private key and certificate entry.
|`nifi.security.keystore.certificate` | File path to `PEM` certificate chain file containing one or more X.509 certificates each having a `BEGIN CERTIFICATE` header and `END CERTIFICATE` footer.
The first certificate entry is the server certificate corresponding to the server private key.
This property requires setting `nifi.security.keystoreType` to `PEM`.
|`nifi.security.keystore.privateKey` | File path to `PEM` key file containing the server private key corresponding to the `PEM` server certificate entry.
Supported formats include PKCS1 with `BEGIN RSA PRIVATE KEY` as the header, and PKCS8 with `BEGIN PRIVATE KEY` as the header.
Supported key algorithms include RSA, Ed25519, and ECDSA with NIST curves P-256, P-384, and P-521.
|`nifi.security.keystoreType` | The type of key store. Supported types include `BCFKS`, `JKS`, `PEM`, and `PKCS12`.
The `PEM` type requires configuring the `nifi.security.keystore.privateKey` and `nifi.security.keystore.certificate` properties.
|`nifi.security.keystorePasswd` | The password for the key store. This property will be used as the key password when `nifi.security.keyPasswd` is not configured.
|`nifi.security.keyPasswd` | The password for the server private key entry in the key store. The `nifi.security.keystorePasswd` property will be used when this property is not configured.
|`nifi.security.truststore` | File path to the trust store containing one or more certificates of trusted authorities for TLS connections.
|`nifi.security.truststore.certificate` | File path to `PEM` trust store file containing one or more X.509 certificates each having a `BEGIN CERTIFICATE` header and `END CERTIFICATE` footer.
This property requires setting `nifi.security.truststoreType` to `PEM`.
|`nifi.security.truststoreType` | The type of trust store. Supported types include `BCFKS`, `JKS`, `PEM`, and `PKCS12`.
The `PEM` type requires configuring the `nifi.security.truststore.certificate` property.
|`nifi.security.truststorePasswd` | The password for the trust store.
|==================================================================================================================================================
Once the above properties have been configured, we can enable the User Interface to be accessed over HTTPS instead of HTTP. This is accomplished

View File

@ -20,6 +20,8 @@ import org.apache.nifi.framework.ssl.FrameworkSslContextHolder;
import org.apache.nifi.framework.ssl.SecurityStoreChangedPathListener;
import org.apache.nifi.framework.ssl.WatchServiceMonitorCommand;
import org.apache.nifi.security.ssl.KeyManagerListener;
import org.apache.nifi.security.ssl.PemCertificateKeyStoreBuilder;
import org.apache.nifi.security.ssl.PemPrivateKeyCertificateKeyStoreBuilder;
import org.apache.nifi.security.ssl.StandardKeyStoreBuilder;
import org.apache.nifi.security.ssl.TrustManagerListener;
import org.apache.nifi.util.FormatUtils;
@ -54,9 +56,12 @@ import java.util.concurrent.TimeUnit;
import static org.apache.nifi.util.NiFiProperties.SECURITY_AUTO_RELOAD_ENABLED;
import static org.apache.nifi.util.NiFiProperties.SECURITY_AUTO_RELOAD_INTERVAL;
import static org.apache.nifi.util.NiFiProperties.SECURITY_KEYSTORE;
import static org.apache.nifi.util.NiFiProperties.SECURITY_KEYSTORE_CERTIFICATE;
import static org.apache.nifi.util.NiFiProperties.SECURITY_KEYSTORE_PASSWD;
import static org.apache.nifi.util.NiFiProperties.SECURITY_KEYSTORE_PRIVATE_KEY;
import static org.apache.nifi.util.NiFiProperties.SECURITY_KEYSTORE_TYPE;
import static org.apache.nifi.util.NiFiProperties.SECURITY_TRUSTSTORE;
import static org.apache.nifi.util.NiFiProperties.SECURITY_TRUSTSTORE_CERTIFICATE;
import static org.apache.nifi.util.NiFiProperties.SECURITY_TRUSTSTORE_PASSWD;
import static org.apache.nifi.util.NiFiProperties.SECURITY_TRUSTSTORE_TYPE;
@ -70,6 +75,8 @@ public class SslContextConfiguration {
private static final String EMPTY = "";
private static final String PEM_STORE_TYPE = "PEM";
private NiFiProperties properties;
@Autowired
@ -146,7 +153,8 @@ public class SslContextConfiguration {
final WatchService watchService;
final String keyStoreProperty = properties.getProperty(SECURITY_KEYSTORE);
if (keyStoreProperty == null || keyStoreProperty.isBlank()) {
final String keyStorePrivateKeyProperty = properties.getProperty(SECURITY_KEYSTORE_PRIVATE_KEY);
if ((keyStoreProperty == null || keyStoreProperty.isBlank()) && (keyStorePrivateKeyProperty == null || keyStorePrivateKeyProperty.isBlank())) {
watchService = null;
} else if (isReloadEnabled()) {
final Set<Path> storeDirectories = getStoreDirectories();
@ -172,7 +180,22 @@ public class SslContextConfiguration {
final KeyStore keyStore;
final String keyStorePath = properties.getProperty(SECURITY_KEYSTORE);
if (keyStorePath == null || keyStorePath.isBlank()) {
if (isPemStoreType(SECURITY_KEYSTORE_TYPE)) {
final Path privateKeyPath = getPropertyPath(SECURITY_KEYSTORE_PRIVATE_KEY);
final Path certificatePath = getPropertyPath(SECURITY_KEYSTORE_CERTIFICATE);
try (
InputStream privateKeyInputStream = Files.newInputStream(privateKeyPath);
InputStream certificateInputStream = Files.newInputStream(certificatePath)
) {
keyStore = new PemPrivateKeyCertificateKeyStoreBuilder()
.privateKeyInputStream(privateKeyInputStream)
.certificateInputStream(certificateInputStream)
.build();
} catch (final IOException e) {
throw new IllegalStateException("Failed to load Key Store Certificate [%s] and Key [%s] Type [PEM]".formatted(certificatePath, privateKeyPath), e);
}
} else if (keyStorePath == null || keyStorePath.isBlank()) {
keyStore = null;
} else {
final char[] keyStorePassword = properties.getProperty(SECURITY_KEYSTORE_PASSWD, EMPTY).toCharArray();
@ -197,7 +220,14 @@ public class SslContextConfiguration {
final KeyStore trustStore;
final String trustStorePath = properties.getProperty(SECURITY_TRUSTSTORE);
if (trustStorePath == null || trustStorePath.isBlank()) {
if (isPemStoreType(SECURITY_TRUSTSTORE_TYPE)) {
final Path trustStoreCertificatePath = getPropertyPath(SECURITY_TRUSTSTORE_CERTIFICATE);
try (InputStream inputStream = Files.newInputStream(trustStoreCertificatePath)) {
trustStore = new PemCertificateKeyStoreBuilder().inputStream(inputStream).build();
} catch (final IOException e) {
throw new IllegalStateException("Failed to load Trust Store Certificate [%s] Type [PEM]".formatted(trustStorePath), e);
}
} else if (trustStorePath == null || trustStorePath.isBlank()) {
trustStore = null;
} else {
final char[] trustStorePassword = properties.getProperty(SECURITY_TRUSTSTORE_PASSWD, EMPTY).toCharArray();
@ -220,11 +250,23 @@ public class SslContextConfiguration {
private Set<Path> getStoreFileNames() {
final Set<Path> storeFileNames = new HashSet<>();
final Path keyStorePath = getKeyStorePath();
if (isPemStoreType(SECURITY_KEYSTORE_TYPE)) {
final Path keyStorePrivateKeyPath = getPropertyPath(SECURITY_KEYSTORE_PRIVATE_KEY);
addStoreFileName(keyStorePrivateKeyPath, storeFileNames);
final Path keyStoreCertificatePath = getPropertyPath(SECURITY_KEYSTORE_CERTIFICATE);
addStoreFileName(keyStoreCertificatePath, storeFileNames);
} else {
final Path keyStorePath = getPropertyPath(SECURITY_KEYSTORE);
addStoreFileName(keyStorePath, storeFileNames);
}
final Path trustStorePath = getTrustStorePath();
if (isPemStoreType(SECURITY_TRUSTSTORE_TYPE)) {
final Path trustStoreCertificatePath = getPropertyPath(SECURITY_TRUSTSTORE_CERTIFICATE);
addStoreFileName(trustStoreCertificatePath, storeFileNames);
} else {
final Path trustStorePath = getPropertyPath(SECURITY_TRUSTSTORE);
addStoreFileName(trustStorePath, storeFileNames);
}
return storeFileNames;
}
@ -245,11 +287,23 @@ public class SslContextConfiguration {
private Set<Path> getStoreDirectories() {
final Set<Path> storeDirectories = new HashSet<>();
final Path keyStorePath = getKeyStorePath();
if (isPemStoreType(SECURITY_KEYSTORE_TYPE)) {
final Path keyStorePrivateKeyPath = getPropertyPath(SECURITY_KEYSTORE_PRIVATE_KEY);
addStorePath(keyStorePrivateKeyPath, storeDirectories);
final Path keyStoreCertificatePath = getPropertyPath(SECURITY_KEYSTORE_CERTIFICATE);
addStorePath(keyStoreCertificatePath, storeDirectories);
} else {
final Path keyStorePath = getPropertyPath(SECURITY_KEYSTORE);
addStorePath(keyStorePath, storeDirectories);
}
final Path trustStorePath = getTrustStorePath();
if (isPemStoreType(SECURITY_TRUSTSTORE_TYPE)) {
final Path trustStoreCertificatePath = getPropertyPath(SECURITY_TRUSTSTORE_CERTIFICATE);
addStorePath(trustStoreCertificatePath, storeDirectories);
} else {
final Path trustStorePath = getPropertyPath(SECURITY_TRUSTSTORE);
addStorePath(trustStorePath, storeDirectories);
}
return storeDirectories;
}
@ -269,25 +323,22 @@ public class SslContextConfiguration {
}
}
private Path getKeyStorePath() {
final String keyStoreProperty = properties.getProperty(SECURITY_KEYSTORE);
if (keyStoreProperty == null || keyStoreProperty.isBlank()) {
throw new IllegalStateException("Security Property [%s] not configured".formatted(SECURITY_KEYSTORE));
}
return Paths.get(keyStoreProperty).toAbsolutePath();
private Path getPropertyPath(final String propertyName) {
final String propertyPath = properties.getProperty(propertyName);
if (propertyPath == null || propertyPath.isBlank()) {
throw new IllegalStateException("Security Property [%s] not configured".formatted(propertyName));
}
private Path getTrustStorePath() {
final String trustStoreProperty = properties.getProperty(SECURITY_TRUSTSTORE);
if (trustStoreProperty == null || trustStoreProperty.isBlank()) {
throw new IllegalStateException("Security Property [%s] not configured".formatted(SECURITY_TRUSTSTORE));
}
return Paths.get(trustStoreProperty).toAbsolutePath();
return Paths.get(propertyPath);
}
private boolean isReloadEnabled() {
final String reloadEnabledProperty = properties.getProperty(SECURITY_AUTO_RELOAD_ENABLED);
return Boolean.parseBoolean(reloadEnabledProperty);
}
private boolean isPemStoreType(final String storeTypePropertyName) {
final String storeType = properties.getProperty(storeTypePropertyName);
return PEM_STORE_TYPE.contentEquals(storeType);
}
}

View File

@ -17,6 +17,7 @@
package org.apache.nifi.framework.ssl;
import org.apache.nifi.security.ssl.BuilderConfigurationException;
import org.apache.nifi.security.ssl.PemPrivateKeyCertificateKeyStoreBuilder;
import org.apache.nifi.security.ssl.StandardKeyManagerBuilder;
import org.apache.nifi.security.ssl.StandardKeyStoreBuilder;
import org.apache.nifi.security.ssl.StandardX509ExtendedKeyManager;
@ -37,10 +38,18 @@ import java.util.Objects;
public class FrameworkKeyManagerBuilder extends StandardKeyManagerBuilder {
private static final Logger logger = LoggerFactory.getLogger(FrameworkKeyManagerBuilder.class);
private static final char[] EMPTY_PROTECTION_PARAMETER = new char[]{};
private final Path keyStorePath;
private final StandardKeyStoreBuilder keyStoreBuilder;
private final Path privateKeyPath;
private final Path certificatePath;
private final PemPrivateKeyCertificateKeyStoreBuilder pemKeyStoreBuilder;
public FrameworkKeyManagerBuilder(
final Path keyStorePath,
final StandardKeyStoreBuilder keyStoreBuilder,
@ -49,6 +58,23 @@ public class FrameworkKeyManagerBuilder extends StandardKeyManagerBuilder {
this.keyStorePath = Objects.requireNonNull(keyStorePath, "Key Store Path required");
this.keyStoreBuilder = Objects.requireNonNull(keyStoreBuilder, "Key Store Builder required");
keyPassword(Objects.requireNonNull(keyPassword, "Key Password required"));
this.privateKeyPath = null;
this.certificatePath = null;
this.pemKeyStoreBuilder = null;
}
public FrameworkKeyManagerBuilder(
final Path privateKeyPath,
final Path certificatePath,
final PemPrivateKeyCertificateKeyStoreBuilder pemKeyStoreBuilder
) {
this.keyStorePath = null;
this.keyStoreBuilder = null;
this.privateKeyPath = Objects.requireNonNull(privateKeyPath, "Private Key Path required");
this.certificatePath = Objects.requireNonNull(certificatePath, "Certificate Path required");
this.pemKeyStoreBuilder = Objects.requireNonNull(pemKeyStoreBuilder, "PEM Key Store Builder required");
}
/**
@ -58,13 +84,36 @@ public class FrameworkKeyManagerBuilder extends StandardKeyManagerBuilder {
*/
@Override
public X509ExtendedKeyManager build() {
this.loadKeyStore();
if (privateKeyPath == null) {
loadKeyStore();
} else {
// Set empty key password as placeholder for construction of Key Managers
keyPassword(EMPTY_PROTECTION_PARAMETER);
loadPemKeyStore();
}
final X509ExtendedKeyManager keyManager = super.build();
if (privateKeyPath == null) {
logger.info("Key Manager loaded from Key Store [{}]", keyStorePath);
} else {
logger.info("Key Manager loaded from PEM Private Key [{}] and Certificate [{}]", privateKeyPath, certificatePath);
}
return new StandardX509ExtendedKeyManager(keyManager);
}
private void loadPemKeyStore() {
try (
InputStream privateKeyInputStream = Files.newInputStream(privateKeyPath);
InputStream certificateInputStream = Files.newInputStream(certificatePath)
) {
final KeyStore loadedKeyStore = pemKeyStoreBuilder.privateKeyInputStream(privateKeyInputStream).certificateInputStream(certificateInputStream).build();
keyStore(loadedKeyStore);
} catch (final IOException e) {
throw new BuilderConfigurationException("PEM Key Store loading failed", e);
}
}
private void loadKeyStore() {
try (InputStream inputStream = Files.newInputStream(keyStorePath)) {
final KeyStore loadedKeyStore = keyStoreBuilder.inputStream(inputStream).build();

View File

@ -17,6 +17,8 @@
package org.apache.nifi.framework.ssl;
import org.apache.nifi.security.ssl.KeyManagerBuilder;
import org.apache.nifi.security.ssl.PemCertificateKeyStoreBuilder;
import org.apache.nifi.security.ssl.PemPrivateKeyCertificateKeyStoreBuilder;
import org.apache.nifi.security.ssl.StandardKeyStoreBuilder;
import org.apache.nifi.security.ssl.StandardSslContextBuilder;
import org.apache.nifi.security.ssl.TrustManagerBuilder;
@ -31,10 +33,13 @@ import java.util.Objects;
import java.util.Optional;
import static org.apache.nifi.util.NiFiProperties.SECURITY_KEYSTORE;
import static org.apache.nifi.util.NiFiProperties.SECURITY_KEYSTORE_CERTIFICATE;
import static org.apache.nifi.util.NiFiProperties.SECURITY_KEYSTORE_PRIVATE_KEY;
import static org.apache.nifi.util.NiFiProperties.SECURITY_KEYSTORE_PASSWD;
import static org.apache.nifi.util.NiFiProperties.SECURITY_KEYSTORE_TYPE;
import static org.apache.nifi.util.NiFiProperties.SECURITY_KEY_PASSWD;
import static org.apache.nifi.util.NiFiProperties.SECURITY_TRUSTSTORE;
import static org.apache.nifi.util.NiFiProperties.SECURITY_TRUSTSTORE_CERTIFICATE;
import static org.apache.nifi.util.NiFiProperties.SECURITY_TRUSTSTORE_PASSWD;
import static org.apache.nifi.util.NiFiProperties.SECURITY_TRUSTSTORE_TYPE;
@ -44,6 +49,8 @@ import static org.apache.nifi.util.NiFiProperties.SECURITY_TRUSTSTORE_TYPE;
public class FrameworkSslContextProvider {
private static final String EMPTY = "";
private static final String PEM_STORE_TYPE = "PEM";
private final NiFiProperties properties;
public FrameworkSslContextProvider(final NiFiProperties properties) {
@ -83,7 +90,7 @@ public class FrameworkSslContextProvider {
final KeyManagerBuilder keyManagerBuilder;
if (isPropertyConfigured(SECURITY_KEYSTORE) && isPropertyConfigured(SECURITY_KEYSTORE_PASSWD)) {
final Path keyStorePath = getKeyStorePath();
final Path keyStorePath = getPropertyPath(SECURITY_KEYSTORE);
final String keyStorePassword = properties.getProperty(SECURITY_KEYSTORE_PASSWD, EMPTY);
final char[] keyPassword = properties.getProperty(SECURITY_KEY_PASSWD, keyStorePassword).toCharArray();
final String keyStoreType = properties.getProperty(SECURITY_KEYSTORE_TYPE);
@ -92,6 +99,12 @@ public class FrameworkSslContextProvider {
.type(keyStoreType);
keyManagerBuilder = new FrameworkKeyManagerBuilder(keyStorePath, keyStoreBuilder, keyPassword);
} else if (isPemStoreType(SECURITY_KEYSTORE_TYPE)) {
final Path privateKeyPath = getPropertyPath(SECURITY_KEYSTORE_PRIVATE_KEY);
final Path certificatePath = getPropertyPath(SECURITY_KEYSTORE_CERTIFICATE);
final PemPrivateKeyCertificateKeyStoreBuilder pemKeyStoreBuilder = new PemPrivateKeyCertificateKeyStoreBuilder();
keyManagerBuilder = new FrameworkKeyManagerBuilder(privateKeyPath, certificatePath, pemKeyStoreBuilder);
} else {
keyManagerBuilder = null;
}
@ -103,7 +116,7 @@ public class FrameworkSslContextProvider {
final TrustManagerBuilder trustManagerBuilder;
if (isPropertyConfigured(SECURITY_TRUSTSTORE) && isPropertyConfigured(SECURITY_TRUSTSTORE_PASSWD)) {
final Path trustStorePath = getTrustStorePath();
final Path trustStorePath = getPropertyPath(SECURITY_TRUSTSTORE);
final String trustStorePassword = properties.getProperty(SECURITY_TRUSTSTORE_PASSWD, EMPTY);
final String trustStoreType = properties.getProperty(SECURITY_TRUSTSTORE_TYPE);
final StandardKeyStoreBuilder trustStoreBuilder = new StandardKeyStoreBuilder()
@ -111,6 +124,10 @@ public class FrameworkSslContextProvider {
.type(trustStoreType);
trustManagerBuilder = new FrameworkTrustManagerBuilder(trustStorePath, trustStoreBuilder);
} else if (isPemStoreType(SECURITY_TRUSTSTORE_TYPE)) {
final Path trustStoreCertificatePath = getPropertyPath(SECURITY_TRUSTSTORE_CERTIFICATE);
final PemCertificateKeyStoreBuilder trustStoreBuilder = new PemCertificateKeyStoreBuilder();
trustManagerBuilder = new FrameworkTrustManagerBuilder(trustStoreCertificatePath, trustStoreBuilder);
} else {
trustManagerBuilder = null;
}
@ -118,25 +135,22 @@ public class FrameworkSslContextProvider {
return trustManagerBuilder;
}
private Path getKeyStorePath() {
final String keyStoreProperty = properties.getProperty(SECURITY_KEYSTORE);
if (keyStoreProperty == null || keyStoreProperty.isBlank()) {
throw new IllegalStateException("Security Property [%s] not configured".formatted(SECURITY_KEYSTORE));
}
return Paths.get(keyStoreProperty);
private Path getPropertyPath(final String propertyName) {
final String propertyPath = properties.getProperty(propertyName);
if (propertyPath == null || propertyPath.isBlank()) {
throw new IllegalStateException("Security Property [%s] not configured".formatted(propertyName));
}
private Path getTrustStorePath() {
final String trustStoreProperty = properties.getProperty(SECURITY_TRUSTSTORE);
if (trustStoreProperty == null || trustStoreProperty.isBlank()) {
throw new IllegalStateException("Security Property [%s] not configured".formatted(SECURITY_TRUSTSTORE));
}
return Paths.get(trustStoreProperty);
return Paths.get(propertyPath);
}
private boolean isPropertyConfigured(final String propertyName) {
final String value = properties.getProperty(propertyName);
return value != null && !value.isBlank();
}
private boolean isPemStoreType(final String storeTypePropertyName) {
final String storeType = properties.getProperty(storeTypePropertyName);
return PEM_STORE_TYPE.contentEquals(storeType);
}
}

View File

@ -17,7 +17,7 @@
package org.apache.nifi.framework.ssl;
import org.apache.nifi.security.ssl.BuilderConfigurationException;
import org.apache.nifi.security.ssl.StandardKeyStoreBuilder;
import org.apache.nifi.security.ssl.InputStreamKeyStoreBuilder;
import org.apache.nifi.security.ssl.StandardTrustManagerBuilder;
import org.apache.nifi.security.ssl.StandardX509ExtendedTrustManager;
import org.slf4j.Logger;
@ -32,16 +32,16 @@ import java.security.KeyStore;
import java.util.Objects;
/**
* Framework implementation fo Trust Manager Builder capable of reloading a Trust Store when building a Trust Manager
* Framework implementation of Trust Manager Builder capable of reloading a Trust Store when building a Trust Manager
*/
public class FrameworkTrustManagerBuilder extends StandardTrustManagerBuilder {
private static final Logger logger = LoggerFactory.getLogger(FrameworkTrustManagerBuilder.class);
private final Path trustStorePath;
private final StandardKeyStoreBuilder trustStoreBuilder;
private final InputStreamKeyStoreBuilder trustStoreBuilder;
public FrameworkTrustManagerBuilder(final Path trustStorePath, final StandardKeyStoreBuilder trustStoreBuilder) {
public FrameworkTrustManagerBuilder(final Path trustStorePath, final InputStreamKeyStoreBuilder trustStoreBuilder) {
this.trustStorePath = Objects.requireNonNull(trustStorePath, "Trust Store Path required");
this.trustStoreBuilder = Objects.requireNonNull(trustStoreBuilder, "Trust Store Builder required");
}

View File

@ -147,10 +147,13 @@
<nifi.security.autoreload.enabled>false</nifi.security.autoreload.enabled>
<nifi.security.autoreload.interval>10 secs</nifi.security.autoreload.interval>
<nifi.security.keystore>./conf/keystore.p12</nifi.security.keystore>
<nifi.security.keystore.certificate />
<nifi.security.keystore.privateKey />
<nifi.security.keystoreType>PKCS12</nifi.security.keystoreType>
<nifi.security.keystorePasswd />
<nifi.security.keyPasswd />
<nifi.security.truststore>./conf/truststore.p12</nifi.security.truststore>
<nifi.security.truststore.certificate />
<nifi.security.truststoreType>PKCS12</nifi.security.truststoreType>
<nifi.security.truststorePasswd />
<nifi.security.user.authorizer>single-user-authorizer</nifi.security.user.authorizer>

View File

@ -189,12 +189,16 @@ nifi.sensitive.props.algorithm=${nifi.sensitive.props.algorithm}
nifi.security.autoreload.enabled=${nifi.security.autoreload.enabled}
nifi.security.autoreload.interval=${nifi.security.autoreload.interval}
nifi.security.keystore=${nifi.security.keystore}
nifi.security.keystore.certificate=${nifi.security.keystore.certificate}
nifi.security.keystore.privateKey=${nifi.security.keystore.privateKey}
nifi.security.keystoreType=${nifi.security.keystoreType}
nifi.security.keystorePasswd=${nifi.security.keystorePasswd}
nifi.security.keyPasswd=${nifi.security.keyPasswd}
nifi.security.truststore=${nifi.security.truststore}
nifi.security.truststore.certificate=${nifi.security.truststore.certificate}
nifi.security.truststoreType=${nifi.security.truststoreType}
nifi.security.truststorePasswd=${nifi.security.truststorePasswd}
nifi.security.user.authorizer=${nifi.security.user.authorizer}
nifi.security.allow.anonymous.authentication=${nifi.security.allow.anonymous.authentication}
nifi.security.user.login.identity.provider=${nifi.security.user.login.identity.provider}