org.metaeffekt.dcc.commons.pki.CertificateManager.java Source code

Java tutorial

Introduction

Here is the source code for org.metaeffekt.dcc.commons.pki.CertificateManager.java

Source

/**
 * Copyright 2009-2017 the original author or authors.
 *
 * 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 org.metaeffekt.dcc.commons.pki;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.TimeZone;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang3.StringUtils;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.taskdefs.Delete;
import org.apache.tools.ant.types.FileSet;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.AccessDescription;
import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.CRLDistPoint;
import org.bouncycastle.asn1.x509.DistributionPoint;
import org.bouncycastle.asn1.x509.DistributionPointName;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.OperatorException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
import org.metaeffekt.core.common.kernel.ant.log.LoggingProjectAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.metaeffekt.dcc.commons.ant.PropertyUtils;

public class CertificateManager {

    private static final Logger LOG = LoggerFactory.getLogger(CertificateManager.class);

    private static final String PURPOSE_SERVER_AUTHENTICATION = "Server-Authentication";
    private static final String PURPOSE_CLIENT_AUTHENTICATION = "Client-Authentication";

    private static final String PROPERTY_COMPONENT = "component";
    private static final String PROPERTY_CN = "cn";

    private static final String PROPERTY_CERT_PURPOSE = "cert.purpose";
    private static final String PROPERTY_CERT_VALID_FROM = "cert.valid.from";
    private static final String PROPERTY_CERT_VALID_TO = "cert.valid.to";
    private static final String PROPERTY_CERT_VALID_MONTHS = "cert.valid.months";
    private static final String PROPERTY_CERT_USAGE = "cert.usage";
    private static final String PROPERTY_CERT_SIGNATURE_ALGORITHM = "cert.signature.algorithm";
    private static final String PROPERTY_CERT_CHAIN_LENGTH = "cert.chain.length";
    private static final String PROPERTY_CERT_TYPE = "cert.type";
    private static final String PROPERTY_CERT_SELFSIGNED = "cert.selfsigned";

    // not documented in external documentation. Using defaults.
    private static final String PROPERTY_CERT_CRITICAL_NAMES = "cert.critical.names";
    private static final String PROPERTY_CERT_CRITICAL_KEY_PURPOSE = "cert.critical.key.purpose";
    private static final String PROPERTY_CERT_CRITICAL_KEY_USAGE = "cert.critical.key.usage";
    private static final String PROPERTY_CERT_CRITICAL_CA = "cert.critical.ca";
    private static final String PROPERTY_CERT_CRITICAL_KEY_IDENTIFIER = "cert.critical.key.identifier";
    private static final String PROPERTY_CERT_CRITICAL_CRL_DISTRIBUTION_POINTS = "cert.critical.crl.distribution.points";
    private static final String PROPERTY_CERT_CRITICAL_AUTHORITY_INFORMATION_ACCESS = "cert.critical.authority.information.access";

    private static final String PROPERTY_PREFIX_CRL_DISTRIBUTION_POINT = "cert.crl.distribution.point";
    private static final String PROPERTY_PREFIX_AUTHORITY_INFORMATION_ACCESS = "cert.authority.information.access";
    private static final String PROPERTY_PREFIX_CERT_NAME = "cert.name";
    private static final String PROPERTY_PREFIX_SUBJECT = "subject";
    private static final String PROPERTY_PREFIX_TRUST = "trust";

    private static final String PROPERTY_CSR_SIGNATURE_ALGORITHM = "csr.signature.algorithm";

    private static final String NAME_OTHER = "other";
    private static final String NAME_IP = "ip";
    private static final String NAME_DIRECTORY = "directory";
    private static final String NAME_DNS = "dns";

    private static final String CERT_TYPE_CA_OLD = "issuer";
    private static final String CERT_TYPE_CA = "ca";
    private static final String CERT_TYPE_TLS = "tls";
    private static final String CERT_TYPE_TOKEN_ISSUER = "token-issuer";

    private static final String PROPERTY_SIGNER_COMPONENT = "signer.component";
    private static final String PROPERTY_ISSUER_COMPONENT = "issuer.component";
    private static final String PROPERTY_KEY_SIZE = "key.size";
    private static final String PROPERTY_KEY_ALGORITHM = "key.algorithm";

    private static final String BOOLEAN_STRING_FALSE = "false";

    private static final String DEFAULT_SIGNING_ALGORITHM = "SHA256withRSA";
    private static final String DEFAULT_KEY_ALGORITHM = "RSA";
    private static final int DEFAULT_KEY_SIZE = 2048;
    private static final int DEFAULT_VALIDITY_IN_MONTHS = 3 * 12;

    private static final int DEFAULT_KEYUSAGE_ISSUER = KeyUsage.keyCertSign | KeyUsage.digitalSignature
            | KeyUsage.keyEncipherment | KeyUsage.cRLSign;
    private static final int DEFAULT_KEYUSAGE_TOKEN_ISSUER = KeyUsage.digitalSignature | KeyUsage.keyEncipherment;
    private static final int DEFAULT_KEYUSAGE_TLS = KeyUsage.dataEncipherment | KeyUsage.keyAgreement
            | KeyUsage.keyEncipherment;

    private static final String FILE_EXTENSION_CERT = "cert.pem";
    private static final String FILE_PATTERN_CERT = "*." + FILE_EXTENSION_CERT;

    private static final String FILENAME_PRIVATE_KEY = "private.key.pem";
    private static final String FILENAME_PUBLIC_KEY = "public.key.pem";
    private static final String FILENAME_CERT = FILE_EXTENSION_CERT;
    private static final String FILENAME_CERT_REQUEST = "cert.request.pem";
    private static final String FILENAME_COMPONENT_PROPERTIES = "config/component.properties";

    private static final String FOLDERNAME_SIGNER_CHAIN = "signer-chain";
    private static final String FOLDERNAME_TRUST = "trust";

    private final Random random = new SecureRandom();

    private final File componentBaseDir;;
    private final File componentDir;
    private final String componentName;

    private transient Properties componentProperties;

    private transient File privateKeyFile;
    private transient File publicKeyFile;
    private transient File certFile;
    private transient File certRequestFile;

    private boolean finalIteration = false;

    private List<String> messages = new ArrayList<>();

    private Project antProject;

    private TimeZone timeZone = TimeZone.getDefault();

    private boolean processed = false;

    public CertificateManager(File componentBaseDir, String componentName) {
        super();
        this.componentBaseDir = componentBaseDir;
        this.componentName = componentName;
        this.componentDir = new File(componentBaseDir, componentName);
    }

    public File getComponentDir() {
        return componentDir;
    }

    public void init() {
        if (this.componentProperties == null) {
            Validate.isTrue(componentDir.exists(), String.format("Directory '%s' must exist.", componentDir));

            final String propertiesFilename = FILENAME_COMPONENT_PROPERTIES;
            File propertiesFile = new File(componentDir, propertiesFilename);

            Validate.isTrue(componentDir.exists(), String.format("Property file '%s' must exist.", propertiesFile));
            this.componentProperties = PropertyUtils.loadPropertyFile(propertiesFile);

            privateKeyFile = new File(componentDir, FILENAME_PRIVATE_KEY);
            publicKeyFile = new File(componentDir, FILENAME_PUBLIC_KEY);
            certFile = new File(componentDir, FILENAME_CERT);
            certRequestFile = new File(componentDir, FILENAME_CERT_REQUEST);
        }
    }

    public void createOrComplete() throws IOException, GeneralSecurityException, OperatorException {
        if (!processed) {
            this.processed = true;
            createOrCompleteKeyPair();
            createOrCompleteCertificates();
            createOrCompleteTrust();
            updateSignerChain();
        }
    }

    public void createOrCompleteTrust() throws IOException, GeneralSecurityException, OperatorException {
        init();
        String prefix = PROPERTY_PREFIX_TRUST;

        File trustedCertTargetDir = new File(componentDir, FOLDERNAME_TRUST);

        // support the array notations <prefix>.component[index]=<name>
        for (Object key : componentProperties.keySet()) {
            final String attributeKey = String.valueOf(key);
            if (attributeKey.startsWith(prefix + ".")) {
                String attributeName = attributeKey.substring(prefix.length() + 1);
                if (attributeName.contains("[")) {
                    attributeName = attributeName.substring(0, attributeName.indexOf("["));
                    if (attributeName.equals(PROPERTY_COMPONENT)) {
                        String trustedComponentName = getProperty(attributeKey);
                        copyTrustedCert(trustedComponentName, trustedCertTargetDir);
                    }
                }
            }
        }

        // also support the none array notations <prefix>.component=<name>
        String trustedComponentName = getProperty(prefix + "." + PROPERTY_COMPONENT);
        if (trustedComponentName != null) {
            copyTrustedCert(trustedComponentName, trustedCertTargetDir);
        }
    }

    protected void copyTrustedCert(String trustedComponentName, File trustedCertTargetDir)
            throws IOException, CertificateException {
        File trustedDir = new File(componentBaseDir, trustedComponentName);
        File trustedCert = new File(trustedDir, FILENAME_CERT);
        File trustedCertTargetFile = new File(trustedCertTargetDir,
                String.format("%s.%s", trustedComponentName, FILE_EXTENSION_CERT));

        trustedCertTargetDir.mkdirs();

        if (trustedCert.exists()) {
            final X509Certificate signerCertificate = (X509Certificate) KeyUtils
                    .loadCertificate(trustedCert.getPath());
            persist(signerCertificate, trustedCertTargetFile);
        } else {
            if (finalIteration) {
                final String message = String.format(
                        "Cannot complete trust folder for component '%s'. "
                                + "Certificate of trusted component '%s' not available.",
                        componentName, trustedComponentName);
                LOG.warn(message);
                messages.add(message);
            }
        }
    }

    public void createOrCompleteKeyPair() throws IOException, GeneralSecurityException, OperatorException {
        init();
        if (!publicKeyFile.exists() && !privateKeyFile.exists() && !certFile.exists()) {
            final KeyPair keyPair = generateKeyPair();
            persist(keyPair, privateKeyFile, publicKeyFile);
        } else {
            LOG.debug(
                    "Skipping key pair generation for '{}'. Either public key, private key or cert file already exists.",
                    componentName);
        }
    }

    public void createOrCompleteCertificates() throws IOException, GeneralSecurityException, OperatorException {
        init();
        if (!certFile.exists()) {
            final X509Certificate caCert = generateCertificate();
            if (caCert != null) {
                persist(caCert, certFile);
            } else {
                // certificate could not be created due to missing issuer / signer
                if (finalIteration) {
                    // when this is the final iteration we file a CSR if not already exists
                    if (!certRequestFile.exists()) {
                        final PKCS10CertificationRequest certRequest = generateCertificateRequest();
                        persist(certRequest, certRequestFile);
                    } else {
                        LOG.debug(
                                "Skipping certification request generation for '{}'. Request file already exists.",
                                componentName);
                    }
                    final String message = String.format(
                            "Cannot complete certificate configuration for component '%s'. "
                                    + "A Certification Signing Request (CSR) has been created in file '%s'.",
                            componentName, certRequestFile);
                    LOG.warn(message);
                    messages.add(message);
                }
            }
        } else {
            LOG.debug(
                    "Skipping certificate generation for '{}'. Either public key or private key file already exists.",
                    componentName);
        }
    }

    protected void persist(final KeyPair systemKeyPair, File privateKeyFile, File publicKeyFile)
            throws IOException {
        ensureParentDirExists(privateKeyFile);
        ensureParentDirExists(publicKeyFile);

        persistKey(privateKeyFile, systemKeyPair.getPrivate());
        persistKey(publicKeyFile, systemKeyPair.getPublic());
    }

    protected void persist(final X509Certificate certificate, File certFile) throws IOException {
        ensureParentDirExists(certFile);
        persistPemObject(certFile, certificate);
    }

    protected void persist(final PKCS10CertificationRequest certificateRequest, File certRequestFile)
            throws IOException {
        ensureParentDirExists(certRequestFile);
        persistPemObject(certRequestFile, certificateRequest);
    }

    protected boolean ensureParentDirExists(File privateKeyFile) {
        final File parent = privateKeyFile.getParentFile();
        return parent != null ? parent.mkdirs() : false;
    }

    private void persistPemObject(File file, Object serverCertificate) throws IOException {
        BufferedWriter out = new BufferedWriter(new FileWriter(file));
        JcaPEMWriter writer = new JcaPEMWriter(out);
        try {
            writer.writeObject(serverCertificate);
        } finally {
            IOUtils.closeQuietly(writer);
            IOUtils.closeQuietly(out);
        }
    }

    private void persistKey(File file, Key key) throws IOException {
        BufferedWriter out = new BufferedWriter(new FileWriter(file));
        JcaPEMWriter writer = new JcaPEMWriter(out);
        try {
            writer.writeObject(key);
        } finally {
            IOUtils.closeQuietly(writer);
            IOUtils.closeQuietly(out);
        }
    }

    private void roundFloor(Calendar calendar) {
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        calendar.setTimeZone(timeZone);
    }

    private void roundCeiling(Calendar calendar) {
        calendar.set(Calendar.HOUR_OF_DAY, 23);
        calendar.set(Calendar.MINUTE, 59);
        calendar.set(Calendar.SECOND, 59);
        calendar.set(Calendar.MILLISECOND, 0);
    }

    private KeyPair generateKeyPair() throws NoSuchAlgorithmException {
        String keyAlgorithm = getProperty(PROPERTY_KEY_ALGORITHM, DEFAULT_KEY_ALGORITHM);
        int keySize = getProperty(PROPERTY_KEY_SIZE, DEFAULT_KEY_SIZE);

        final KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance(keyAlgorithm);
        keyGenerator.initialize(keySize);
        return keyGenerator.generateKeyPair();
    }

    public void updateSignerChain() throws CertificateException, IOException {
        String issuerComponentName = getIssuerComponentName();

        String signerComponentName = getSignerComponentName(issuerComponentName);

        if (!componentName.contentEquals(signerComponentName)) {
            File signerDir = new File(componentBaseDir, signerComponentName);
            copyCertificateChain(signerDir, signerComponentName);
        }
    }

    public String getIssuerComponentName() {
        return getProperty(PROPERTY_ISSUER_COMPONENT, componentName);
    }

    private X509Certificate generateCertificate() throws GeneralSecurityException, IOException, OperatorException {
        String issuerComponentName = getIssuerComponentName();

        // determine signer; per default issuer is signer (issuer can be subject --> self-signed)
        String signerComponentName = getSignerComponentName(issuerComponentName);

        if (signerComponentName.equals(componentName)) {
            // self-signed certs is not our goal
            if (BOOLEAN_STRING_FALSE.equals(getProperty(PROPERTY_CERT_SELFSIGNED, BOOLEAN_STRING_FALSE))) {
                return null;
            }
        }

        PublicKey publicKey = loadPublicKey();

        final Calendar begin = getValidityPeriodBegin();
        final Calendar end = getValidityPeriodEnd(begin);

        final X500Name name = createSubjectNameBuilder();

        final BigInteger serialNo = new BigInteger(String.valueOf(random.nextInt()));

        JcaX509v3CertificateBuilder certBuilder = null;

        X509Certificate issuerCertificate = null;

        if (issuerComponentName.equals(componentName)) {
            // check whether this and the issuer are the same and user the already constructed name
            if (issuerComponentName.equals(componentName)) {
                certBuilder = new JcaX509v3CertificateBuilder(name, serialNo, begin.getTime(), end.getTime(), name,
                        publicKey);
            }
        } else {
            // lookup the certificate of the referenced issuer
            File issuerDir = new File(componentBaseDir, issuerComponentName);
            File issuerCert = new File(issuerDir, FILENAME_CERT);
            if (issuerCert.exists()) {
                issuerCertificate = (X509Certificate) KeyUtils.loadCertificate(issuerCert.getPath());
                certBuilder = new JcaX509v3CertificateBuilder(issuerCertificate, serialNo, begin.getTime(),
                        end.getTime(), name, publicKey);
            }
        }

        if (certBuilder == null) {
            // issuer cert was not found. Potentially it was not yet created
            return null;
        }

        List<Extension> extensions = createExtensions(publicKey, issuerCertificate);

        for (Extension extension : extensions) {
            certBuilder.addExtension(extension);
        }

        // load the private key of the signer (signer may be issuer, may be self)
        PrivateKey signerPrivateKey = null;
        File signerDir = new File(componentBaseDir, signerComponentName);
        File signerPrivateKeyFile = new File(signerDir, FILENAME_PRIVATE_KEY);
        if (signerPrivateKeyFile.exists()) {
            signerPrivateKey = KeyUtils.loadKey(signerPrivateKeyFile.getPath());
        } else {
            // when we cannot access the signer we cannot provide a certificate
            return null;
        }

        final String signatureAlgorithm = getProperty(PROPERTY_CERT_SIGNATURE_ALGORITHM, DEFAULT_SIGNING_ALGORITHM);
        final X509CertificateHolder certificateHolder = certBuilder
                .build(new JcaContentSignerBuilder(signatureAlgorithm).build(signerPrivateKey));

        return new JcaX509CertificateConverter().getCertificate(certificateHolder);
    }

    public String getSignerComponentName(String issuerComponentName) {
        String signerComponentName = getProperty(PROPERTY_SIGNER_COMPONENT, issuerComponentName);
        return signerComponentName;
    }

    protected List<Extension> createExtensions(PublicKey publicKey, X509Certificate issuerCertificate)
            throws CertIOException, NoSuchAlgorithmException, IOException {

        List<Extension> extensions = new ArrayList<>();

        String certType = getProperty(PROPERTY_CERT_TYPE, CERT_TYPE_TLS);

        // backward compatibility
        if (CERT_TYPE_CA_OLD.equals(certType)) {
            certType = CERT_TYPE_CA;
        }

        // subject key identifier
        boolean criticalKeyIdentifier = getProperty(PROPERTY_CERT_CRITICAL_KEY_IDENTIFIER, false);
        extensions.add(new Extension(Extension.subjectKeyIdentifier, criticalKeyIdentifier,
                new JcaX509ExtensionUtils().createSubjectKeyIdentifier(publicKey).getEncoded()));

        // basic constraints
        if (CERT_TYPE_CA.equals(certType)) {
            boolean criticalCaConstraints = getProperty(PROPERTY_CERT_CRITICAL_CA, true);
            int chainLengthConstraint = getProperty(PROPERTY_CERT_CHAIN_LENGTH, 0);
            if (chainLengthConstraint > 0) {
                extensions.add(new Extension(Extension.basicConstraints, criticalCaConstraints,
                        new BasicConstraints(chainLengthConstraint).getEncoded()));
            } else {
                extensions.add(new Extension(Extension.basicConstraints, criticalCaConstraints,
                        new BasicConstraints(true).getEncoded()));
            }
        }

        // key usage
        int keyUsageInt = getKeyUsage(certType);
        if (keyUsageInt != 0) {
            // FIXME: test whether we can default to true here
            boolean criticalKeyUsage = getProperty(PROPERTY_CERT_CRITICAL_KEY_USAGE, false);
            KeyUsage keyUsage = new KeyUsage(keyUsageInt);
            extensions.add(new Extension(Extension.keyUsage, criticalKeyUsage, keyUsage.getEncoded()));
        }

        // extended key usage
        KeyPurposeId[] keyPurposeDefault = null;
        if (CERT_TYPE_TLS.equals(certType)) {
            // defaults for TLS
            keyPurposeDefault = new KeyPurposeId[] { KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth };
        }
        boolean criticalKeyPurpose = getProperty(PROPERTY_CERT_CRITICAL_KEY_PURPOSE, false);
        KeyPurposeId[] keyPurpose = createKeyPurposeIds(keyPurposeDefault);
        if (keyPurpose != null) {
            extensions.add(new Extension(Extension.extendedKeyUsage, criticalKeyPurpose,
                    new ExtendedKeyUsage(keyPurpose).getEncoded()));
        }

        // subjectAlternativeName
        List<ASN1Encodable> subjectAlternativeNames = extractAlternativeNames(PROPERTY_PREFIX_CERT_NAME);
        if (!subjectAlternativeNames.isEmpty()) {
            boolean criticalNames = getProperty(PROPERTY_CERT_CRITICAL_NAMES, false);
            DERSequence subjectAlternativeNamesExtension = new DERSequence(
                    subjectAlternativeNames.toArray(new ASN1Encodable[subjectAlternativeNames.size()]));
            extensions.add(new Extension(Extension.subjectAlternativeName, criticalNames,
                    subjectAlternativeNamesExtension.getEncoded()));
        }

        if (issuerCertificate == null) {
            // crl distribution point
            DistributionPoint[] crlDistributionPoints = createCrlDistributionPoints();
            if (crlDistributionPoints != null) {
                boolean criticalCrlDistPoints = getProperty(PROPERTY_CERT_CRITICAL_CRL_DISTRIBUTION_POINTS, false);
                extensions.add(new Extension(Extension.cRLDistributionPoints, criticalCrlDistPoints,
                        new CRLDistPoint(crlDistributionPoints).getEncoded()));
            }

            // authority information access
            AccessDescription[] accessDescriptions = createAccessDescriptions();
            if (accessDescriptions != null) {
                boolean criticalAuthorityInformationAccess = getProperty(
                        PROPERTY_CERT_CRITICAL_AUTHORITY_INFORMATION_ACCESS, false);
                extensions.add(new Extension(Extension.authorityInfoAccess, criticalAuthorityInformationAccess,
                        new AuthorityInformationAccess(accessDescriptions).getEncoded()));
            }
        } else {
            copyExtension(Extension.cRLDistributionPoints, issuerCertificate, extensions);
            copyExtension(Extension.authorityInfoAccess, issuerCertificate, extensions);
        }
        return extensions;
    }

    protected void copyExtension(final ASN1ObjectIdentifier extensionType, X509Certificate issuerCertificate,
            List<Extension> extensions) {
        final byte[] encodedAttribute = issuerCertificate.getExtensionValue(extensionType.getId());
        ASN1OctetString data = ASN1OctetString.getInstance(encodedAttribute);
        boolean isCritical = issuerCertificate.getCriticalExtensionOIDs().contains(extensionType.getId());
        if (encodedAttribute != null) {
            extensions.add(new Extension(extensionType, isCritical, data));
        }
    }

    private DistributionPoint[] createCrlDistributionPoints() {
        List<DistributionPoint> list = new ArrayList<>();
        Set<String> keys = getArrayKeys(PROPERTY_PREFIX_CRL_DISTRIBUTION_POINT);
        for (String dpPrefix : keys) {
            final String uriKey = dpPrefix + ".uri";
            String uri = getMandatoryProperty(uriKey);

            DistributionPointName dpName = new DistributionPointName(
                    new GeneralNames(new GeneralName(GeneralName.uniformResourceIdentifier, uri)));
            list.add(new DistributionPoint(dpName, null, null));
        }
        if (list.isEmpty())
            return null;
        return list.toArray(new DistributionPoint[list.size()]);
    }

    private AccessDescription[] createAccessDescriptions() {
        List<AccessDescription> list = new ArrayList<>();
        Set<String> keys = getArrayKeys(PROPERTY_PREFIX_AUTHORITY_INFORMATION_ACCESS);
        for (String dpPrefix : keys) {
            final String typeKey = dpPrefix + ".type";
            final String type = getMandatoryProperty(typeKey);

            final String uriKey = dpPrefix + ".uri";
            final String uri = getMandatoryProperty(uriKey);

            ASN1ObjectIdentifier aiaId = null;
            switch (type) {
            case "ocsp":
                aiaId = AccessDescription.id_ad_ocsp;
                break;
            case "issuer":
                aiaId = AccessDescription.id_ad_caIssuers;
                break;
            default:
                throw new IllegalArgumentException(
                        String.format("Value '%s' not supported for '%s'. Supported values are 'ocsp' or 'issuer'.",
                                type, typeKey));
            }

            AccessDescription accessDescription = new AccessDescription(aiaId,
                    new GeneralName(GeneralName.uniformResourceIdentifier, uri));

            list.add(accessDescription);
        }

        if (list.isEmpty())
            return null;
        return list.toArray(new AccessDescription[list.size()]);
    }

    protected Set<String> getArrayKeys(String prefix) {
        Set<String> keys = new LinkedHashSet<>();
        for (Object key : componentProperties.keySet()) {
            final String attributeKey = String.valueOf(key);
            if (attributeKey.startsWith(prefix + "[")) {
                String dpPrefix = attributeKey.substring(0, attributeKey.lastIndexOf("]") + 1);
                keys.add(dpPrefix);
            }
        }
        return keys;
    }

    protected String getMandatoryProperty(final String uriKey) {
        String uri = getProperty(uriKey);
        if (StringUtils.isBlank(uri)) {
            throw new IllegalArgumentException(String.format("The property '%s' must be defined.", uriKey));
        }
        return uri;
    }

    private KeyPurposeId[] createKeyPurposeIds(KeyPurposeId[] defaultKeyPurposeIds) {
        String purpose = getProperty(PROPERTY_CERT_PURPOSE, "");

        String[] split = purpose.split(",");

        List<KeyPurposeId> purposeList = new ArrayList<>();

        for (int i = 0; i < split.length; i++) {
            String p = split[i].trim();
            if (StringUtils.isNotBlank(p)) {
                switch (p) {
                case PURPOSE_CLIENT_AUTHENTICATION:
                    purposeList.add(KeyPurposeId.id_kp_clientAuth);
                    break;
                case PURPOSE_SERVER_AUTHENTICATION:
                    purposeList.add(KeyPurposeId.id_kp_serverAuth);
                    break;
                default:
                    try {
                        ASN1ObjectIdentifier newKeyPurposeIdOID = new ASN1ObjectIdentifier(p);
                        purposeList.add(KeyPurposeId.getInstance(newKeyPurposeIdOID));
                    } catch (IllegalArgumentException e) {
                        throw new IllegalArgumentException(String.format(
                                "Certificate purpose '%s' not supported. "
                                        + "Supported values are '%s', '%s', or any valid OID.",
                                p, PURPOSE_CLIENT_AUTHENTICATION, PURPOSE_SERVER_AUTHENTICATION));
                    }
                }
            }
        }

        if (purposeList.isEmpty()) {
            return defaultKeyPurposeIds;
        }

        return purposeList.toArray(new KeyPurposeId[purposeList.size()]);
    }

    protected List<ASN1Encodable> extractAlternativeNames(String prefix) {
        List<ASN1Encodable> subjectAlternativeNames = new ArrayList<ASN1Encodable>();
        for (Object key : componentProperties.keySet()) {
            final String attributeKey = String.valueOf(key);
            if (attributeKey.startsWith(prefix)) {
                String nameTypeString = attributeKey.substring(attributeKey.lastIndexOf(".") + 1);
                String nameValue = getProperty(attributeKey);
                int nameType = 0;
                switch (nameTypeString) {
                case NAME_DNS:
                    nameType = GeneralName.dNSName;
                    break;
                case NAME_DIRECTORY:
                    nameType = GeneralName.directoryName;
                    break;
                case NAME_IP:
                    nameType = GeneralName.iPAddress;
                    break;
                case NAME_OTHER:
                    nameType = GeneralName.otherName;
                    break;
                default:
                    throw new IllegalArgumentException(
                            String.format("Alternative name '%s' not supported.", nameTypeString));
                }

                if (StringUtils.isNotBlank(nameValue)) {
                    subjectAlternativeNames.add(new GeneralName(nameType, nameValue));
                }
            }
        }

        return subjectAlternativeNames;
    }

    protected PKCS10CertificationRequest generateCertificateRequest()
            throws IOException, OperatorCreationException, NoSuchAlgorithmException {
        PublicKey publicKey = loadPublicKey();
        PrivateKey privateKey = loadPrivateKey();

        final X500Name name = createSubjectNameBuilder();

        JcaPKCS10CertificationRequestBuilder certReqBuilder = new JcaPKCS10CertificationRequestBuilder(name,
                publicKey);

        List<Extension> extensionList = createExtensions(publicKey, null);
        Extensions extensions = new Extensions(extensionList.toArray(new Extension[extensionList.size()]));

        certReqBuilder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensions);

        final String signatureAlgorithm = getProperty(PROPERTY_CSR_SIGNATURE_ALGORITHM, DEFAULT_SIGNING_ALGORITHM);
        JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder(signatureAlgorithm);
        ContentSigner signer = csBuilder.build(privateKey);
        return certReqBuilder.build(signer);
    }

    protected X500Name createSubjectNameBuilder() {
        final X500NameBuilder nameBuilder = createNameBuilder(PROPERTY_PREFIX_SUBJECT);
        return nameBuilder.build();
    }

    protected PrivateKey loadPrivateKey() throws IOException {
        return KeyUtils.loadKey(privateKeyFile.getPath());
    }

    protected PublicKey loadPublicKey() throws IOException {
        PublicKey publicKey = null;
        if (publicKeyFile.exists()) {
            publicKey = KeyUtils.loadPublicKey(publicKeyFile.getPath());
        } else {
            if (privateKeyFile.exists()) {
                publicKey = KeyUtils.loadPublicKeyFromKeyPair(privateKeyFile.getPath());
            }
        }

        Validate.notNull(publicKey, String.format("Cannot access public key. Either '%s' or '%s' must exist.",
                publicKeyFile, privateKeyFile));
        return publicKey;
    }

    protected boolean copyCertificateChain(File signerDir, String signerComponentName)
            throws IOException, CertificateException {

        final File targetChainDir = new File(componentDir, FOLDERNAME_SIGNER_CHAIN);

        if (targetChainDir.exists()) {
            Delete delete = new Delete();
            delete.setProject(getAntProject());
            delete.setIncludes(FILE_PATTERN_CERT);
            delete.setDir(targetChainDir);
            delete.execute();
        }

        targetChainDir.mkdirs();
        final File sourceChainDir = new File(signerDir, FOLDERNAME_SIGNER_CHAIN);
        int foundFiles = 0;
        if (sourceChainDir.exists()) {
            // copy the issuer certificate chain
            Copy copy = new Copy();
            copy.setOverwrite(true);
            copy.setProject(getAntProject());
            copy.setTodir(targetChainDir);
            FileSet set = new FileSet();
            set.setDir(sourceChainDir);
            set.setIncludes(FILE_PATTERN_CERT);
            copy.add(set);
            copy.execute();

            DirectoryScanner scanner = new DirectoryScanner();
            scanner.setBasedir(sourceChainDir);
            scanner.setIncludes(new String[] { FILE_PATTERN_CERT });
            scanner.scan();

            foundFiles = scanner.getIncludedFilesCount();
        }

        File signerCert = new File(signerDir, FILENAME_CERT);
        File signerCertTargetFile = new File(targetChainDir,
                String.format("%02d-%s.%s", foundFiles + 1, signerComponentName, FILE_EXTENSION_CERT));

        // NOTE: the signer cert may not yet exist due to a pending csr
        if (signerCert.exists()) {
            final X509Certificate signerCertificate = (X509Certificate) KeyUtils
                    .loadCertificate(signerCert.getPath());
            persist(signerCertificate, signerCertTargetFile);
            return true;
        } else {
            if (finalIteration) {
                final String message = String.format(
                        "Cannot complete signer chain for component '%s'. "
                                + "Signer certificate of component '%s' not available.",
                        componentName, signerComponentName);
                LOG.warn(message);
                messages.add(message);
            }

            return false;
        }
    }

    protected int getKeyUsage(String certType) {
        int defaultKeyUsage = 0;
        switch (certType) {
        case CERT_TYPE_CA:
            defaultKeyUsage = DEFAULT_KEYUSAGE_ISSUER;
            break;
        case CERT_TYPE_TOKEN_ISSUER:
            defaultKeyUsage = DEFAULT_KEYUSAGE_TOKEN_ISSUER;
            break;
        case CERT_TYPE_TLS:
            defaultKeyUsage = DEFAULT_KEYUSAGE_TLS;
            break;
        default:
            throw new IllegalArgumentException(String.format("Certificate type '%s' not supported.", certType));
        }
        return getProperty(PROPERTY_CERT_USAGE, defaultKeyUsage);
    }

    protected Calendar getValidityPeriodBegin() {
        // construct default
        final Calendar beginCalendar = Calendar.getInstance(timeZone);
        roundFloor(beginCalendar);
        Date begin = beginCalendar.getTime();

        // overwrite from configuration
        String configuredBegin = getProperty(PROPERTY_CERT_VALID_FROM);
        if (StringUtils.isNotBlank(configuredBegin)) {
            begin = parseDate(configuredBegin);
            beginCalendar.setTime(begin);
            roundFloor(beginCalendar);
            begin = beginCalendar.getTime();
        }
        return beginCalendar;
    }

    protected Calendar getValidityPeriodEnd(final Calendar beginCalendar) {
        int validityInMonths = getProperty(PROPERTY_CERT_VALID_MONTHS, DEFAULT_VALIDITY_IN_MONTHS);
        // construct default (based on begin + validity)
        final Calendar endCalendar = Calendar.getInstance(timeZone);
        endCalendar.set(Calendar.YEAR, beginCalendar.get(Calendar.YEAR));
        endCalendar.set(Calendar.MONTH, beginCalendar.get(Calendar.MONTH));
        endCalendar.add(Calendar.MONTH, validityInMonths);
        endCalendar.set(Calendar.DAY_OF_MONTH, beginCalendar.get(Calendar.DAY_OF_MONTH));
        roundCeiling(endCalendar);

        // overwrite from configuration
        String configuredEnd = getProperty(PROPERTY_CERT_VALID_TO);
        if (StringUtils.isNotBlank(configuredEnd)) {
            Date end = endCalendar.getTime();
            end = parseDate(configuredEnd);
            endCalendar.setTime(end);
            roundCeiling(endCalendar);
            end = endCalendar.getTime();
        }

        return endCalendar;
    }

    protected Date parseDate(String dateString) {
        try {
            return new SimpleDateFormat("yyyy-MM-dd").parse(dateString.trim());
        } catch (ParseException e) {
            throw new IllegalArgumentException(String
                    .format("Date '%s' could not be parsed. Expecting 'yyyy-MM-dd' formatted date.", dateString));
        }
    }

    protected Project getAntProject() {
        if (antProject == null) {
            antProject = new LoggingProjectAdapter();
        }
        return antProject;
    }

    protected X500NameBuilder createNameBuilder(String prefix) {
        final X500NameBuilder nameBuilder = new X500NameBuilder();

        String subjectString = getProperty(prefix);
        if (subjectString != null) {
            RDN[] rdns = BCStyle.INSTANCE.fromString(subjectString);
            for (RDN rdn : rdns) {
                if (rdn.isMultiValued()) {
                    nameBuilder.addMultiValuedRDN(rdn.getTypesAndValues());
                } else {
                    nameBuilder.addRDN(rdn.getFirst());
                }
            }
        }

        // multiple attributes can be added using an array-like notation
        for (Object key : componentProperties.keySet()) {
            final String attributeKey = String.valueOf(key);
            if (attributeKey.startsWith(prefix + ".")) {
                String attributeName = attributeKey.substring(prefix.length() + 1);
                if (attributeName.contains("[")) {
                    attributeName = attributeName.substring(0, attributeName.indexOf("["));
                    final ASN1ObjectIdentifier oid = BCStyle.INSTANCE.attrNameToOID(attributeName);
                    nameBuilder.addRDN(oid, getProperty(attributeKey));
                }
            }
        }

        // the prefix.CN specifies the main CN. Per default it is the component
        // name in upper case.
        String componentCN = getProperty(prefix + "." + PROPERTY_CN, componentName.toUpperCase());
        nameBuilder.addRDN(BCStyle.CN, componentCN);

        return nameBuilder;
    }

    protected int getProperty(String key, int defaultInt) {
        return Integer.parseInt(getProperty(PROPERTY_CERT_USAGE, String.valueOf(defaultInt)));
    }

    protected boolean getProperty(String key, boolean defaultBoolean) {
        return Boolean.parseBoolean(getProperty(PROPERTY_CERT_USAGE, String.valueOf(defaultBoolean)));
    }

    protected String getProperty(String key, String defaultString) {
        return componentProperties.getProperty(key, defaultString);
    }

    protected String getProperty(String key) {
        return componentProperties.getProperty(key);
    }

    public boolean isFinalIteration() {
        return finalIteration;
    }

    public void setFinalIteration(boolean finalIteration) {
        this.finalIteration = finalIteration;
    }

    public List<String> getMessages() {
        return Collections.unmodifiableList(messages);
    }

    public void setAntProject(Project antProject) {
        this.antProject = antProject;
    }

    public String getComponentName() {
        return componentName;
    }

    public boolean isProcessed() {
        return processed;
    }

    @Override
    public String toString() {
        return getComponentName();
    }

}