/* * 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 java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import javax.crypto.Cipher; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.lang3.StringUtils; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.bouncycastle.asn1.x500.X500Name; 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.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; import org.bouncycastle.jce.provider.BouncyCastleProvider; 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.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.io.pem.PemWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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; // 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; } 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 { SecretKeySpec keySpec = new SecretKeySpec(token.getBytes(StandardCharsets.UTF_8), "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)); } public static KeyPair parseKeyPair(Reader pemEncodedKeyPair) throws IOException { return new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getKeyPair(parsePem(PEMKeyPair.class, pemEncodedKeyPair)); } public static <T> T parsePem(Class<T> clazz, Reader pemReader) throws IOException { try (PEMParser pemParser = new PEMParser(pemReader)) { Object object = pemParser.readObject(); if (!clazz.isInstance(object)) { throw new IOException("Expected " + clazz); } return (T) object; } } public static KeyPair generateKeyPair(String algorithm, int keySize) throws NoSuchAlgorithmException { return createKeyPairGenerator(algorithm, keySize).generateKeyPair(); } public static JcaPKCS10CertificationRequest generateCertificationRequest(String requestedDn, String domainAlternativeNames, KeyPair keyPair, String signingAlgorithm) throws OperatorCreationException { JcaPKCS10CertificationRequestBuilder jcaPKCS10CertificationRequestBuilder = new JcaPKCS10CertificationRequestBuilder(new X500Name(requestedDn), keyPair.getPublic()); // add Subject Alternative Name(s) if(StringUtils.isNotBlank(domainAlternativeNames)) { try { jcaPKCS10CertificationRequestBuilder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, createDomainAlternativeNamesExtensions(domainAlternativeNames)); } 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(String domainAlternativeNames) throws IOException { List<GeneralName> namesList = new ArrayList<>(); for(String alternativeName : domainAlternativeNames.split(",")) { namesList.add(new GeneralName(GeneralName.dNSName, alternativeName)); } GeneralNames subjectAltNames = new GeneralNames(namesList.toArray(new GeneralName [] {})); ExtensionsGenerator extGen = new ExtensionsGenerator(); extGen.addExtension(Extension.subjectAlternativeName, false, subjectAltNames); return extGen.generate(); } }