From fd775cfbee6f6eba5a7ab6be522e5298bb6a6e4f Mon Sep 17 00:00:00 2001 From: P3701066 Date: Mon, 20 May 2019 14:57:59 +0300 Subject: [PATCH] ENA4.8 certificate upload improved endpoint --- api/src/com/cloud/network/lb/CertService.java | 15 +- .../com/cloud/server/ManagementService.java | 13 ++ .../apache/cloudstack/api/ApiConstants.java | 6 + ...oadCustomCertificateWithValidationCmd.java | 108 +++++++++++++ .../cloud/server/ManagementServerImpl.java | 152 +++++++++++++++++- .../network/lb/CertServiceImpl.java | 38 ++++- 6 files changed, 316 insertions(+), 16 deletions(-) create mode 100644 api/src/org/apache/cloudstack/api/command/admin/resource/UploadCustomCertificateWithValidationCmd.java diff --git a/api/src/com/cloud/network/lb/CertService.java b/api/src/com/cloud/network/lb/CertService.java index e9219e078785..904fa6c76a9d 100644 --- a/api/src/com/cloud/network/lb/CertService.java +++ b/api/src/com/cloud/network/lb/CertService.java @@ -16,6 +16,9 @@ // under the License. package com.cloud.network.lb; +import java.io.IOException; +import java.security.PrivateKey; +import java.security.cert.Certificate; import java.util.List; import org.apache.cloudstack.api.command.user.loadbalancer.DeleteSslCertCmd; @@ -25,9 +28,15 @@ public interface CertService { - public SslCertResponse uploadSslCert(UploadSslCertCmd certCmd); + SslCertResponse uploadSslCert(UploadSslCertCmd certCmd); - public void deleteSslCert(DeleteSslCertCmd deleteSslCertCmd); + void deleteSslCert(DeleteSslCertCmd deleteSslCertCmd); - public List listSslCerts(ListSslCertsCmd listSslCertCmd); + List listSslCerts(ListSslCertsCmd listSslCertCmd); + + Certificate parseCertificate(final String cert); + + void validateChain(final List chain, final Certificate cert); + + PrivateKey parsePrivateKey(final String key) throws IOException; } \ No newline at end of file diff --git a/api/src/com/cloud/server/ManagementService.java b/api/src/com/cloud/server/ManagementService.java index 7f3141bc4183..c43c457f68fe 100644 --- a/api/src/com/cloud/server/ManagementService.java +++ b/api/src/com/cloud/server/ManagementService.java @@ -16,6 +16,9 @@ // under the License. package com.cloud.server; +import java.io.IOException; +import java.security.KeyStoreException; +import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -38,6 +41,7 @@ import org.apache.cloudstack.api.command.admin.resource.ListAlertsCmd; import org.apache.cloudstack.api.command.admin.resource.ListCapacityCmd; import org.apache.cloudstack.api.command.admin.resource.UploadCustomCertificateCmd; +import org.apache.cloudstack.api.command.admin.resource.UploadCustomCertificateWithValidationCmd; import org.apache.cloudstack.api.command.admin.systemvm.DestroySystemVmCmd; import org.apache.cloudstack.api.command.admin.systemvm.ListSystemVMsCmd; import org.apache.cloudstack.api.command.admin.systemvm.RebootSystemVmCmd; @@ -325,6 +329,15 @@ public interface ManagementService { */ String uploadCertificate(UploadCustomCertificateCmd cmd); + /** + * This method uploads a custom cert to the db and performs the proper validations on it, and patches every cpvm with it on the current ms + * + * @param cmd + * -- upload certificate cmd + * @return -- returns a string on success + */ + String uploadCertificateWithValidation(UploadCustomCertificateWithValidationCmd cmd) throws KeyStoreException, CertificateException, IOException; + String getVersion(); /** diff --git a/api/src/org/apache/cloudstack/api/ApiConstants.java b/api/src/org/apache/cloudstack/api/ApiConstants.java index 75ed58117612..af83837d6f12 100644 --- a/api/src/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/org/apache/cloudstack/api/ApiConstants.java @@ -41,6 +41,9 @@ public class ApiConstants { public static final String CATEGORY = "category"; public static final String CAN_REVERT = "canrevert"; public static final String CERTIFICATE = "certificate"; + public static final String ROOT_CERTIFICATE = "rootcertificate"; + public static final String INTERMIDIATE_CERTIFICATES = "intermediatecertificates"; + public static final String SERVER_CERTIFICATE = "servercertificate"; public static final String CERTIFICATE_CHAIN = "certchain"; public static final String CERTIFICATE_FINGERPRINT = "fingerprint"; public static final String CERTIFICATE_ID = "certid"; @@ -646,6 +649,9 @@ public class ApiConstants { public static final String ADMIN = "admin"; + public static final String HAS_ANNOTATION = "hasannotation"; + public static final String LAST_ANNOTATED = "lastannotated"; + public static final String SHOWHIDDEN = "showhidden"; public enum HostDetails { diff --git a/api/src/org/apache/cloudstack/api/command/admin/resource/UploadCustomCertificateWithValidationCmd.java b/api/src/org/apache/cloudstack/api/command/admin/resource/UploadCustomCertificateWithValidationCmd.java new file mode 100644 index 000000000000..3d95da595e86 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/resource/UploadCustomCertificateWithValidationCmd.java @@ -0,0 +1,108 @@ +package org.apache.cloudstack.api.command.admin.resource; + +import java.io.IOException; +import java.security.KeyStoreException; +import java.security.cert.CertificateException; +import java.util.Map; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.CustomCertificateResponse; +import org.apache.log4j.Logger; + +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; + +@APICommand(name = "uploadCustomCertificateWithValidation", responseObject = CustomCertificateResponse.class, description = "Uploads a custom certificate for the console proxy VMs to use for SSL using an endpoint with proper validation. " + + "Can be used to upload a single certificate signed by a known CA. Can also be used, through multiple calls, to upload a " + + "chain of certificates from CA to the custom certificate itself.", requestHasSensitiveInfo = true, responseHasSensitiveInfo = false) +public class UploadCustomCertificateWithValidationCmd extends BaseAsyncCmd { + + public static final Logger s_logger = Logger.getLogger(UploadCustomCertificateCmd.class.getName()); + private static final String s_name = "uploadcustomcertificateresponse"; + + @Parameter(name = ApiConstants.ROOT_CERTIFICATE, type = CommandType.STRING, required = false, description = "The root certificate to be uploaded.", length = 65535) + private String rootCertificate; + + @Parameter(name = ApiConstants.INTERMIDIATE_CERTIFICATES, type = CommandType.MAP, required = false, description = "The list of intermediate certificates to be uploaded.") + private Map intermediateCertificates; + + @Parameter(name = ApiConstants.SERVER_CERTIFICATE, type = CommandType.STRING, required = true, description = "The server certificate to be uploaded.", length = 65535) + private String serverCertificate; + + @Parameter(name = ApiConstants.PRIVATE_KEY, type = CommandType.STRING, required = true, description = "The private key for the attached certificate.", length = 65535) + private String privateKey; + + @Parameter(name = ApiConstants.DOMAIN_SUFFIX, type = CommandType.STRING, required = true, description = "DNS domain suffix that the certificate is granted for.") + private String domainSuffix; + + public String getRootCertificate() { + return rootCertificate; + } + + public Map getIntermediateCertificates() { + return intermediateCertificates; + } + + public String getServerCertificate() { + return serverCertificate; + } + + public String getPrivateKey() { + return privateKey; + } + + public String getDomainSuffix() { + return domainSuffix; + } + + @Override + public String getEventType() { + return EventTypes.EVENT_UPLOAD_CUSTOM_CERTIFICATE; + } + + @Override + public String getEventDescription() { + return ("Uploading custom certificate to the db with validation, and applying it to all the cpvms in the system"); + } + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; // no account info given, parent this command to SYSTEM so ERROR events are tracked + } + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, + NetworkRuleConflictException { + + String result = null; + try { + result = _mgr.uploadCertificateWithValidation(this); + } catch (KeyStoreException | CertificateException | IOException e) { + e.printStackTrace(); + } + if (result != null) { + CustomCertificateResponse response = new CustomCertificateResponse(); + response.setResponseName(getCommandName()); + response.setResultMessage(result); + response.setObjectName("customcertificate"); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to upload custom certificate"); + } + } +} \ No newline at end of file diff --git a/server/src/com/cloud/server/ManagementServerImpl.java b/server/src/com/cloud/server/ManagementServerImpl.java index 9b423902678f..19bab65e47cd 100644 --- a/server/src/com/cloud/server/ManagementServerImpl.java +++ b/server/src/com/cloud/server/ManagementServerImpl.java @@ -16,7 +16,18 @@ // under the License. package com.cloud.server; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringReader; import java.lang.reflect.Field; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateFactory; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -149,6 +160,7 @@ import org.apache.cloudstack.api.command.admin.resource.ListAlertsCmd; import org.apache.cloudstack.api.command.admin.resource.ListCapacityCmd; import org.apache.cloudstack.api.command.admin.resource.UploadCustomCertificateCmd; +import org.apache.cloudstack.api.command.admin.resource.UploadCustomCertificateWithValidationCmd; import org.apache.cloudstack.api.command.admin.router.ConfigureOvsElementCmd; import org.apache.cloudstack.api.command.admin.router.ConfigureVirtualRouterElementCmd; import org.apache.cloudstack.api.command.admin.router.CreateVirtualRouterElementCmd; @@ -509,6 +521,7 @@ import org.apache.cloudstack.framework.config.impl.ConfigurationVO; import org.apache.cloudstack.framework.security.keystore.KeystoreManager; import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import com.cloud.network.lb.CertService; import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; @@ -516,6 +529,8 @@ import org.apache.cloudstack.utils.identity.ManagementServerNode; import org.apache.commons.codec.binary.Base64; import org.apache.log4j.Logger; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; import com.cloud.agent.AgentManager; import com.cloud.agent.api.GetVncPortAnswer; @@ -648,6 +663,7 @@ import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; import com.cloud.utils.db.TransactionCallbackNoReturn; import com.cloud.utils.db.TransactionStatus; import com.cloud.utils.exception.CloudRuntimeException; @@ -671,12 +687,19 @@ import com.cloud.vm.dao.SecondaryStorageVmDao; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; +import com.google.common.base.Preconditions; public class ManagementServerImpl extends ManagerBase implements ManagementServer, Configurable { public static final Logger s_logger = Logger.getLogger(ManagementServerImpl.class.getName()); - static final ConfigKey vmPasswordLength = new ConfigKey("Advanced", Integer.class, "vm.password.length", "10", - "Specifies the length of a randomly generated password", false); + static final ConfigKey vmPasswordLength = new ConfigKey("Advanced", Integer.class, "vm.password.length", "6", + "Specifies the length of a randomly generated password", false); + static final ConfigKey sshKeyLength = new ConfigKey("Advanced", Integer.class, "ssh.key.length", "2048", "Specifies custom SSH key length (bit)", true, + ConfigKey.Scope.Global); + public static final String ROOT_NAME = "root"; + public static final String INTERMEDIATE_NAME = "intermediate"; + public static final String SERVER_NAME = "server"; + @Inject public AccountManager _accountMgr; @Inject @@ -791,6 +814,8 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe private final ScheduledExecutorService _alertExecutor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("AlertChecker")); @Inject private KeystoreManager _ksMgr; + @Inject + private CertService _certService; private Map _configs; @@ -3515,9 +3540,126 @@ public String uploadCertificate(final UploadCustomCertificateCmd cmd) { } _consoleProxyMgr.setManagementState(ConsoleProxyManagementState.ResetSuspending); - return "Certificate has been successfully updated, if its the server certificate we would reboot all " + - "running console proxy VMs and secondary storage VMs to propagate the new certificate, " + - "please give a few minutes for console access and storage services service to be up and working again"; + return "Certificate has been successfully updated, if its the server certificate we would reboot all " + + "running console proxy VMs and secondary storage VMs to propagate the new certificate, " + + "please give a few minutes for console access and storage services service to be up and working again"; + } + + @Override + @DB + public String uploadCertificateWithValidation(UploadCustomCertificateWithValidationCmd cmd) throws KeyStoreException, CertificateException, IOException { + final String rootCertificate; + final Map intermediateCertificates; + final String serverCertificate; + final String privateKey; + final String dnsSuffix; + + rootCertificate = getRootCertificate(cmd); + intermediateCertificates = cmd.getIntermediateCertificates(); + serverCertificate = cmd.getServerCertificate(); + privateKey = cmd.getPrivateKey(); + dnsSuffix = cmd.getDomainSuffix(); + + try { + return Transaction.execute(new TransactionCallback() { + @Override + public String doInTransaction(TransactionStatus status) { + + if (rootCertificate == null) { + throw new InvalidParameterValueException("No root certificate provided and there is no default certificate present."); + } + if (serverCertificate == null) { + throw new InvalidParameterValueException("Server certificate cannot be empty."); + } + if (privateKey == null) { + throw new InvalidParameterValueException("Private key cannot be empty"); + } + if (dnsSuffix == null) { + throw new InvalidParameterValueException("DNS Suffix cannot be empty"); + } + + try { + validateCertificatesFormatAndValidity(rootCertificate, intermediateCertificates, serverCertificate); + validateCertificateChainAndPrivateKey(rootCertificate, intermediateCertificates, serverCertificate, privateKey); + } catch (CertificateNotYetValidException | CertificateExpiredException | IOException e) { + throw new RuntimeException(e.getMessage()); //did this to stop the transaction but not hide the cause, it should be changed + } + + _ksMgr.saveCertificate(ROOT_NAME, rootCertificate, privateKey, dnsSuffix); + for (Map.Entry intermediateCertificate : intermediateCertificates.entrySet()) { + _ksMgr.saveCertificate(INTERMEDIATE_NAME + intermediateCertificate.getKey().toString(), intermediateCertificate.getValue(), privateKey, dnsSuffix); + } + _ksMgr.saveCertificate(SERVER_NAME, rootCertificate, privateKey, dnsSuffix); + + final List alreadyRunning = _secStorageVmDao.getSecStorageVmListInStates(null, State.Running, State.Migrating, State.Starting); + for (final SecondaryStorageVmVO ssVmVm : alreadyRunning) { + _secStorageVmMgr.rebootSecStorageVm(ssVmVm.getId()); + } + + return "Certificate has been successfully updated, we will reboot all " + + "running console proxy VMs and secondary storage VMs to propagate the new certificate, " + + "please give a few minutes for console access and storage services service to be up and working again"; + } + }); + } catch (Exception e) { + return "Certificate upload failed!"; + } + } + + private String getRootCertificate(UploadCustomCertificateWithValidationCmd cmd) throws KeyStoreException { + String rootCertificate; + if (cmd.getRootCertificate() == null) { + rootCertificate = getExistingCertificateFromKeystore(); + } else { + rootCertificate = cmd.getRootCertificate(); + } + return rootCertificate; + } + + private String getExistingCertificateFromKeystore() throws KeyStoreException { + KeyStore ks = KeyStore.getInstance("JKS"); + Certificate existingCertificate = ks.getCertificate("root"); + return existingCertificate.toString(); + } + + private void validateCertificatesFormatAndValidity(String rootCertificate, Map intermediateCertificates, String serverCertificate) + throws CertificateNotYetValidException, CertificateExpiredException { + validateCertificateFormatAndValidity(rootCertificate); + for (String intermediateCertificate : intermediateCertificates.values()) { + validateCertificateFormatAndValidity(intermediateCertificate); + } + validateCertificateFormatAndValidity(serverCertificate); + } + + private void validateCertificateFormatAndValidity(String certificateInputToValidate) throws CertificateNotYetValidException, CertificateExpiredException { + final Certificate certificate = _certService.parseCertificate(certificateInputToValidate); + Preconditions.checkNotNull(certificate); + if (!(certificate instanceof X509Certificate)) { + throw new IllegalArgumentException("Invalid certificate format. Expected X509 certificate"); + } + ((X509Certificate)certificate).checkValidity(); + } + + private void validateCertificateChainAndPrivateKey(String rootCertificate, Map intermediateCertificates, String serverCertificate, String privateKey) + throws IOException { + List certificateChain = new ArrayList<>(); + for (String intermediateCertificate : intermediateCertificates.values()) { + certificateChain.add(generateCertificateFromString(intermediateCertificate)); + } + certificateChain.add(generateCertificateFromString(serverCertificate)); + _certService.parsePrivateKey(privateKey); + _certService.validateChain(certificateChain, generateCertificateFromString(rootCertificate)); + } + + private Certificate generateCertificateFromString(String intermediateCertificate) { + try (final PemReader pemReader = new PemReader(new StringReader(intermediateCertificate))) { + PemObject pemObject = pemReader.readPemObject(); + CertificateFactory certificateFactory = CertificateFactory.getInstance("X509"); + ByteArrayInputStream inputStream = new ByteArrayInputStream(pemObject.getContent()); + return certificateFactory.generateCertificate(inputStream); + } catch (Exception e) { + return null; + } } @Override diff --git a/server/src/org/apache/cloudstack/network/lb/CertServiceImpl.java b/server/src/org/apache/cloudstack/network/lb/CertServiceImpl.java index 8315beed3cea..2add3d2652b9 100644 --- a/server/src/org/apache/cloudstack/network/lb/CertServiceImpl.java +++ b/server/src/org/apache/cloudstack/network/lb/CertServiceImpl.java @@ -18,17 +18,18 @@ import java.io.IOException; import java.io.StringReader; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.Principal; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; +import java.security.Principal; +import java.security.KeyPair; +import java.security.KeyFactory; +import java.security.MessageDigest; import java.security.Security; +import java.security.NoSuchProviderException; +import java.security.InvalidKeyException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; import java.security.cert.CertPathBuilder; import java.security.cert.CertPathBuilderException; import java.security.cert.CertStore; @@ -39,6 +40,8 @@ import java.security.cert.TrustAnchor; import java.security.cert.X509CertSelector; import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -51,6 +54,8 @@ import javax.ejb.Local; import javax.inject.Inject; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.api.command.user.loadbalancer.DeleteSslCertCmd; import org.apache.cloudstack.api.command.user.loadbalancer.ListSslCertsCmd; @@ -83,6 +88,8 @@ import com.cloud.utils.db.DB; import com.cloud.utils.db.EntityManager; import com.cloud.utils.exception.CloudRuntimeException; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; @Local(value = {CertService.class}) public class CertServiceImpl implements CertService { @@ -386,7 +393,7 @@ private void validateKeys(PublicKey pubKey, PrivateKey privKey) { } } - private void validateChain(List chain, Certificate cert) { + public void validateChain(List chain, Certificate cert) { List certs = new ArrayList(); Set anchors = new HashSet(); @@ -456,6 +463,21 @@ public PrivateKey parsePrivateKey(String key, String password) throws IOExceptio } } + public PrivateKey parsePrivateKey(final String key) throws IOException { + Preconditions.checkArgument(!Strings.isNullOrEmpty(key)); + try (final PemReader pemReader = new PemReader(new StringReader(key));) { + final PemObject pemObject = pemReader.readPemObject(); + final byte[] content = pemObject.getContent(); + final PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(content); + final KeyFactory factory = KeyFactory.getInstance("RSA", "BC"); + return factory.generatePrivate(privKeySpec); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new IOException("No encryption provider available.", e); + } catch (final InvalidKeySpecException e) { + throw new IOException("Invalid Key format.", e); + } + } + public Certificate parseCertificate(String cert) { PEMReader certPem = new PEMReader(new StringReader(cert)); try {