mirror of https://github.com/apache/nifi.git
NIFI-14001 Added Framework Support for PEM Keys and Certificates (#9517)
This commit is contained in:
parent
6f941bd622
commit
e33cbe6df5
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -262,14 +262,24 @@ 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.
|
||||
| Property Name | Description
|
||||
|`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
|
||||
|
|
|
@ -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();
|
||||
addStoreFileName(keyStorePath, storeFileNames);
|
||||
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();
|
||||
addStoreFileName(trustStorePath, storeFileNames);
|
||||
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();
|
||||
addStorePath(keyStorePath, storeDirectories);
|
||||
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();
|
||||
addStorePath(trustStorePath, storeDirectories);
|
||||
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 getTrustStorePath() {
|
||||
final String trustStoreProperty = properties.getProperty(SECURITY_TRUSTSTORE);
|
||||
if (trustStoreProperty == null || trustStoreProperty.isBlank()) {
|
||||
throw new IllegalStateException("Security Property [%s] not configured".formatted(SECURITY_TRUSTSTORE));
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
logger.info("Key Manager loaded from Key Store [{}]", keyStorePath);
|
||||
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();
|
||||
|
|
|
@ -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 getTrustStorePath() {
|
||||
final String trustStoreProperty = properties.getProperty(SECURITY_TRUSTSTORE);
|
||||
if (trustStoreProperty == null || trustStoreProperty.isBlank()) {
|
||||
throw new IllegalStateException("Security Property [%s] not configured".formatted(SECURITY_TRUSTSTORE));
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Reference in New Issue