| // 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.net.InetAddress; |
| 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.ArrayList; |
| 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 javax.xml.bind.DatatypeConverter; |
| |
| 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.asn1.pkcs.Attribute; |
| import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; |
| 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.jce.provider.BouncyCastleProvider; |
| import org.bouncycastle.operator.OperatorCreationException; |
| import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest; |
| 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 org.apache.commons.lang3.StringUtils; |
| |
| 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 || StringUtils.isEmpty(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> names, final List<String> ips, final int validityDays) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, CertificateException, SignatureException, IOException, OperatorCreationException { |
| final List<String> dnsNames = new ArrayList<>(); |
| final List<String> ipAddresses = new ArrayList<>(); |
| |
| if (names != null) { |
| dnsNames.addAll(names); |
| } |
| if (ips != null) { |
| ipAddresses.addAll(ips); |
| } |
| |
| 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 JcaPKCS10CertificationRequest request = new JcaPKCS10CertificationRequest(pemObject.getContent()); |
| final String subject = request.getSubject().toString(); |
| for (final Attribute attribute : request.getAttributes()) { |
| if (attribute == null) { |
| continue; |
| } |
| if (attribute.getAttrType().equals(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest)) { |
| final Extensions extensions = Extensions.getInstance(attribute.getAttrValues().getObjectAt(0)); |
| final GeneralNames gns = GeneralNames.fromExtensions(extensions, Extension.subjectAlternativeName); |
| if (gns != null && gns.getNames() != null && gns.getNames().length > 0) { |
| for (final GeneralName name : gns.getNames()) { |
| if (name.getTagNo() == GeneralName.dNSName) { |
| dnsNames.add(name.getName().toString()); |
| } |
| if (name.getTagNo() == GeneralName.iPAddress) { |
| final InetAddress address = InetAddress.getByAddress(DatatypeConverter.parseHexBinary(name.getName().toString().substring(1))); |
| ipAddresses.add(address.toString().replace("/", "")); |
| } |
| } |
| } |
| } |
| } |
| |
| final X509Certificate clientCertificate = CertUtils.generateV3Certificate( |
| caCertificate, caKeyPair, request.getPublicKey(), |
| subject, CAManager.CertSignatureAlgorithm.value(), |
| validityDays, dnsNames, 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(); |
| // If authStrictness require SSL and validate client cert, otherwise prefer SSL but don't validate client cert |
| if (authStrictness) { |
| sslEngine.setNeedClientAuth(true); // Require SSL and client cert validation |
| } else { |
| sslEngine.setWantClientAuth(true); // Prefer SSL but don't validate client cert |
| } |
| |
| return sslEngine; |
| } |
| |
| @Override |
| public KeyStore getManagementKeyStore() throws KeyStoreException { |
| return managementKeyStore; |
| } |
| |
| @Override |
| public char[] getKeyStorePassphrase() { |
| return KeyStoreUtils.DEFAULT_KS_PASSPHRASE; |
| } |
| |
| ///////////////////////////////////////////////// |
| /////////////// 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 (StringUtils.isAnyEmpty(rootCAPublicKey.value(), 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 (StringUtils.isEmpty(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()), |
| NetUtils.getAllDefaultNicIps(), 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"; |
| } |
| } |