/** * Copyright (c) Codice Foundation * <p> * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or any later version. * <p> * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. **/ package org.codice.ddf.security.certificate.generator; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.security.GeneralSecurityException; import java.security.Key; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Arrays; import java.util.Base64; import java.util.regex.Pattern; import org.apache.commons.lang.Validate; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.X500NameBuilder; import org.bouncycastle.asn1.x500.style.RFC4519Style; import org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class is a home for helper functions that did not belong to other classes. */ public abstract class PkiTools { public static final int RSA_KEY_LENGTH = 2048; public static final String ALGORITHM = "RSA"; private static final Logger LOGGER = LoggerFactory.getLogger(PkiTools.class); /** * Convert a byte array to a Java String. * * @param bytes DER encoded bytes * @return PEM encoded bytes */ public static String derToPem(byte[] bytes) { Validate.isTrue(bytes != null, "Argument bytes cannot be null"); return Base64.getEncoder() .encodeToString(bytes); } /** * If input is a character array, return the character array. If input is null, return a zero length * character array * * @param password character array * @return character array */ static char[] formatPassword(char[] password) { return password == null ? new char[0] : password; } /** * @param filePath path to local keystore file * @return instance of File * @throws IOException */ static File createFileObject(String filePath) throws IOException { File file; if (filePath == null) { throw new IllegalArgumentException("File path to security file is null"); } file = new File(filePath); if (!file.exists()) { throw new FileNotFoundException( "Cannot find security file at " + file.getAbsolutePath()); } if (!file.canRead()) { String msg = String.format( "Cannot read security file (possible file permission problem) or %s is a directory", file.getAbsolutePath()); throw new IOException(msg); } return file; } /** * Given an X509 certificate, return a PEM encoded string representation of the certificate. * * @param cert certificate * @return PEM encoded String */ public static String certificateToPem(X509Certificate cert) { Validate.isTrue(cert != null, "Certificate cannot be null"); try { return derToPem(cert.getEncoded()); } catch (RuntimeException | CertificateEncodingException e) { throw new CertificateGeneratorException( "Unable to convert the certificate to a PEM object", e); } } /** * Given a byte array that represents a DER encoded X509 certificate, return the certificate object * * @param certDer byte array representing a DER encoded X509 certificate * @return instance of X509 certificate */ public static X509Certificate derToCertificate(byte[] certDer) { return PkiTools.pemToCertificate(derToPem(certDer)); } /** * Given a byte array that represents a DER encoded private key, return the private key object * * @param privateKeyDer byte array representing a DER encoded private key * @return instance of private key */ public static PrivateKey derToPrivateKey(byte[] privateKeyDer) { return PkiTools.pemToPrivateKey(derToPem(privateKeyDer)); } /** * Get the host name or DNS name associated with the machine running the JVM. This * method is public so client code can easily check the name and decide if it should be used in the generated * certificate. * * @return String. Hostname of this machine. Hostname should be the same as the machine's DNS name. */ public static String getHostName() { //getCannonicalHostName returns the IP address. getHostName is the closet Java method to getting // the FQDN. try { return InetAddress.getLocalHost() .getHostName(); } catch (UnknownHostException e) { throw new CertificateGeneratorException( "Cannot get this machine's host name. On *NIX machines, check hosts file for entry with machines's IP addresses. Localhost entries do not work.", e); } } /** * Generate new RSA public/private key pair with 2048 bit key * * @return new generated key pair * @throws CertificateGeneratorException */ public static KeyPair generateRsaKeyPair() { try { KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM, BouncyCastleProvider.PROVIDER_NAME); keyGen.initialize(RSA_KEY_LENGTH); return keyGen.generateKeyPair(); } catch (Exception e) { throw new CertificateGeneratorException( "Failed to generate new public/private key pair.", e); } } /** * Serialize a Key object as a DER encoded byte array. * * @param key instance of Key object * @return byte[] */ public static byte[] keyToDer(Key key) { Validate.isTrue(key != null, "Key cannot be null"); return pemToDer(keyToPem(key)); } /** * @param key object * @return PEM encoded string represents the bytes of the key */ public static String keyToPem(Key key) { Validate.isTrue(key != null, "Key cannot be null"); return derToPem(key.getEncoded()); } /** * Create an X500 name with a single populated attribute, the "common name". An X500 name object details the * identity of a machine, person, or organization. The name object is used as the "subject" of a certificate. * SSL/TLS typically uses a subject's common name as the DNS name for a machine and this name must be correct * or SSl/TLS will not trust the machine's certificate. * <p> * TLS can use a different set of attributes to, the Subject Alternative Names. SANs are extensions to the * X509 specification and can include IP addresses, DNS names and other machine information. This package does * not use SANs. * * @param commonName the fully qualified host name of the end entity * @return X500 name object with common name attribute set * @see <a href="https://www.ietf.org/rfc/rfc4514.txt">RFC 4514, section 'LDAP: Distinguished Names'</a> * @see <a href="https://tools.ietf.org/html/rfc4519">RFC 4519 details the exact construction of distinguished names</a> * @see <a href="https://en.wikipedia.org/wiki/SubjectAltName">Subject Alternative Names on Wikipedia'</a> */ public static X500Name makeDistinguishedName(String commonName) { Validate.isTrue(commonName != null, "Certificate common name cannot be null"); assert commonName != null; if (commonName.isEmpty()) { LOGGER.warn( "Setting certificate common name to empty string. This could result in an unusable TLS certificate."); } X500NameBuilder nameBuilder = new X500NameBuilder(RFC4519Style.INSTANCE); //Add more nameBuilder.addRDN(....) statements to support more X500 attributes. nameBuilder.addRDN(RFC4519Style.cn, commonName); return nameBuilder.build(); } public static X500Name convertDistinguishedName(String... tuples) { Validate.isTrue(tuples != null && tuples.length > 0, "Distinguished name must consist of at least one component"); assert tuples != null && tuples.length > 0; Pattern tuplePattern = Pattern.compile(".*[=].*"); Validate.isTrue(Arrays.stream(tuples) .allMatch(t -> tuplePattern.matcher(t) .matches()), "Distinguished name components must be in the format symbol=value"); AttributeNameChecker style = new AttributeNameChecker(); Validate.isTrue(Arrays.stream(tuples) .map(t -> t.split("[=]")[0]) .map(String::trim) .allMatch(style::isValidName)); X500NameBuilder nameBuilder = new X500NameBuilder(RFC4519Style.INSTANCE); Arrays.stream(tuples) .map(t -> t.split("[=]")) .forEach(t -> nameBuilder.addRDN(style.lookupByName(t[0].trim()), t[1].trim())); return nameBuilder.build(); } /** * Given a PEM encoded X509 certificate, return an object representation of the certificate * * @param certString PEM encoded X509 certificate * @return instance of X509 certificate */ public static X509Certificate pemToCertificate(String certString) { CertificateFactory cf = new CertificateFactory(); ByteArrayInputStream in = new ByteArrayInputStream(PkiTools.pemToDer(certString)); X509Certificate cert; try { cert = (X509Certificate) cf.engineGenerateCertificate(in); } catch (CertificateException e) { throw new CertificateGeneratorException( "Cannot convert this PEM object to X509 certificate", e); } if (cert == null) { throw new CertificateGeneratorException( "Cannot convert this PEM object to X509 certificate"); } return cert; } /** * Convert a Java String to a byte array * * @param string PEM encoded bytes * @return DER encoded bytes */ public static byte[] pemToDer(String string) { Validate.isTrue(string != null, "PEM string cannot be null"); assert string != null; return Base64.getDecoder() .decode(string); } /** * Convert a Java String to an private key * * @param keyString encoded RSA private key. Assume PKCS#8 format * @return Instance of PrivateKey */ public static PrivateKey pemToPrivateKey(String keyString) { try { return PkiTools.getRsaKeyFactory() .generatePrivate(new PKCS8EncodedKeySpec(pemToDer(keyString))); } catch (Exception e) { throw new CertificateGeneratorException("Could not convert String to Private Key", e.getCause()); } } static KeyFactory getRsaKeyFactory() throws GeneralSecurityException { return KeyFactory.getInstance(ALGORITHM, BouncyCastleProvider.PROVIDER_NAME); } private static class AttributeNameChecker extends RFC4519Style { ASN1ObjectIdentifier lookupByName(String name) { return (ASN1ObjectIdentifier) defaultLookUp.get(name.toLowerCase()); } boolean isValidName(String name) { return defaultLookUp.containsKey(name.toLowerCase()); } } }