org.codice.ddf.security.certificate.generator.PkiTools.java Source code

Java tutorial

Introduction

Here is the source code for org.codice.ddf.security.certificate.generator.PkiTools.java

Source

/**
 * 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());
        }
    }
}