net.maritimecloud.pki.CAHandler.java Source code

Java tutorial

Introduction

Here is the source code for net.maritimecloud.pki.CAHandler.java

Source

/*
 * Copyright 2017 Danish Maritime Authority.
 *
 *  Licensed 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 net.maritimecloud.pki;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableEntryException;
import java.security.cert.CRLReason;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import static net.maritimecloud.pki.PKIConstants.KEYSTORE_TYPE;
import static net.maritimecloud.pki.PKIConstants.ROOT_CERT_ALIAS;

@Slf4j
@AllArgsConstructor
public class CAHandler {

    private CertificateBuilder certificateBuilder;
    private PKIConfiguration pkiConfiguration;

    /**
     * Creates a sub Certificate Authority for the MC PKI. The certificate and keypair is placed in a "SubCaKeystore"
     * defined in PKIConfiguration and in the truststore, also defined in PKIConfiguration. The SubCaKeystore will be
     * created if it does not exist already, but the truststore is expected to exists already. It is also expected that
     * a RootCaKeystore is defined in PKIConfiguration and exists.
     *
     * @param subCaCertDN The DN of the new sub CA certificate.
     */
    public void createSubCa(String subCaCertDN) {

        // Open the various keystores
        KeyStore rootKeystore;
        InputStream rootKeystoreIS = null;
        KeyStore subCaKeystore;
        KeyStore truststore;
        FileInputStream subCaFis = null;
        FileInputStream trustFis = null;
        try {
            // Open the root keystore
            rootKeystore = KeyStore.getInstance(KEYSTORE_TYPE);
            rootKeystoreIS = new FileInputStream(pkiConfiguration.getRootCaKeystorePath());
            rootKeystore.load(rootKeystoreIS, pkiConfiguration.getRootCaKeystorePassword().toCharArray());

            // Open or create the sub CA keystore
            subCaKeystore = KeyStore.getInstance(KEYSTORE_TYPE);
            if (new File(pkiConfiguration.getSubCaKeystorePath()).exists()) {
                subCaFis = new FileInputStream(pkiConfiguration.getSubCaKeystorePath());
                subCaKeystore.load(subCaFis, pkiConfiguration.getSubCaKeystorePassword().toCharArray());
            } else {
                subCaKeystore.load(null, pkiConfiguration.getSubCaKeystorePassword().toCharArray());
            }

            // Open the truststore
            trustFis = new FileInputStream(pkiConfiguration.getTruststorePath());
            truststore = KeyStore.getInstance(KeyStore.getDefaultType());
            truststore.load(trustFis, pkiConfiguration.getTruststorePassword().toCharArray());

        } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
            throw new RuntimeException(e);
        } finally {
            safeClose(rootKeystoreIS);
            safeClose(trustFis);
            safeClose(subCaFis);
        }

        // Extract the root certificate
        KeyStore.ProtectionParameter protParam = new KeyStore.PasswordProtection(
                pkiConfiguration.getRootCaKeystorePassword().toCharArray());
        KeyStore.PrivateKeyEntry rootCertEntry;
        X500Name rootCertX500Name;
        String crlUrl;
        try {
            rootCertEntry = (KeyStore.PrivateKeyEntry) rootKeystore.getEntry(ROOT_CERT_ALIAS, protParam);
            rootCertX500Name = new JcaX509CertificateHolder((X509Certificate) rootCertEntry.getCertificate())
                    .getSubject();
        } catch (NoSuchAlgorithmException | UnrecoverableEntryException | KeyStoreException
                | CertificateEncodingException e) {
            throw new RuntimeException(e);
        }
        try {
            List<String> crlPoints = CRLVerifier
                    .getCrlDistributionPoints((X509Certificate) rootCertEntry.getCertificate());
            crlUrl = crlPoints.get(0);
        } catch (CertificateParsingException | IOException e) {
            throw new RuntimeException(e);
        }

        // Create the sub CA certificate
        KeyPair subCaKeyPair = CertificateBuilder.generateKeyPair();
        X509Certificate subCaCert;
        X500Name subCaCertX500Name = new X500Name(subCaCertDN);
        String alias = CertificateHandler.getElement(subCaCertX500Name, BCStyle.UID);
        if (alias == null || alias.trim().isEmpty()) {
            throw new RuntimeException("UID must be defined for sub CA! It will be used as the sub CA alias.");
        }
        try {
            subCaCert = certificateBuilder.buildAndSignCert(certificateBuilder.generateSerialNumber(),
                    rootCertEntry.getPrivateKey(), rootCertEntry.getCertificate().getPublicKey(),
                    subCaKeyPair.getPublic(), rootCertX500Name, subCaCertX500Name, null, "INTERMEDIATE", null,
                    crlUrl);
        } catch (Exception e) {
            throw new RuntimeException("Could not create sub CA certificate!", e);
        }

        // Store the sub CA certificate in the Sub CA keystore and the MC truststore
        FileOutputStream trustFos = null;
        FileOutputStream subCaFos = null;
        try {
            Certificate[] certChain = new Certificate[2];
            certChain[0] = subCaCert;
            certChain[1] = rootCertEntry.getCertificate();
            subCaFos = new FileOutputStream(pkiConfiguration.getSubCaKeystorePath());
            subCaKeystore.setKeyEntry(alias, subCaKeyPair.getPrivate(),
                    pkiConfiguration.getSubCaKeyPassword().toCharArray(), certChain);
            subCaKeystore.store(subCaFos, pkiConfiguration.getSubCaKeystorePassword().toCharArray());

            trustFos = new FileOutputStream(pkiConfiguration.getTruststorePath());
            truststore.setCertificateEntry(alias, subCaCert);
            truststore.store(trustFos, pkiConfiguration.getTruststorePassword().toCharArray());

        } catch (NoSuchAlgorithmException | KeyStoreException | CertificateException | IOException e) {
            throw new RuntimeException(e);
        } finally {
            safeClose(trustFos);
            safeClose(subCaFos);
        }

    }

    /**
     * Generates a self-signed certificate and saves it in the keystore and truststore.
     * Should only be used to init the root CA. It is expected that info about the root keystore and the truststore
     * is available in PKIConfiguration. If they already exists they will be overwritten!
     *
     * @param rootCertX500Name The DN of the new root CA Certificate
     * @param crlUrl CRL endpoint
     */
    public void initRootCA(String rootCertX500Name, String crlUrl) {
        KeyPair cakp = CertificateBuilder.generateKeyPair();
        KeyStore rootks;
        KeyStore ts;
        FileOutputStream rootfos = null;
        FileOutputStream tsfos = null;
        try {
            rootks = KeyStore.getInstance(KEYSTORE_TYPE);
            rootks.load(null, pkiConfiguration.getRootCaKeystorePassword().toCharArray());
            // Store away the keystore.
            rootfos = new FileOutputStream(pkiConfiguration.getRootCaKeystorePath());
            X509Certificate cacert;
            try {
                cacert = certificateBuilder.buildAndSignCert(certificateBuilder.generateSerialNumber(),
                        cakp.getPrivate(), cakp.getPublic(), cakp.getPublic(), new X500Name(rootCertX500Name),
                        new X500Name(rootCertX500Name), null, "ROOTCA", null, crlUrl);
            } catch (Exception e) {
                throw new RuntimeException(e.getMessage(), e);
            }

            Certificate[] certChain = new Certificate[1];
            certChain[0] = cacert;
            rootks.setKeyEntry(ROOT_CERT_ALIAS, cakp.getPrivate(),
                    pkiConfiguration.getRootCaKeyPassword().toCharArray(), certChain);
            rootks.store(rootfos, pkiConfiguration.getRootCaKeystorePassword().toCharArray());
            rootks = KeyStore.getInstance(KeyStore.getDefaultType());
            rootks.load(null, pkiConfiguration.getRootCaKeystorePassword().toCharArray());

            // Store away the truststore.
            ts = KeyStore.getInstance(KeyStore.getDefaultType());
            ts.load(null, pkiConfiguration.getTruststorePassword().toCharArray());
            tsfos = new FileOutputStream(pkiConfiguration.getTruststorePath());
            ts.setCertificateEntry(ROOT_CERT_ALIAS, cacert);
            ts.store(tsfos, pkiConfiguration.getTruststorePassword().toCharArray());
        } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
            throw new RuntimeException(e.getMessage(), e);
        } finally {
            safeClose(rootfos);
            safeClose(tsfos);
        }
    }

    private void safeClose(InputStream stream) {
        if (stream != null) {
            try {
                stream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void safeClose(OutputStream stream) {
        if (stream != null) {
            try {
                stream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Loads a CSV file with information about revoked certificates into a RevocationInfo list.
     * The CSV file must use semi-colon for separation and in the format:
     * serial-number;revocation-reason;date
     * An example:
     * 345678954765889809876543;cacompromise;2017-04-31
     *
     * @param revocationFile Path to the file that should be loaded.
     * @return List of certificates that has been/should be revoked.
     */
    public List<RevocationInfo> loadRevocationFile(String revocationFile) {
        String csvLine;
        String cvsSplitBy = ";";
        List<RevocationInfo> revocationInfos = new ArrayList<>();
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        try (BufferedReader br = new BufferedReader(new FileReader(revocationFile))) {
            while ((csvLine = br.readLine()) != null) {
                if (csvLine.trim().isEmpty()) {
                    continue;
                }
                String[] revocationInfoSplit = csvLine.split(cvsSplitBy);
                if (revocationInfoSplit.length != 3) {
                    throw new RuntimeException("Missing info from line: " + csvLine);
                }
                RevocationInfo info = new RevocationInfo();
                info.setSerialNumber(new BigInteger(revocationInfoSplit[0].trim()));
                info.setRevokeReason(CRLReason.values()[Revocation
                        .getCRLReasonFromString(revocationInfoSplit[1].trim().toLowerCase())]);
                Date revokedAt = format.parse(revocationInfoSplit[2].trim());
                if (revokedAt == null) {
                    throw new RuntimeException("Invalid date format!");
                }
                info.setRevokedAt(revokedAt);
                revocationInfos.add(info);
            }
        } catch (FileNotFoundException e) {
            throw new RuntimeException("Could not find the revocation info file!", e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (ParseException e) {
            throw new RuntimeException("Invalid date format!", e);
        }

        return revocationInfos;
    }

    /**
     * Generates a CRL for the root CA. It is expected that
     * a RootCaKeystore is defined in PKIConfiguration and exists.
     *
     * @param outputCaCrlPath Output path where to place the CRL.
     * @param revocationFile Path to the CSV file which contains revocation info.
     */
    public void generateRootCRL(String outputCaCrlPath, String revocationFile) {
        List<RevocationInfo> revocationInfos = loadRevocationFile(revocationFile);

        try {
            KeyStore rootks = KeyStore.getInstance(KEYSTORE_TYPE);
            InputStream readStream = new FileInputStream(pkiConfiguration.getRootCaKeystorePath());
            rootks.load(readStream, pkiConfiguration.getRootCaKeystorePassword().toCharArray());
            KeyStore.ProtectionParameter protParam = new KeyStore.PasswordProtection(
                    pkiConfiguration.getRootCaKeystorePassword().toCharArray());
            KeyStore.PrivateKeyEntry rootCertEntry;
            rootCertEntry = (KeyStore.PrivateKeyEntry) rootks.getEntry(ROOT_CERT_ALIAS, protParam);
            String rootCertX500Name = new JcaX509CertificateHolder((X509Certificate) rootCertEntry.getCertificate())
                    .getSubject().toString();
            Revocation.generateRootCACRL(rootCertX500Name, revocationInfos, rootCertEntry, outputCaCrlPath);
        } catch (NoSuchAlgorithmException | UnrecoverableEntryException | KeyStoreException e) {
            throw new RuntimeException("Unable to generate RootCACRL", e);
        } catch (CertificateException e) {
            throw new RuntimeException("Could not load root certificate!", e);
        } catch (FileNotFoundException e) {
            throw new RuntimeException("Could not find root keystore!", e);
        } catch (IOException e) {
            throw new RuntimeException("Could not load root keystore!", e);
        }
    }
}