org.apache.nifi.toolkit.tls.util.TlsHelper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of nifi-toolkit-tls Show documentation
Show all versions of nifi-toolkit-tls Show documentation
Tooling to make tls configuration easier
/*
* 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.toolkit.tls.util;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.DERNull;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.pkcs.RSAPrivateKey;
import org.bouncycastle.asn1.pkcs.RSAPublicKey;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x500.style.IETFUtils;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.asn1.x509.ExtensionsGenerator;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMException;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
import org.bouncycastle.util.IPAddress;
import org.bouncycastle.util.io.pem.PemWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
public class TlsHelper {
private static final Logger logger = LoggerFactory.getLogger(TlsHelper.class);
private static final int DEFAULT_MAX_ALLOWED_KEY_LENGTH = 128;
public static final String JCE_URL = "http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html";
public static final String ILLEGAL_KEY_SIZE = "illegal key size";
private static boolean isUnlimitedStrengthCryptographyEnabled;
private static boolean isVerbose = true;
// Evaluate an unlimited strength algorithm to determine if we support the capability we have on the system
static {
try {
isUnlimitedStrengthCryptographyEnabled = (Cipher.getMaxAllowedKeyLength("AES") > DEFAULT_MAX_ALLOWED_KEY_LENGTH);
} catch (NoSuchAlgorithmException e) {
// if there are issues with this, we default back to the value established
isUnlimitedStrengthCryptographyEnabled = false;
}
}
private static void logTruncationWarning(File file) {
String fileToString = file.toString();
String fileName = file.getName();
logger.warn("**********************************************************************************");
logger.warn(" WARNING!!!!");
logger.warn("**********************************************************************************");
logger.warn("Unlimited JCE Policy is not installed which means we cannot utilize a");
logger.warn("PKCS12 password longer than 7 characters.");
logger.warn("Autogenerated password has been reduced to 7 characters.");
logger.warn("");
logger.warn("Please strongly consider installing Unlimited JCE Policy at");
logger.warn(JCE_URL);
logger.warn("");
logger.warn("Another alternative is to add a stronger password with the openssl tool to the");
logger.warn("resulting client certificate: " + fileToString);
logger.warn("");
logger.warn("openssl pkcs12 -in '" + fileToString + "' -out '/tmp/" + fileName + "'");
logger.warn("openssl pkcs12 -export -in '/tmp/" + fileName + "' -out '" + fileToString + "'");
logger.warn("rm -f '/tmp/" + fileName + "'");
logger.warn("");
logger.warn("**********************************************************************************");
}
private TlsHelper() {
}
public static boolean isUnlimitedStrengthCryptographyEnabled() {
return isUnlimitedStrengthCryptographyEnabled;
}
public static String writeKeyStore(KeyStore keyStore, OutputStreamFactory outputStreamFactory, File file, String password, boolean generatedPassword) throws IOException, GeneralSecurityException {
try (OutputStream fileOutputStream = outputStreamFactory.create(file)) {
keyStore.store(fileOutputStream, password.toCharArray());
} catch (IOException e) {
if (e.getMessage().toLowerCase().contains(ILLEGAL_KEY_SIZE) && !isUnlimitedStrengthCryptographyEnabled()) {
if (generatedPassword) {
file.delete();
String truncatedPassword = password.substring(0, 7);
try (OutputStream fileOutputStream = outputStreamFactory.create(file)) {
keyStore.store(fileOutputStream, truncatedPassword.toCharArray());
}
logTruncationWarning(file);
return truncatedPassword;
} else {
throw new GeneralSecurityException("Specified password for " + file + " too long to work without unlimited JCE policy installed."
+ System.lineSeparator() + "Please see " + JCE_URL);
}
} else {
throw e;
}
}
return password;
}
public static HashMap extractCerts(KeyStore keyStore) throws KeyStoreException {
HashMap certs = new HashMap<>();
Enumeration certAliases = keyStore.aliases();
while(certAliases.hasMoreElements()) {
String alias = certAliases.nextElement();
certs.put(alias, keyStore.getCertificate(alias));
}
return certs;
}
public static HashMap extractKeys(KeyStore keyStore, char[] privKeyPass) throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException {
HashMap keys = new HashMap<>();
Enumeration keyAliases = keyStore.aliases();
while(keyAliases.hasMoreElements()) {
String alias = keyAliases.nextElement();
Key key = keyStore.getKey(alias, privKeyPass);
if(key != null) {
keys.put(alias, key);
} else {
logger.warn("Key does not exist: Certificate with alias '" + alias + "' had no private key.");
}
}
return keys;
}
public static void outputCertsAsPem(HashMap certs, File directory, String extension) {
certs.forEach((String alias, Certificate cert)->{
try {
TlsHelper.outputAsPem(cert, alias, directory, extension);
} catch (IOException e) {
e.printStackTrace();
}
});
}
public static void outputKeysAsPem(HashMap keys, File directory, String extension) {
keys.forEach((String alias, Key key) -> {
try {
TlsHelper.outputAsPem(key, alias, directory, extension);
} catch (IOException e) {
e.printStackTrace();
}
});
}
private static void outputAsPem(Object pemObj, String filename, File directory, String extension) throws IOException {
try (OutputStream outputStream = new FileOutputStream(new File(directory, TlsHelper.escapeFilename(filename) + extension))) {
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
JcaPEMWriter pemWriter = new JcaPEMWriter(outputStreamWriter);
JcaMiscPEMGenerator pemGen = new JcaMiscPEMGenerator(pemObj);
pemWriter.writeObject(pemGen);
pemWriter.close();
}
}
private static KeyPairGenerator createKeyPairGenerator(String algorithm, int keySize) throws NoSuchAlgorithmException {
KeyPairGenerator instance = KeyPairGenerator.getInstance(algorithm);
instance.initialize(keySize);
return instance;
}
public static byte[] calculateHMac(String token, PublicKey publicKey) throws GeneralSecurityException {
if (token == null) {
throw new IllegalArgumentException("Token cannot be null");
}
byte[] tokenBytes = token.getBytes(StandardCharsets.UTF_8);
if (tokenBytes.length < 16) {
throw new GeneralSecurityException("Token does not meet minimum size of 16 bytes.");
}
SecretKeySpec keySpec = new SecretKeySpec(tokenBytes, "RAW");
Mac mac = Mac.getInstance("Hmac-SHA256", BouncyCastleProvider.PROVIDER_NAME);
mac.init(keySpec);
return mac.doFinal(getKeyIdentifier(publicKey));
}
public static byte[] getKeyIdentifier(PublicKey publicKey) throws NoSuchAlgorithmException {
return new JcaX509ExtensionUtils().createSubjectKeyIdentifier(publicKey).getKeyIdentifier();
}
public static String pemEncodeJcaObject(Object object) throws IOException {
StringWriter writer = new StringWriter();
try (PemWriter pemWriter = new PemWriter(writer)) {
pemWriter.writeObject(new JcaMiscPEMGenerator(object));
}
return writer.toString();
}
public static JcaPKCS10CertificationRequest parseCsr(String pemEncodedCsr) throws IOException {
try (PEMParser pemParser = new PEMParser(new StringReader(pemEncodedCsr))) {
Object o = pemParser.readObject();
if (!PKCS10CertificationRequest.class.isInstance(o)) {
throw new IOException("Expecting instance of " + PKCS10CertificationRequest.class + " but got " + o);
}
return new JcaPKCS10CertificationRequest((PKCS10CertificationRequest) o);
}
}
public static X509Certificate parseCertificate(Reader pemEncodedCertificate) throws IOException, CertificateException {
return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(parsePem(X509CertificateHolder.class, pemEncodedCertificate));
}
/**
* Returns the parsed {@link KeyPair} from the provided {@link Reader}. The incoming format can be PKCS #8 or PKCS #1.
*
* @param pemKeyPairReader a reader with access to the serialized key pair
* @return the key pair
* @throws IOException if there is an error reading the key pair
*/
public static KeyPair parseKeyPairFromReader(Reader pemKeyPairReader) throws IOException {
// Instantiate PEMParser from Reader
try (PEMParser pemParser = new PEMParser(pemKeyPairReader)) {
// Read the object (deserialize)
Object parsedObject = pemParser.readObject();
// If this is an ASN.1 private key, it's in PKCS #8 format and wraps the actual RSA private key
if (PrivateKeyInfo.class.isInstance(parsedObject)) {
if (isVerbose()) {
logger.info("Provided private key is in PKCS #8 format");
}
PEMKeyPair keyPair = convertPrivateKeyFromPKCS8ToPKCS1((PrivateKeyInfo) parsedObject);
return getKeyPair(keyPair);
} else if (PEMKeyPair.class.isInstance(parsedObject)) {
// Already in PKCS #1 format
return getKeyPair((PEMKeyPair)parsedObject);
} else {
logger.warn("Expected one of {} or {} but got {}", PrivateKeyInfo.class, PEMKeyPair.class, parsedObject.getClass());
throw new IOException("Expected private key in PKCS #1 or PKCS #8 unencrypted format");
}
}
}
/**
* Returns a {@link KeyPair} instance containing the {@link X509Certificate} public key and the {@link java.security.spec.PKCS8EncodedKeySpec} private key from the PEM-encoded {@link PEMKeyPair}.
*
* @param keyPair the key pair in PEM format
* @return the key pair in a format which provides for direct access to the keys
* @throws PEMException if there is an error converting the key pair
*/
private static KeyPair getKeyPair(PEMKeyPair keyPair) throws PEMException {
return new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getKeyPair(keyPair);
}
/**
* Returns a {@link PEMKeyPair} object with direct access to the public and private keys given a PKCS #8 private key.
*
* @param privateKeyInfo the PKCS #8 private key info
* @return the PKCS #1 public and private key pair
* @throws IOException if there is an error converting the key pair
*/
private static PEMKeyPair convertPrivateKeyFromPKCS8ToPKCS1(PrivateKeyInfo privateKeyInfo) throws IOException {
// Parse the key wrapping to determine the internal key structure
ASN1Encodable asn1PrivateKey = privateKeyInfo.parsePrivateKey();
// Convert the parsed key to an RSA private key
RSAPrivateKey keyStruct = RSAPrivateKey.getInstance(asn1PrivateKey);
// Create the RSA public key from the modulus and exponent
RSAPublicKey pubSpec = new RSAPublicKey(
keyStruct.getModulus(), keyStruct.getPublicExponent());
// Create an algorithm identifier for forming the key pair
AlgorithmIdentifier algId = new AlgorithmIdentifier(PKCSObjectIdentifiers.rsaEncryption, DERNull.INSTANCE);
if (isVerbose()) {
logger.info("Converted private key from PKCS #8 to PKCS #1 RSA private key");
}
// Create the key pair container
return new PEMKeyPair(new SubjectPublicKeyInfo(algId, pubSpec), new PrivateKeyInfo(algId, keyStruct));
}
public static T parsePem(Class clazz, Reader pemReader) throws IOException {
try (PEMParser pemParser = new PEMParser(pemReader)) {
Object object = pemParser.readObject();
if (!clazz.isInstance(object)) {
throw new IOException("Expected " + clazz + " but got " + object.getClass());
}
return (T) object;
}
}
public static KeyPair generateKeyPair(String algorithm, int keySize) throws NoSuchAlgorithmException {
return createKeyPairGenerator(algorithm, keySize).generateKeyPair();
}
public static JcaPKCS10CertificationRequest generateCertificationRequest(String requestedDn, List domainAlternativeNames,
KeyPair keyPair, String signingAlgorithm) throws OperatorCreationException {
JcaPKCS10CertificationRequestBuilder jcaPKCS10CertificationRequestBuilder = new JcaPKCS10CertificationRequestBuilder(new X500Name(requestedDn), keyPair.getPublic());
// add Subject Alternative Name(s)
try {
jcaPKCS10CertificationRequestBuilder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, createDomainAlternativeNamesExtensions(domainAlternativeNames, requestedDn));
} catch (IOException e) {
throw new OperatorCreationException("Error while adding " + domainAlternativeNames + " as Subject Alternative Name.", e);
}
JcaContentSignerBuilder jcaContentSignerBuilder = new JcaContentSignerBuilder(signingAlgorithm);
return new JcaPKCS10CertificationRequest(jcaPKCS10CertificationRequestBuilder.build(jcaContentSignerBuilder.build(keyPair.getPrivate())));
}
public static Extensions createDomainAlternativeNamesExtensions(List domainAlternativeNames, String requestedDn) throws IOException {
List namesList = new ArrayList<>();
try {
final String cn = IETFUtils.valueToString(new X500Name(requestedDn).getRDNs(BCStyle.CN)[0].getFirst().getValue());
namesList.add(new GeneralName(GeneralName.dNSName, cn));
} catch (Exception e) {
throw new IOException("Failed to extract CN from request DN: " + requestedDn, e);
}
if (domainAlternativeNames != null) {
for (String alternativeName : domainAlternativeNames) {
namesList.add(new GeneralName(IPAddress.isValid(alternativeName) ? GeneralName.iPAddress : GeneralName.dNSName, alternativeName));
}
}
GeneralNames subjectAltNames = new GeneralNames(namesList.toArray(new GeneralName[]{}));
ExtensionsGenerator extGen = new ExtensionsGenerator();
extGen.addExtension(Extension.subjectAlternativeName, false, subjectAltNames);
return extGen.generate();
}
/**
* Removes special characters (particularly forward and back slashes) from strings that become file names.
*
* @param filename A filename you plan to write to disk which needs to be escaped.
* @return String with special characters converted to underscores.
*/
public static final String escapeFilename(String filename) {
return filename.replaceAll("[^\\w\\.\\-\\=]+", "_");
}
/**
* Returns true if the {@code certificate} is signed by one of the {@code signingCertificates}. The list should
* include the certificate itself to allow for self-signed certificates. If it does not, a self-signed certificate
* will return {@code false}.
*
* @param certificate the certificate containing the signature being verified
* @param signingCertificates a list of certificates which may have signed the certificate
* @return true if one of the signing certificates did sign the certificate
*/
public static boolean verifyCertificateSignature(X509Certificate certificate, List signingCertificates) {
String certificateDisplayInfo = getCertificateDisplayInfo(certificate);
if (isVerbose()) {
logger.info("Verifying the certificate signature for " + certificateDisplayInfo);
}
boolean signatureMatches = false;
for (X509Certificate signingCert : signingCertificates) {
final String signingCertDisplayInfo = getCertificateDisplayInfo(signingCert);
try {
if (isVerbose()) {
logger.info("Attempting to verify certificate " + certificateDisplayInfo + " signature with " + signingCertDisplayInfo);
}
PublicKey pub = signingCert.getPublicKey();
certificate.verify(pub);
if (isVerbose()) {
logger.info("Certificate was signed by " + signingCertDisplayInfo);
}
signatureMatches = true;
break;
} catch (Exception e) {
// Expected if the signature does not match
if (isVerbose()) {
logger.warn("Certificate " + certificateDisplayInfo + " not signed by " + signingCertDisplayInfo + " [" + e.getLocalizedMessage() + "]");
}
}
}
return signatureMatches;
}
private static String getCertificateDisplayInfo(X509Certificate certificate) {
return certificate.getSubjectX500Principal().getName();
}
private static boolean isVerbose() {
// TODO: When verbose mode is enabled via command-line flag, this will read the variable
return isVerbose;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy