org.apache.cloudstack.ca.provider.RootCAProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.cloudstack.ca.provider.RootCAProvider.java

Source

// 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.cloudstack.ca.provider;

import java.io.IOException;
import java.io.StringReader;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyManagementException;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.SignatureException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;
import javax.naming.ConfigurationException;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

import org.apache.cloudstack.ca.CAManager;
import org.apache.cloudstack.framework.ca.CAProvider;
import org.apache.cloudstack.framework.ca.Certificate;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.utils.security.CertUtils;
import org.apache.cloudstack.utils.security.KeyStoreUtils;
import org.apache.log4j.Logger;
import org.bouncycastle.jce.PKCS10CertificationRequest;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;

import com.cloud.certificate.dao.CrlDao;
import com.cloud.utils.component.AdapterBase;
import com.cloud.utils.db.GlobalLock;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.NetUtils;
import com.google.common.base.Strings;

public final class RootCAProvider extends AdapterBase implements CAProvider, Configurable {
    private static final Logger LOG = Logger.getLogger(RootCAProvider.class);

    public static final Integer caValidityYears = 30;
    public static final String caAlias = "root";
    public static final String managementAlias = "management";

    private static KeyPair caKeyPair = null;
    private static X509Certificate caCertificate = null;
    private static KeyStore managementKeyStore = null;

    @Inject
    private ConfigurationDao configDao;
    @Inject
    private CrlDao crlDao;

    ////////////////////////////////////////////////////
    /////////////// Root CA Settings ///////////////////
    ////////////////////////////////////////////////////

    private static ConfigKey<String> rootCAPrivateKey = new ConfigKey<>("Hidden", String.class,
            "ca.plugin.root.private.key", null, "The ROOT CA private key.", true);

    private static ConfigKey<String> rootCAPublicKey = new ConfigKey<>("Hidden", String.class,
            "ca.plugin.root.public.key", null, "The ROOT CA public key.", true);

    private static ConfigKey<String> rootCACertificate = new ConfigKey<>("Hidden", String.class,
            "ca.plugin.root.ca.certificate", null, "The ROOT CA certificate.", true);

    private static ConfigKey<String> rootCAIssuerDN = new ConfigKey<>("Advanced", String.class,
            "ca.plugin.root.issuer.dn", "CN=ca.cloudstack.apache.org", "The ROOT CA issuer distinguished name.",
            true);

    protected static ConfigKey<Boolean> rootCAAuthStrictness = new ConfigKey<>("Advanced", Boolean.class,
            "ca.plugin.root.auth.strictness", "false",
            "Set client authentication strictness, setting to true will enforce and require client certificate for authentication in applicable CA providers.",
            true);

    private static ConfigKey<Boolean> rootCAAllowExpiredCert = new ConfigKey<>("Advanced", Boolean.class,
            "ca.plugin.root.allow.expired.cert", "true",
            "When set to true, it will allow expired client certificate during SSL handshake.", true);

    ///////////////////////////////////////////////////////////
    /////////////// Root CA Private Methods ///////////////////
    ///////////////////////////////////////////////////////////

    private Certificate generateCertificate(final List<String> domainNames, final List<String> ipAddresses,
            final int validityDays) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException,
            CertificateException, SignatureException, IOException, OperatorCreationException {
        if (domainNames == null || domainNames.size() < 1 || Strings.isNullOrEmpty(domainNames.get(0))) {
            throw new CloudRuntimeException("No domain name is specified, cannot generate certificate");
        }
        final String subject = "CN=" + domainNames.get(0);

        final KeyPair keyPair = CertUtils.generateRandomKeyPair(CAManager.CertKeySize.value());
        final X509Certificate clientCertificate = CertUtils.generateV3Certificate(caCertificate, caKeyPair,
                keyPair.getPublic(), subject, CAManager.CertSignatureAlgorithm.value(), validityDays, domainNames,
                ipAddresses);
        return new Certificate(clientCertificate, keyPair.getPrivate(), Collections.singletonList(caCertificate));
    }

    private Certificate generateCertificateUsingCsr(final String csr, final List<String> domainNames,
            final List<String> ipAddresses, final int validityDays)
            throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, CertificateException,
            SignatureException, IOException, OperatorCreationException {
        PemObject pemObject = null;

        try {
            final PemReader pemReader = new PemReader(new StringReader(csr));
            pemObject = pemReader.readPemObject();
        } catch (IOException e) {
            LOG.error("Failed to read provided CSR string as a PEM object", e);
        }

        if (pemObject == null) {
            throw new CloudRuntimeException("Unable to read/process CSR: " + csr);
        }

        final PKCS10CertificationRequest request = new PKCS10CertificationRequest(pemObject.getContent());

        final String subject = request.getCertificationRequestInfo().getSubject().toString();
        final X509Certificate clientCertificate = CertUtils.generateV3Certificate(caCertificate, caKeyPair,
                request.getPublicKey(), subject, CAManager.CertSignatureAlgorithm.value(), validityDays,
                domainNames, ipAddresses);
        return new Certificate(clientCertificate, null, Collections.singletonList(caCertificate));
    }

    ////////////////////////////////////////////////////////
    /////////////// Root CA API Handlers ///////////////////
    ////////////////////////////////////////////////////////

    @Override
    public boolean canProvisionCertificates() {
        return true;
    }

    @Override
    public List<X509Certificate> getCaCertificate() {
        return Collections.singletonList(caCertificate);
    }

    @Override
    public Certificate issueCertificate(final List<String> domainNames, final List<String> ipAddresses,
            final int validityDays) {
        try {
            return generateCertificate(domainNames, ipAddresses, validityDays);
        } catch (final CertificateException | IOException | SignatureException | NoSuchAlgorithmException
                | NoSuchProviderException | InvalidKeyException | OperatorCreationException e) {
            LOG.error("Failed to create client certificate, due to: ", e);
            throw new CloudRuntimeException("Failed to generate certificate due to:" + e.getMessage());
        }
    }

    @Override
    public Certificate issueCertificate(final String csr, final List<String> domainNames,
            final List<String> ipAddresses, final int validityDays) {
        try {
            return generateCertificateUsingCsr(csr, domainNames, ipAddresses, validityDays);
        } catch (final CertificateException | IOException | SignatureException | NoSuchAlgorithmException
                | NoSuchProviderException | InvalidKeyException | OperatorCreationException e) {
            LOG.error("Failed to generate certificate from CSR: ", e);
            throw new CloudRuntimeException("Failed to generate certificate using CSR due to:" + e.getMessage());
        }
    }

    @Override
    public boolean revokeCertificate(final BigInteger certSerial, final String certCn) {
        return true;
    }

    ////////////////////////////////////////////////////////////
    /////////////// Root CA Trust Management ///////////////////
    ////////////////////////////////////////////////////////////

    private KeyStore getCaKeyStore()
            throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException {
        final KeyStore ks = KeyStore.getInstance("JKS");
        ks.load(null, null);
        if (caKeyPair != null && caCertificate != null) {
            ks.setKeyEntry(caAlias, caKeyPair.getPrivate(), getKeyStorePassphrase(),
                    new X509Certificate[] { caCertificate });
        } else {
            return null;
        }
        return ks;
    }

    @Override
    public SSLEngine createSSLEngine(final SSLContext sslContext, final String remoteAddress,
            final Map<String, X509Certificate> certMap) throws KeyManagementException, UnrecoverableKeyException,
            NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException {
        final KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
        final TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");

        final KeyStore ks = getCaKeyStore();
        kmf.init(ks, getKeyStorePassphrase());
        tmf.init(ks);

        final boolean authStrictness = rootCAAuthStrictness.value();
        final boolean allowExpiredCertificate = rootCAAllowExpiredCert.value();

        TrustManager[] tms = new TrustManager[] { new RootCACustomTrustManager(remoteAddress, authStrictness,
                allowExpiredCertificate, certMap, caCertificate, crlDao) };
        sslContext.init(kmf.getKeyManagers(), tms, new SecureRandom());
        final SSLEngine sslEngine = sslContext.createSSLEngine();
        sslEngine.setNeedClientAuth(authStrictness);
        return sslEngine;
    }

    @Override
    public KeyStore getManagementKeyStore() throws KeyStoreException {
        return managementKeyStore;
    }

    @Override
    public char[] getKeyStorePassphrase() {
        return KeyStoreUtils.defaultKeystorePassphrase;
    }

    /////////////////////////////////////////////////
    /////////////// Root CA Setup ///////////////////
    /////////////////////////////////////////////////

    private int getCaValidityDays() {
        return 365 * caValidityYears;
    }

    private boolean saveNewRootCAKeypair() {
        try {
            LOG.debug("Generating root CA public/private keys");
            final KeyPair keyPair = CertUtils.generateRandomKeyPair(2 * CAManager.CertKeySize.value());
            if (!configDao.update(rootCAPublicKey.key(), rootCAPublicKey.category(),
                    CertUtils.publicKeyToPem(keyPair.getPublic()))) {
                LOG.error("Failed to save RootCA public key");
            }
            if (!configDao.update(rootCAPrivateKey.key(), rootCAPrivateKey.category(),
                    CertUtils.privateKeyToPem(keyPair.getPrivate()))) {
                LOG.error("Failed to save RootCA private key");
            }
        } catch (final NoSuchProviderException | NoSuchAlgorithmException | IOException e) {
            LOG.error("Failed to generate/save RootCA private/public keys due to exception:", e);
        }
        return loadRootCAKeyPair();
    }

    private boolean saveNewRootCACertificate() {
        if (caKeyPair == null) {
            throw new CloudRuntimeException(
                    "Cannot issue self-signed root CA certificate as CA keypair is not initialized");
        }
        try {
            LOG.debug("Generating root CA certificate");
            final X509Certificate rootCaCertificate = CertUtils.generateV3Certificate(null, caKeyPair,
                    caKeyPair.getPublic(), rootCAIssuerDN.value(), CAManager.CertSignatureAlgorithm.value(),
                    getCaValidityDays(), null, null);
            if (!configDao.update(rootCACertificate.key(), rootCACertificate.category(),
                    CertUtils.x509CertificateToPem(rootCaCertificate))) {
                LOG.error("Failed to update RootCA public/x509 certificate");
            }
        } catch (final CertificateException | NoSuchAlgorithmException | NoSuchProviderException
                | SignatureException | InvalidKeyException | OperatorCreationException | IOException e) {
            LOG.error("Failed to generate RootCA certificate from private/public keys due to exception:", e);
            return false;
        }
        return loadRootCACertificate();
    }

    private boolean loadRootCAKeyPair() {
        if (Strings.isNullOrEmpty(rootCAPublicKey.value()) || Strings.isNullOrEmpty(rootCAPrivateKey.value())) {
            return false;
        }
        try {
            caKeyPair = new KeyPair(CertUtils.pemToPublicKey(rootCAPublicKey.value()),
                    CertUtils.pemToPrivateKey(rootCAPrivateKey.value()));
        } catch (InvalidKeySpecException | IOException e) {
            LOG.error("Failed to load saved RootCA private/public keys due to exception:", e);
            return false;
        }
        return caKeyPair.getPrivate() != null && caKeyPair.getPublic() != null;
    }

    private boolean loadRootCACertificate() {
        if (Strings.isNullOrEmpty(rootCACertificate.value())) {
            return false;
        }
        try {
            caCertificate = CertUtils.pemToX509Certificate(rootCACertificate.value());
            caCertificate.verify(caKeyPair.getPublic());
        } catch (final IOException | CertificateException | NoSuchAlgorithmException | InvalidKeyException
                | SignatureException | NoSuchProviderException e) {
            LOG.error("Failed to load saved RootCA certificate due to exception:", e);
            return false;
        }
        return caCertificate != null;
    }

    private boolean loadManagementKeyStore() {
        if (managementKeyStore != null) {
            return true;
        }
        final Certificate serverCertificate = issueCertificate(Collections.singletonList(NetUtils.getHostName()),
                Collections.singletonList(NetUtils.getDefaultHostIp()), getCaValidityDays());
        if (serverCertificate == null || serverCertificate.getPrivateKey() == null) {
            throw new CloudRuntimeException(
                    "Failed to generate management server certificate and load management server keystore");
        }
        LOG.info("Creating new management server certificate and keystore");
        try {
            managementKeyStore = KeyStore.getInstance("JKS");
            managementKeyStore.load(null, null);
            managementKeyStore.setCertificateEntry(caAlias, caCertificate);
            managementKeyStore.setKeyEntry(managementAlias, serverCertificate.getPrivateKey(),
                    getKeyStorePassphrase(),
                    new X509Certificate[] { serverCertificate.getClientCertificate(), caCertificate });
        } catch (final CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException e) {
            LOG.error("Failed to load root CA management-server keystore due to exception: ", e);
            return false;
        }
        return managementKeyStore != null;
    }

    private boolean setupCA() {
        if (!loadRootCAKeyPair() && !saveNewRootCAKeypair()) {
            LOG.error("Failed to save and load root CA keypair");
            return false;
        }
        if (!loadRootCACertificate() && !saveNewRootCACertificate()) {
            LOG.error("Failed to save and load root CA certificate");
            return false;
        }
        if (!loadManagementKeyStore()) {
            LOG.error("Failed to check and configure management server keystore");
            return false;
        }
        return true;
    }

    @Override
    public boolean start() {
        return loadRootCAKeyPair() && loadRootCAKeyPair() && loadManagementKeyStore();
    }

    @Override
    public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
        super.configure(name, params);
        Security.addProvider(new BouncyCastleProvider());
        final GlobalLock caLock = GlobalLock.getInternLock("RootCAProviderSetup");
        try {
            if (caLock.lock(5 * 60)) {
                try {
                    return setupCA();
                } finally {
                    caLock.unlock();
                }
            } else {
                LOG.error(
                        "Failed to grab lock and setup CA, startup method will try to load the CA certificate and keypair.");
            }
        } finally {
            caLock.releaseRef();
        }
        return true;
    }

    ///////////////////////////////////////////////////////
    /////////////// Root CA Descriptors ///////////////////
    ///////////////////////////////////////////////////////

    @Override
    public String getConfigComponentName() {
        return RootCAProvider.class.getSimpleName();
    }

    @Override
    public ConfigKey<?>[] getConfigKeys() {
        return new ConfigKey<?>[] { rootCAPrivateKey, rootCAPublicKey, rootCACertificate, rootCAIssuerDN,
                rootCAAuthStrictness, rootCAAllowExpiredCert };
    }

    @Override
    public String getProviderName() {
        return "root";
    }

    @Override
    public String getDescription() {
        return "CloudStack's Root CA provider plugin";
    }
}