diff --git a/.travis.yml b/.travis.yml index 4301d7569cd..b25becb6e05 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,6 +34,7 @@ env: matrix: - TESTS="smoke/test_affinity_groups smoke/test_affinity_groups_projects + smoke/test_certauthority_root smoke/test_deploy_vgpu_enabled_vm smoke/test_deploy_vm_iso smoke/test_deploy_vm_root_resize diff --git a/agent/src/com/cloud/agent/Agent.java b/agent/src/com/cloud/agent/Agent.java index 7fab5f4d301..7e802205f5c 100644 --- a/agent/src/com/cloud/agent/Agent.java +++ b/agent/src/com/cloud/agent/Agent.java @@ -16,12 +16,14 @@ // under the License. package com.cloud.agent; +import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.channels.ClosedChannelException; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -35,7 +37,13 @@ import java.util.concurrent.atomic.AtomicInteger; import javax.naming.ConfigurationException; +import org.apache.cloudstack.ca.SetupCertificateAnswer; +import org.apache.cloudstack.ca.SetupCertificateCommand; +import org.apache.cloudstack.ca.SetupKeyStoreCommand; +import org.apache.cloudstack.ca.SetupKeystoreAnswer; import org.apache.cloudstack.managed.context.ManagedContextTimerTask; +import org.apache.cloudstack.utils.security.KeyStoreUtils; +import org.apache.commons.io.FileUtils; import org.apache.log4j.Logger; import org.slf4j.MDC; @@ -68,6 +76,7 @@ import com.cloud.utils.nio.NioConnection; import com.cloud.utils.nio.Task; import com.cloud.utils.script.OutputInterpreter; import com.cloud.utils.script.Script; +import com.google.common.base.Strings; /** * @config @@ -126,6 +135,9 @@ public class Agent implements HandlerFactory, IAgentControl { private final ThreadPoolExecutor _ugentTaskPool; ExecutorService _executor; + private String _keystoreSetupPath; + private String _keystoreCertImportPath; + // for simulator use only public Agent(final IAgentShell shell) { _shell = shell; @@ -166,7 +178,8 @@ public class Agent implements HandlerFactory, IAgentControl { throw new ConfigurationException("Unable to configure " + _resource.getName()); } - _connection = new NioClient("Agent", _shell.getHost(), _shell.getPort(), _shell.getWorkers(), this); + final String host = _shell.getHost(); + _connection = new NioClient("Agent", host, _shell.getPort(), _shell.getWorkers(), this); // ((NioClient)_connection).setBindAddress(_shell.getPrivateIp()); @@ -182,7 +195,7 @@ public class Agent implements HandlerFactory, IAgentControl { "agentRequest-Handler")); s_logger.info("Agent [id = " + (_id != null ? _id : "new") + " : type = " + getResourceName() + " : zone = " + _shell.getZone() + " : pod = " + _shell.getPod() + - " : workers = " + _shell.getWorkers() + " : host = " + _shell.getHost() + " : port = " + _shell.getPort()); + " : workers = " + _shell.getWorkers() + " : host = " + host + " : port = " + _shell.getPort()); } public String getVersion() { @@ -224,6 +237,16 @@ public class Agent implements HandlerFactory, IAgentControl { throw new CloudRuntimeException("Unable to start the resource: " + _resource.getName()); } + _keystoreSetupPath = Script.findScript("scripts/util/", KeyStoreUtils.keyStoreSetupScript); + if (_keystoreSetupPath == null) { + throw new CloudRuntimeException(String.format("Unable to find the '%s' script", KeyStoreUtils.keyStoreSetupScript)); + } + + _keystoreCertImportPath = Script.findScript("scripts/util/", KeyStoreUtils.keyStoreImportScript); + if (_keystoreCertImportPath == null) { + throw new CloudRuntimeException(String.format("Unable to find the '%s' script", KeyStoreUtils.keyStoreImportScript)); + } + try { _connection.start(); } catch (final NioConnectionException e) { @@ -231,8 +254,10 @@ public class Agent implements HandlerFactory, IAgentControl { s_logger.info("Attempted to connect to the server, but received an unexpected exception, trying again..."); } while (!_connection.isStartup()) { + final String host = _shell.getHost(); _shell.getBackoffAlgorithm().waitBeforeRetry(); - _connection = new NioClient("Agent", _shell.getHost(), _shell.getPort(), _shell.getWorkers(), this); + _connection = new NioClient("Agent", host, _shell.getPort(), _shell.getWorkers(), this); + s_logger.info("Connecting to host:" + host); try { _connection.start(); } catch (final NioConnectionException e) { @@ -408,14 +433,21 @@ public class Agent implements HandlerFactory, IAgentControl { _shell.getBackoffAlgorithm().waitBeforeRetry(); } - _connection = new NioClient("Agent", _shell.getHost(), _shell.getPort(), _shell.getWorkers(), this); + final String host = _shell.getHost(); do { - s_logger.info("Reconnecting..."); + _connection = new NioClient("Agent", host, _shell.getPort(), _shell.getWorkers(), this); + s_logger.info("Reconnecting to host:" + host); try { _connection.start(); } catch (final NioConnectionException e) { s_logger.warn("NIO Connection Exception " + e); s_logger.info("Attempted to connect to the server, but received an unexpected exception, trying again..."); + _connection.stop(); + try { + _connection.cleanUp(); + } catch (final IOException ex) { + s_logger.warn("Fail to clean up old connection. " + ex); + } } _shell.getBackoffAlgorithm().waitBeforeRetry(); } while (!_connection.isStartup()); @@ -515,7 +547,10 @@ public class Agent implements HandlerFactory, IAgentControl { s_logger.warn("No handler found to process cmd: " + cmd.toString()); answer = new AgentControlAnswer(cmd); } - + } else if (cmd instanceof SetupKeyStoreCommand && ((SetupKeyStoreCommand) cmd).isHandleByAgent()) { + answer = setupAgentKeystore((SetupKeyStoreCommand) cmd); + } else if (cmd instanceof SetupCertificateCommand && ((SetupCertificateCommand) cmd).isHandleByAgent()) { + answer = setupAgentCertificate((SetupCertificateCommand) cmd); } else { if (cmd instanceof ReadyCommand) { processReadyCommand(cmd); @@ -565,6 +600,86 @@ public class Agent implements HandlerFactory, IAgentControl { } } + public Answer setupAgentKeystore(final SetupKeyStoreCommand cmd) { + final String keyStorePassword = cmd.getKeystorePassword(); + final long validityDays = cmd.getValidityDays(); + + s_logger.debug("Setting up agent keystore file and generating CSR"); + + final File agentFile = PropertiesUtil.findConfigFile("agent.properties"); + if (agentFile == null) { + return new Answer(cmd, false, "Failed to find agent.properties file"); + } + final String keyStoreFile = agentFile.getParent() + "/" + KeyStoreUtils.defaultKeystoreFile; + final String csrFile = agentFile.getParent() + "/" + KeyStoreUtils.defaultCsrFile; + + String storedPassword = _shell.getPersistentProperty(null, KeyStoreUtils.passphrasePropertyName); + if (Strings.isNullOrEmpty(storedPassword)) { + storedPassword = keyStorePassword; + _shell.setPersistentProperty(null, KeyStoreUtils.passphrasePropertyName, storedPassword); + } + + Script script = new Script(_keystoreSetupPath, 60000, s_logger); + script.add(agentFile.getAbsolutePath()); + script.add(keyStoreFile); + script.add(storedPassword); + script.add(String.valueOf(validityDays)); + script.add(csrFile); + String result = script.execute(); + if (result != null) { + throw new CloudRuntimeException("Unable to setup keystore file"); + } + + final String csrString; + try { + csrString = FileUtils.readFileToString(new File(csrFile), Charset.defaultCharset()); + } catch (IOException e) { + throw new CloudRuntimeException("Unable to read generated CSR file", e); + } + return new SetupKeystoreAnswer(csrString); + } + + private Answer setupAgentCertificate(final SetupCertificateCommand cmd) { + final String certificate = cmd.getCertificate(); + final String privateKey = cmd.getPrivateKey(); + final String caCertificates = cmd.getCaCertificates(); + + s_logger.debug("Importing received certificate to agent's keystore"); + + final File agentFile = PropertiesUtil.findConfigFile("agent.properties"); + if (agentFile == null) { + return new Answer(cmd, false, "Failed to find agent.properties file"); + } + final String keyStoreFile = agentFile.getParent() + "/" + KeyStoreUtils.defaultKeystoreFile; + final String certFile = agentFile.getParent() + "/" + KeyStoreUtils.defaultCertFile; + final String privateKeyFile = agentFile.getParent() + "/" + KeyStoreUtils.defaultPrivateKeyFile; + final String caCertFile = agentFile.getParent() + "/" + KeyStoreUtils.defaultCaCertFile; + + try { + FileUtils.writeStringToFile(new File(certFile), certificate, Charset.defaultCharset()); + FileUtils.writeStringToFile(new File(caCertFile), caCertificates, Charset.defaultCharset()); + s_logger.debug("Saved received client certificate to: " + certFile); + } catch (IOException e) { + throw new CloudRuntimeException("Unable to save received agent client and ca certificates", e); + } + + Script script = new Script(_keystoreCertImportPath, 60000, s_logger); + script.add(agentFile.getAbsolutePath()); + script.add(keyStoreFile); + script.add(KeyStoreUtils.agentMode); + script.add(certFile); + script.add(""); + script.add(caCertFile); + script.add(""); + script.add(privateKeyFile); + script.add(privateKey); + String result = script.execute(); + if (result != null) { + throw new CloudRuntimeException("Unable to import certificate into keystore file"); + } + return new SetupCertificateAnswer(true); + } + public void processResponse(final Response response, final Link link) { final Answer answer = response.getAnswer(); if (s_logger.isDebugEnabled()) { diff --git a/agent/src/com/cloud/agent/AgentShell.java b/agent/src/com/cloud/agent/AgentShell.java index 5e0da68c6d6..5950bc78e61 100644 --- a/agent/src/com/cloud/agent/AgentShell.java +++ b/agent/src/com/cloud/agent/AgentShell.java @@ -67,6 +67,7 @@ public class AgentShell implements IAgentShell, Daemon { private int _proxyPort; private int _workers; private String _guid; + private int _hostCounter = 0; private int _nextAgentId = 1; private volatile boolean _exit = false; private int _pingRetries; @@ -107,7 +108,17 @@ public class AgentShell implements IAgentShell, Daemon { @Override public String getHost() { - return _host; + final String[] hosts = _host.split(","); + if (_hostCounter >= hosts.length) { + _hostCounter = 0; + } + final String host = hosts[_hostCounter % hosts.length]; + _hostCounter++; + return host; + } + + public void setHost(final String host) { + _host = host; } @Override diff --git a/agent/src/com/cloud/agent/dao/impl/PropertiesStorage.java b/agent/src/com/cloud/agent/dao/impl/PropertiesStorage.java index df1b1ea7b27..e9eac645cb7 100644 --- a/agent/src/com/cloud/agent/dao/impl/PropertiesStorage.java +++ b/agent/src/com/cloud/agent/dao/impl/PropertiesStorage.java @@ -51,6 +51,9 @@ public class PropertiesStorage implements StorageComponent { @Override public synchronized void persist(String key, String value) { + if (!loadFromFile(_file)) { + s_logger.error("Failed to load changes and then write to them"); + } _properties.setProperty(key, value); FileOutputStream output = null; try { @@ -65,6 +68,20 @@ public class PropertiesStorage implements StorageComponent { } } + private synchronized boolean loadFromFile(final File file) { + try { + PropertiesUtil.loadFromFile(_properties, file); + _file = file; + } catch (FileNotFoundException e) { + s_logger.error("How did we get here? ", e); + return false; + } catch (IOException e) { + s_logger.error("IOException: ", e); + return false; + } + return true; + } + @Override public synchronized boolean configure(String name, Map params) { _name = name; @@ -86,17 +103,7 @@ public class PropertiesStorage implements StorageComponent { return false; } } - try { - PropertiesUtil.loadFromFile(_properties, file); - _file = file; - } catch (FileNotFoundException e) { - s_logger.error("How did we get here? ", e); - return false; - } catch (IOException e) { - s_logger.error("IOException: ", e); - return false; - } - return true; + return loadFromFile(file); } @Override diff --git a/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java b/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java index 08f09823986..1fed3be753c 100644 --- a/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java +++ b/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java @@ -32,11 +32,8 @@ import java.util.Properties; import javax.naming.ConfigurationException; -import org.apache.log4j.Logger; - -import com.google.gson.Gson; - import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.log4j.Logger; import com.cloud.agent.Agent.ExitStatus; import com.cloud.agent.api.AgentControlAnswer; @@ -64,6 +61,7 @@ import com.cloud.resource.ServerResourceBase; import com.cloud.utils.NumbersUtil; import com.cloud.utils.net.NetUtils; import com.cloud.utils.script.Script; +import com.google.gson.Gson; /** * @@ -240,9 +238,11 @@ public class ConsoleProxyResource extends ServerResourceBase implements ServerRe _proxyVmId = NumbersUtil.parseLong(value, 0); if (_localgw != null) { - String mgmtHost = (String)params.get("host"); + String mgmtHosts = (String)params.get("host"); if (_eth1ip != null) { - addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, mgmtHost); + for (final String mgmtHost : mgmtHosts.split(",")) { + addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, mgmtHost); + } String internalDns1 = (String) params.get("internaldns1"); if (internalDns1 == null) { s_logger.warn("No DNS entry found during configuration of NfsSecondaryStorage"); diff --git a/agent/test/com/cloud/agent/AgentShellTest.java b/agent/test/com/cloud/agent/AgentShellTest.java index 5baa7bf800e..8ceba4531d1 100644 --- a/agent/test/com/cloud/agent/AgentShellTest.java +++ b/agent/test/com/cloud/agent/AgentShellTest.java @@ -16,6 +16,8 @@ // under the License. package com.cloud.agent; +import java.util.Arrays; +import java.util.List; import java.util.UUID; import javax.naming.ConfigurationException; @@ -23,6 +25,8 @@ import javax.naming.ConfigurationException; import org.junit.Assert; import org.junit.Test; +import com.cloud.utils.StringUtils; + public class AgentShellTest { @Test public void parseCommand() throws ConfigurationException { @@ -44,4 +48,15 @@ public class AgentShellTest { Assert.assertNotNull(shell.getProperties()); Assert.assertFalse(shell.getProperties().entrySet().isEmpty()); } + + @Test + public void testGetHost() { + AgentShell shell = new AgentShell(); + List hosts = Arrays.asList("10.1.1.1", "20.2.2.2", "30.3.3.3", "2001:db8::1"); + shell.setHost(StringUtils.listToCsvTags(hosts)); + for (String host : hosts) { + Assert.assertEquals(host, shell.getHost()); + } + Assert.assertEquals(shell.getHost(), hosts.get(0)); + } } diff --git a/api/pom.xml b/api/pom.xml index 93859f1b40d..6352e117bfe 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -51,6 +51,11 @@ cloud-framework-config ${project.version} + + org.apache.cloudstack + cloud-framework-ca + ${project.version} + diff --git a/api/src/com/cloud/event/EventTypes.java b/api/src/com/cloud/event/EventTypes.java index 32a3138172f..66bfbfe6d28 100644 --- a/api/src/com/cloud/event/EventTypes.java +++ b/api/src/com/cloud/event/EventTypes.java @@ -16,6 +16,14 @@ // under the License. package com.cloud.event; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cloudstack.acl.Role; +import org.apache.cloudstack.acl.RolePermission; +import org.apache.cloudstack.config.Configuration; +import org.apache.cloudstack.usage.Usage; + import com.cloud.dc.DataCenter; import com.cloud.dc.Pod; import com.cloud.dc.StorageNetworkIpRange; @@ -54,10 +62,10 @@ import com.cloud.offering.NetworkOffering; import com.cloud.offering.ServiceOffering; import com.cloud.projects.Project; import com.cloud.server.ResourceTag; -import com.cloud.storage.StoragePool; import com.cloud.storage.GuestOS; import com.cloud.storage.GuestOSHypervisor; import com.cloud.storage.Snapshot; +import com.cloud.storage.StoragePool; import com.cloud.storage.Volume; import com.cloud.storage.snapshot.SnapshotPolicy; import com.cloud.template.VirtualMachineTemplate; @@ -66,13 +74,6 @@ import com.cloud.user.User; import com.cloud.vm.Nic; import com.cloud.vm.NicSecondaryIp; import com.cloud.vm.VirtualMachine; -import org.apache.cloudstack.acl.Role; -import org.apache.cloudstack.acl.RolePermission; -import org.apache.cloudstack.config.Configuration; -import org.apache.cloudstack.usage.Usage; - -import java.util.HashMap; -import java.util.Map; public class EventTypes { @@ -176,6 +177,11 @@ public class EventTypes { public static final String EVENT_ROLE_PERMISSION_UPDATE = "ROLE.PERMISSION.UPDATE"; public static final String EVENT_ROLE_PERMISSION_DELETE = "ROLE.PERMISSION.DELETE"; + // CA events + public static final String EVENT_CA_CERTIFICATE_ISSUE = "CA.CERTIFICATE.ISSUE"; + public static final String EVENT_CA_CERTIFICATE_REVOKE = "CA.CERTIFICATE.REVOKE"; + public static final String EVENT_CA_CERTIFICATE_PROVISION = "CA.CERTIFICATE.PROVISION"; + // Account events public static final String EVENT_ACCOUNT_ENABLE = "ACCOUNT.ENABLE"; public static final String EVENT_ACCOUNT_DISABLE = "ACCOUNT.DISABLE"; diff --git a/api/src/org/apache/cloudstack/alert/AlertService.java b/api/src/org/apache/cloudstack/alert/AlertService.java index ad711ecea4e..841296996ea 100644 --- a/api/src/org/apache/cloudstack/alert/AlertService.java +++ b/api/src/org/apache/cloudstack/alert/AlertService.java @@ -67,6 +67,7 @@ public interface AlertService { public static final AlertType ALERT_TYPE_SYNC = new AlertType((short)27, "ALERT.TYPE.SYNC", true); public static final AlertType ALERT_TYPE_UPLOAD_FAILED = new AlertType((short)28, "ALERT.UPLOAD.FAILED", true); public static final AlertType ALERT_TYPE_OOBM_AUTH_ERROR = new AlertType((short)29, "ALERT.OOBM.AUTHERROR", true); + public static final AlertType ALERT_TYPE_CA_CERT = new AlertType((short)31, "ALERT.CA.CERT", true); public short getType() { return type; diff --git a/api/src/org/apache/cloudstack/api/ApiConstants.java b/api/src/org/apache/cloudstack/api/ApiConstants.java index b77b83a20bf..da39ff84fae 100644 --- a/api/src/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/org/apache/cloudstack/api/ApiConstants.java @@ -37,10 +37,12 @@ public class ApiConstants { public static final String BYTES_WRITE_RATE = "byteswriterate"; public static final String CATEGORY = "category"; public static final String CAN_REVERT = "canrevert"; + public static final String CA_CERTIFICATES = "cacertificates"; public static final String CERTIFICATE = "certificate"; public static final String CERTIFICATE_CHAIN = "certchain"; public static final String CERTIFICATE_FINGERPRINT = "fingerprint"; public static final String CERTIFICATE_ID = "certid"; + public static final String CSR = "csr"; public static final String PRIVATE_KEY = "privatekey"; public static final String DOMAIN_SUFFIX = "domainsuffix"; public static final String DNS_SEARCH_ORDER = "dnssearchorder"; @@ -54,6 +56,7 @@ public class ApiConstants { public static final String CLUSTER_ID = "clusterid"; public static final String CLUSTER_NAME = "clustername"; public static final String CLUSTER_TYPE = "clustertype"; + public static final String CN = "cn"; public static final String COMMAND = "command"; public static final String CMD_EVENT_TYPE = "cmdeventtype"; public static final String COMPONENT = "component"; @@ -216,6 +219,7 @@ public class ApiConstants { public static final String PUBLIC_END_PORT = "publicendport"; public static final String PUBLIC_ZONE = "publiczone"; public static final String RECEIVED_BYTES = "receivedbytes"; + public static final String RECONNECT = "reconnect"; public static final String REQUIRES_HVM = "requireshvm"; public static final String RESOURCE_TYPE = "resourcetype"; public static final String RESPONSE = "response"; @@ -234,6 +238,7 @@ public class ApiConstants { public static final String SECURITY_GROUP_ID = "securitygroupid"; public static final String SENT = "sent"; public static final String SENT_BYTES = "sentbytes"; + public static final String SERIAL = "serial"; public static final String SERVICE_OFFERING_ID = "serviceofferingid"; public static final String SESSIONKEY = "sessionkey"; public static final String SHOW_CAPACITIES = "showcapacities"; diff --git a/api/src/org/apache/cloudstack/api/command/admin/ca/IssueCertificateCmd.java b/api/src/org/apache/cloudstack/api/command/admin/ca/IssueCertificateCmd.java new file mode 100644 index 00000000000..8926829205f --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/ca/IssueCertificateCmd.java @@ -0,0 +1,162 @@ +// 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.api.command.admin.ca; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +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.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.CertificateResponse; +import org.apache.cloudstack.ca.CAManager; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.utils.security.CertUtils; +import org.apache.log4j.Logger; + +import com.cloud.event.EventTypes; +import com.google.common.base.Strings; + +@APICommand(name = IssueCertificateCmd.APINAME, + description = "Issues a client certificate using configured or provided CA plugin", + responseObject = CertificateResponse.class, + requestHasSensitiveInfo = true, + responseHasSensitiveInfo = true, + since = "4.11.0", + authorized = {RoleType.Admin}) +public class IssueCertificateCmd extends BaseAsyncCmd { + private static final Logger LOG = Logger.getLogger(IssueCertificateCmd.class); + + public static final String APINAME = "issueCertificate"; + + @Inject + private CAManager caManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.CSR, type = BaseCmd.CommandType.STRING, description = "The certificate signing request (in pem format), if CSR is not provided then configured/provided options are considered", length = 65535) + private String csr; + + @Parameter(name = ApiConstants.DOMAIN, type = BaseCmd.CommandType.STRING, description = "Comma separated list of domains, the certificate should be issued for. When csr is not provided, the first domain is used as a subject/CN") + private String domains; + + @Parameter(name = ApiConstants.IP_ADDRESS, type = BaseCmd.CommandType.STRING, description = "Comma separated list of IP addresses, the certificate should be issued for") + private String addresses; + + @Parameter(name = ApiConstants.DURATION, type = CommandType.INTEGER, description = "Certificate validity duration in number of days, when not provided the default configured value will be used") + private Integer validityDuration; + + @Parameter(name = ApiConstants.PROVIDER, type = BaseCmd.CommandType.STRING, description = "Name of the CA service provider, otherwise the default configured provider plugin will be used") + private String provider; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getCsr() { + return csr; + } + + private List processList(final String string) { + final List list = new ArrayList<>(); + if (!Strings.isNullOrEmpty(string)) { + for (final String address: string.split(",")) { + list.add(address.trim()); + } + } + return list; + } + + public List getAddresses() { + return processList(addresses); + } + + public List getDomains() { + return processList(domains); + } + + public Integer getValidityDuration() { + return validityDuration; + } + + public String getProvider() { + return provider; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + if (Strings.isNullOrEmpty(getCsr()) && getDomains().isEmpty()) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Please provide the domains or the CSR, none of them are provided"); + } + final Certificate certificate = caManager.issueCertificate(getCsr(), getDomains(), getAddresses(), getValidityDuration(), getProvider()); + if (certificate == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to issue client certificate with given provider"); + } + + final CertificateResponse certificateResponse = new CertificateResponse(); + try { + certificateResponse.setCertificate(CertUtils.x509CertificateToPem(certificate.getClientCertificate())); + if (certificate.getPrivateKey() != null) { + certificateResponse.setPrivateKey(CertUtils.privateKeyToPem(certificate.getPrivateKey())); + } + if (certificate.getCaCertificates() != null) { + certificateResponse.setCaCertificate(CertUtils.x509CertificatesToPem(certificate.getCaCertificates())); + } + } catch (final IOException e) { + LOG.error("Failed to generate and convert client certificate(s) to PEM due to error: ", e); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to process and return client certificate"); + } + certificateResponse.setResponseName(getCommandName()); + setResponseObject(certificateResponse); + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_CA_CERTIFICATE_ISSUE; + } + + @Override + public String getEventDescription() { + return "issuing certificate for domain(s)=" + domains + ", ip(s)=" + addresses; + } +} diff --git a/api/src/org/apache/cloudstack/api/command/admin/ca/ListCAProvidersCmd.java b/api/src/org/apache/cloudstack/api/command/admin/ca/ListCAProvidersCmd.java new file mode 100644 index 00000000000..e1e8e375163 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/ca/ListCAProvidersCmd.java @@ -0,0 +1,102 @@ +// 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.api.command.admin.ca; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.CAProviderResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.ca.CAManager; +import org.apache.cloudstack.framework.ca.CAProvider; + +import com.cloud.user.Account; + +@APICommand(name = ListCAProvidersCmd.APINAME, + description = "Lists available certificate authority providers in CloudStack", + responseObject = CAProviderResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.11.0", + authorized = {RoleType.Admin}) +public class ListCAProvidersCmd extends BaseCmd { + public static final String APINAME = "listCAProviders"; + + @Inject + private CAManager caManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "List CA service provider by name") + private String name; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getName() { + return name; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } + + private void setupResponse(final List providers) { + final ListResponse response = new ListResponse<>(); + final List responses = new ArrayList<>(); + for (final CAProvider provider : providers) { + if (provider == null || (getName() != null && !provider.getProviderName().equals(getName()))) { + continue; + } + final CAProviderResponse caProviderResponse = new CAProviderResponse(); + caProviderResponse.setName(provider.getProviderName()); + caProviderResponse.setDescription(provider.getDescription()); + caProviderResponse.setObjectName("caprovider"); + responses.add(caProviderResponse); + } + response.setResponses(responses); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public void execute() { + final List caProviders = caManager.getCaProviders(); + setupResponse(caProviders); + } +} diff --git a/api/src/org/apache/cloudstack/api/command/admin/ca/ListCaCertificateCmd.java b/api/src/org/apache/cloudstack/api/command/admin/ca/ListCaCertificateCmd.java new file mode 100644 index 00000000000..1baa84179f0 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/ca/ListCaCertificateCmd.java @@ -0,0 +1,90 @@ +// 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.api.command.admin.ca; + +import java.io.IOException; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.CertificateResponse; +import org.apache.cloudstack.ca.CAManager; + +import com.cloud.user.Account; +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = ListCaCertificateCmd.APINAME, + description = "Lists the CA public certificate(s) as support by the configured/provided CA plugin", + responseObject = CertificateResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.11.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListCaCertificateCmd extends BaseCmd { + public static final String APINAME = "listCaCertificate"; + + @Inject + private CAManager caManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, description = "Name of the CA service provider, otherwise the default configured provider plugin will be used") + private String provider; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getProvider() { + return provider; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + final String caCertificates; + try { + caCertificates = caManager.getCaCertificate(getProvider()); + } catch (final IOException e) { + throw new CloudRuntimeException("Failed to get CA certificates for given CA provider"); + } + final CertificateResponse certificateResponse = new CertificateResponse("cacertificates"); + certificateResponse.setCertificate(caCertificates); + certificateResponse.setResponseName(getCommandName()); + setResponseObject(certificateResponse); + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_TYPE_NORMAL; + } +} diff --git a/api/src/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java b/api/src/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java new file mode 100644 index 00000000000..2745f071dd0 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java @@ -0,0 +1,125 @@ +// 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.api.command.admin.ca; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandJobType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.ca.CAManager; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; +import com.cloud.host.Host; + +@APICommand(name = ProvisionCertificateCmd.APINAME, + description = "Issues and propagates client certificate on a connected host/agent using configured CA plugin", + responseObject = SuccessResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.11.0", + authorized = {RoleType.Admin}) +public class ProvisionCertificateCmd extends BaseAsyncCmd { + public static final String APINAME = "provisionCertificate"; + + @Inject + private CAManager caManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, required = true, entityType = HostResponse.class, + description = "The host/agent uuid to which the certificate has to be provisioned (issued and propagated)") + private Long hostId; + + @Parameter(name = ApiConstants.RECONNECT, type = CommandType.BOOLEAN, + description = "Whether to attempt reconnection with host/agent after successful deployment of certificate. When option is not provided, configured global setting is used") + private Boolean reconnect; + + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, + description = "Name of the CA service provider, otherwise the default configured provider plugin will be used") + private String provider; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getHostId() { + return hostId; + } + + public Boolean getReconnect() { + return reconnect; + } + + public String getProvider() { + return provider; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + final Host host = _resourceService.getHost(getHostId()); + if (host == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to find host by ID: " + getHostId()); + } + + boolean result = caManager.provisionCertificate(host, getReconnect(), getProvider()); + SuccessResponse response = new SuccessResponse(getCommandName()); + response.setSuccess(result); + setResponseObject(response); + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_CA_CERTIFICATE_PROVISION; + } + + @Override + public String getEventDescription() { + return "provisioning certificate for host id=" + hostId + " using provider=" + provider; + } + + @Override + public ApiCommandJobType getInstanceType() { + return ApiCommandJobType.Host; + } +} diff --git a/api/src/org/apache/cloudstack/api/command/admin/ca/RevokeCertificateCmd.java b/api/src/org/apache/cloudstack/api/command/admin/ca/RevokeCertificateCmd.java new file mode 100644 index 00000000000..0f154f045df --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/ca/RevokeCertificateCmd.java @@ -0,0 +1,116 @@ +// 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.api.command.admin.ca; + +import java.math.BigInteger; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +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.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.ca.CAManager; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; +import com.google.common.base.Strings; + +@APICommand(name = RevokeCertificateCmd.APINAME, + description = "Revokes certificate using configured CA plugin", + responseObject = SuccessResponse.class, + requestHasSensitiveInfo = true, + responseHasSensitiveInfo = false, + since = "4.11.0", + authorized = {RoleType.Admin}) +public class RevokeCertificateCmd extends BaseAsyncCmd { + + public static final String APINAME = "revokeCertificate"; + + @Inject + private CAManager caManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.SERIAL, type = BaseCmd.CommandType.STRING, required = true, description = "The certificate serial number, as a hex value") + private String serial; + + @Parameter(name = ApiConstants.CN, type = BaseCmd.CommandType.STRING, description = "The certificate CN") + private String cn; + + @Parameter(name = ApiConstants.PROVIDER, type = BaseCmd.CommandType.STRING, description = "Name of the CA service provider, otherwise the default configured provider plugin will be used") + private String provider; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public BigInteger getSerialBigInteger() { + if (Strings.isNullOrEmpty(serial)) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Certificate serial cannot be empty"); + } + return new BigInteger(serial, 16); + } + + public String getCn() { + return cn; + } + + public String getProvider() { + return provider; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + boolean result = caManager.revokeCertificate(getSerialBigInteger(), getCn(), getProvider()); + SuccessResponse response = new SuccessResponse(getCommandName()); + response.setSuccess(result); + setResponseObject(response); + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_CA_CERTIFICATE_REVOKE; + } + + @Override + public String getEventDescription() { + return "revoking certificate with serial id=" + serial + ", cn=" + cn; + } +} diff --git a/api/src/org/apache/cloudstack/api/response/CAProviderResponse.java b/api/src/org/apache/cloudstack/api/response/CAProviderResponse.java new file mode 100644 index 00000000000..94d5882e18a --- /dev/null +++ b/api/src/org/apache/cloudstack/api/response/CAProviderResponse.java @@ -0,0 +1,52 @@ +// 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.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.framework.ca.CAProvider; + +@EntityReference(value = CAProvider.class) +public class CAProviderResponse extends BaseResponse { + @SerializedName(ApiConstants.NAME) + @Param(description = "the CA service provider name") + private String name; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "the description of the CA service provider") + private String description; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/api/src/org/apache/cloudstack/api/response/CertificateResponse.java b/api/src/org/apache/cloudstack/api/response/CertificateResponse.java new file mode 100644 index 00000000000..f8c3ecc7404 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/response/CertificateResponse.java @@ -0,0 +1,58 @@ +// 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.api.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class CertificateResponse extends BaseResponse { + @SerializedName(ApiConstants.CERTIFICATE) + @Param(description = "The client certificate") + private String certificate = ""; + + @SerializedName(ApiConstants.PRIVATE_KEY) + @Param(description = "Private key for the certificate") + private String privateKey; + + @SerializedName(ApiConstants.CA_CERTIFICATES) + @Param(description = "The CA certificate(s)") + private String caCertificate; + + public CertificateResponse() { + setObjectName("certificates"); + } + + public CertificateResponse(final String objectName) { + setObjectName(objectName); + } + + public void setCertificate(final String certificate) { + this.certificate = certificate; + } + + public void setPrivateKey(final String privateKey) { + this.privateKey = privateKey; + } + + public void setCaCertificate(final String caCertificate) { + this.caCertificate = caCertificate; + } +} diff --git a/api/src/org/apache/cloudstack/ca/CAManager.java b/api/src/org/apache/cloudstack/ca/CAManager.java new file mode 100644 index 00000000000..c32cfbfe345 --- /dev/null +++ b/api/src/org/apache/cloudstack/ca/CAManager.java @@ -0,0 +1,163 @@ +// 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; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.framework.ca.CAProvider; +import org.apache.cloudstack.framework.ca.CAService; +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; + +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.Host; +import com.cloud.utils.component.PluggableService; + +public interface CAManager extends CAService, Configurable, PluggableService { + + ConfigKey CAProviderPlugin = new ConfigKey<>("Advanced", String.class, + "ca.framework.provider.plugin", + "root", + "The CA provider plugin that is used for secure CloudStack management server-agent communication for encryption and authentication. Restart management server(s) when changed.", true); + + ConfigKey CertKeySize = new ConfigKey<>("Advanced", Integer.class, + "ca.framework.cert.keysize", + "2048", + "The key size to be used for random certificate keypair generation.", true); + + ConfigKey CertSignatureAlgorithm = new ConfigKey<>("Advanced", String.class, + "ca.framework.cert.signature.algorithm", + "SHA256withRSA", + "The default signature algorithm to use for certificate generation.", true); + + + ConfigKey CertValidityPeriod = new ConfigKey<>("Advanced", Integer.class, + "ca.framework.cert.validity.period", + "365", + "The validity period of a client certificate in number of days. Set the value to be more than the expiry alert period.", true); + + ConfigKey AutomaticCertRenewal = new ConfigKey<>("Advanced", Boolean.class, + "ca.framework.cert.automatic.renewal", + "true", + "Enable automatic renewal and provisioning of certificate to agents as supported by the configured CA plugin.", true, ConfigKey.Scope.Cluster); + + ConfigKey CABackgroundJobDelay = new ConfigKey<>("Advanced", Long.class, + "ca.framework.background.task.delay", + "3600", + "The CA framework background task delay in seconds. Background task runs expiry checks and renews certificate if auto-renewal is enabled.", true); + + ConfigKey CertExpiryAlertPeriod = new ConfigKey<>("Advanced", Integer.class, + "ca.framework.cert.expiry.alert.period", + "15", + "The number of days before expiry of a client certificate, the validations are checked. Admins are alerted when auto-renewal is not allowed, otherwise auto-renewal is attempted.", true, ConfigKey.Scope.Cluster); + + /** + * Returns a list of available CA provider plugins + * @return returns list of CAProvider + */ + List getCaProviders(); + + /** + * Returns a map of active agents/hosts certificates + * @return returns a non-null map + */ + Map getActiveCertificatesMap(); + + /** + * Checks whether the configured CA plugin can provision/create certificates + * @return returns certificate creation capability + */ + boolean canProvisionCertificates(); + + /** + * Returns PEM-encoded chained CA certificate + * @param caProvider + * @return returns CA certificate chain string + */ + String getCaCertificate(final String caProvider) throws IOException; + + /** + * Issues client Certificate + * @param csr + * @param ipAddresses + * @param domainNames + * @param validityDays + * @param provider + * @return returns Certificate + */ + Certificate issueCertificate(final String csr, final List domainNames, final List ipAddresses, final Integer validityDays, final String provider); + + /** + * Revokes certificate from provided serial and CN + * @param certSerial + * @param certCn + * @return returns success/failure as boolean + */ + boolean revokeCertificate(final BigInteger certSerial, final String certCn, final String provider); + + /** + * Provisions certificate for given active and connected agent host + * @param host + * @param provider + * @return returns success/failure as boolean + */ + boolean provisionCertificate(final Host host, final Boolean reconnect, final String provider); + + /** + * Setups up a new keystore and generates CSR for a host + * @param host + * @param sshAccessDetails when provided, VirtualRoutingResource uses router proxy to execute commands via SSH in systemvms + * @return + * @throws AgentUnavailableException + * @throws OperationTimedoutException + */ + String generateKeyStoreAndCsr(final Host host, final Map sshAccessDetails) throws AgentUnavailableException, OperationTimedoutException; + + /** + * Deploys a Certificate payload to a provided host + * @param host + * @param certificate + * @param reconnect when true the host/agent is reconnected on successful deployment of the certificate + * @param sshAccessDetails when provided, VirtualRoutingResource uses router proxy to execute commands via SSH in systemvms + * @return + * @throws AgentUnavailableException + * @throws OperationTimedoutException + */ + boolean deployCertificate(final Host host, final Certificate certificate, final Boolean reconnect, final Map sshAccessDetails) throws AgentUnavailableException, OperationTimedoutException; + + /** + * Removes the host from an internal active client/certificate map + * @param host + */ + void purgeHostCertificate(final Host host); + + /** + * Sends a CA cert event alert to admins with a subject and a message + * @param host + * @param subject + * @param message + */ + void sendAlert(final Host host, final String subject, final String message); + +} diff --git a/api/src/org/apache/cloudstack/poll/BackgroundPollTask.java b/api/src/org/apache/cloudstack/poll/BackgroundPollTask.java index 8eea147955b..5f1b3300c48 100644 --- a/api/src/org/apache/cloudstack/poll/BackgroundPollTask.java +++ b/api/src/org/apache/cloudstack/poll/BackgroundPollTask.java @@ -18,4 +18,10 @@ package org.apache.cloudstack.poll; public interface BackgroundPollTask extends Runnable { + /** + * Returns delay in milliseconds between two rounds + * When it returns null a default value is used + * @return + */ + Long getDelay(); } diff --git a/client/pom.xml b/client/pom.xml index 9b8fa3866ee..7af4324c579 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -63,6 +63,11 @@ cloud-plugin-acl-dynamic-role-based ${project.version} + + org.apache.cloudstack + cloud-plugin-ca-rootca + ${project.version} + org.apache.cloudstack cloud-plugin-dedicated-resources @@ -274,6 +279,11 @@ cloud-mom-kafka ${project.version} + + org.apache.cloudstack + cloud-framework-ca + ${project.version} + org.apache.cloudstack cloud-framework-ipc @@ -478,6 +488,7 @@ /client ${project.build.directory}/utilities/scripts/db/;${project.build.directory}/utilities/scripts/db/db/ + .*/cloud.*jar$|.*/classes/.* diff --git a/client/tomcatconf/cloudmanagementserver.keystore b/client/tomcatconf/cloudmanagementserver.keystore deleted file mode 100644 index 3ee4d13565a..00000000000 Binary files a/client/tomcatconf/cloudmanagementserver.keystore and /dev/null differ diff --git a/client/tomcatconf/server-ssl.xml.in b/client/tomcatconf/server-ssl.xml.in index 595879fbfd1..8368a65c3bc 100755 --- a/client/tomcatconf/server-ssl.xml.in +++ b/client/tomcatconf/server-ssl.xml.in @@ -94,7 +94,7 @@ maxThreads="150" scheme="https" secure="true" URIEncoding="UTF-8" clientAuth="false" sslProtocol="TLS" keystoreType="JKS" - keystoreFile="/etc/cloudstack/management/cloudmanagementserver.keystore" + keystoreFile="/etc/cloudstack/management/cloud.jks" keystorePass="vmops.com"/> diff --git a/client/tomcatconf/server7-ssl.xml.in b/client/tomcatconf/server7-ssl.xml.in index 2633bcad760..d4fe899b57a 100755 --- a/client/tomcatconf/server7-ssl.xml.in +++ b/client/tomcatconf/server7-ssl.xml.in @@ -94,7 +94,7 @@ maxThreads="150" scheme="https" secure="true" URIEncoding="UTF-8" clientAuth="false" sslProtocol="TLS" keystoreType="JKS" - keystoreFile="/etc/cloudstack/management/cloudmanagementserver.keystore" + keystoreFile="/etc/cloudstack/management/cloud.jks" keystorePass="vmops.com"/> diff --git a/client/tomcatconf/tomcat6-ssl.conf.in b/client/tomcatconf/tomcat6-ssl.conf.in index e7c53ac9f8f..1d6f59b0787 100644 --- a/client/tomcatconf/tomcat6-ssl.conf.in +++ b/client/tomcatconf/tomcat6-ssl.conf.in @@ -40,7 +40,7 @@ CATALINA_TMPDIR="@MSENVIRON@/temp" # Use JAVA_OPTS to set java.library.path for libtcnative.so #JAVA_OPTS="-Djava.library.path=/usr/lib64" -JAVA_OPTS="-Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Djavax.net.ssl.trustStore=/etc/cloudstack/management/cloudmanagementserver.keystore -Djavax.net.ssl.trustStorePassword=vmops.com -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=@MSLOGDIR@ -XX:MaxPermSize=800m -XX:PermSize=512M -Djava.security.properties=/etc/cloudstack/management/java.security.ciphers" +JAVA_OPTS="-Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Djavax.net.ssl.trustStore=/etc/cloudstack/management/cloud.jks -Djavax.net.ssl.trustStorePassword=vmops.com -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=@MSLOGDIR@ -XX:MaxPermSize=800m -XX:PermSize=512M -Djava.security.properties=/etc/cloudstack/management/java.security.ciphers" # What user should run tomcat TOMCAT_USER="@MSUSER@" diff --git a/core/resources/META-INF/cloudstack/ca/module.properties b/core/resources/META-INF/cloudstack/ca/module.properties new file mode 100644 index 00000000000..1a6915aea90 --- /dev/null +++ b/core/resources/META-INF/cloudstack/ca/module.properties @@ -0,0 +1,21 @@ +# +# 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. +# + +name=ca +parent=backend diff --git a/core/resources/META-INF/cloudstack/ca/spring-core-lifecycle-ca-context-inheritable.xml b/core/resources/META-INF/cloudstack/ca/spring-core-lifecycle-ca-context-inheritable.xml new file mode 100644 index 00000000000..1566a4b076b --- /dev/null +++ b/core/resources/META-INF/cloudstack/ca/spring-core-lifecycle-ca-context-inheritable.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml b/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml index a9124d37ac1..d5b912ac892 100644 --- a/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml +++ b/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml @@ -312,4 +312,8 @@ + + + diff --git a/core/src/com/cloud/agent/api/routing/NetworkElementCommand.java b/core/src/com/cloud/agent/api/routing/NetworkElementCommand.java index a7b5b5e6d17..ae482ac71ec 100644 --- a/core/src/com/cloud/agent/api/routing/NetworkElementCommand.java +++ b/core/src/com/cloud/agent/api/routing/NetworkElementCommand.java @@ -20,6 +20,7 @@ package com.cloud.agent.api.routing; import java.util.HashMap; +import java.util.Map; import com.cloud.agent.api.Command; @@ -46,6 +47,18 @@ public abstract class NetworkElementCommand extends Command { super(); } + public void setAccessDetail(final Map details) { + if (details == null) { + return; + } + for (final Map.Entry detail : details.entrySet()) { + if (detail == null) { + continue; + } + setAccessDetail(detail.getKey(), detail.getValue()); + } + } + public void setAccessDetail(final String name, final String value) { accessDetails.put(name, value); } diff --git a/core/src/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java b/core/src/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java index 75f55f949a2..d7c3d56f9b9 100644 --- a/core/src/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java +++ b/core/src/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java @@ -35,6 +35,11 @@ import java.util.concurrent.locks.ReentrantLock; import javax.naming.ConfigurationException; +import org.apache.cloudstack.ca.SetupCertificateAnswer; +import org.apache.cloudstack.ca.SetupCertificateCommand; +import org.apache.cloudstack.ca.SetupKeyStoreCommand; +import org.apache.cloudstack.ca.SetupKeystoreAnswer; +import org.apache.cloudstack.utils.security.KeyStoreUtils; import org.apache.log4j.Logger; import com.cloud.agent.api.Answer; @@ -108,6 +113,14 @@ public class VirtualRoutingResource { return executeQueryCommand(cmd); } + if (cmd instanceof SetupKeyStoreCommand) { + return execute((SetupKeyStoreCommand) cmd); + } + + if (cmd instanceof SetupCertificateCommand) { + return execute((SetupCertificateCommand) cmd); + } + if (cmd instanceof AggregationControlCommand) { return execute((AggregationControlCommand)cmd); } @@ -139,6 +152,37 @@ public class VirtualRoutingResource { } } + private Answer execute(final SetupKeyStoreCommand cmd) { + final String args = String.format("/usr/local/cloud/systemvm/conf/agent.properties " + + "/usr/local/cloud/systemvm/conf/%s " + + "%s %d " + + "/usr/local/cloud/systemvm/conf/%s", + KeyStoreUtils.defaultKeystoreFile, + cmd.getKeystorePassword(), + cmd.getValidityDays(), + KeyStoreUtils.defaultCsrFile); + ExecutionResult result = _vrDeployer.executeInVR(cmd.getRouterAccessIp(), KeyStoreUtils.keyStoreSetupScript, args); + return new SetupKeystoreAnswer(result.getDetails()); + } + + private Answer execute(final SetupCertificateCommand cmd) { + final String args = String.format("/usr/local/cloud/systemvm/conf/agent.properties " + + "/usr/local/cloud/systemvm/conf/%s %s " + + "/usr/local/cloud/systemvm/conf/%s \"%s\" " + + "/usr/local/cloud/systemvm/conf/%s \"%s\" " + + "/usr/local/cloud/systemvm/conf/%s \"%s\"", + KeyStoreUtils.defaultKeystoreFile, + KeyStoreUtils.sshMode, + KeyStoreUtils.defaultCertFile, + cmd.getEncodedCertificate(), + KeyStoreUtils.defaultCaCertFile, + cmd.getEncodedCaCertificates(), + KeyStoreUtils.defaultPrivateKeyFile, + cmd.getEncodedPrivateKey()); + ExecutionResult result = _vrDeployer.executeInVR(cmd.getRouterAccessIp(), KeyStoreUtils.keyStoreImportScript, args); + return new SetupCertificateAnswer(result.isSuccess()); + } + private Answer executeQueryCommand(NetworkElementCommand cmd) { if (cmd instanceof CheckRouterCommand) { return execute((CheckRouterCommand)cmd); diff --git a/core/src/org/apache/cloudstack/ca/SetupCertificateAnswer.java b/core/src/org/apache/cloudstack/ca/SetupCertificateAnswer.java new file mode 100644 index 00000000000..4df5d158da9 --- /dev/null +++ b/core/src/org/apache/cloudstack/ca/SetupCertificateAnswer.java @@ -0,0 +1,29 @@ +// +// 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; + +import com.cloud.agent.api.Answer; + +public class SetupCertificateAnswer extends Answer { + public SetupCertificateAnswer(final boolean result) { + super(null); + this.result = result; + } +} diff --git a/core/src/org/apache/cloudstack/ca/SetupCertificateCommand.java b/core/src/org/apache/cloudstack/ca/SetupCertificateCommand.java new file mode 100644 index 00000000000..1cd31509d39 --- /dev/null +++ b/core/src/org/apache/cloudstack/ca/SetupCertificateCommand.java @@ -0,0 +1,99 @@ +// +// 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; + +import java.io.IOException; +import java.util.Map; + +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.utils.security.CertUtils; +import org.apache.cloudstack.utils.security.KeyStoreUtils; + +import com.cloud.agent.api.LogLevel; +import com.cloud.agent.api.routing.NetworkElementCommand; +import com.cloud.utils.exception.CloudRuntimeException; + +public class SetupCertificateCommand extends NetworkElementCommand { + @LogLevel(LogLevel.Log4jLevel.Off) + private String certificate; + @LogLevel(LogLevel.Log4jLevel.Off) + private String privateKey = ""; + @LogLevel(LogLevel.Log4jLevel.Off) + private String caCertificates; + + private boolean handleByAgent = true; + + public SetupCertificateCommand(final Certificate certificate) { + super(); + if (certificate == null) { + throw new CloudRuntimeException("A null certificate was provided to setup"); + } + setWait(60); + try { + this.certificate = CertUtils.x509CertificateToPem(certificate.getClientCertificate()); + this.caCertificates = CertUtils.x509CertificatesToPem(certificate.getCaCertificates()); + if (certificate.getPrivateKey() != null) { + this.privateKey = CertUtils.privateKeyToPem(certificate.getPrivateKey()); + } + } catch (final IOException e) { + throw new CloudRuntimeException("Failed to transform X509 cert to PEM format", e); + } + } + + @Override + public void setAccessDetail(final Map accessDetails) { + handleByAgent = false; + super.setAccessDetail(accessDetails); + } + + @Override + public void setAccessDetail(String name, String value) { + handleByAgent = false; + super.setAccessDetail(name, value); + } + + public String getPrivateKey() { + return privateKey; + } + + public String getCertificate() { + return certificate; + } + + public String getCaCertificates() { + return caCertificates; + } + + public String getEncodedPrivateKey() { + return privateKey.replace("\n", KeyStoreUtils.certNewlineEncoder).replace(" ", KeyStoreUtils.certSpaceEncoder); + } + + public String getEncodedCertificate() { + return certificate.replace("\n", KeyStoreUtils.certNewlineEncoder).replace(" ", KeyStoreUtils.certSpaceEncoder); + } + + public String getEncodedCaCertificates() { + return caCertificates.replace("\n", KeyStoreUtils.certNewlineEncoder).replace(" ", KeyStoreUtils.certSpaceEncoder); + } + + public boolean isHandleByAgent() { + return handleByAgent; + } +} diff --git a/core/src/org/apache/cloudstack/ca/SetupKeyStoreCommand.java b/core/src/org/apache/cloudstack/ca/SetupKeyStoreCommand.java new file mode 100644 index 00000000000..7cd5cbee20f --- /dev/null +++ b/core/src/org/apache/cloudstack/ca/SetupKeyStoreCommand.java @@ -0,0 +1,75 @@ +// +// 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; + +import java.util.Map; + +import com.cloud.agent.api.LogLevel; +import com.cloud.agent.api.routing.NetworkElementCommand; +import com.cloud.utils.PasswordGenerator; + +public class SetupKeyStoreCommand extends NetworkElementCommand { + @LogLevel(LogLevel.Log4jLevel.Off) + private int validityDays; + @LogLevel(LogLevel.Log4jLevel.Off) + private String keystorePassword; + + private boolean handleByAgent = true; + + public SetupKeyStoreCommand(final int validityDays) { + super(); + setWait(60); + this.validityDays = validityDays; + if (this.validityDays < 1) { + this.validityDays = 1; + } + this.keystorePassword = PasswordGenerator.generateRandomPassword(16); + } + + @Override + public void setAccessDetail(final Map accessDetails) { + handleByAgent = false; + super.setAccessDetail(accessDetails); + } + + + @Override + public void setAccessDetail(String name, String value) { + handleByAgent = false; + super.setAccessDetail(name, value); + } + + public int getValidityDays() { + return validityDays; + } + + public String getKeystorePassword() { + return keystorePassword; + } + + public boolean isHandleByAgent() { + return handleByAgent; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/org/apache/cloudstack/ca/SetupKeystoreAnswer.java b/core/src/org/apache/cloudstack/ca/SetupKeystoreAnswer.java new file mode 100644 index 00000000000..16ddc96c5ea --- /dev/null +++ b/core/src/org/apache/cloudstack/ca/SetupKeystoreAnswer.java @@ -0,0 +1,37 @@ +// +// 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; + +import com.cloud.agent.api.LogLevel; +import com.google.common.base.Strings; + +public class SetupKeystoreAnswer extends SetupCertificateAnswer { + @LogLevel(LogLevel.Log4jLevel.Off) + private final String csr; + + public SetupKeystoreAnswer(final String csr) { + super(!Strings.isNullOrEmpty(csr)); + this.csr = csr; + } + + public String getCsr() { + return csr; + } +} diff --git a/debian/cloudstack-management.postinst b/debian/cloudstack-management.postinst index 240224d9252..5c9a7fa8ceb 100644 --- a/debian/cloudstack-management.postinst +++ b/debian/cloudstack-management.postinst @@ -50,9 +50,6 @@ if [ "$1" = configure ]; then cp -a $OLDCONFDIR/$FILE $NEWCONFDIR/$FILE fi done - if [ -f "$OLDCONFDIR/cloud.keystore" ]; then - cp -a $OLDCONFDIR/cloud.keystore $NEWCONFDIR/cloudmanagementserver.keystore - fi fi CONFDIR="/etc/cloudstack/management" diff --git a/developer/developer-prefill.sql b/developer/developer-prefill.sql index 7fd85c61553..cc67748b8dd 100644 --- a/developer/developer-prefill.sql +++ b/developer/developer-prefill.sql @@ -119,6 +119,11 @@ INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) VALUES ('Advanced', 'DEFAULT', 'RoleService', 'dynamic.apichecker.enabled', 'true'); +-- Enable RootCA auth strictness for fresh deployments +INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) + VALUES ('Advanced', 'DEFAULT', 'RootCAProvider', + 'ca.plugin.root.auth.strictness', 'true'); + -- Add developer configuration entry; allows management server to be run as a user other than "cloud" INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) VALUES ('Advanced', 'DEFAULT', 'management-server', diff --git a/engine/api/src/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java b/engine/api/src/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java index 13bfee6ea27..68531e3ab3d 100644 --- a/engine/api/src/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java +++ b/engine/api/src/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java @@ -98,6 +98,8 @@ public interface NetworkOrchestrationService { List getNicProfiles(VirtualMachine vm); + Map getSystemVMAccessDetails(VirtualMachine vm); + Pair implementNetwork(long networkId, DeployDestination dest, ReservationContext context) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException; diff --git a/engine/orchestration/src/com/cloud/agent/manager/AgentManagerImpl.java b/engine/orchestration/src/com/cloud/agent/manager/AgentManagerImpl.java index 597ea6733e9..325f3ec739f 100644 --- a/engine/orchestration/src/com/cloud/agent/manager/AgentManagerImpl.java +++ b/engine/orchestration/src/com/cloud/agent/manager/AgentManagerImpl.java @@ -37,6 +37,7 @@ import java.util.concurrent.locks.ReentrantLock; import javax.inject.Inject; import javax.naming.ConfigurationException; +import org.apache.cloudstack.ca.CAManager; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; @@ -134,6 +135,8 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl protected int _monitorId = 0; private final Lock _agentStatusLock = new ReentrantLock(); + @Inject + protected CAManager caService; @Inject protected EntityManager _entityMgr; @@ -223,7 +226,7 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl // allow core threads to time out even when there are no items in the queue _connectExecutor.allowCoreThreadTimeOut(true); - _connection = new NioServer("AgentManager", Port.value(), Workers.value() + 10, this); + _connection = new NioServer("AgentManager", Port.value(), Workers.value() + 10, this, caService); s_logger.info("Listening on " + Port.value() + " with " + Workers.value() + " workers"); // executes all agent commands other than cron and ping @@ -813,6 +816,7 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl s_logger.debug("The next status of agent " + hostId + "is " + nextStatus + ", current status is " + currentStatus); } } + caService.purgeHostCertificate(host); } if (s_logger.isDebugEnabled()) { diff --git a/engine/orchestration/src/com/cloud/agent/manager/ClusteredAgentManagerImpl.java b/engine/orchestration/src/com/cloud/agent/manager/ClusteredAgentManagerImpl.java index b69ca5bb94d..faf3a3bfcd4 100644 --- a/engine/orchestration/src/com/cloud/agent/manager/ClusteredAgentManagerImpl.java +++ b/engine/orchestration/src/com/cloud/agent/manager/ClusteredAgentManagerImpl.java @@ -495,6 +495,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } final String ip = ms.getServiceIP(); InetAddress addr; + int port = Port.value(); try { addr = InetAddress.getByName(ip); } catch (final UnknownHostException e) { @@ -502,21 +503,21 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } SocketChannel ch1 = null; try { - ch1 = SocketChannel.open(new InetSocketAddress(addr, Port.value())); + ch1 = SocketChannel.open(new InetSocketAddress(addr, port)); ch1.configureBlocking(false); ch1.socket().setKeepAlive(true); ch1.socket().setSoTimeout(60 * 1000); try { - final SSLContext sslContext = Link.initSSLContext(true); - sslEngine = sslContext.createSSLEngine(ip, Port.value()); + SSLContext sslContext = Link.initClientSSLContext(); + sslEngine = sslContext.createSSLEngine(ip, port); sslEngine.setUseClientMode(true); sslEngine.setEnabledProtocols(SSLUtils.getSupportedProtocols(sslEngine.getEnabledProtocols())); sslEngine.beginHandshake(); if (!Link.doHandshake(ch1, sslEngine, true)) { ch1.close(); - throw new IOException("SSL handshake failed!"); + throw new IOException(String.format("SSL: Handshake failed with peer management server '%s' on %s:%d ", peerName, ip, port)); } - s_logger.info("SSL: Handshake done"); + s_logger.info(String.format("SSL: Handshake done with peer management server '%s' on %s:%d ", peerName, ip, port)); } catch (final Exception e) { ch1.close(); throw new IOException("SSL: Fail to init SSL! " + e); @@ -528,10 +529,12 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust _sslEngines.put(peerName, sslEngine); return ch1; } catch (final IOException e) { - try { - ch1.close(); - } catch (final IOException ex) { - s_logger.error("failed to close failed peer socket: " + ex); + if (ch1 != null) { + try { + ch1.close(); + } catch (final IOException ex) { + s_logger.error("failed to close failed peer socket: " + ex); + } } s_logger.warn("Unable to connect to peer management server: " + peerName + ", ip: " + ip + " due to " + e.getMessage(), e); return null; diff --git a/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java index babfbdb54cb..638a00047e8 100755 --- a/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java @@ -22,6 +22,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Iterator; @@ -39,12 +40,14 @@ import javax.naming.ConfigurationException; import com.cloud.agent.api.AttachOrDettachConfigDriveCommand; import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; +import org.apache.cloudstack.ca.CAManager; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo; import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator; +import org.apache.cloudstack.framework.ca.Certificate; import org.apache.cloudstack.framework.config.ConfigDepot; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; @@ -97,6 +100,7 @@ import com.cloud.agent.api.StopCommand; import com.cloud.agent.api.UnPlugNicAnswer; import com.cloud.agent.api.UnPlugNicCommand; import com.cloud.agent.api.UnregisterVMCommand; +import com.cloud.agent.api.routing.NetworkElementCommand; import com.cloud.agent.api.to.DiskTO; import com.cloud.agent.api.to.GPUDeviceTO; import com.cloud.agent.api.to.NicTO; @@ -205,6 +209,7 @@ import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.snapshot.VMSnapshotManager; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import com.google.common.base.Strings; public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMachineManager, VmWorkJobHandler, Listener, Configurable { private static final Logger s_logger = Logger.getLogger(VirtualMachineManagerImpl.class); @@ -284,7 +289,9 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac @Inject protected UserVmDetailsDao _vmDetailsDao; @Inject - ServiceOfferingDao _serviceOfferingDao = null; + protected ServiceOfferingDao _serviceOfferingDao = null; + @Inject + protected CAManager caManager; @Inject ConfigDepot _configDepot; @@ -1023,7 +1030,6 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac cmds.addCommand(new StartCommand(vmTO, dest.getHost(), getExecuteInSequence(vm.getHypervisorType()))); - vmGuru.finalizeDeployment(cmds, vmProfile, dest, ctx); work = _workDao.findById(work.getId()); @@ -1073,6 +1079,23 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac if (s_logger.isDebugEnabled()) { s_logger.debug("Start completed for VM " + vm); } + final Host vmHost = _hostDao.findById(destHostId); + if (vmHost != null && (VirtualMachine.Type.ConsoleProxy.equals(vm.getType()) || + VirtualMachine.Type.SecondaryStorageVm.equals(vm.getType())) && caManager.canProvisionCertificates()) { + final Map sshAccessDetails = _networkMgr.getSystemVMAccessDetails(vm); + final String csr = caManager.generateKeyStoreAndCsr(vmHost, sshAccessDetails); + if (!Strings.isNullOrEmpty(csr)) { + final Map ipAddressDetails = new HashMap<>(sshAccessDetails); + ipAddressDetails.remove(NetworkElementCommand.ROUTER_NAME); + final Certificate certificate = caManager.issueCertificate(csr, Arrays.asList(vm.getHostName(), vm.getInstanceName()), new ArrayList<>(ipAddressDetails.values()), CAManager.CertValidityPeriod.value(), null); + final boolean result = caManager.deployCertificate(vmHost, certificate, false, sshAccessDetails); + if (!result) { + s_logger.error("Failed to setup certificate for system vm: " + vm.getInstanceName()); + } + } else { + s_logger.error("Failed to setup keystore and generate CSR for system vm: " + vm.getInstanceName()); + } + } return; } else { if (s_logger.isDebugEnabled()) { diff --git a/engine/orchestration/src/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 37f53302455..3956617859c 100644 --- a/engine/orchestration/src/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -68,6 +68,7 @@ import com.cloud.agent.api.CheckNetworkCommand; import com.cloud.agent.api.Command; import com.cloud.agent.api.StartupCommand; import com.cloud.agent.api.StartupRoutingCommand; +import com.cloud.agent.api.routing.NetworkElementCommand; import com.cloud.agent.api.to.NicTO; import com.cloud.alert.AlertManager; import com.cloud.configuration.ConfigurationManager; @@ -215,6 +216,7 @@ import com.cloud.vm.dao.NicSecondaryIpDao; import com.cloud.vm.dao.NicSecondaryIpVO; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; +import com.google.common.base.Strings; /** * NetworkManagerImpl implements NetworkManager. @@ -3488,6 +3490,39 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra return profiles; } + @Override + public Map getSystemVMAccessDetails(final VirtualMachine vm) { + final Map accessDetails = new HashMap<>(); + accessDetails.put(NetworkElementCommand.ROUTER_NAME, vm.getInstanceName()); + String privateIpAddress = null; + for (final NicProfile profile : getNicProfiles(vm)) { + if (profile == null) { + continue; + } + final Network network = _networksDao.findById(profile.getNetworkId()); + if (network == null) { + continue; + } + final String address = profile.getIPv4Address(); + if (network.getTrafficType() == Networks.TrafficType.Control) { + accessDetails.put(NetworkElementCommand.ROUTER_IP, address); + } + if (network.getTrafficType() == Networks.TrafficType.Guest) { + accessDetails.put(NetworkElementCommand.ROUTER_GUEST_IP, address); + } + if (network.getTrafficType() == Networks.TrafficType.Management) { + privateIpAddress = address; + } + if (network.getTrafficType() != null && !Strings.isNullOrEmpty(address)) { + accessDetails.put(network.getTrafficType().name(), address); + } + } + if (privateIpAddress != null && Strings.isNullOrEmpty(accessDetails.get(NetworkElementCommand.ROUTER_IP))) { + accessDetails.put(NetworkElementCommand.ROUTER_IP, privateIpAddress); + } + return accessDetails; + } + protected boolean stateTransitTo(final NetworkVO network, final Network.Event e) throws NoTransitionException { return _stateMachine.transitTo(network, e, null, _networksDao); } diff --git a/engine/schema/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 8b7a604979e..e8d6633d163 100644 --- a/engine/schema/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -130,6 +130,7 @@ + diff --git a/engine/schema/src/com/cloud/certificate/CrlVO.java b/engine/schema/src/com/cloud/certificate/CrlVO.java new file mode 100644 index 00000000000..6df7530b290 --- /dev/null +++ b/engine/schema/src/com/cloud/certificate/CrlVO.java @@ -0,0 +1,85 @@ +// 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 com.cloud.certificate; + +import java.math.BigInteger; +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.apache.cloudstack.api.InternalIdentity; + +@Entity +@Table(name = "crl") +public class CrlVO implements InternalIdentity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id = null; + + @Column(name = "serial") + private String certSerial; + + @Column(name = "cn") + private String certCn; + + @Column(name = "revoker_uuid") + private String revokerUuid; + + @Temporal(value = TemporalType.TIMESTAMP) + @Column(name = "revoked", updatable = true) + private Date revoked; + + public CrlVO() { + } + + public CrlVO(final BigInteger certSerial, final String certCn, final String revokerUuid) { + this.certSerial = certSerial.toString(16); + this.certCn = certCn; + this.revokerUuid = revokerUuid; + this.revoked = new Date(); + } + + @Override + public long getId() { + return id; + } + + public BigInteger getCertSerial() { + return new BigInteger(certSerial, 16); + } + + public String getCertCn() { + return certCn; + } + + public String getRevokerUuid() { + return revokerUuid; + } + + public Date getRevoked() { + return revoked; + } +} diff --git a/engine/schema/src/com/cloud/certificate/dao/CrlDao.java b/engine/schema/src/com/cloud/certificate/dao/CrlDao.java new file mode 100644 index 00000000000..613d9caf487 --- /dev/null +++ b/engine/schema/src/com/cloud/certificate/dao/CrlDao.java @@ -0,0 +1,28 @@ +// 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 com.cloud.certificate.dao; + +import java.math.BigInteger; + +import com.cloud.certificate.CrlVO; +import com.cloud.utils.db.GenericDao; + +public interface CrlDao extends GenericDao { + CrlVO findBySerial(final BigInteger certSerial); + CrlVO revokeCertificate(final BigInteger certSerial, final String certCn); +} \ No newline at end of file diff --git a/engine/schema/src/com/cloud/certificate/dao/CrlDaoImpl.java b/engine/schema/src/com/cloud/certificate/dao/CrlDaoImpl.java new file mode 100644 index 00000000000..2eee308f115 --- /dev/null +++ b/engine/schema/src/com/cloud/certificate/dao/CrlDaoImpl.java @@ -0,0 +1,57 @@ +// 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 com.cloud.certificate.dao; + +import java.math.BigInteger; + +import org.apache.cloudstack.context.CallContext; + +import com.cloud.certificate.CrlVO; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@DB +public class CrlDaoImpl extends GenericDaoBase implements CrlDao { + + private final SearchBuilder CrlBySerialSearch; + + public CrlDaoImpl() { + super(); + + CrlBySerialSearch = createSearchBuilder(); + CrlBySerialSearch.and("certSerial", CrlBySerialSearch.entity().getCertSerial(), SearchCriteria.Op.EQ); + CrlBySerialSearch.done(); + } + + @Override + public CrlVO findBySerial(final BigInteger certSerial) { + if (certSerial == null) { + return null; + } + final SearchCriteria sc = CrlBySerialSearch.create("certSerial", certSerial.toString(16)); + return findOneBy(sc); + } + + @Override + public CrlVO revokeCertificate(final BigInteger certSerial, final String certCn) { + final CrlVO revokedCertificate = new CrlVO(certSerial, certCn == null ? "" : certCn, CallContext.current().getCallingUserUuid()); + return persist(revokedCertificate); + } +} diff --git a/engine/schema/src/com/cloud/host/dao/HostDao.java b/engine/schema/src/com/cloud/host/dao/HostDao.java index 7ffe1ed2ed5..f98e8c19eeb 100644 --- a/engine/schema/src/com/cloud/host/dao/HostDao.java +++ b/engine/schema/src/com/cloud/host/dao/HostDao.java @@ -101,4 +101,6 @@ public interface HostDao extends GenericDao, StateDao listClustersByHostTag(String hostTagOnOffering); List listByType(Type type); + + HostVO findByIp(String ip); } diff --git a/engine/schema/src/com/cloud/host/dao/HostDaoImpl.java b/engine/schema/src/com/cloud/host/dao/HostDaoImpl.java index 309d17eaa64..0ac4f43b7fd 100644 --- a/engine/schema/src/com/cloud/host/dao/HostDaoImpl.java +++ b/engine/schema/src/com/cloud/host/dao/HostDaoImpl.java @@ -89,6 +89,7 @@ public class HostDaoImpl extends GenericDaoBase implements HostDao protected SearchBuilder DcPrivateIpAddressSearch; protected SearchBuilder DcStorageIpAddressSearch; protected SearchBuilder PublicIpAddressSearch; + protected SearchBuilder AnyIpAddressSearch; protected SearchBuilder GuidSearch; protected SearchBuilder DcSearch; @@ -216,6 +217,11 @@ public class HostDaoImpl extends GenericDaoBase implements HostDao PublicIpAddressSearch.and("publicIpAddress", PublicIpAddressSearch.entity().getPublicIpAddress(), SearchCriteria.Op.EQ); PublicIpAddressSearch.done(); + AnyIpAddressSearch = createSearchBuilder(); + AnyIpAddressSearch.or("publicIpAddress", AnyIpAddressSearch.entity().getPublicIpAddress(), SearchCriteria.Op.EQ); + AnyIpAddressSearch.or("privateIpAddress", AnyIpAddressSearch.entity().getPrivateIpAddress(), SearchCriteria.Op.EQ); + AnyIpAddressSearch.done(); + GuidSearch = createSearchBuilder(); GuidSearch.and("guid", GuidSearch.entity().getGuid(), SearchCriteria.Op.EQ); GuidSearch.done(); @@ -1118,6 +1124,13 @@ public class HostDaoImpl extends GenericDaoBase implements HostDao return findOneBy(sc); } + @Override + public HostVO findByIp(final String ipAddress) { + SearchCriteria sc = AnyIpAddressSearch.create(); + sc.setParameters("publicIpAddress", ipAddress); + sc.setParameters("privateIpAddress", ipAddress); + return findOneBy(sc); + } @Override public List findHypervisorHostInCluster(long clusterId) { diff --git a/framework/ca/pom.xml b/framework/ca/pom.xml new file mode 100644 index 00000000000..9412649b3b9 --- /dev/null +++ b/framework/ca/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + cloud-framework-ca + Apache CloudStack Framework - Certificate Authority + + org.apache.cloudstack + cloudstack-framework + 4.11.0.0-SNAPSHOT + ../pom.xml + + diff --git a/framework/ca/src/org/apache/cloudstack/framework/ca/CAProvider.java b/framework/ca/src/org/apache/cloudstack/framework/ca/CAProvider.java new file mode 100644 index 00000000000..8dc343bd592 --- /dev/null +++ b/framework/ca/src/org/apache/cloudstack/framework/ca/CAProvider.java @@ -0,0 +1,93 @@ +// 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.framework.ca; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +public interface CAProvider { + + /** + * Method returns capability of the plugin to participate in certificate issuance, revocation and provisioning + * @return returns true when CA provider can do certificate lifecycle tasks + */ + boolean canProvisionCertificates(); + + /** + * Returns root CA certificate + * @return returns concatenated root CA certificate string + */ + List getCaCertificate(); + + /** + * Issues certificate with provided options + * @param domainNames + * @param ipAddresses + * @param validityDays + * @return returns issued certificate + */ + Certificate issueCertificate(final List domainNames, final List ipAddresses, final int validityDays); + + /** + * Issues certificate using given CSR and other options + * @param csr + * @param domainNames + * @param ipAddresses + * @param validityDays + * @return returns issued certificate using provided CSR and other options + */ + Certificate issueCertificate(final String csr, final List domainNames, final List ipAddresses, final int validityDays); + + /** + * Revokes certificate using certificate serial and CN + * @param certSerial + * @param certCn + * @return returns true on success + */ + boolean revokeCertificate(final BigInteger certSerial, final String certCn); + + /** + * This method can add/inject custom TrustManagers for client connection validations. + * @param sslContext The SSL context used while accepting a client connection + * @param remoteAddress + * @param certMap + * @return returns created SSL engine instance + * @throws GeneralSecurityException + * @throws IOException + */ + SSLEngine createSSLEngine(final SSLContext sslContext, final String remoteAddress, final Map certMap) throws GeneralSecurityException, IOException; + + /** + * Returns the unique name of the provider + * @return returns provider name + */ + String getProviderName(); + + /** + * Returns description about the CA provider plugin + * @return returns description + */ + String getDescription(); +} diff --git a/framework/ca/src/org/apache/cloudstack/framework/ca/CAService.java b/framework/ca/src/org/apache/cloudstack/framework/ca/CAService.java new file mode 100644 index 00000000000..3aacb3b2b85 --- /dev/null +++ b/framework/ca/src/org/apache/cloudstack/framework/ca/CAService.java @@ -0,0 +1,36 @@ +// 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.framework.ca; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +public interface CAService { + /** + * Returns a SSLEngine to be used for handling client connections + * @param context + * @param remoteAddress + * @return + * @throws GeneralSecurityException + * @throws IOException + */ + SSLEngine createSSLEngine(final SSLContext context, final String remoteAddress) throws GeneralSecurityException, IOException; +} diff --git a/framework/ca/src/org/apache/cloudstack/framework/ca/Certificate.java b/framework/ca/src/org/apache/cloudstack/framework/ca/Certificate.java new file mode 100644 index 00000000000..b3a230d5a99 --- /dev/null +++ b/framework/ca/src/org/apache/cloudstack/framework/ca/Certificate.java @@ -0,0 +1,46 @@ +// 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.framework.ca; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.List; + +public class Certificate { + private X509Certificate clientCertificate; + private PrivateKey privateKey; + private List caCertificates; + + public Certificate(final X509Certificate clientCertificate, final PrivateKey privateKey, final List caCertificates) { + this.clientCertificate = clientCertificate; + this.privateKey = privateKey; + this.caCertificates = caCertificates; + } + + public X509Certificate getClientCertificate() { + return clientCertificate; + } + + public PrivateKey getPrivateKey() { + return privateKey; + } + + public List getCaCertificates() { + return caCertificates; + } +} diff --git a/framework/pom.xml b/framework/pom.xml index 3fccc4c383d..5bfb1d094fb 100644 --- a/framework/pom.xml +++ b/framework/pom.xml @@ -44,6 +44,7 @@ ipc + ca rest events jobs diff --git a/packaging/centos63/cloud.spec b/packaging/centos63/cloud.spec index abe50bce65e..dbf55bb580b 100644 --- a/packaging/centos63/cloud.spec +++ b/packaging/centos63/cloud.spec @@ -499,12 +499,6 @@ else echo "Unable to determine ssl settings for tomcat.conf, please run cloudstack-setup-management manually" fi -if [ -f "%{_sysconfdir}/cloud.rpmsave/management/cloud.keystore" ]; then - cp -p %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore %{_sysconfdir}/%{name}/management/cloudmanagementserver.keystore - # make sure we only do this on the first install of this RPM, don't want to overwrite on a reinstall - mv %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore.rpmsave -fi - %preun agent /sbin/service cloudstack-agent stop || true if [ "$1" == "0" ] ; then diff --git a/packaging/fedora20/cloud.spec b/packaging/fedora20/cloud.spec index 546e439da2b..3f960cf7491 100644 --- a/packaging/fedora20/cloud.spec +++ b/packaging/fedora20/cloud.spec @@ -464,12 +464,6 @@ else echo "Unable to determine ssl settings for tomcat.conf, please run cloudstack-setup-management manually" fi -if [ -f "%{_sysconfdir}/cloud.rpmsave/management/cloud.keystore" ]; then - cp -p %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore %{_sysconfdir}/%{name}/management/cloudmanagementserver.keystore - # make sure we only do this on the first install of this RPM, don't want to overwrite on a reinstall - mv %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore.rpmsave -fi - %preun agent /sbin/service cloudstack-agent stop || true if [ "$1" == "0" ] ; then diff --git a/packaging/fedora21/cloud.spec b/packaging/fedora21/cloud.spec index a79d172eab7..de05370c86b 100644 --- a/packaging/fedora21/cloud.spec +++ b/packaging/fedora21/cloud.spec @@ -464,12 +464,6 @@ else echo "Unable to determine ssl settings for tomcat.conf, please run cloudstack-setup-management manually" fi -if [ -f "%{_sysconfdir}/cloud.rpmsave/management/cloud.keystore" ]; then - cp -p %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore %{_sysconfdir}/%{name}/management/cloudmanagementserver.keystore - # make sure we only do this on the first install of this RPM, don't want to overwrite on a reinstall - mv %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore.rpmsave -fi - %preun agent /sbin/service cloudstack-agent stop || true if [ "$1" == "0" ] ; then diff --git a/packaging/systemd/cloudstack-management.default b/packaging/systemd/cloudstack-management.default index 6e5fcf9d8d6..eb5b654b1b0 100644 --- a/packaging/systemd/cloudstack-management.default +++ b/packaging/systemd/cloudstack-management.default @@ -28,8 +28,8 @@ JASPER_HOME="/usr/share/cloudstack-management" CATALINA_TMPDIR="/usr/share/cloudstack-management/temp" -if [ -r "/etc/cloudstack/management/cloudmanagementserver.keystore" ] ; then - JAVA_OPTS="-Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cloudstack/management/ -XX:PermSize=512M -XX:MaxPermSize=800m -Djavax.net.ssl.trustStore=/etc/cloudstack/management/cloudmanagementserver.keystore -Djavax.net.ssl.trustStorePassword=vmops.com " +if [ -r "/etc/cloudstack/management/cloud.jks" ] ; then + JAVA_OPTS="-Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cloudstack/management/ -XX:PermSize=512M -XX:MaxPermSize=800m -Djavax.net.ssl.trustStore=/etc/cloudstack/management/cloud.jks -Djavax.net.ssl.trustStorePassword=vmops.com " else JAVA_OPTS="-Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cloudstack/management/ -XX:PermSize=512M -XX:MaxPermSize=800m" fi diff --git a/packaging/systemd/cloudstack-management.default.ubuntu b/packaging/systemd/cloudstack-management.default.ubuntu index 0087495dbcd..9b3d0c475b5 100644 --- a/packaging/systemd/cloudstack-management.default.ubuntu +++ b/packaging/systemd/cloudstack-management.default.ubuntu @@ -28,8 +28,8 @@ JASPER_HOME="/usr/share/cloudstack-management" CATALINA_TMPDIR="/usr/share/cloudstack-management/temp" -if [ -r "/etc/cloudstack/management/cloudmanagementserver.keystore" ] ; then - JAVA_OPTS="-Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cloudstack/management/ -XX:PermSize=512M -XX:MaxPermSize=800m -Djavax.net.ssl.trustStore=/etc/cloudstack/management/cloudmanagementserver.keystore -Djavax.net.ssl.trustStorePassword=vmops.com " +if [ -r "/etc/cloudstack/management/cloud.jks" ] ; then + JAVA_OPTS="-Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cloudstack/management/ -XX:PermSize=512M -XX:MaxPermSize=800m -Djavax.net.ssl.trustStore=/etc/cloudstack/management/cloud.jks -Djavax.net.ssl.trustStorePassword=vmops.com " else JAVA_OPTS="-Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cloudstack/management/ -XX:PermSize=512M -XX:MaxPermSize=800m" fi diff --git a/plugins/ca/root-ca/pom.xml b/plugins/ca/root-ca/pom.xml new file mode 100644 index 00000000000..e27f4911223 --- /dev/null +++ b/plugins/ca/root-ca/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + cloud-plugin-ca-rootca + Apache CloudStack Plugin - Inbuilt Root Certificate Authority + + org.apache.cloudstack + cloudstack-plugins + 4.11.0.0-SNAPSHOT + ../../pom.xml + + + + org.apache.cloudstack + cloud-utils + ${project.version} + + + org.apache.cloudstack + cloud-api + ${project.version} + + + org.apache.cloudstack + cloud-framework-ca + ${project.version} + + + diff --git a/plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/module.properties b/plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/module.properties new file mode 100644 index 00000000000..e086bc03073 --- /dev/null +++ b/plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/module.properties @@ -0,0 +1,18 @@ +# 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. +name=root-ca +parent=ca diff --git a/plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/spring-root-ca-context.xml b/plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/spring-root-ca-context.xml new file mode 100644 index 00000000000..46503febd7b --- /dev/null +++ b/plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/spring-root-ca-context.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/plugins/ca/root-ca/src/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java b/plugins/ca/root-ca/src/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java new file mode 100644 index 00000000000..90f620393ee --- /dev/null +++ b/plugins/ca/root-ca/src/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java @@ -0,0 +1,146 @@ +// 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.math.BigInteger; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.X509TrustManager; + +import org.apache.log4j.Logger; + +import com.cloud.certificate.dao.CrlDao; +import com.google.common.base.Strings; + +public final class RootCACustomTrustManager implements X509TrustManager { + private static final Logger LOG = Logger.getLogger(RootCACustomTrustManager.class); + + private String clientAddress = "Unknown"; + private boolean authStrictness = true; + private boolean allowExpiredCertificate = true; + private CrlDao crlDao; + private X509Certificate caCertificate; + private Map activeCertMap; + + public RootCACustomTrustManager(final String clientAddress, final boolean authStrictness, final boolean allowExpiredCertificate, final Map activeCertMap, final X509Certificate caCertificate, final CrlDao crlDao) { + if (!Strings.isNullOrEmpty(clientAddress)) { + this.clientAddress = clientAddress.replace("/", "").split(":")[0]; + } + this.authStrictness = authStrictness; + this.allowExpiredCertificate = allowExpiredCertificate; + this.activeCertMap = activeCertMap; + this.caCertificate = caCertificate; + this.crlDao = crlDao; + } + + private void printCertificateChain(final X509Certificate[] certificates, final String s) throws CertificateException { + if (certificates == null) { + return; + } + final StringBuilder builder = new StringBuilder(); + builder.append("A client/agent attempting connection from address=").append(clientAddress).append(" has presented these certificate(s):"); + int counter = 1; + for (final X509Certificate certificate: certificates) { + builder.append("\nCertificate [").append(counter++).append("] :"); + builder.append(String.format("\n Serial: %x", certificate.getSerialNumber())); + builder.append("\n Not Before:" + certificate.getNotBefore()); + builder.append("\n Not After:" + certificate.getNotAfter()); + builder.append("\n Signature Algorithm:" + certificate.getSigAlgName()); + builder.append("\n Version:" + certificate.getVersion()); + builder.append("\n Subject DN:" + certificate.getSubjectDN()); + builder.append("\n Issuer DN:" + certificate.getIssuerDN()); + builder.append("\n Alternative Names:" + certificate.getSubjectAlternativeNames()); + } + LOG.debug(builder.toString()); + } + + @Override + public void checkClientTrusted(final X509Certificate[] certificates, final String s) throws CertificateException { + if (LOG.isDebugEnabled()) { + printCertificateChain(certificates, s); + } + if (!authStrictness) { + return; + } + if (certificates == null || certificates.length < 1 || certificates[0] == null) { + throw new CertificateException("In strict auth mode, certificate(s) are expected from client:" + clientAddress); + } + final X509Certificate primaryClientCertificate = certificates[0]; + + // Revocation check + final BigInteger serialNumber = primaryClientCertificate.getSerialNumber(); + if (serialNumber == null || crlDao.findBySerial(serialNumber) != null) { + final String errorMsg = String.format("Client is using revoked certificate of serial=%x, subject=%s from address=%s", + primaryClientCertificate.getSerialNumber(), primaryClientCertificate.getSubjectDN(), clientAddress); + LOG.error(errorMsg); + throw new CertificateException(errorMsg); + } + + // Validity check + if (!allowExpiredCertificate) { + try { + primaryClientCertificate.checkValidity(); + } catch (final CertificateExpiredException | CertificateNotYetValidException e) { + final String errorMsg = String.format("Client certificate has expired with serial=%x, subject=%s from address=%s", + primaryClientCertificate.getSerialNumber(), primaryClientCertificate.getSubjectDN(), clientAddress); + LOG.error(errorMsg); + throw new CertificateException(errorMsg); } + } + + // Ownership check + boolean certMatchesOwnership = false; + if (primaryClientCertificate.getSubjectAlternativeNames() != null) { + for (final List list : primaryClientCertificate.getSubjectAlternativeNames()) { + if (list != null && list.size() == 2 && list.get(1) instanceof String) { + final String alternativeName = (String) list.get(1); + if (clientAddress.equals(alternativeName)) { + certMatchesOwnership = true; + } + } + } + } + if (!certMatchesOwnership) { + final String errorMsg = "Certificate ownership verification failed for client: " + clientAddress; + LOG.error(errorMsg); + throw new CertificateException(errorMsg); + } + if (activeCertMap != null && !Strings.isNullOrEmpty(clientAddress)) { + activeCertMap.put(clientAddress, primaryClientCertificate); + } + if (LOG.isDebugEnabled()) { + LOG.debug("Client/agent connection from ip=" + clientAddress + " has been validated and trusted."); + } + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + if (!authStrictness) { + return null; + } + return new X509Certificate[]{caCertificate}; + } +} diff --git a/plugins/ca/root-ca/src/org/apache/cloudstack/ca/provider/RootCAProvider.java b/plugins/ca/root-ca/src/org/apache/cloudstack/ca/provider/RootCAProvider.java new file mode 100644 index 00000000000..1f39853568b --- /dev/null +++ b/plugins/ca/root-ca/src/org/apache/cloudstack/ca/provider/RootCAProvider.java @@ -0,0 +1,465 @@ +// 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.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.StringReader; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +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.PropertiesUtil; +import com.cloud.utils.component.AdapterBase; +import com.cloud.utils.db.DbProperties; +import com.cloud.utils.db.GlobalLock; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.NetUtils; +import com.cloud.utils.nio.Link; +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; + + @Inject + private ConfigurationDao configDao; + @Inject + private CrlDao crlDao; + + //////////////////////////////////////////////////// + /////////////// Root CA Settings /////////////////// + //////////////////////////////////////////////////// + + private static ConfigKey rootCAPrivateKey = new ConfigKey<>("Hidden", String.class, + "ca.plugin.root.private.key", + null, + "The ROOT CA private key.", true); + + private static ConfigKey rootCAPublicKey = new ConfigKey<>("Hidden", String.class, + "ca.plugin.root.public.key", + null, + "The ROOT CA public key.", true); + + private static ConfigKey rootCACertificate = new ConfigKey<>("Hidden", String.class, + "ca.plugin.root.ca.certificate", + null, + "The ROOT CA certificate.", true); + + private static ConfigKey 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 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 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 domainNames, final List 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.getPrivate(), + 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 domainNames, final List 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 X509Certificate clientCertificate = CertUtils.generateV3Certificate( + caCertificate, caKeyPair.getPrivate(), + request.getPublicKey(), + request.getCertificationRequestInfo().getSubject().toString(), + 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 getCaCertificate() { + return Collections.singletonList(caCertificate); + } + + @Override + public Certificate issueCertificate(final List domainNames, final List 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 domainNames, final List 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 char[] getCaKeyStorePassphrase() { + return KeyStoreUtils.defaultKeystorePassphrase; + } + + 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(), getCaKeyStorePassphrase(), new X509Certificate[]{caCertificate}); + } else { + return null; + } + return ks; + } + + @Override + public SSLEngine createSSLEngine(final SSLContext sslContext, final String remoteAddress, final Map 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, getCaKeyStorePassphrase()); + 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.setWantClientAuth(authStrictness); + return sslEngine; + } + + ////////////////////////////////////////////////// + /////////////// Root CA Config /////////////////// + ////////////////////////////////////////////////// + + private char[] findKeyStorePassphrase() { + char[] passphrase = KeyStoreUtils.defaultKeystorePassphrase; + final String configuredPassphrase = DbProperties.getDbProperties().getProperty("db.cloud.keyStorePassphrase"); + if (configuredPassphrase != null) { + passphrase = configuredPassphrase.toCharArray(); + } + return passphrase; + } + + private boolean createManagementServerKeystore(final String keyStoreFilePath, final char[] passphrase) { + final Certificate managementServerCertificate = issueCertificate(Collections.singletonList(NetUtils.getHostName()), + Collections.singletonList(NetUtils.getDefaultHostIp()), caValidityYears * 365); + if (managementServerCertificate == null || managementServerCertificate.getPrivateKey() == null) { + throw new CloudRuntimeException("Failed to generate certificate and setup management server keystore"); + } + LOG.info("Creating new management server certificate and keystore"); + try { + final KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null, null); + keyStore.setCertificateEntry(caAlias, caCertificate); + keyStore.setKeyEntry(managementAlias, managementServerCertificate.getPrivateKey(), passphrase, + new X509Certificate[]{managementServerCertificate.getClientCertificate(), caCertificate}); + final String tmpFile = KeyStoreUtils.defaultTmpKeyStoreFile; + final FileOutputStream stream = new FileOutputStream(tmpFile); + keyStore.store(stream, passphrase); + stream.close(); + KeyStoreUtils.copyKeystore(keyStoreFilePath, tmpFile); + LOG.debug("Saved default root CA (server) keystore file at:" + keyStoreFilePath); + } catch (final CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException e) { + LOG.error("Failed to save root CA (server) keystore due to exception: ", e); + return false; + } + return true; + } + + private boolean checkManagementServerKeystore() { + final File confFile = PropertiesUtil.findConfigFile("db.properties"); + if (confFile == null) { + return false; + } + final char[] passphrase = findKeyStorePassphrase(); + final String keystorePath = confFile.getParent() + "/" + KeyStoreUtils.defaultKeystoreFile; + final File keystoreFile = new File(keystorePath); + if (keystoreFile.exists()) { + try { + final KeyStore msKeystore = Link.loadKeyStore(new FileInputStream(keystorePath), passphrase); + try { + final java.security.cert.Certificate[] msCertificates = msKeystore.getCertificateChain(managementAlias); + if (msCertificates != null && msCertificates.length > 1) { + msCertificates[0].verify(caKeyPair.getPublic()); + ((X509Certificate)msCertificates[0]).checkValidity(); + return true; + } + } catch (final CertificateException | NoSuchAlgorithmException | InvalidKeyException | NoSuchProviderException | SignatureException e) { + LOG.info("Renewing management server keystore, current certificate has expired"); + return createManagementServerKeystore(keystoreFile.getAbsolutePath(), passphrase); + } + } catch (final GeneralSecurityException | IOException e) { + LOG.error("Failed to read current management server keystore, renewing keystore!"); + } + } + return createManagementServerKeystore(keystoreFile.getAbsolutePath(), passphrase); + } + + ///////////////////////////////////////////////// + /////////////// Root CA Setup /////////////////// + ///////////////////////////////////////////////// + + 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.generateV1Certificate( + caKeyPair, + rootCAIssuerDN.value(), + rootCAIssuerDN.value(), + caValidityYears, + CAManager.CertSignatureAlgorithm.value()); + 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 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 (!checkManagementServerKeystore()) { + LOG.error("Failed to check and configure management server keystore"); + return false; + } + return true; + } + + @Override + public boolean start() { + return loadRootCAKeyPair() && loadRootCAKeyPair() && checkManagementServerKeystore(); + } + + @Override + public boolean configure(String name, Map 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"; + } +} diff --git a/plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java b/plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java new file mode 100644 index 00000000000..cab1941fa96 --- /dev/null +++ b/plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java @@ -0,0 +1,111 @@ +// +// 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.math.BigInteger; +import java.security.KeyPair; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cloudstack.utils.security.CertUtils; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import com.cloud.certificate.CrlVO; +import com.cloud.certificate.dao.CrlDao; + +@RunWith(MockitoJUnitRunner.class) +public class RootCACustomTrustManagerTest { + + @Mock + private CrlDao crlDao; + private KeyPair caKeypair; + private KeyPair clientKeypair; + private X509Certificate caCertificate; + private X509Certificate expiredClientCertificate; + private String clientIp = "1.2.3.4"; + private Map certMap = new HashMap<>(); + + @Before + public void setUp() throws Exception { + certMap.clear(); + caKeypair = CertUtils.generateRandomKeyPair(1024); + clientKeypair = CertUtils.generateRandomKeyPair(1024); + caCertificate = CertUtils.generateV1Certificate(caKeypair, "CN=ca", "CN=ca", 1, + "SHA256withRSA"); + expiredClientCertificate = CertUtils.generateV3Certificate(caCertificate, caKeypair.getPrivate(), clientKeypair.getPublic(), + "CN=cloudstack.apache.org", "SHA256withRSA", 0, Collections.singletonList("cloudstack.apache.org"), Collections.singletonList(clientIp)); + } + + @Test + public void testAuthNotStrict() throws Exception { + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao); + trustManager.checkClientTrusted(null, null); + Assert.assertNull(trustManager.getAcceptedIssuers()); + } + + @Test(expected = CertificateException.class) + public void testAuthStrictWithInvalidCert() throws Exception { + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao); + trustManager.checkClientTrusted(null, null); + } + + @Test(expected = CertificateException.class) + public void testAuthStrictWithRevokedCert() throws Exception { + Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(new CrlVO()); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao); + trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, "RSA"); + } + + @Test(expected = CertificateException.class) + public void testAuthStrictWithInvalidCertOwnership() throws Exception { + Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao); + trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, "RSA"); + } + + @Test(expected = CertificateException.class) + public void testAuthStrictWithDenyExpiredCertAndOwnership() throws Exception { + Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, false, certMap, caCertificate, crlDao); + trustManager.checkClientTrusted(new X509Certificate[]{expiredClientCertificate}, "RSA"); + } + + @Test + public void testAuthStrictWithAllowExpiredCertAndOwnership() throws Exception { + Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao); + Assert.assertTrue(trustManager.getAcceptedIssuers() != null); + Assert.assertTrue(trustManager.getAcceptedIssuers().length == 1); + Assert.assertEquals(trustManager.getAcceptedIssuers()[0], caCertificate); + trustManager.checkClientTrusted(new X509Certificate[]{expiredClientCertificate}, "RSA"); + Assert.assertTrue(certMap.containsKey(clientIp)); + Assert.assertEquals(certMap.get(clientIp), expiredClientCertificate); + } + +} \ No newline at end of file diff --git a/plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCAProviderTest.java b/plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCAProviderTest.java new file mode 100644 index 00000000000..bdd3f08ddfe --- /dev/null +++ b/plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCAProviderTest.java @@ -0,0 +1,155 @@ +// +// 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.lang.reflect.Field; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +import javax.net.ssl.SSLEngine; + +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.utils.security.CertUtils; +import org.apache.cloudstack.utils.security.SSLUtils; +import org.joda.time.DateTime; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class RootCAProviderTest { + + private KeyPair caKeyPair; + private X509Certificate caCertificate; + + private RootCAProvider provider; + + private void addField(final RootCAProvider provider, final String name, final Object o) throws IllegalAccessException, NoSuchFieldException { + Field f = RootCAProvider.class.getDeclaredField(name); + f.setAccessible(true); + f.set(provider, o); + } + + private void overrideDefaultConfigValue(final ConfigKey configKey, final String name, final Object o) throws IllegalAccessException, NoSuchFieldException { + Field f = ConfigKey.class.getDeclaredField(name); + f.setAccessible(true); + f.set(configKey, o); + } + + @Before + public void setUp() throws Exception { + caKeyPair = CertUtils.generateRandomKeyPair(1024); + caCertificate = CertUtils.generateV1Certificate(caKeyPair, "CN=ca", "CN=ca", 1, "SHA256withRSA"); + + provider = new RootCAProvider(); + + addField(provider, "caKeyPair", caKeyPair); + addField(provider, "caCertificate", caCertificate); + addField(provider, "caKeyPair", caKeyPair); + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void testCanProvisionCertificates() { + Assert.assertTrue(provider.canProvisionCertificates()); + } + + @Test + public void testGetCaCertificate() { + Assert.assertTrue(provider.getCaCertificate().size() == 1); + Assert.assertEquals(provider.getCaCertificate().get(0), caCertificate); + } + + @Test + public void testIssueCertificateWithoutCsr() throws NoSuchProviderException, CertificateException, NoSuchAlgorithmException, InvalidKeyException, SignatureException { + final Certificate certificate = provider.issueCertificate(Arrays.asList("domain1.com", "domain2.com"), null, 1); + Assert.assertTrue(certificate != null); + Assert.assertTrue(certificate.getPrivateKey() != null); + Assert.assertEquals(certificate.getCaCertificates().get(0), caCertificate); + Assert.assertEquals(certificate.getClientCertificate().getIssuerDN(), caCertificate.getIssuerDN()); + Assert.assertTrue(certificate.getClientCertificate().getNotAfter().before(new DateTime().plusDays(1).toDate())); + certificate.getClientCertificate().verify(caCertificate.getPublicKey()); + } + + @Test + public void testIssueCertificateWithCsr() throws NoSuchProviderException, CertificateException, NoSuchAlgorithmException, InvalidKeyException, SignatureException { + final String csr = "-----BEGIN NEW CERTIFICATE REQUEST-----\n" + + "MIICxTCCAa0CAQAwUDETMBEGA1UEBhMKY2xvdWRzdGFjazETMBEGA1UEChMKY2xvdWRzdGFjazET\n" + + "MBEGA1UECxMKY2xvdWRzdGFjazEPMA0GA1UEAxMGdi0xLVZNMIIBIjANBgkqhkiG9w0BAQEFAAOC\n" + + "AQ8AMIIBCgKCAQEAhi3hOrt/p0hUmoW2A+2gFAMxSINItRrHfQ6VUnHhYKZGcTN9honVFuu30tz7\n" + + "oSLUUx1laWEWLlIozpUcPSjOuPa5a0JS8kjplMd8DLfLNeQ6gcuEWznMRJqCaKM72qn/FAK3r11l\n" + + "2NofEfWbHU5QVQ5CsYF0JndspLcnmf0tnmreAzz6vlSEPQd4g2hTSsPb72eAqYd0eJnl2oXe7cF3\n" + + "iemg6/lWoxlh8njVFDKJ5ibNQA/RSc5syzzaQ8fn/AkZlChR5pml47elfC3GuqetfZPAEP4rebXV\n" + + "zEw+UVbMo5bWx4AYm1S2HxhmsWC/1J5oxluZDtC6tjMqnkKQze8HbQIDAQABoDAwLgYJKoZIhvcN\n" + + "AQkOMSEwHzAdBgNVHQ4EFgQUdgA1C/7vW3lUcb/dnolGjZB55/AwDQYJKoZIhvcNAQELBQADggEB\n" + + "AH6ynWbyW5o4h2yEvmcr+upmu/LZYkpfwIWIo+dfrHX9OHu0rhHDIgMgqEStWzrOfhAkcEocQo21\n" + + "E4Q39nECO+cgTCQ1nfH5BVqaMEg++n6tqXBwLmAQJkftEmB+YUPFB9OGn5TQY9Pcnof95Y8xnvtR\n" + + "0DvVQa9RM9IsqxgvU4wQCcaNHuEC46Wzo7lyYJ6p//GLw8UQnHxsWktt8U+vyaqXjOvz0+nJobUz\n" + + "Jv7r7DFkOwgS6ObBczaZsv1yx2YklcKfbsI7xVsvZAXFey2RsvSJi1QPEJC5XbwDenWnCSrPfjJg\n" + + "SLJ0p9tV70D6v07r1OOmBtvU5AH4N+vioAZA0BE=\n" + + "-----END NEW CERTIFICATE REQUEST-----\n"; + final Certificate certificate = provider.issueCertificate(csr, Arrays.asList("v-1-VM", "domain1.com", "domain2.com"), null, 1); + Assert.assertTrue(certificate != null); + Assert.assertTrue(certificate.getPrivateKey() == null); + Assert.assertEquals(certificate.getCaCertificates().get(0), caCertificate); + Assert.assertTrue(certificate.getClientCertificate().getSubjectDN().toString().startsWith("CN=v-1-VM,")); + certificate.getClientCertificate().verify(caCertificate.getPublicKey()); + } + + @Test + public void testRevokeCertificate() throws Exception { + Assert.assertTrue(provider.revokeCertificate(CertUtils.generateRandomBigInt(), "anyString")); + } + + @Test + public void testCreateSSLEngineWithoutAuthStrictness() throws Exception { + overrideDefaultConfigValue(RootCAProvider.rootCAAuthStrictness, "_defaultValue", "false"); + final SSLEngine e = provider.createSSLEngine(SSLUtils.getSSLContext(), "/1.2.3.4:5678", null); + Assert.assertFalse(e.getUseClientMode()); + Assert.assertFalse(e.getWantClientAuth()); + } + + @Test + public void testCreateSSLEngineWithAuthStrictness() throws Exception { + overrideDefaultConfigValue(RootCAProvider.rootCAAuthStrictness, "_defaultValue", "true"); + final SSLEngine e = provider.createSSLEngine(SSLUtils.getSSLContext(), "/1.2.3.4:5678", null); + Assert.assertFalse(e.getUseClientMode()); + Assert.assertTrue(e.getWantClientAuth()); + } + + @Test + public void testGetProviderName() throws Exception { + Assert.assertEquals(provider.getProviderName(), "root"); + } + +} \ No newline at end of file diff --git a/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManager.java b/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManager.java index 0851023a7ef..3a315509b49 100644 --- a/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManager.java +++ b/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManager.java @@ -20,6 +20,9 @@ import java.util.Map; import javax.naming.ConfigurationException; +import org.apache.cloudstack.ca.SetupCertificateCommand; +import org.apache.cloudstack.ca.SetupKeyStoreCommand; + import com.cloud.agent.api.Answer; import com.cloud.agent.api.CheckHealthCommand; import com.cloud.agent.api.CheckNetworkCommand; @@ -52,6 +55,10 @@ public interface MockAgentManager extends Manager { Answer pingTest(PingTestCommand cmd); + Answer setupKeyStore(SetupKeyStoreCommand cmd); + + Answer setupCertificate(SetupCertificateCommand cmd); + MockHost getHost(String guid); Answer maintain(MaintainCommand cmd); diff --git a/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManagerImpl.java b/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManagerImpl.java index 9211214ccdc..9d1e4079d42 100644 --- a/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManagerImpl.java +++ b/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManagerImpl.java @@ -31,7 +31,10 @@ import java.util.regex.PatternSyntaxException; import javax.inject.Inject; import javax.naming.ConfigurationException; -import com.cloud.user.AccountManager; +import org.apache.cloudstack.ca.SetupCertificateAnswer; +import org.apache.cloudstack.ca.SetupCertificateCommand; +import org.apache.cloudstack.ca.SetupKeyStoreCommand; +import org.apache.cloudstack.ca.SetupKeystoreAnswer; import org.apache.cloudstack.context.CallContext; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; @@ -62,6 +65,7 @@ import com.cloud.simulator.MockHostVO; import com.cloud.simulator.MockVMVO; import com.cloud.simulator.dao.MockHostDao; import com.cloud.simulator.dao.MockVMDao; +import com.cloud.user.AccountManager; import com.cloud.utils.Pair; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.concurrency.NamedThreadFactory; @@ -459,6 +463,24 @@ public class MockAgentManagerImpl extends ManagerBase implements MockAgentManage return new Answer(cmd); } + @Override + public Answer setupKeyStore(SetupKeyStoreCommand cmd) { + return new SetupKeystoreAnswer( + "-----BEGIN CERTIFICATE REQUEST-----\n" + + "MIIBHjCByQIBADBkMQswCQYDVQQGEwJJTjELMAkGA1UECAwCSFIxETAPBgNVBAcM\n" + + "CEd1cnVncmFtMQ8wDQYDVQQKDAZBcGFjaGUxEzARBgNVBAsMCkNsb3VkU3RhY2sx\n" + + "DzANBgNVBAMMBnYtMS1WTTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD46KFWKYrJ\n" + + "F43Y1oqWUfrl4mj4Qm05Bgsi6nuigZv7ufiAKK0nO4iJKdRa2hFMUvBi2/bU3IyY\n" + + "Nvg7cdJsn4K9AgMBAAGgADANBgkqhkiG9w0BAQUFAANBAIta9glu/ZSjA/ncyXix\n" + + "yDOyAKmXXxsRIsdrEuIzakUuJS7C8IG0FjUbDyIaiwWQa5x+Lt4oMqCmpNqRzaGP\n" + + "fOo=\n" + "-----END CERTIFICATE REQUEST-----"); + } + + @Override + public Answer setupCertificate(SetupCertificateCommand cmd) { + return new SetupCertificateAnswer(true); + } + @Override public boolean start() { for (Discoverer discoverer : discoverers) { diff --git a/plugins/hypervisors/simulator/src/com/cloud/agent/manager/SimulatorManagerImpl.java b/plugins/hypervisors/simulator/src/com/cloud/agent/manager/SimulatorManagerImpl.java index 03593392ba7..b20bd3d8034 100644 --- a/plugins/hypervisors/simulator/src/com/cloud/agent/manager/SimulatorManagerImpl.java +++ b/plugins/hypervisors/simulator/src/com/cloud/agent/manager/SimulatorManagerImpl.java @@ -26,6 +26,8 @@ import java.util.Map; import javax.inject.Inject; import javax.naming.ConfigurationException; +import org.apache.cloudstack.ca.SetupCertificateCommand; +import org.apache.cloudstack.ca.SetupKeyStoreCommand; import org.apache.cloudstack.storage.command.DeleteCommand; import org.apache.cloudstack.storage.command.DownloadCommand; import org.apache.cloudstack.storage.command.DownloadProgressCommand; @@ -280,6 +282,10 @@ public class SimulatorManagerImpl extends ManagerBase implements SimulatorManage answer = _mockAgentMgr.checkHealth((CheckHealthCommand)cmd); } else if (cmd instanceof PingTestCommand) { answer = _mockAgentMgr.pingTest((PingTestCommand)cmd); + } else if (cmd instanceof SetupKeyStoreCommand) { + answer = _mockAgentMgr.setupKeyStore((SetupKeyStoreCommand)cmd); + } else if (cmd instanceof SetupCertificateCommand) { + answer = _mockAgentMgr.setupCertificate((SetupCertificateCommand)cmd); } else if (cmd instanceof PrepareForMigrationCommand) { answer = _mockVmMgr.prepareForMigrate((PrepareForMigrationCommand)cmd); } else if (cmd instanceof MigrateCommand) { diff --git a/plugins/pom.xml b/plugins/pom.xml index 4045349d715..bcc7240d02f 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -53,6 +53,7 @@ acl/dynamic-role-based affinity-group-processors/host-anti-affinity affinity-group-processors/explicit-dedication + ca/root-ca deployment-planners/user-concentrated-pod deployment-planners/user-dispersing deployment-planners/implicit-dedication diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java index e2d1b888460..d280ed5adaf 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java @@ -16,13 +16,34 @@ // under the License. package org.apache.cloudstack.saml; -import com.cloud.domain.Domain; -import com.cloud.user.DomainManager; -import com.cloud.user.User; -import com.cloud.user.UserVO; -import com.cloud.user.dao.UserDao; -import com.cloud.utils.PropertiesUtil; -import com.cloud.utils.component.AdapterBase; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutput; +import java.io.ObjectOutputStream; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +import javax.inject.Inject; +import javax.xml.stream.FactoryConfigurationError; + import org.apache.cloudstack.api.command.AuthorizeSAMLSSOCmd; import org.apache.cloudstack.api.command.GetServiceProviderMetaDataCmd; import org.apache.cloudstack.api.command.ListAndSwitchSAMLAccountCmd; @@ -34,9 +55,11 @@ import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.security.keystore.KeystoreDao; import org.apache.cloudstack.framework.security.keystore.KeystoreVO; +import org.apache.cloudstack.utils.security.CertUtils; import org.apache.commons.codec.binary.Base64; import org.apache.commons.httpclient.HttpClient; import org.apache.log4j.Logger; +import org.bouncycastle.operator.OperatorCreationException; import org.opensaml.DefaultBootstrap; import org.opensaml.common.xml.SAMLConstants; import org.opensaml.saml2.metadata.ContactPerson; @@ -61,32 +84,13 @@ import org.opensaml.xml.security.credential.UsageType; import org.opensaml.xml.security.keyinfo.KeyInfoHelper; import org.springframework.stereotype.Component; -import javax.inject.Inject; -import javax.xml.stream.FactoryConfigurationError; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutput; -import java.io.ObjectOutputStream; -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.SignatureException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Timer; -import java.util.TimerTask; +import com.cloud.domain.Domain; +import com.cloud.user.DomainManager; +import com.cloud.user.User; +import com.cloud.user.UserVO; +import com.cloud.user.dao.UserDao; +import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.component.AdapterBase; @Component public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManager, Configurable { @@ -141,12 +145,14 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage KeystoreVO keyStoreVO = _ksDao.findByName(SAMLPluginConstants.SAMLSP_KEYPAIR); if (keyStoreVO == null) { try { - KeyPair keyPair = SAMLUtils.generateRandomKeyPair(); - _ksDao.save(SAMLPluginConstants.SAMLSP_KEYPAIR, SAMLUtils.savePrivateKey(keyPair.getPrivate()), SAMLUtils.savePublicKey(keyPair.getPublic()), "samlsp-keypair"); + KeyPair keyPair = CertUtils.generateRandomKeyPair(4096); + _ksDao.save(SAMLPluginConstants.SAMLSP_KEYPAIR, + CertUtils.privateKeyToPem(keyPair.getPrivate()), + CertUtils.publicKeyToPem(keyPair.getPublic()), "samlsp-keypair"); keyStoreVO = _ksDao.findByName(SAMLPluginConstants.SAMLSP_KEYPAIR); s_logger.info("No SAML keystore found, created and saved a new Service Provider keypair"); - } catch (NoSuchProviderException | NoSuchAlgorithmException e) { - s_logger.error("Unable to create and save SAML keypair: " + e.toString()); + } catch (final NoSuchProviderException | NoSuchAlgorithmException | IOException e) { + s_logger.error("Unable to create and save SAML keypair, due to: ", e); } } @@ -160,8 +166,19 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage KeyPair spKeyPair = null; X509Certificate spX509Key = null; if (keyStoreVO != null) { - PrivateKey privateKey = SAMLUtils.loadPrivateKey(keyStoreVO.getCertificate()); - PublicKey publicKey = SAMLUtils.loadPublicKey(keyStoreVO.getKey()); + + PrivateKey privateKey = null; + try { + privateKey = CertUtils.pemToPrivateKey(keyStoreVO.getCertificate()); + } catch (final InvalidKeySpecException | IOException e) { + s_logger.error("Failed to read private key, due to error: ", e); + } + PublicKey publicKey = null; + try { + publicKey = CertUtils.pemToPublicKey(keyStoreVO.getKey()); + } catch (final InvalidKeySpecException | IOException e) { + s_logger.error("Failed to read public key, due to error: ", e); + } if (privateKey != null && publicKey != null) { spKeyPair = new KeyPair(publicKey, privateKey); KeystoreVO x509VO = _ksDao.findByName(SAMLPluginConstants.SAMLSP_X509CERT); @@ -174,8 +191,8 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage out.flush(); _ksDao.save(SAMLPluginConstants.SAMLSP_X509CERT, Base64.encodeBase64String(bos.toByteArray()), "", "samlsp-x509cert"); bos.close(); - } catch (NoSuchAlgorithmException | NoSuchProviderException | CertificateEncodingException | SignatureException | InvalidKeyException | IOException e) { - s_logger.error("SAML Plugin won't be able to use X509 signed authentication"); + } catch (final NoSuchAlgorithmException | NoSuchProviderException | CertificateException | SignatureException | InvalidKeyException | IOException | OperatorCreationException e) { + s_logger.error("SAML plugin won't be able to use X509 signed authentication", e); } } else { try { diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java index ec6b2c11e5e..364ef86103f 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java @@ -19,14 +19,41 @@ package org.apache.cloudstack.saml; -import com.cloud.utils.HttpUtils; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.FactoryConfigurationError; + import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.response.LoginCmdResponse; +import org.apache.cloudstack.utils.security.CertUtils; import org.apache.log4j.Logger; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.x509.X509V1CertificateGenerator; +import org.bouncycastle.operator.OperatorCreationException; import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; import org.opensaml.Configuration; import org.opensaml.DefaultBootstrap; import org.opensaml.common.SAMLVersion; @@ -63,41 +90,7 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.SAXException; -import javax.security.auth.x500.X500Principal; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletResponse; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.stream.FactoryConfigurationError; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.StringWriter; -import java.io.UnsupportedEncodingException; -import java.math.BigInteger; -import java.net.URLEncoder; -import java.nio.charset.Charset; -import java.security.InvalidKeyException; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.SecureRandom; -import java.security.Security; -import java.security.Signature; -import java.security.SignatureException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.List; -import java.util.zip.Deflater; -import java.util.zip.DeflaterOutputStream; +import com.cloud.utils.HttpUtils; public class SAMLUtils { public static final Logger s_logger = Logger.getLogger(SAMLUtils.class); @@ -271,89 +264,10 @@ public class SAMLUtils { return url; } - public static KeyFactory getKeyFactory() { - KeyFactory keyFactory = null; - try { - Security.addProvider(new BouncyCastleProvider()); - keyFactory = KeyFactory.getInstance("RSA", "BC"); - } catch (NoSuchAlgorithmException | NoSuchProviderException e) { - s_logger.error("Unable to create KeyFactory:" + e.getMessage()); - } - return keyFactory; - } - - public static String savePublicKey(PublicKey key) { - try { - KeyFactory keyFactory = SAMLUtils.getKeyFactory(); - if (keyFactory == null) return null; - X509EncodedKeySpec spec = keyFactory.getKeySpec(key, X509EncodedKeySpec.class); - return new String(org.bouncycastle.util.encoders.Base64.encode(spec.getEncoded()), Charset.forName("UTF-8")); - } catch (InvalidKeySpecException e) { - s_logger.error("Unable to create KeyFactory:" + e.getMessage()); - } - return null; - } - - public static String savePrivateKey(PrivateKey key) { - try { - KeyFactory keyFactory = SAMLUtils.getKeyFactory(); - if (keyFactory == null) return null; - PKCS8EncodedKeySpec spec = keyFactory.getKeySpec(key, - PKCS8EncodedKeySpec.class); - return new String(org.bouncycastle.util.encoders.Base64.encode(spec.getEncoded()), Charset.forName("UTF-8")); - } catch (InvalidKeySpecException e) { - s_logger.error("Unable to create KeyFactory:" + e.getMessage()); - } - return null; - } - - public static PublicKey loadPublicKey(String publicKey) { - byte[] sigBytes = org.bouncycastle.util.encoders.Base64.decode(publicKey); - X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(sigBytes); - KeyFactory keyFact = SAMLUtils.getKeyFactory(); - if (keyFact == null) - return null; - try { - return keyFact.generatePublic(x509KeySpec); - } catch (InvalidKeySpecException e) { - s_logger.error("Unable to create PrivateKey from privateKey string:" + e.getMessage()); - } - return null; - } - - public static PrivateKey loadPrivateKey(String privateKey) { - byte[] sigBytes = org.bouncycastle.util.encoders.Base64.decode(privateKey); - PKCS8EncodedKeySpec pkscs8KeySpec = new PKCS8EncodedKeySpec(sigBytes); - KeyFactory keyFact = SAMLUtils.getKeyFactory(); - if (keyFact == null) - return null; - try { - return keyFact.generatePrivate(pkscs8KeySpec); - } catch (InvalidKeySpecException e) { - s_logger.error("Unable to create PrivateKey from privateKey string:" + e.getMessage()); - } - return null; - } - - public static KeyPair generateRandomKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException { - Security.addProvider(new BouncyCastleProvider()); - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); - keyPairGenerator.initialize(4096, new SecureRandom()); - return keyPairGenerator.generateKeyPair(); - } - - public static X509Certificate generateRandomX509Certificate(KeyPair keyPair) throws NoSuchAlgorithmException, NoSuchProviderException, CertificateEncodingException, SignatureException, InvalidKeyException { - DateTime now = DateTime.now(DateTimeZone.UTC); - X500Principal dnName = new X500Principal("CN=ApacheCloudStack"); - X509V1CertificateGenerator certGen = new X509V1CertificateGenerator(); - certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); - certGen.setSubjectDN(dnName); - certGen.setIssuerDN(dnName); - certGen.setNotBefore(now.minusDays(1).toDate()); - certGen.setNotAfter(now.plusYears(3).toDate()); - certGen.setPublicKey(keyPair.getPublic()); - certGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); - return certGen.generate(keyPair.getPrivate(), "BC"); + public static X509Certificate generateRandomX509Certificate(KeyPair keyPair) throws NoSuchAlgorithmException, NoSuchProviderException, CertificateException, SignatureException, InvalidKeyException, OperatorCreationException { + return CertUtils.generateV1Certificate(keyPair, + "CN=ApacheCloudStack", "CN=ApacheCloudStack", + 3, "SHA256WithRSA"); } public static void setupSamlUserCookies(final LoginCmdResponse loginResponse, final HttpServletResponse resp) throws IOException { diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java index 86009ac4e5c..3df0fccadfa 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java @@ -19,13 +19,22 @@ package org.apache.cloudstack; -import com.cloud.utils.HttpUtils; +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.security.KeyPair; +import java.security.cert.X509Certificate; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + import org.apache.cloudstack.api.ApiServerService; import org.apache.cloudstack.api.auth.APIAuthenticationType; import org.apache.cloudstack.api.command.GetServiceProviderMetaDataCmd; import org.apache.cloudstack.saml.SAML2AuthManager; import org.apache.cloudstack.saml.SAMLProviderMetadata; import org.apache.cloudstack.saml.SAMLUtils; +import org.apache.cloudstack.utils.security.CertUtils; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -33,20 +42,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import java.lang.reflect.Field; -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.SignatureException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateParsingException; -import java.security.cert.X509Certificate; -import java.net.InetAddress; -import java.net.UnknownHostException; +import com.cloud.utils.HttpUtils; @RunWith(MockitoJUnitRunner.class) public class GetServiceProviderMetaDataCmdTest { @@ -67,7 +63,7 @@ public class GetServiceProviderMetaDataCmdTest { HttpServletRequest req; @Test - public void testAuthenticate() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, CertificateParsingException, CertificateEncodingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException, UnknownHostException { + public void testAuthenticate() throws Exception { GetServiceProviderMetaDataCmd cmd = new GetServiceProviderMetaDataCmd(); Field apiServerField = GetServiceProviderMetaDataCmd.class.getDeclaredField("_apiServer"); @@ -80,7 +76,7 @@ public class GetServiceProviderMetaDataCmdTest { String spId = "someSPID"; String url = "someUrl"; - KeyPair kp = SAMLUtils.generateRandomKeyPair(); + KeyPair kp = CertUtils.generateRandomKeyPair(4096); X509Certificate cert = SAMLUtils.generateRandomX509Certificate(kp); SAMLProviderMetadata providerMetadata = new SAMLProviderMetadata(); diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java index bd87831913c..4986d7a2b31 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java @@ -19,15 +19,17 @@ package org.apache.cloudstack; -import junit.framework.TestCase; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; + import org.apache.cloudstack.saml.SAMLUtils; +import org.apache.cloudstack.utils.security.CertUtils; import org.junit.Test; import org.opensaml.saml2.core.AuthnRequest; import org.opensaml.saml2.core.LogoutRequest; -import java.security.KeyPair; -import java.security.PrivateKey; -import java.security.PublicKey; +import junit.framework.TestCase; public class SAMLUtilsTest extends TestCase { @@ -60,13 +62,13 @@ public class SAMLUtilsTest extends TestCase { @Test public void testX509Helpers() throws Exception { - KeyPair keyPair = SAMLUtils.generateRandomKeyPair(); + KeyPair keyPair = CertUtils.generateRandomKeyPair(4096); - String privateKeyString = SAMLUtils.savePrivateKey(keyPair.getPrivate()); - String publicKeyString = SAMLUtils.savePublicKey(keyPair.getPublic()); + String privateKeyString = CertUtils.privateKeyToPem(keyPair.getPrivate()); + String publicKeyString = CertUtils.publicKeyToPem(keyPair.getPublic()); - PrivateKey privateKey = SAMLUtils.loadPrivateKey(privateKeyString); - PublicKey publicKey = SAMLUtils.loadPublicKey(publicKeyString); + PrivateKey privateKey = CertUtils.pemToPrivateKey(privateKeyString); + PublicKey publicKey = CertUtils.pemToPublicKey(publicKeyString); assertTrue(privateKey.equals(keyPair.getPrivate())); assertTrue(publicKey.equals(keyPair.getPublic())); diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java index 36140f2bedd..2ce88414b40 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java @@ -21,12 +21,16 @@ package org.apache.cloudstack.api.command; import static org.junit.Assert.assertFalse; -import com.cloud.domain.Domain; -import com.cloud.user.AccountService; -import com.cloud.user.DomainManager; -import com.cloud.user.UserAccountVO; -import com.cloud.user.dao.UserAccountDao; -import com.cloud.utils.HttpUtils; +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; import org.apache.cloudstack.api.ApiServerService; import org.apache.cloudstack.api.BaseCmd; @@ -36,6 +40,7 @@ import org.apache.cloudstack.saml.SAML2AuthManager; import org.apache.cloudstack.saml.SAMLPluginConstants; import org.apache.cloudstack.saml.SAMLProviderMetadata; import org.apache.cloudstack.saml.SAMLUtils; +import org.apache.cloudstack.utils.security.CertUtils; import org.joda.time.DateTime; import org.junit.Assert; import org.junit.Test; @@ -64,16 +69,12 @@ import org.opensaml.saml2.core.impl.StatusBuilder; import org.opensaml.saml2.core.impl.StatusCodeBuilder; import org.opensaml.saml2.core.impl.SubjectBuilder; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - -import java.lang.reflect.Field; -import java.security.KeyPair; -import java.security.cert.X509Certificate; -import java.util.HashMap; -import java.util.Map; -import java.net.InetAddress; +import com.cloud.domain.Domain; +import com.cloud.user.AccountService; +import com.cloud.user.DomainManager; +import com.cloud.user.UserAccountVO; +import com.cloud.user.dao.UserAccountDao; +import com.cloud.utils.HttpUtils; @RunWith(MockitoJUnitRunner.class) public class SAML2LoginAPIAuthenticatorCmdTest { @@ -158,7 +159,7 @@ public class SAML2LoginAPIAuthenticatorCmdTest { userAccountDaoField.setAccessible(true); userAccountDaoField.set(cmd, userAccountDao); - KeyPair kp = SAMLUtils.generateRandomKeyPair(); + KeyPair kp = CertUtils.generateRandomKeyPair(4096); X509Certificate cert = SAMLUtils.generateRandomX509Certificate(kp); SAMLProviderMetadata providerMetadata = new SAMLProviderMetadata(); diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java index cbfcc55c540..09391c5d7d2 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java @@ -19,11 +19,19 @@ package org.apache.cloudstack.api.command; -import com.cloud.utils.HttpUtils; +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.security.cert.X509Certificate; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + import org.apache.cloudstack.api.ApiServerService; import org.apache.cloudstack.api.auth.APIAuthenticationType; import org.apache.cloudstack.saml.SAML2AuthManager; import org.apache.cloudstack.saml.SAMLUtils; +import org.apache.cloudstack.utils.security.CertUtils; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -31,12 +39,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import java.lang.reflect.Field; -import java.security.cert.X509Certificate; -import java.net.InetAddress; +import com.cloud.utils.HttpUtils; @RunWith(MockitoJUnitRunner.class) public class SAML2LogoutAPIAuthenticatorCmdTest { @@ -70,7 +73,7 @@ public class SAML2LogoutAPIAuthenticatorCmdTest { String spId = "someSPID"; String url = "someUrl"; - X509Certificate cert = SAMLUtils.generateRandomX509Certificate(SAMLUtils.generateRandomKeyPair()); + X509Certificate cert = SAMLUtils.generateRandomX509Certificate(CertUtils.generateRandomKeyPair(4096)); Mockito.when(session.getAttribute(Mockito.anyString())).thenReturn(null); cmd.authenticate("command", null, session, InetAddress.getByName("127.0.0.1"), HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp); diff --git a/pom.xml b/pom.xml index edfd649d759..2ff63e6f455 100644 --- a/pom.xml +++ b/pom.xml @@ -64,7 +64,7 @@ 4.12 1.3 1.12.0 - 1.55 + 1.57 0.1.54 2.1.1 1.9.2 @@ -225,6 +225,11 @@ bcprov-jdk15on ${cs.bcprov.version} + + org.bouncycastle + bcpkix-jdk15on + ${cs.bcprov.version} + org.apache.xmlgraphics batik-css diff --git a/scripts/common/keys/ssl-keys.py b/scripts/common/keys/ssl-keys.py deleted file mode 100644 index d6804cca19d..00000000000 --- a/scripts/common/keys/ssl-keys.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash -# 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. - - -# Copies keys that enable SSH communication with system vms -# $1 = new public key -# $2 = new private key -''' -All imports go here... -''' -from subprocess import call -import socket -import sys -import os -import subprocess -import traceback - -def generateSSLKey(outputPath): - logf = open("ssl-keys.log", "w") - hostName = socket.gethostbyname(socket.gethostname()) - keyFile = outputPath + os.sep + "cloudmanagementserver.keystore" - logf.write("HostName = %s\n" % hostName) - logf.write("OutputPath = %s\n" % keyFile) - dname='cn="Cloudstack User",ou="' + hostName + '",o="' + hostName + '",c="Unknown"'; - logf.write("dname = %s\n" % dname) - logf.flush() - try : - return_code = subprocess.Popen(["keytool", "-genkey", "-keystore", keyFile, "-storepass", "vmops.com", "-keypass", "vmops.com", "-keyalg", "RSA", "-validity", "3650", "-dname", dname],shell=True,stdout=logf, stderr=logf) - return_code.wait() - except OSError as e: - logf.flush() - traceback.print_exc(file=logf) - logf.flush() - logf.write("SSL key generated is : %s" % return_code) - logf.flush() - -argsSize=len(sys.argv) -if argsSize != 2: - print("Usage: ssl-keys.py ") - sys.exit(None) -sslKeyPath=sys.argv[1] - -generateSSLKey(sslKeyPath) \ No newline at end of file diff --git a/scripts/installer/windows/acs.wxs b/scripts/installer/windows/acs.wxs index fa8ff41a8a2..6f2aec07d97 100644 --- a/scripts/installer/windows/acs.wxs +++ b/scripts/installer/windows/acs.wxs @@ -255,9 +255,6 @@ - - - - diff --git a/scripts/network/domr/router_proxy.sh b/scripts/network/domr/router_proxy.sh index f9cb7ca0157..945ca968d26 100755 --- a/scripts/network/domr/router_proxy.sh +++ b/scripts/network/domr/router_proxy.sh @@ -35,13 +35,11 @@ check_gw() { cert="/root/.ssh/id_rsa.cloud" -script=$1 -shift - -domRIp=$1 -shift +script="$1" +domRIp="$2" check_gw "$domRIp" -ssh -p 3922 -q -o StrictHostKeyChecking=no -i $cert root@$domRIp "/opt/cloud/bin/$script $*" +ssh -p 3922 -q -o StrictHostKeyChecking=no -i $cert root@$domRIp "/opt/cloud/bin/$script ${@:3}" + exit $? diff --git a/scripts/util/keystore-cert-import b/scripts/util/keystore-cert-import new file mode 100755 index 00000000000..bb03b6f68b4 --- /dev/null +++ b/scripts/util/keystore-cert-import @@ -0,0 +1,100 @@ +#!/bin/bash +# 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. + +PROPS_FILE="$1" +KS_FILE="$2" +MODE="$3" +CERT_FILE="$4" +CERT=$(echo "$5" | tr '^' '\n' | tr '~' ' ') +CACERT_FILE="$6" +CACERT=$(echo "$7" | tr '^' '\n' | tr '~' ' ') +PRIVKEY_FILE="$8" +PRIVKEY=$(echo "$9" | tr '^' '\n' | tr '~' ' ') + +ALIAS="cloud" +SYSTEM_FILE="/var/cache/cloud/cmdline" + +# Find keystore password +KS_PASS=$(sed -n '/keystore.passphrase/p' "$PROPS_FILE" 2>/dev/null | sed 's/keystore.passphrase=//g' 2>/dev/null) + +if [ -z "${KS_PASS// }" ]; then + echo "Failed to find keystore passphrase from file: $PROPS_FILE, quiting!" + exit 1 +fi + +# Use a new keystore file +NEW_KS_FILE="$KS_FILE.new" + +# Import certificate +if [ ! -z "${CERT// }" ]; then + echo "$CERT" > "$CERT_FILE" +fi + +# Import ca certs +if [ ! -z "${CACERT// }" ]; then + echo "$CACERT" > "$CACERT_FILE" +fi + +# Import cacerts into the keystore +awk '/-----BEGIN CERTIFICATE-----?/{n++}{print > "cloudca." n }' "$CACERT_FILE" +for caChain in $(ls cloudca.*); do + keytool -delete -noprompt -alias "$caChain" -keystore "$NEW_KS_FILE" -storepass "$KS_PASS" > /dev/null 2>&1 || true + keytool -import -noprompt -storepass "$KS_PASS" -trustcacerts -alias "$caChain" -file "$caChain" -keystore "$NEW_KS_FILE" > /dev/null 2>&1 +done +rm -f cloudca.* + +# Import private key if available +if [ ! -z "${PRIVKEY// }" ]; then + echo "$PRIVKEY" > "$PRIVKEY_FILE" + # Re-initialize keystore when private key is provided + keytool -delete -noprompt -alias "$ALIAS" -keystore "$NEW_KS_FILE" -storepass "$KS_PASS" 2>/dev/null || true + openssl pkcs12 -export -name "$ALIAS" -in "$CERT_FILE" -inkey "$PRIVKEY_FILE" -out "$NEW_KS_FILE.p12" -password pass:"$KS_PASS" > /dev/null 2>&1 + keytool -importkeystore -srckeystore "$NEW_KS_FILE.p12" -destkeystore "$NEW_KS_FILE" -srcstoretype PKCS12 -alias "$ALIAS" -deststorepass "$KS_PASS" -destkeypass "$KS_PASS" -srcstorepass "$KS_PASS" -srckeypass "$KS_PASS" > /dev/null 2>&1 +else + # Import certificate into the keystore + keytool -import -storepass "$KS_PASS" -alias "$ALIAS" -file "$CERT_FILE" -keystore "$NEW_KS_FILE" > /dev/null 2>&1 || true + # Export private key from keystore + rm -f "$PRIVKEY_FILE" + keytool -importkeystore -srckeystore "$NEW_KS_FILE" -destkeystore "$NEW_KS_FILE.p12" -deststoretype PKCS12 -srcalias "$ALIAS" -deststorepass "$KS_PASS" -destkeypass "$KS_PASS" -srcstorepass "$KS_PASS" -srckeypass "$KS_PASS" > /dev/null 2>&1 + openssl pkcs12 -in "$NEW_KS_FILE.p12" -nodes -nocerts -nomac -password pass:"$KS_PASS" 2>/dev/null | openssl rsa -out "$PRIVKEY_FILE" > /dev/null 2>&1 +fi + +# Commit the new keystore +rm -f "$NEW_KS_FILE.p12" +mv -f "$NEW_KS_FILE" "$KS_FILE" + +# Update ca-certs if we're in systemvm +if [ -f "$SYSTEM_FILE" ]; then + mkdir -p /usr/local/share/ca-certificates/cloudstack + cp "$CACERT_FILE" /usr/local/share/ca-certificates/cloudstack/ca.crt + chmod 755 /usr/local/share/ca-certificates/cloudstack + chmod 644 /usr/local/share/ca-certificates/cloudstack/ca.crt + update-ca-certificates > /dev/null 2>&1 || true +fi + +# Restart cloud service if we're in systemvm +if [ "$MODE" == "ssh" ] && [ -f $SYSTEM_FILE ]; then + /etc/init.d/cloud stop > /dev/null 2>&1 + sleep 2 + /etc/init.d/cloud start > /dev/null 2>&1 +fi + +# Fix file permission +chmod 600 $CACERT_FILE +chmod 600 $CERT_FILE +chmod 600 $PRIVKEY_FILE diff --git a/scripts/util/keystore-setup b/scripts/util/keystore-setup new file mode 100755 index 00000000000..28ce61c846a --- /dev/null +++ b/scripts/util/keystore-setup @@ -0,0 +1,51 @@ +#!/bin/bash +# 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. + +PROPS_FILE="$1" +KS_FILE="$2.new" +KS_PASS="$3" +KS_VALIDITY="$4" +CSR_FILE="$5" + +ALIAS="cloud" + +# Re-use existing password or use the one provided +if [ -f "$PROPS_FILE" ]; then + OLD_PASS=$(sed -n '/keystore.passphrase/p' "$PROPS_FILE" 2>/dev/null | sed 's/keystore.passphrase=//g' 2>/dev/null) + if [ ! -z "${OLD_PASS// }" ]; then + KS_PASS="$OLD_PASS" + else + sed -i "/keystore.passphrase.*/d" $PROPS_FILE 2> /dev/null || true + echo "keystore.passphrase=$KS_PASS" >> $PROPS_FILE + fi +fi + +# Generate keystore +rm -f "$KS_FILE" +CN=$(hostname --fqdn) +keytool -genkey -storepass "$KS_PASS" -keypass "$KS_PASS" -alias "$ALIAS" -keyalg RSA -validity "$KS_VALIDITY" -dname cn="$CN",ou="cloudstack",o="cloudstack",c="cloudstack" -keystore "$KS_FILE" + +# Generate CSR +rm -f "$CSR_FILE" +keytool -certreq -storepass "$KS_PASS" -alias "$ALIAS" -file $CSR_FILE -keystore "$KS_FILE" +cat "$CSR_FILE" + +# Fix file permissions +chmod 600 $KS_FILE +chmod 600 $PROPS_FILE +chmod 600 $CSR_FILE diff --git a/server/pom.xml b/server/pom.xml index c49b614c1a0..4067b8590a0 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -52,6 +52,11 @@ httpcore ${cs.httpcore.version} + + org.apache.cloudstack + cloud-framework-ca + ${project.version} + org.apache.cloudstack cloud-framework-jobs diff --git a/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index 56da591cdea..8a8d6452c23 100644 --- a/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -87,8 +87,10 @@ + + @@ -277,4 +279,10 @@ + + + + + + diff --git a/server/src/com/cloud/alert/AlertManagerImpl.java b/server/src/com/cloud/alert/AlertManagerImpl.java index c751c6adb35..0232843050d 100644 --- a/server/src/com/cloud/alert/AlertManagerImpl.java +++ b/server/src/com/cloud/alert/AlertManagerImpl.java @@ -759,7 +759,8 @@ public class AlertManagerImpl extends ManagerBase implements AlertManager, Confi (alertType != AlertManager.AlertType.ALERT_TYPE_MANAGMENT_NODE) && (alertType != AlertManager.AlertType.ALERT_TYPE_RESOURCE_LIMIT_EXCEEDED) && (alertType != AlertManager.AlertType.ALERT_TYPE_UPLOAD_FAILED) && - (alertType != AlertManager.AlertType.ALERT_TYPE_OOBM_AUTH_ERROR)) { + (alertType != AlertManager.AlertType.ALERT_TYPE_OOBM_AUTH_ERROR) && + (alertType != AlertManager.AlertType.ALERT_TYPE_CA_CERT)) { alert = _alertDao.getLastAlert(alertType.getType(), dataCenterId, podId, clusterId); } diff --git a/server/src/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java index 1817ade2874..cee07455d3a 100644 --- a/server/src/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -110,6 +110,7 @@ import com.cloud.user.AccountManager; import com.cloud.utils.DateUtil; import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; +import com.cloud.utils.StringUtils; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.db.DB; import com.cloud.utils.db.GlobalLock; @@ -1354,7 +1355,7 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy StringBuilder buf = profile.getBootArgsBuilder(); buf.append(" template=domP type=consoleproxy"); - buf.append(" host=").append(ApiServiceConfiguration.ManagementHostIPAdr.value()); + buf.append(" host=").append(StringUtils.shuffleCSVList(ApiServiceConfiguration.ManagementHostIPAdr.value())); buf.append(" port=").append(_mgmtPort); buf.append(" name=").append(profile.getVirtualMachine().getHostName()); if (_sslEnabled) { diff --git a/server/src/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java b/server/src/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java index 13a1a64cd21..ac5b48aa2b5 100644 --- a/server/src/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java +++ b/server/src/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java @@ -16,6 +16,23 @@ // under the License. package com.cloud.hypervisor.kvm.discoverer; +import java.net.InetAddress; +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.ca.CAManager; +import org.apache.cloudstack.ca.SetupCertificateCommand; +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.utils.security.KeyStoreUtils; +import org.apache.log4j.Logger; + import com.cloud.agent.AgentManager; import com.cloud.agent.Listener; import com.cloud.agent.api.AgentControlAnswer; @@ -42,17 +59,10 @@ import com.cloud.resource.DiscovererBase; import com.cloud.resource.ResourceStateAdapter; import com.cloud.resource.ServerResource; import com.cloud.resource.UnableDeleteHostException; +import com.cloud.utils.PasswordGenerator; +import com.cloud.utils.StringUtils; import com.cloud.utils.ssh.SSHCmdHelper; -import org.apache.log4j.Logger; - -import javax.inject.Inject; -import javax.naming.ConfigurationException; -import java.net.InetAddress; -import java.net.URI; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import com.trilead.ssh2.Connection; public abstract class LibvirtServerDiscoverer extends DiscovererBase implements Discoverer, Listener, ResourceStateAdapter { private static final Logger s_logger = Logger.getLogger(LibvirtServerDiscoverer.class); @@ -62,7 +72,9 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements private String _kvmPublicNic; private String _kvmGuestNic; @Inject - AgentManager _agentMgr; + private AgentManager agentMgr; + @Inject + private CAManager caManager; @Override public abstract Hypervisor.HypervisorType getHypervisorType(); @@ -125,6 +137,73 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements return false; } + private void setupAgentSecurity(final Connection sshConnection, final String agentIp, final String agentHostname) { + if (!caManager.canProvisionCertificates()) { + s_logger.warn("Cannot secure agent communication because configure CA plugin cannot provision client certificate"); + return; + } + + if (sshConnection == null) { + s_logger.warn("Cannot secure agent communication because ssh connection is invalid for host ip=" + agentIp); + return; + } + + Integer validityPeriod = CAManager.CertValidityPeriod.value(); + if (validityPeriod < 1) { + validityPeriod = 1; + } + + final SSHCmdHelper.SSHCmdResult keystoreSetupResult = SSHCmdHelper.sshExecuteCmdWithResult(sshConnection, + String.format("/usr/share/cloudstack-common/scripts/util/%s " + + "/etc/cloudstack/agent/agent.properties " + + "/etc/cloudstack/agent/%s " + + "%s %d " + + "/etc/cloudstack/agent/%s", + KeyStoreUtils.keyStoreSetupScript, + KeyStoreUtils.defaultKeystoreFile, + PasswordGenerator.generateRandomPassword(16), + validityPeriod, + KeyStoreUtils.defaultCsrFile)); + + if (!keystoreSetupResult.isSuccess()) { + s_logger.error("Failing, the keystore setup script failed execution on the KVM host: " + agentIp); + return; + } + + final Certificate certificate = caManager.issueCertificate(keystoreSetupResult.getStdOut(), Collections.singletonList(agentHostname), Collections.singletonList(agentIp), null, null); + if (certificate == null || certificate.getClientCertificate() == null) { + s_logger.error("Failing, the configured CA plugin failed to issue certificates for KVM host agent: " + agentIp); + return; + } + + final SetupCertificateCommand certificateCommand = new SetupCertificateCommand(certificate); + final SSHCmdHelper.SSHCmdResult setupCertResult = SSHCmdHelper.sshExecuteCmdWithResult(sshConnection, + String.format("/usr/share/cloudstack-common/scripts/util/%s " + + "/etc/cloudstack/agent/agent.properties " + + "/etc/cloudstack/agent/%s %s " + + "/etc/cloudstack/agent/%s \"%s\" " + + "/etc/cloudstack/agent/%s \"%s\" " + + "/etc/cloudstack/agent/%s \"%s\"", + KeyStoreUtils.keyStoreImportScript, + KeyStoreUtils.defaultKeystoreFile, + KeyStoreUtils.sshMode, + KeyStoreUtils.defaultCertFile, + certificateCommand.getEncodedCertificate(), + KeyStoreUtils.defaultCaCertFile, + certificateCommand.getEncodedCaCertificates(), + KeyStoreUtils.defaultPrivateKeyFile, + certificateCommand.getEncodedPrivateKey())); + + if (setupCertResult != null && !setupCertResult.isSuccess()) { + s_logger.error("Failed to setup certificate in the KVM agent's keystore file, please configure manually!"); + return; + } + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Succeeded to import certificate in the keystore for agent on the KVM host: " + agentIp + ". Agent secured and trusted."); + } + } + @Override public Map> find(long dcId, Long podId, Long clusterId, URI uri, String username, String password, List hostTags) throws DiscoveryException { @@ -143,7 +222,7 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements s_logger.debug(msg); return null; } - com.trilead.ssh2.Connection sshConnection = null; + Connection sshConnection = null; String agentIp = null; try { @@ -162,7 +241,7 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements } } - sshConnection = new com.trilead.ssh2.Connection(agentIp, 22); + sshConnection = new Connection(agentIp, 22); sshConnection.connect(null, 60000, 60000); if (!sshConnection.authenticateWithPassword(username, password)) { @@ -170,7 +249,7 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements throw new DiscoveredWithErrorException("Authentication error"); } - if (!SSHCmdHelper.sshExecuteCmd(sshConnection, "lsmod|grep kvm", 3)) { + if (!SSHCmdHelper.sshExecuteCmd(sshConnection, "lsmod|grep kvm")) { s_logger.debug("It's not a KVM enabled machine"); return null; } @@ -210,7 +289,9 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements kvmGuestNic = (kvmPublicNic != null) ? kvmPublicNic : kvmPrivateNic; } - String parameters = " -m " + _hostIp + " -z " + dcId + " -p " + podId + " -c " + clusterId + " -g " + guid + " -a"; + setupAgentSecurity(sshConnection, agentIp, hostname); + + String parameters = " -m " + StringUtils.shuffleCSVList(_hostIp) + " -z " + dcId + " -p " + podId + " -c " + clusterId + " -g " + guid + " -a"; parameters += " --pubNic=" + kvmPublicNic; parameters += " --prvNic=" + kvmPrivateNic; @@ -221,8 +302,7 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements if (!username.equals("root")) { setupAgentCommand = "sudo cloudstack-setup-agent "; } - if (!SSHCmdHelper.sshExecuteCmd(sshConnection, - setupAgentCommand + parameters, 3)) { + if (!SSHCmdHelper.sshExecuteCmd(sshConnection, setupAgentCommand + parameters)) { s_logger.info("cloudstack agent setup command failed: " + setupAgentCommand + parameters); return null; @@ -392,7 +472,7 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements _resourceMgr.deleteRoutingHost(host, isForced, isForceDeleteStorage); try { ShutdownCommand cmd = new ShutdownCommand(ShutdownCommand.DeleteHost, null); - _agentMgr.send(host.getId(), cmd); + agentMgr.send(host.getId(), cmd); } catch (AgentUnavailableException e) { s_logger.warn("Sending ShutdownCommand failed: ", e); } catch (OperationTimedoutException e) { diff --git a/server/src/com/cloud/resource/ResourceManagerImpl.java b/server/src/com/cloud/resource/ResourceManagerImpl.java index af5344c8e62..5eee2469bbb 100755 --- a/server/src/com/cloud/resource/ResourceManagerImpl.java +++ b/server/src/com/cloud/resource/ResourceManagerImpl.java @@ -2272,7 +2272,8 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, } try { - SSHCmdHelper.sshExecuteCmdOneShot(connection, "service cloudstack-agent restart"); + SSHCmdHelper.SSHCmdResult result = SSHCmdHelper.sshExecuteCmdOneShot(connection, "service cloudstack-agent restart"); + s_logger.debug("cloudstack-agent restart result: " + result.toString()); } catch (final SshException e) { return false; } diff --git a/server/src/com/cloud/server/ConfigurationServerImpl.java b/server/src/com/cloud/server/ConfigurationServerImpl.java index c94d92c85dc..27172b47bbc 100644 --- a/server/src/com/cloud/server/ConfigurationServerImpl.java +++ b/server/src/com/cloud/server/ConfigurationServerImpl.java @@ -23,8 +23,6 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; import java.security.NoSuchAlgorithmException; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -36,24 +34,21 @@ import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.UUID; -import java.util.regex.Pattern; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.inject.Inject; import javax.naming.ConfigurationException; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.log4j.Logger; - import org.apache.cloudstack.config.ApiServiceConfiguration; import org.apache.cloudstack.framework.config.ConfigDepot; import org.apache.cloudstack.framework.config.ConfigDepotAdmin; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.config.impl.ConfigurationVO; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; import com.cloud.configuration.Config; import com.cloud.configuration.ConfigurationManager; @@ -117,7 +112,6 @@ import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.db.TransactionStatus; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.NetUtils; -import com.cloud.utils.nio.Link; import com.cloud.utils.script.Script; public class ConfigurationServerImpl extends ManagerBase implements ConfigurationServer { @@ -306,9 +300,6 @@ public class ConfigurationServerImpl extends ManagerBase implements Configuratio // Update resource count if needed updateResourceCount(); - // keystore for SSL/TLS connection - updateSSLKeystore(); - // store the public and private keys in the database updateKeyPairs(); @@ -544,117 +535,6 @@ public class ConfigurationServerImpl extends ManagerBase implements Configuratio } } - static String getBase64Keystore(String keystorePath) throws IOException { - byte[] storeBytes = FileUtils.readFileToByteArray(new File(keystorePath)); - if (storeBytes.length > 3000) { // Base64 codec would enlarge data by 1/3, and we have 4094 bytes in database entry at most - throw new IOException("KeyStore is too big for database! Length " + storeBytes.length); - } - - return new String(Base64.encodeBase64(storeBytes)); - } - - private void generateDefaultKeystore(String keystorePath) throws IOException { - String cn = "Cloudstack User"; - String ou; - - try { - ou = InetAddress.getLocalHost().getCanonicalHostName(); - String[] group = ou.split("\\."); - - // Simple check to see if we got IP Address... - boolean isIPAddress = Pattern.matches("[0-9]$", group[group.length - 1]); - if (isIPAddress) { - ou = "cloud.com"; // leaving this example reference to cloud.com as it has no real world relevance - } else { - ou = group[group.length - 1]; - for (int i = group.length - 2; i >= 0 && i >= group.length - 3; i--) - ou = group[i] + "." + ou; - } - } catch (UnknownHostException ex) { - s_logger.info("Fail to get user's domain name. Would use cloud.com. ", ex); - ou = "cloud.com"; // leaving this example reference to cloud.com as it has no real world relevance - } - - String o = ou; - String c = "Unknown"; - String dname = "cn=\"" + cn + "\",ou=\"" + ou + "\",o=\"" + o + "\",c=\"" + c + "\""; - Script script = new Script(true, "keytool", 5000, null); - script.add("-genkey"); - script.add("-keystore", keystorePath); - script.add("-storepass", "vmops.com"); - script.add("-keypass", "vmops.com"); - script.add("-keyalg", "RSA"); - script.add("-validity", "3650"); - script.add("-dname", dname); - String result = script.execute(); - if (result != null) { - throw new IOException("Fail to generate certificate!: " + result); - } - } - - protected void updateSSLKeystore() { - if (s_logger.isInfoEnabled()) { - s_logger.info("Processing updateSSLKeyStore"); - } - - String dbString = _configDao.getValue("ssl.keystore"); - - File confFile = PropertiesUtil.findConfigFile("db.properties"); - String confPath = null; - String keystorePath = null; - File keystoreFile = null; - - if (null != confFile) { - confPath = confFile.getParent(); - keystorePath = confPath + Link.keystoreFile; - keystoreFile = new File(keystorePath); - } - - boolean dbExisted = (dbString != null && !dbString.isEmpty()); - - s_logger.info("SSL keystore located at " + keystorePath); - try { - if (!dbExisted && null != confFile) { - if (!keystoreFile.exists()) { - generateDefaultKeystore(keystorePath); - s_logger.info("Generated SSL keystore."); - } - String base64Keystore = getBase64Keystore(keystorePath); - ConfigurationVO configVO = - new ConfigurationVO("Hidden", "DEFAULT", "management-server", "ssl.keystore", base64Keystore, - "SSL Keystore for the management servers"); - _configDao.persist(configVO); - s_logger.info("Stored SSL keystore to database."); - } else { // !keystoreFile.exists() and dbExisted - // Export keystore to local file - byte[] storeBytes = Base64.decodeBase64(dbString); - String tmpKeystorePath = "/tmp/tmpkey"; - try ( - FileOutputStream fo = new FileOutputStream(tmpKeystorePath); - ) { - fo.write(storeBytes); - Script script = new Script(true, "cp", 5000, null); - script.add("-f"); - script.add(tmpKeystorePath); - - //There is a chance, although small, that the keystorePath is null. In that case, do not add it to the script. - if (null != keystorePath) { - script.add(keystorePath); - } - String result = script.execute(); - if (result != null) { - throw new IOException(); - } - } catch (Exception e) { - throw new IOException("Fail to create keystore file!", e); - } - s_logger.info("Stored database keystore to local."); - } - } catch (Exception ex) { - s_logger.warn("Would use fail-safe keystore to continue.", ex); - } - } - @DB protected void updateSystemvmPassword() { String userid = System.getProperty("user.name"); diff --git a/server/src/org/apache/cloudstack/ca/CAManagerImpl.java b/server/src/org/apache/cloudstack/ca/CAManagerImpl.java new file mode 100644 index 00000000000..b8b752de651 --- /dev/null +++ b/server/src/org/apache/cloudstack/ca/CAManagerImpl.java @@ -0,0 +1,428 @@ +// 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; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.admin.ca.IssueCertificateCmd; +import org.apache.cloudstack.api.command.admin.ca.ListCAProvidersCmd; +import org.apache.cloudstack.api.command.admin.ca.ListCaCertificateCmd; +import org.apache.cloudstack.api.command.admin.ca.ProvisionCertificateCmd; +import org.apache.cloudstack.api.command.admin.ca.RevokeCertificateCmd; +import org.apache.cloudstack.context.CallContext; +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.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.poll.BackgroundPollManager; +import org.apache.cloudstack.poll.BackgroundPollTask; +import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.cloudstack.utils.security.CertUtils; +import org.apache.log4j.Logger; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import com.cloud.agent.AgentManager; +import com.cloud.alert.AlertManager; +import com.cloud.certificate.CrlVO; +import com.cloud.certificate.dao.CrlDao; +import com.cloud.event.ActionEvent; +import com.cloud.event.EventTypes; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.Host; +import com.cloud.host.Status; +import com.cloud.host.dao.HostDao; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.exception.CloudRuntimeException; +import com.google.common.base.Strings; + +public class CAManagerImpl extends ManagerBase implements CAManager { + public static final Logger LOG = Logger.getLogger(CAManagerImpl.class); + + @Inject + private CrlDao crlDao; + @Inject + private HostDao hostDao; + @Inject + private AgentManager agentManager; + @Inject + private BackgroundPollManager backgroundPollManager; + @Inject + private AlertManager alertManager; + + private static CAProvider configuredCaProvider; + private static Map caProviderMap = new HashMap<>(); + private static Map alertMap = new ConcurrentHashMap<>(); + private static Map activeCertMap = new ConcurrentHashMap<>(); + + private List caProviders; + + private CAProvider getConfiguredCaProvider() { + if (configuredCaProvider != null) { + return configuredCaProvider; + } + if (caProviderMap.containsKey(CAProviderPlugin.value()) && caProviderMap.get(CAProviderPlugin.value()) != null) { + configuredCaProvider = caProviderMap.get(CAProviderPlugin.value()); + return configuredCaProvider; + } + throw new CloudRuntimeException("Failed to find default configured CA provider plugin"); + } + + private CAProvider getCAProvider(final String provider) { + if (Strings.isNullOrEmpty(provider)) { + return getConfiguredCaProvider(); + } + final String caProviderName = provider.toLowerCase(); + if (!caProviderMap.containsKey(caProviderName)) { + throw new CloudRuntimeException(String.format("CA provider plugin '%s' not found", caProviderName)); + } + final CAProvider caProvider = caProviderMap.get(caProviderName); + if (caProvider == null) { + throw new CloudRuntimeException(String.format("CA provider plugin '%s' returned is null", caProviderName)); + } + return caProvider; + } + + /////////////////////////////////////////////////////////// + /////////////// CA Manager API Handlers /////////////////// + /////////////////////////////////////////////////////////// + + @Override + public List getCaProviders() { + return caProviders; + } + + @Override + public Map getActiveCertificatesMap() { + return activeCertMap; + } + + @Override + public boolean canProvisionCertificates() { + return getConfiguredCaProvider().canProvisionCertificates(); + } + + @Override + public String getCaCertificate(final String caProvider) throws IOException { + final CAProvider provider = getCAProvider(caProvider); + return CertUtils.x509CertificatesToPem(provider.getCaCertificate()); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_CA_CERTIFICATE_ISSUE, eventDescription = "issuing certificate", async = true) + public Certificate issueCertificate(final String csr, final List domainNames, final List ipAddresses, final Integer validityDuration, final String caProvider) { + CallContext.current().setEventDetails("domain(s): " + domainNames + " addresses: " + ipAddresses); + final CAProvider provider = getCAProvider(caProvider); + Integer validity = CAManager.CertValidityPeriod.value(); + if (validityDuration != null) { + validity = validityDuration; + } + if (Strings.isNullOrEmpty(csr)) { + if (domainNames == null || domainNames.isEmpty()) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "No domains or CSR provided"); + } + return provider.issueCertificate(domainNames, ipAddresses, validity); + } + return provider.issueCertificate(csr, domainNames, ipAddresses, validity); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_CA_CERTIFICATE_REVOKE, eventDescription = "revoking certificate", async = true) + public boolean revokeCertificate(final BigInteger certSerial, final String certCn, final String caProvider) { + CallContext.current().setEventDetails("cert serial: " + certSerial); + final CrlVO crl = crlDao.revokeCertificate(certSerial, certCn); + if (crl != null && crl.getCertSerial().equals(certSerial)) { + final CAProvider provider = getCAProvider(caProvider); + return provider.revokeCertificate(certSerial, certCn); + } + return false; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_CA_CERTIFICATE_PROVISION, eventDescription = "provisioning certificate for host", async = true) + public boolean provisionCertificate(final Host host, final Boolean reconnect, final String caProvider) { + if (host == null) { + throw new CloudRuntimeException("Unable to find valid host to renew certificate for"); + } + CallContext.current().setEventDetails("host id: " + host.getId()); + CallContext.current().putContextParameter(Host.class, host.getUuid()); + final String csr; + try { + csr = generateKeyStoreAndCsr(host, null); + if (Strings.isNullOrEmpty(csr)) { + return false; + } + final Certificate certificate = issueCertificate(csr, Collections.singletonList(host.getName()), Arrays.asList(host.getPrivateIpAddress(), host.getPublicIpAddress(), host.getStorageIpAddress()), CAManager.CertValidityPeriod.value(), caProvider); + return deployCertificate(host, certificate, reconnect, null); + } catch (final AgentUnavailableException | OperationTimedoutException e) { + LOG.error("Host/agent is not available or operation timed out, failed to setup keystore and generate CSR for host/agent id=" + host.getId() + ", due to: ", e); + throw new CloudRuntimeException("Failed to generate keystore and get CSR from the host/agent id=" + host.getId()); + } + } + + @Override + public String generateKeyStoreAndCsr(final Host host, final Map sshAccessDetails) throws AgentUnavailableException, OperationTimedoutException { + final SetupKeyStoreCommand cmd = new SetupKeyStoreCommand(CertValidityPeriod.value()); + if (sshAccessDetails != null && !sshAccessDetails.isEmpty()) { + cmd.setAccessDetail(sshAccessDetails); + } + CallContext.current().setEventDetails("generating keystore and CSR for host id: " + host.getId()); + final SetupKeystoreAnswer answer = (SetupKeystoreAnswer) agentManager.send(host.getId(), cmd); + return answer.getCsr(); + } + + @Override + public boolean deployCertificate(final Host host, final Certificate certificate, final Boolean reconnect, final Map sshAccessDetails) throws AgentUnavailableException, OperationTimedoutException { + final SetupCertificateCommand cmd = new SetupCertificateCommand(certificate); + if (sshAccessDetails != null && !sshAccessDetails.isEmpty()) { + cmd.setAccessDetail(sshAccessDetails); + } + CallContext.current().setEventDetails("deploying certificate for host id: " + host.getId()); + final SetupCertificateAnswer answer = (SetupCertificateAnswer) agentManager.send(host.getId(), cmd); + if (answer.getResult()) { + CallContext.current().setEventDetails("successfully deployed certificate for host id: " + host.getId()); + } else { + CallContext.current().setEventDetails("failed to deploy certificate for host id: " + host.getId()); + } + + if (answer.getResult()) { + getActiveCertificatesMap().put(host.getPrivateIpAddress(), certificate.getClientCertificate()); + if (sshAccessDetails == null && reconnect != null && reconnect) { + LOG.info(String.format("Successfully setup certificate on host, reconnecting with agent with id=%d, name=%s, address=%s", + host.getId(), host.getName(), host.getPublicIpAddress())); + return agentManager.reconnect(host.getId()); + } + return true; + } + return false; + } + + @Override + public void purgeHostCertificate(final Host host) { + if (host == null) { + return; + } + final String privateAddress = host.getPrivateIpAddress(); + final String publicAddress = host.getPublicIpAddress(); + final Map activeCertsMap = getActiveCertificatesMap(); + if (!Strings.isNullOrEmpty(privateAddress) && activeCertsMap.containsKey(privateAddress)) { + activeCertsMap.remove(privateAddress); + } + if (!Strings.isNullOrEmpty(publicAddress) && activeCertsMap.containsKey(publicAddress)) { + activeCertsMap.remove(publicAddress); + } + } + + @Override + public void sendAlert(final Host host, final String subject, final String message) { + if (host == null) { + return; + } + alertManager.sendAlert(AlertManager.AlertType.ALERT_TYPE_CA_CERT, + host.getDataCenterId(), host.getPodId(), subject, message); + } + + @Override + public SSLEngine createSSLEngine(final SSLContext sslContext, final String remoteAddress) throws GeneralSecurityException, IOException { + if (sslContext == null) { + throw new CloudRuntimeException("SSLContext provided to create SSLEngine is null, aborting"); + } + if (Strings.isNullOrEmpty(remoteAddress)) { + throw new CloudRuntimeException("Remote client address connecting to mgmt server cannot be empty/null"); + } + return getConfiguredCaProvider().createSSLEngine(sslContext, remoteAddress, getActiveCertificatesMap()); + } + + //////////////////////////////////////////////////// + /////////////// CA Manager Setup /////////////////// + //////////////////////////////////////////////////// + + public static final class CABackgroundTask extends ManagedContextRunnable implements BackgroundPollTask { + private CAManager caManager; + private HostDao hostDao; + + public CABackgroundTask(final CAManager caManager, final HostDao hostDao) { + this.caManager = caManager; + this.hostDao = hostDao; + } + + @Override + protected void runInContext() { + try { + if (LOG.isTraceEnabled()) { + LOG.trace("CA background task is running..."); + } + final DateTime now = DateTime.now(DateTimeZone.UTC); + final Map certsMap = caManager.getActiveCertificatesMap(); + for (final Iterator> it = certsMap.entrySet().iterator(); it.hasNext(); ) { + final Map.Entry entry = it.next(); + if (entry == null) { + continue; + } + final String hostIp = entry.getKey(); + final X509Certificate certificate = entry.getValue(); + if (certificate == null) { + it.remove(); + continue; + } + final Host host = hostDao.findByIp(hostIp); + if (host == null || host.getManagementServerId() == null || + host.getManagementServerId() != ManagementServerNode.getManagementServerId() || + host.getStatus() != Status.Up) { + if (host == null || + (host.getManagementServerId() != null && + host.getManagementServerId() != ManagementServerNode.getManagementServerId())) { + it.remove(); + } + continue; + } + + final String hostDescription = String.format("host id=%d, uuid=%s, name=%s, ip=%s, zone id=%d", + host.getId(), host.getUuid(), host.getName(), hostIp, host.getDataCenterId()); + + try { + certificate.checkValidity(now.plusDays(CertExpiryAlertPeriod.valueIn(host.getClusterId())).toDate()); + } catch (final CertificateExpiredException | CertificateNotYetValidException e) { + LOG.warn("Certificate is going to expire for " + hostDescription); + if (AutomaticCertRenewal.valueIn(host.getClusterId())) { + try { + LOG.debug("Attempting certificate auto-renewal for " + hostDescription); + boolean result = caManager.provisionCertificate(host, false, null); + if (result) { + LOG.debug("Succeeded in auto-renewing certificate for " + hostDescription); + } else { + LOG.debug("Failed in auto-renewing certificate for " + hostDescription); + } + } catch (final Throwable ex) { + LOG.warn("Failed to auto-renew certificate for " + hostDescription + ", with error=", ex); + caManager.sendAlert(host, "Certificate auto-renewal failed for " + hostDescription, + String.format("Certificate is going to expire for %s. Auto-renewal failed to renew the certificate, please renew it manually. It is not valid after %s.", hostDescription, certificate.getNotAfter())); + } + } else { + if (alertMap.containsKey(hostIp)) { + final Date lastSentDate = alertMap.get(hostIp); + if (now.minusDays(1).toDate().before(lastSentDate)) { + continue; + } + } + caManager.sendAlert(host, "Certificate expiring soon for " + hostDescription, + String.format("Certificate is going to expire for %s. Please renew it, it is not valid after %s.", + hostDescription, certificate.getNotAfter())); + alertMap.put(hostIp, new Date()); + } + } + } + } catch (final Throwable t) { + LOG.error("Error trying to run CA background task", t); + } + } + + @Override + public Long getDelay() { + return CABackgroundJobDelay.value() * 1000L; + } + } + + public void setCaProviders(final List caProviders) { + this.caProviders = caProviders; + initializeCaProviderMap(); + } + + private void initializeCaProviderMap() { + if (caProviderMap != null && caProviderMap.size() != caProviders.size()) { + for (final CAProvider caProvider : caProviders) { + caProviderMap.put(caProvider.getProviderName().toLowerCase(), caProvider); + } + } + } + + @Override + public boolean start() { + super.start(); + initializeCaProviderMap(); + if (caProviderMap.containsKey(CAProviderPlugin.value())) { + configuredCaProvider = caProviderMap.get(CAProviderPlugin.value()); + } + if (configuredCaProvider == null) { + LOG.error("Failed to find valid configured CA provider, please check!"); + return false; + } + return true; + } + + @Override + public boolean configure(final String name, final Map params) throws ConfigurationException { + backgroundPollManager.submitTask(new CABackgroundTask(this, hostDao)); + return true; + } + + ////////////////////////////////////////////////////////// + /////////////// CA Manager Descriptors /////////////////// + ////////////////////////////////////////////////////////// + + @Override + public List> getCommands() { + final List> cmdList = new ArrayList>(); + cmdList.add(ListCAProvidersCmd.class); + cmdList.add(ListCaCertificateCmd.class); + cmdList.add(IssueCertificateCmd.class); + cmdList.add(ProvisionCertificateCmd.class); + cmdList.add(RevokeCertificateCmd.class); + return cmdList; + } + + @Override + public String getConfigComponentName() { + return CAManager.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{ + CAProviderPlugin, + CertKeySize, + CertSignatureAlgorithm, + CertValidityPeriod, + AutomaticCertRenewal, + CABackgroundJobDelay, + CertExpiryAlertPeriod + }; + } +} diff --git a/server/src/org/apache/cloudstack/outofbandmanagement/OutOfBandManagementServiceImpl.java b/server/src/org/apache/cloudstack/outofbandmanagement/OutOfBandManagementServiceImpl.java index 4f0ac74c916..cb6ac106bfa 100644 --- a/server/src/org/apache/cloudstack/outofbandmanagement/OutOfBandManagementServiceImpl.java +++ b/server/src/org/apache/cloudstack/outofbandmanagement/OutOfBandManagementServiceImpl.java @@ -577,5 +577,11 @@ public class OutOfBandManagementServiceImpl extends ManagerBase implements OutOf LOG.error("Error trying to retrieve host out-of-band management stats", t); } } + + @Override + public Long getDelay() { + return null; + } + } } diff --git a/server/src/org/apache/cloudstack/poll/BackgroundPollManagerImpl.java b/server/src/org/apache/cloudstack/poll/BackgroundPollManagerImpl.java index c0a7f1c3957..f4a634032d4 100644 --- a/server/src/org/apache/cloudstack/poll/BackgroundPollManagerImpl.java +++ b/server/src/org/apache/cloudstack/poll/BackgroundPollManagerImpl.java @@ -52,7 +52,11 @@ public final class BackgroundPollManagerImpl extends ManagerBase implements Back } backgroundPollTaskScheduler = Executors.newScheduledThreadPool(submittedTasks.size() + 1, new NamedThreadFactory("BackgroundTaskPollManager")); for (final BackgroundPollTask task : submittedTasks) { - backgroundPollTaskScheduler.scheduleWithFixedDelay(task, getInitialDelay(), getRoundDelay(), TimeUnit.MILLISECONDS); + Long delay = task.getDelay(); + if (delay == null) { + delay = getRoundDelay(); + } + backgroundPollTaskScheduler.scheduleWithFixedDelay(task, getInitialDelay(), delay, TimeUnit.MILLISECONDS); LOG.debug("Scheduled background poll task: " + task.getClass().getName()); } isConfiguredAndStarted = true; diff --git a/server/test/com/cloud/server/ConfigurationServerImplTest.java b/server/test/com/cloud/server/ConfigurationServerImplTest.java index b64f3f72aa2..8b0af999011 100644 --- a/server/test/com/cloud/server/ConfigurationServerImplTest.java +++ b/server/test/com/cloud/server/ConfigurationServerImplTest.java @@ -16,8 +16,17 @@ // under the License. package com.cloud.server; -import java.io.File; -import java.io.IOException; +import org.apache.cloudstack.framework.config.ConfigDepot; +import org.apache.cloudstack.framework.config.ConfigDepotAdmin; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; import com.cloud.configuration.ConfigurationManager; import com.cloud.configuration.dao.ResourceCountDao; @@ -32,19 +41,6 @@ import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.storage.dao.DiskOfferingDao; import com.cloud.user.dao.AccountDao; import com.cloud.utils.db.TransactionLegacy; -import org.apache.cloudstack.framework.config.ConfigDepot; -import org.apache.cloudstack.framework.config.ConfigDepotAdmin; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.io.FileUtils; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.Spy; -import org.mockito.runners.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class ConfigurationServerImplTest { @@ -101,41 +97,6 @@ public class ConfigurationServerImplTest { } }; - final static String TEST = "the quick brown fox jumped over the lazy dog"; - - @Test(expected = IOException.class) - public void testGetBase64KeystoreNoSuchFile() throws IOException { - ConfigurationServerImpl.getBase64Keystore("notexisting" + System.currentTimeMillis()); - } - - @Test(expected = IOException.class) - public void testGetBase64KeystoreTooBigFile() throws IOException { - File temp = File.createTempFile("keystore", ""); - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < 1000; i++) { - builder.append("way too long...\n"); - } - FileUtils.writeStringToFile(temp, builder.toString()); - try { - ConfigurationServerImpl.getBase64Keystore(temp.getPath()); - } finally { - temp.delete(); - } - } - - @Test - public void testGetBase64Keystore() throws IOException { - File temp = File.createTempFile("keystore", ""); - try { - FileUtils.writeStringToFile(temp, Base64.encodeBase64String(TEST.getBytes())); - final String keystore = ConfigurationServerImpl.getBase64Keystore(temp.getPath()); - // let's decode it to make sure it makes sense - Base64.decodeBase64(keystore); - } finally { - temp.delete(); - } - } - @Test public void testWindowsScript() { Assert.assertTrue(windowsImpl.isOnWindows()); diff --git a/server/test/com/cloud/vpc/MockNetworkManagerImpl.java b/server/test/com/cloud/vpc/MockNetworkManagerImpl.java index 495989d65be..f6f818d1733 100644 --- a/server/test/com/cloud/vpc/MockNetworkManagerImpl.java +++ b/server/test/com/cloud/vpc/MockNetworkManagerImpl.java @@ -574,6 +574,11 @@ public class MockNetworkManagerImpl extends ManagerBase implements NetworkOrches return null; } + @Override + public Map getSystemVMAccessDetails(VirtualMachine vm) { + return null; + } + /* (non-Javadoc) * @see com.cloud.network.NetworkManager#implementNetwork(long, com.cloud.deploy.DeployDestination, com.cloud.vm.ReservationContext) */ diff --git a/server/test/org/apache/cloudstack/ca/CABackgroundTaskTest.java b/server/test/org/apache/cloudstack/ca/CABackgroundTaskTest.java new file mode 100644 index 00000000000..d2c800d8272 --- /dev/null +++ b/server/test/org/apache/cloudstack/ca/CABackgroundTaskTest.java @@ -0,0 +1,152 @@ +// +// 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; + +import static org.apache.cloudstack.ca.CAManager.AutomaticCertRenewal; + +import java.lang.reflect.Field; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.cloudstack.utils.security.CertUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.Status; +import com.cloud.host.dao.HostDao; +import com.cloud.storage.Storage; +import com.cloud.utils.exception.CloudRuntimeException; + +@RunWith(MockitoJUnitRunner.class) +public class CABackgroundTaskTest { + + @Mock + private CAManager caManager; + @Mock + private HostDao hostDao; + + private String hostIp = "1.2.3.4"; + private HostVO host = new HostVO(1L, "some.host",Host.Type.Routing, hostIp, "255.255.255.0", null, null, null, null, null, null, null, null, null, null, + UUID.randomUUID().toString(), Status.Up, "1.0", null, null, 1L, null, 0, 0, "aa", 0, Storage.StoragePoolType.NetworkFilesystem); + + private X509Certificate expiredCertificate; + private Map certMap = new HashMap<>(); + private CAManagerImpl.CABackgroundTask task; + + @Before + public void setUp() throws Exception { + host.setManagementServerId(ManagementServerNode.getManagementServerId()); + task = new CAManagerImpl.CABackgroundTask(caManager, hostDao); + final KeyPair keypair = CertUtils.generateRandomKeyPair(1024); + expiredCertificate = CertUtils.generateV1Certificate(keypair, "CN=ca", "CN=ca", 0, + "SHA256withRSA"); + + Mockito.when(hostDao.findByIp(Mockito.anyString())).thenReturn(host); + Mockito.when(caManager.getActiveCertificatesMap()).thenReturn(certMap); + } + + @After + public void tearDown() throws Exception { + certMap.clear(); + Mockito.reset(caManager); + Mockito.reset(hostDao); + } + + private void overrideDefaultConfigValue(final ConfigKey configKey, final String name, final Object o) throws IllegalAccessException, NoSuchFieldException { + Field f = ConfigKey.class.getDeclaredField(name); + f.setAccessible(true); + f.set(configKey, o); + } + + @Test + public void testNullCert() throws Exception { + certMap.put(hostIp, null); + Assert.assertTrue(certMap.size() == 1); + task.runInContext(); + Assert.assertTrue(certMap.size() == 0); + } + + @Test + public void testNullHost() throws Exception { + Mockito.when(hostDao.findByIp(Mockito.anyString())).thenReturn(null); + certMap.put(hostIp, expiredCertificate); + Assert.assertTrue(certMap.size() == 1); + task.runInContext(); + Assert.assertTrue(certMap.size() == 0); + } + + @Test + public void testAutoRenewalEnabledWithNoExceptionsOnProvisioning() throws Exception { + overrideDefaultConfigValue(AutomaticCertRenewal, "_defaultValue", "true"); + host.setManagementServerId(ManagementServerNode.getManagementServerId()); + certMap.put(hostIp, expiredCertificate); + Assert.assertTrue(certMap.size() == 1); + task.runInContext(); + Mockito.verify(caManager, Mockito.times(1)).provisionCertificate(host, false, null); + Mockito.verify(caManager, Mockito.times(0)).sendAlert(Mockito.any(Host.class), Mockito.anyString(), Mockito.anyString()); + } + + @Test + public void testAutoRenewalEnabledWithExceptionsOnProvisioning() throws Exception { + overrideDefaultConfigValue(AutomaticCertRenewal, "_defaultValue", "true"); + Mockito.when(caManager.provisionCertificate(Mockito.any(Host.class), Mockito.anyBoolean(), Mockito.anyString())).thenThrow(new CloudRuntimeException("some error")); + host.setManagementServerId(ManagementServerNode.getManagementServerId()); + certMap.put(hostIp, expiredCertificate); + Assert.assertTrue(certMap.size() == 1); + task.runInContext(); + Mockito.verify(caManager, Mockito.times(1)).provisionCertificate(host, false, null); + Mockito.verify(caManager, Mockito.times(1)).sendAlert(Mockito.any(Host.class), Mockito.anyString(), Mockito.anyString()); + } + + @Test + public void testAutoRenewalDisabled() throws Exception { + overrideDefaultConfigValue(AutomaticCertRenewal, "_defaultValue", "false"); + certMap.put(hostIp, expiredCertificate); + Assert.assertTrue(certMap.size() == 1); + // First round + task.runInContext(); + Mockito.verify(caManager, Mockito.times(0)).provisionCertificate(Mockito.any(Host.class), Mockito.anyBoolean(), Mockito.anyString()); + Mockito.verify(caManager, Mockito.times(1)).sendAlert(Mockito.any(Host.class), Mockito.anyString(), Mockito.anyString()); + Mockito.reset(caManager); + // Second round + task.runInContext(); + Mockito.verify(caManager, Mockito.times(0)).provisionCertificate(Mockito.any(Host.class), Mockito.anyBoolean(), Mockito.anyString()); + Mockito.verify(caManager, Mockito.times(0)).sendAlert(Mockito.any(Host.class), Mockito.anyString(), Mockito.anyString()); + } + + @Test + public void testGetDelay() throws Exception { + Assert.assertTrue(task.getDelay() == CAManager.CABackgroundJobDelay.value() * 1000L); + } + +} \ No newline at end of file diff --git a/server/test/org/apache/cloudstack/ca/CAManagerImplTest.java b/server/test/org/apache/cloudstack/ca/CAManagerImplTest.java new file mode 100644 index 00000000000..87e128c772e --- /dev/null +++ b/server/test/org/apache/cloudstack/ca/CAManagerImplTest.java @@ -0,0 +1,119 @@ +// +// 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; + +import java.lang.reflect.Field; +import java.math.BigInteger; +import java.security.cert.X509Certificate; +import java.util.Collections; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.framework.ca.CAProvider; +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.utils.security.CertUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.certificate.CrlVO; +import com.cloud.certificate.dao.CrlDao; +import com.cloud.host.Host; +import com.cloud.host.dao.HostDao; + +@RunWith(MockitoJUnitRunner.class) +public class CAManagerImplTest { + + @Mock + private HostDao hostDao; + @Mock + private CrlDao crlDao; + @Mock + private AgentManager agentManager; + @Mock + private CAProvider caProvider; + + private CAManagerImpl caManager; + + private void addField(final CAManagerImpl provider, final String name, final Object o) throws IllegalAccessException, NoSuchFieldException { + Field f = CAManagerImpl.class.getDeclaredField(name); + f.setAccessible(true); + f.set(provider, o); + } + + @Before + public void setUp() throws Exception { + caManager = new CAManagerImpl(); + addField(caManager, "crlDao", crlDao); + addField(caManager, "hostDao", hostDao); + addField(caManager, "agentManager", agentManager); + addField(caManager, "configuredCaProvider", caProvider); + + Mockito.when(caProvider.getProviderName()).thenReturn("root"); + caManager.setCaProviders(Collections.singletonList(caProvider)); + } + + @After + public void tearDown() throws Exception { + Mockito.reset(crlDao); + Mockito.reset(agentManager); + Mockito.reset(caProvider); + } + + @Test(expected = ServerApiException.class) + public void testIssueCertificateThrowsException() throws Exception { + caManager.issueCertificate(null, null, null, 1, null); + } + + @Test + public void testIssueCertificate() throws Exception { + caManager.issueCertificate(null, Collections.singletonList("domain.example"), null, 1, null); + Mockito.verify(caProvider, Mockito.times(1)).issueCertificate(Mockito.anyList(), Mockito.anyList(), Mockito.anyInt()); + Mockito.verify(caProvider, Mockito.times(0)).issueCertificate(Mockito.anyString(), Mockito.anyList(), Mockito.anyList(), Mockito.anyInt()); + } + + @Test + public void testRevokeCertificate() throws Exception { + final CrlVO crl = new CrlVO(CertUtils.generateRandomBigInt(), "some.domain", "some-uuid"); + Mockito.when(crlDao.revokeCertificate(Mockito.any(BigInteger.class), Mockito.anyString())).thenReturn(crl); + Mockito.when(caProvider.revokeCertificate(Mockito.any(BigInteger.class), Mockito.anyString())).thenReturn(true); + Assert.assertTrue(caManager.revokeCertificate(crl.getCertSerial(), crl.getCertCn(), null)); + Mockito.verify(caProvider, Mockito.times(1)).revokeCertificate(Mockito.any(BigInteger.class), Mockito.anyString()); + } + + @Test + public void testProvisionCertificate() throws Exception { + final Host host = Mockito.mock(Host.class); + Mockito.when(host.getPrivateIpAddress()).thenReturn("1.2.3.4"); + final X509Certificate certificate = CertUtils.generateV1Certificate(CertUtils.generateRandomKeyPair(1024), "CN=ca", "CN=ca", 1, "SHA256withRSA"); + Mockito.when(caProvider.issueCertificate(Mockito.anyString(), Mockito.anyList(), Mockito.anyList(), Mockito.anyInt())).thenReturn(new Certificate(certificate, null, Collections.singletonList(certificate))); + Mockito.when(agentManager.send(Mockito.anyLong(), Mockito.any(SetupKeyStoreCommand.class))).thenReturn(new SetupKeystoreAnswer("someCsr")); + Mockito.when(agentManager.reconnect(Mockito.anyLong())).thenReturn(true); + Assert.assertTrue(caManager.provisionCertificate(host, true, null)); + Mockito.verify(agentManager, Mockito.times(2)).send(Mockito.anyLong(), Mockito.any(Answer.class)); + Mockito.verify(agentManager, Mockito.times(1)).reconnect(Mockito.anyLong()); + } +} \ No newline at end of file diff --git a/server/test/org/apache/cloudstack/poll/BackgroundPollManagerImplTest.java b/server/test/org/apache/cloudstack/poll/BackgroundPollManagerImplTest.java index 3304abaf611..f35d49cefed 100644 --- a/server/test/org/apache/cloudstack/poll/BackgroundPollManagerImplTest.java +++ b/server/test/org/apache/cloudstack/poll/BackgroundPollManagerImplTest.java @@ -45,6 +45,12 @@ public class BackgroundPollManagerImplTest { didIRun = true; counter++; } + + @Override + public Long getDelay() { + return null; + } + } @Before diff --git a/services/secondary-storage/controller/src/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java b/services/secondary-storage/controller/src/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java index 79c6bab1b4b..273fdd0d116 100644 --- a/services/secondary-storage/controller/src/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java +++ b/services/secondary-storage/controller/src/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java @@ -123,6 +123,7 @@ import com.cloud.user.AccountService; import com.cloud.utils.DateUtil; import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; +import com.cloud.utils.StringUtils; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.db.GlobalLock; import com.cloud.utils.db.QueryBuilder; @@ -1118,7 +1119,7 @@ public class SecondaryStorageManagerImpl extends ManagerBase implements Secondar StringBuilder buf = profile.getBootArgsBuilder(); buf.append(" template=domP type=secstorage"); - buf.append(" host=").append(ApiServiceConfiguration.ManagementHostIPAdr.value()); + buf.append(" host=").append(StringUtils.shuffleCSVList(ApiServiceConfiguration.ManagementHostIPAdr.value())); buf.append(" port=").append(_mgmtPort); buf.append(" name=").append(profile.getVirtualMachine().getHostName()); diff --git a/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java b/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java index 60424c1af3b..4f3ad077452 100644 --- a/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java +++ b/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java @@ -16,6 +16,71 @@ // under the License. package org.apache.cloudstack.storage.resource; +import static com.cloud.utils.StringUtils.join; +import static com.cloud.utils.storage.S3.S3Utils.putFile; +import static java.lang.String.format; +import static java.util.Arrays.asList; +import static org.apache.commons.lang.StringUtils.substringAfterLast; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.URI; +import java.net.UnknownHostException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.framework.security.keystore.KeystoreManager; +import org.apache.cloudstack.storage.command.CopyCmdAnswer; +import org.apache.cloudstack.storage.command.CopyCommand; +import org.apache.cloudstack.storage.command.DeleteCommand; +import org.apache.cloudstack.storage.command.DownloadCommand; +import org.apache.cloudstack.storage.command.DownloadProgressCommand; +import org.apache.cloudstack.storage.command.TemplateOrVolumePostUploadCommand; +import org.apache.cloudstack.storage.command.UploadStatusAnswer; +import org.apache.cloudstack.storage.command.UploadStatusAnswer.UploadStatus; +import org.apache.cloudstack.storage.command.UploadStatusCommand; +import org.apache.cloudstack.storage.template.DownloadManager; +import org.apache.cloudstack.storage.template.DownloadManagerImpl; +import org.apache.cloudstack.storage.template.DownloadManagerImpl.ZfsPathParser; +import org.apache.cloudstack.storage.template.UploadEntity; +import org.apache.cloudstack.storage.template.UploadManager; +import org.apache.cloudstack.storage.template.UploadManagerImpl; +import org.apache.cloudstack.storage.to.SnapshotObjectTO; +import org.apache.cloudstack.storage.to.TemplateObjectTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.imagestore.ImageStoreUtil; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.log4j.Logger; +import org.joda.time.DateTime; +import org.joda.time.format.ISODateTimeFormat; + import com.amazonaws.services.s3.model.S3ObjectSummary; import com.cloud.agent.api.Answer; import com.cloud.agent.api.CheckHealthAnswer; @@ -83,6 +148,7 @@ import com.cloud.utils.storage.S3.S3Utils; import com.cloud.vm.SecondaryStorageVm; import com.google.gson.Gson; import com.google.gson.GsonBuilder; + import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; @@ -96,69 +162,6 @@ import io.netty.handler.codec.http.HttpRequestDecoder; import io.netty.handler.codec.http.HttpResponseEncoder; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; -import org.apache.cloudstack.framework.security.keystore.KeystoreManager; -import org.apache.cloudstack.storage.command.CopyCmdAnswer; -import org.apache.cloudstack.storage.command.CopyCommand; -import org.apache.cloudstack.storage.command.DeleteCommand; -import org.apache.cloudstack.storage.command.DownloadCommand; -import org.apache.cloudstack.storage.command.DownloadProgressCommand; -import org.apache.cloudstack.storage.command.TemplateOrVolumePostUploadCommand; -import org.apache.cloudstack.storage.command.UploadStatusAnswer; -import org.apache.cloudstack.storage.command.UploadStatusAnswer.UploadStatus; -import org.apache.cloudstack.storage.command.UploadStatusCommand; -import org.apache.cloudstack.storage.template.DownloadManager; -import org.apache.cloudstack.storage.template.DownloadManagerImpl; -import org.apache.cloudstack.storage.template.DownloadManagerImpl.ZfsPathParser; -import org.apache.cloudstack.storage.template.UploadEntity; -import org.apache.cloudstack.storage.template.UploadManager; -import org.apache.cloudstack.storage.template.UploadManagerImpl; -import org.apache.cloudstack.storage.to.SnapshotObjectTO; -import org.apache.cloudstack.storage.to.TemplateObjectTO; -import org.apache.cloudstack.storage.to.VolumeObjectTO; -import org.apache.cloudstack.utils.imagestore.ImageStoreUtil; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.utils.URLEncodedUtils; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.log4j.Logger; -import org.joda.time.DateTime; -import org.joda.time.format.ISODateTimeFormat; - -import javax.naming.ConfigurationException; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.math.BigInteger; -import java.net.InetAddress; -import java.net.URI; -import java.net.UnknownHostException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import static com.cloud.utils.StringUtils.join; -import static com.cloud.utils.storage.S3.S3Utils.putFile; -import static java.lang.String.format; -import static java.util.Arrays.asList; -import static org.apache.commons.lang.StringUtils.substringAfterLast; public class NfsSecondaryStorageResource extends ServerResourceBase implements SecondaryStorageResource { @@ -2231,9 +2234,10 @@ public class NfsSecondaryStorageResource extends ServerResourceBase implements S if (_inSystemVM) { _localgw = (String)params.get("localgw"); if (_localgw != null) { // can only happen inside service vm - String mgmtHost = (String)params.get("host"); - addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, mgmtHost); - + String mgmtHosts = (String)params.get("host"); + for (final String mgmtHost : mgmtHosts.split(",")) { + addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, mgmtHost); + } String internalDns1 = (String)params.get("internaldns1"); if (internalDns1 == null) { s_logger.warn("No DNS entry found during configuration of NfsSecondaryStorage"); diff --git a/setup/db/db/schema-41000to41100.sql b/setup/db/db/schema-41000to41100.sql index 911def8f2c3..eacddc15c4d 100644 --- a/setup/db/db/schema-41000to41100.sql +++ b/setup/db/db/schema-41000to41100.sql @@ -123,3 +123,18 @@ CREATE VIEW `template_view` AS OR (`resource_tags`.`resource_type` = 'ISO'))))); UPDATE `cloud`.`configuration` SET value = '600', default_value = '600' WHERE category = 'Advanced' AND name = 'router.aggregation.command.each.timeout'; + +-- CA framework changes +DELETE from `cloud`.`configuration` where name='ssl.keystore'; + +-- Certificate Revocation List +CREATE TABLE IF NOT EXISTS `cloud`.`crl` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `serial` varchar(255) UNIQUE NOT NULL COMMENT 'certificate\'s serial number as hex string', + `cn` varchar(255) COMMENT 'certificate\'s common name', + `revoker_uuid` varchar(40) COMMENT 'revoker user account uuid', + `revoked` datetime COMMENT 'date of revocation', + PRIMARY KEY (`id`), + KEY (`serial`), + UNIQUE KEY (`serial`, `cn`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/setup/db/server-setup.sql b/setup/db/server-setup.sql index df2c9248552..1c4635c72ca 100644 --- a/setup/db/server-setup.sql +++ b/setup/db/server-setup.sql @@ -27,3 +27,6 @@ INSERT INTO `cloud`.`configuration` (category, instance, component, name, value, -- Enable dynamic RBAC by default for fresh deployments INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) VALUES ('Advanced', 'DEFAULT', 'RoleService', 'dynamic.apichecker.enabled', 'true'); + +-- Enable RootCA auth strictness for fresh deployments +INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) VALUES ('Advanced', 'DEFAULT', 'RootCAProvider', 'ca.plugin.root.auth.strictness', 'true'); diff --git a/setup/db/server-setup.xml b/setup/db/server-setup.xml index 178f29aea35..955a3a51806 100755 --- a/setup/db/server-setup.xml +++ b/setup/db/server-setup.xml @@ -246,6 +246,13 @@ under the License. true + + ca.plugin.root.auth.strictness + true + + diff --git a/ui/scripts/ui-custom/ca.js b/ui/scripts/ui-custom/ca.js new file mode 100644 index 00000000000..c5298292371 --- /dev/null +++ b/ui/scripts/ui-custom/ca.js @@ -0,0 +1,53 @@ +// 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. + +(function($, cloudStack) { + $(window).bind('cloudStack.ready', function() { + var caCert = ""; + var downloadCaCert = function() { + var blob = new Blob([caCert], {type: 'application/x-x509-ca-cert'}); + var filename = "cloud-ca.pem"; + if(window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveBlob(blob, filename); + } else{ + var elem = window.document.createElement('a'); + elem.href = window.URL.createObjectURL(blob); + elem.download = filename; + document.body.appendChild(elem) + elem.click(); + document.body.removeChild(elem); + } + }; + + $.ajax({ + url: createURL('listCaCertificate'), + success: function(json) { + caCert = json.listcacertificateresponse.cacertificates.certificate; + if (caCert) { + var $caCertDownloadButton = $('
').addClass('cacert-download'); + $caCertDownloadButton.append($('').addClass('icon').html(' ').attr('title', 'Download CA Certificate')); + $caCertDownloadButton.click(function() { + downloadCaCert(); + }); + $('#header .controls .view-switcher:last').after($caCertDownloadButton); + } + }, + error: function(data) { + } + }); + }); +}(jQuery, cloudStack)); diff --git a/utils/pom.xml b/utils/pom.xml index 013d6831851..52e5dd5fd4f 100755 --- a/utils/pom.xml +++ b/utils/pom.xml @@ -35,6 +35,11 @@ cloud-framework-managed-context ${project.version} + + org.apache.cloudstack + cloud-framework-ca + ${project.version} + org.springframework spring-context @@ -71,6 +76,10 @@ org.bouncycastle bcprov-jdk15on + + org.bouncycastle + bcpkix-jdk15on + com.jcraft jsch diff --git a/utils/src/main/java/com/cloud/utils/StringUtils.java b/utils/src/main/java/com/cloud/utils/StringUtils.java index 9554e87c35e..6ada2ad60bd 100644 --- a/utils/src/main/java/com/cloud/utils/StringUtils.java +++ b/utils/src/main/java/com/cloud/utils/StringUtils.java @@ -21,10 +21,12 @@ package com.cloud.utils; import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Random; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -320,4 +322,10 @@ public class StringUtils { } return listOfChunks; } + + public static String shuffleCSVList(final String csvList) { + List list = csvTagsToList(csvList); + Collections.shuffle(list, new Random(System.nanoTime())); + return join(list, ","); + } } diff --git a/utils/src/main/java/com/cloud/utils/exception/TaskExecutionException.java b/utils/src/main/java/com/cloud/utils/exception/TaskExecutionException.java index be639baeaeb..635874e9c81 100644 --- a/utils/src/main/java/com/cloud/utils/exception/TaskExecutionException.java +++ b/utils/src/main/java/com/cloud/utils/exception/TaskExecutionException.java @@ -25,7 +25,7 @@ import com.cloud.utils.SerialVersionUID; * Used by the Task class to wrap-up its exceptions. */ public class TaskExecutionException extends Exception { - private static final long serialVersionUID = SerialVersionUID.NioConnectionException; + private static final long serialVersionUID = SerialVersionUID.TaskExecutionException; protected int csErrorCode; diff --git a/utils/src/main/java/com/cloud/utils/nio/Link.java b/utils/src/main/java/com/cloud/utils/nio/Link.java index 02ffaab4e02..e8f36c69944 100644 --- a/utils/src/main/java/com/cloud/utils/nio/Link.java +++ b/utils/src/main/java/com/cloud/utils/nio/Link.java @@ -19,20 +19,6 @@ package com.cloud.utils.nio; -import com.cloud.utils.PropertiesUtil; -import com.cloud.utils.db.DbProperties; -import org.apache.cloudstack.utils.security.SSLUtils; -import org.apache.log4j.Logger; - -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLEngineResult; -import javax.net.ssl.SSLEngineResult.HandshakeStatus; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLSession; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -44,8 +30,27 @@ import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.security.SecureRandom; import java.util.concurrent.ConcurrentLinkedQueue; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import org.apache.cloudstack.framework.ca.CAService; +import org.apache.cloudstack.utils.security.KeyStoreUtils; +import org.apache.cloudstack.utils.security.SSLUtils; +import org.apache.log4j.Logger; + +import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.db.DbProperties; + /** */ public class Link { @@ -62,7 +67,6 @@ public class Link { private boolean _gotFollowingPacket; private SSLEngine _sslEngine; - public static final String keystoreFile = "/cloudmanagementserver.keystore"; public Link(InetSocketAddress addr, NioConnection connection) { _addr = addr; @@ -97,49 +101,6 @@ public class Link { _sslEngine = sslEngine; } - /** - * No user, so comment it out. - * - * Static methods for reading from a channel in case - * you need to add a client that doesn't require nio. - * @param ch channel to read from. - * @param bytebuffer to use. - * @return bytes read - * @throws IOException if not read to completion. - public static byte[] read(SocketChannel ch, ByteBuffer buff) throws IOException { - synchronized(buff) { - buff.clear(); - buff.limit(4); - - while (buff.hasRemaining()) { - if (ch.read(buff) == -1) { - throw new IOException("Connection closed with -1 on reading size."); - } - } - - buff.flip(); - - int length = buff.getInt(); - ByteArrayOutputStream output = new ByteArrayOutputStream(length); - WritableByteChannel outCh = Channels.newChannel(output); - - int count = 0; - while (count < length) { - buff.clear(); - int read = ch.read(buff); - if (read < 0) { - throw new IOException("Connection closed with -1 on reading data."); - } - count += read; - buff.flip(); - outCh.write(buff); - } - - return output.toByteArray(); - } - } - */ - private static void doWrite(SocketChannel ch, ByteBuffer[] buffers, SSLEngine sslEngine) throws IOException { SSLSession sslSession = sslEngine.getSession(); ByteBuffer pkgBuf = ByteBuffer.allocate(sslSession.getPacketBufferSize() + 40); @@ -404,44 +365,78 @@ public class Link { _connection.scheduleTask(task); } - public static SSLContext initSSLContext(boolean isClient) throws GeneralSecurityException, IOException { - InputStream stream; - SSLContext sslContext = null; - KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); - TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); - KeyStore ks = KeyStore.getInstance("JKS"); - TrustManager[] tms; + public static SSLEngine initServerSSLEngine(final CAService caService, final String clientAddress) throws GeneralSecurityException, IOException { + final SSLContext sslContext = SSLUtils.getSSLContext(); + if (caService != null) { + return caService.createSSLEngine(sslContext, clientAddress); + } + s_logger.error("CA service is not configured, by-passing CA manager to create SSL engine"); + char[] passphrase = KeyStoreUtils.defaultKeystorePassphrase; + final KeyStore ks = loadKeyStore(NioConnection.class.getResourceAsStream("/cloud.keystore"), passphrase); + final KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + final TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + kmf.init(ks, passphrase); + tmf.init(ks); + sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom()); + return sslContext.createSSLEngine(); + } - File confFile = PropertiesUtil.findConfigFile("db.properties"); - if (null != confFile && !isClient) { - final String pass = DbProperties.getDbProperties().getProperty("db.cloud.keyStorePassphrase"); - char[] passphrase = "vmops.com".toCharArray(); + public static KeyStore loadKeyStore(final InputStream stream, final char[] passphrase) throws GeneralSecurityException, IOException { + final KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(stream, passphrase); + return ks; + } + + public static SSLContext initClientSSLContext() throws GeneralSecurityException, IOException { + final SSLContext sslContext = SSLUtils.getSSLContext(); + + char[] passphrase = KeyStoreUtils.defaultKeystorePassphrase; + File confFile = PropertiesUtil.findConfigFile("agent.properties"); + if (confFile != null) { + s_logger.info("Conf file found: " + confFile.getAbsolutePath()); + final String pass = PropertiesUtil.loadFromFile(confFile).getProperty(KeyStoreUtils.passphrasePropertyName); if (pass != null) { passphrase = pass.toCharArray(); } - String confPath = confFile.getParent(); - String keystorePath = confPath + keystoreFile; - if (new File(keystorePath).exists()) { - stream = new FileInputStream(keystorePath); - } else { - s_logger.warn("SSL: Fail to find the generated keystore. Loading fail-safe one to continue."); - stream = NioConnection.class.getResourceAsStream("/cloud.keystore"); - passphrase = "vmops.com".toCharArray(); - } - ks.load(stream, passphrase); - stream.close(); - kmf.init(ks, passphrase); - tmf.init(ks); - tms = tmf.getTrustManagers(); } else { - ks.load(null, null); - kmf.init(ks, null); - tms = new TrustManager[1]; - tms[0] = new TrustAllManager(); + confFile = PropertiesUtil.findConfigFile("db.properties"); + if (confFile != null) { + final String pass = DbProperties.getDbProperties().getProperty("db.cloud.keyStorePassphrase"); + if (pass != null) { + passphrase = pass.toCharArray(); + } + } } - sslContext = SSLUtils.getSSLContext(); - sslContext.init(kmf.getKeyManagers(), tms, null); + InputStream stream = null; + if (confFile != null) { + final String keystorePath = confFile.getParent() + "/" + KeyStoreUtils.defaultKeystoreFile; + if (new File(keystorePath).exists()) { + stream = new FileInputStream(keystorePath); + } + } + + final KeyStore ks = loadKeyStore(stream, passphrase); + final TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + tmf.init(ks); + TrustManager[] tms; + if (stream != null) { + // This enforces a two-way SSL authentication + tms = tmf.getTrustManagers(); + } else { + // This enforces a one-way SSL authentication + tms = new TrustManager[]{new TrustAllManager()}; + s_logger.warn("Failed to load keystore, using trust all manager"); + } + + if (stream != null) { + stream.close(); + } + + final KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(ks, passphrase); + sslContext.init(kmf.getKeyManagers(), tms, new SecureRandom()); + if (s_logger.isTraceEnabled()) { s_logger.trace("SSL: SSLcontext has been initialized"); } @@ -498,8 +493,9 @@ public class Link { try { result = sslEngine.unwrap(peerNetData, peerAppData); peerNetData.compact(); - } catch (SSLException sslException) { - s_logger.error("SSL error occurred while processing unwrap data: " + sslException.getMessage()); + } catch (final SSLException sslException) { + s_logger.error(String.format("SSL error caught during unwrap data: %s, for local address=%s, remote address=%s. The client may have invalid ca-certificates.", + sslException.getMessage(), socketChannel.getLocalAddress(), socketChannel.getRemoteAddress())); sslEngine.closeOutbound(); return true; } @@ -539,8 +535,9 @@ public class Link { SSLEngineResult result = null; try { result = sslEngine.wrap(myAppData, myNetData); - } catch (SSLException sslException) { - s_logger.error("SSL error occurred while processing wrap data: " + sslException.getMessage()); + } catch (final SSLException sslException) { + s_logger.error(String.format("SSL error caught during wrap data: %s, for local address=%s, remote address=%s.", + sslException.getMessage(), socketChannel.getLocalAddress(), socketChannel.getRemoteAddress())); sslEngine.closeOutbound(); return true; } diff --git a/utils/src/main/java/com/cloud/utils/nio/NioClient.java b/utils/src/main/java/com/cloud/utils/nio/NioClient.java index dc4f670de12..1c29b0c1a2d 100644 --- a/utils/src/main/java/com/cloud/utils/nio/NioClient.java +++ b/utils/src/main/java/com/cloud/utils/nio/NioClient.java @@ -56,7 +56,7 @@ public class NioClient extends NioConnection { _clientConnection.connect(peerAddr); _clientConnection.configureBlocking(false); - final SSLContext sslContext = Link.initSSLContext(true); + final SSLContext sslContext = Link.initClientSSLContext(); SSLEngine sslEngine = sslContext.createSSLEngine(_host, _port); sslEngine.setUseClientMode(true); sslEngine.setEnabledProtocols(SSLUtils.getSupportedProtocols(sslEngine.getEnabledProtocols())); diff --git a/utils/src/main/java/com/cloud/utils/nio/NioConnection.java b/utils/src/main/java/com/cloud/utils/nio/NioConnection.java index ce032462ec2..30000cf618b 100644 --- a/utils/src/main/java/com/cloud/utils/nio/NioConnection.java +++ b/utils/src/main/java/com/cloud/utils/nio/NioConnection.java @@ -19,13 +19,8 @@ package com.cloud.utils.nio; -import com.cloud.utils.concurrency.NamedThreadFactory; -import com.cloud.utils.exception.NioConnectionException; -import org.apache.cloudstack.utils.security.SSLUtils; -import org.apache.log4j.Logger; +import static com.cloud.utils.AutoCloseableUtil.closeAutoCloseable; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; import java.io.IOException; import java.net.ConnectException; import java.net.InetSocketAddress; @@ -49,7 +44,14 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import static com.cloud.utils.AutoCloseableUtil.closeAutoCloseable; +import javax.net.ssl.SSLEngine; + +import org.apache.cloudstack.framework.ca.CAService; +import org.apache.cloudstack.utils.security.SSLUtils; +import org.apache.log4j.Logger; + +import com.cloud.utils.concurrency.NamedThreadFactory; +import com.cloud.utils.exception.NioConnectionException; /** * NioConnection abstracts the NIO socket operations. The Java implementation @@ -70,6 +72,7 @@ public abstract class NioConnection implements Callable { protected String _name; protected ExecutorService _executor; protected ExecutorService _sslHandshakeExecutor; + protected CAService caService; public NioConnection(final String name, final int port, final int workers, final HandlerFactory factory) { _name = name; @@ -81,6 +84,10 @@ public abstract class NioConnection implements Callable { _sslHandshakeExecutor = Executors.newCachedThreadPool(new NamedThreadFactory(name + "-SSLHandshakeHandler")); } + public void setCAService(final CAService caService) { + this.caService = caService; + } + public void start() throws NioConnectionException { _todos = new ArrayList(); @@ -124,7 +131,7 @@ public abstract class NioConnection implements Callable { public Boolean call() throws NioConnectionException { while (_isRunning) { try { - _selector.select(100); + _selector.select(50); // Someone is ready for I/O, get the ready keys final Set readyKeys = _selector.selectedKeys(); @@ -196,10 +203,8 @@ public abstract class NioConnection implements Callable { final SSLEngine sslEngine; try { - final SSLContext sslContext = Link.initSSLContext(false); - sslEngine = sslContext.createSSLEngine(); + sslEngine = Link.initServerSSLEngine(caService, socketChannel.getRemoteAddress().toString()); sslEngine.setUseClientMode(false); - sslEngine.setNeedClientAuth(false); sslEngine.setEnabledProtocols(SSLUtils.getSupportedProtocols(sslEngine.getEnabledProtocols())); final NioConnection nioConnection = this; _sslHandshakeExecutor.submit(new Runnable() { diff --git a/utils/src/main/java/com/cloud/utils/nio/NioServer.java b/utils/src/main/java/com/cloud/utils/nio/NioServer.java index b655f1838bb..ff54165841e 100644 --- a/utils/src/main/java/com/cloud/utils/nio/NioServer.java +++ b/utils/src/main/java/com/cloud/utils/nio/NioServer.java @@ -27,6 +27,7 @@ import java.nio.channels.ServerSocketChannel; import java.nio.channels.spi.SelectorProvider; import java.util.WeakHashMap; +import org.apache.cloudstack.framework.ca.CAService; import org.apache.log4j.Logger; public class NioServer extends NioConnection { @@ -37,8 +38,9 @@ public class NioServer extends NioConnection { protected WeakHashMap _links; - public NioServer(final String name, final int port, final int workers, final HandlerFactory factory) { + public NioServer(final String name, final int port, final int workers, final HandlerFactory factory, final CAService caService) { super(name, port, workers, factory); + setCAService(caService); _localAddr = null; _links = new WeakHashMap(1024); } diff --git a/utils/src/main/java/com/cloud/utils/script/Script.java b/utils/src/main/java/com/cloud/utils/script/Script.java index 76084751417..01f18bda2d2 100644 --- a/utils/src/main/java/com/cloud/utils/script/Script.java +++ b/utils/src/main/java/com/cloud/utils/script/Script.java @@ -37,6 +37,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import org.apache.cloudstack.utils.security.KeyStoreUtils; import org.apache.commons.io.IOUtils; import org.apache.log4j.Logger; import org.joda.time.Duration; @@ -202,7 +203,7 @@ public class Script implements Callable { String[] command = _command.toArray(new String[_command.size()]); if (_logger.isDebugEnabled()) { - _logger.debug("Executing: " + buildCommandLine(command)); + _logger.debug("Executing: " + buildCommandLine(command).split(KeyStoreUtils.defaultKeystoreFile)[0]); } try { diff --git a/utils/src/main/java/com/cloud/utils/ssh/SSHCmdHelper.java b/utils/src/main/java/com/cloud/utils/ssh/SSHCmdHelper.java index 61d01c4ebd9..10407b65642 100644 --- a/utils/src/main/java/com/cloud/utils/ssh/SSHCmdHelper.java +++ b/utils/src/main/java/com/cloud/utils/ssh/SSHCmdHelper.java @@ -22,8 +22,10 @@ package com.cloud.utils.ssh; import java.io.IOException; import java.io.InputStream; +import org.apache.cloudstack.utils.security.KeyStoreUtils; import org.apache.log4j.Logger; +import com.google.common.base.Strings; import com.trilead.ssh2.ChannelCondition; import com.trilead.ssh2.Session; @@ -32,6 +34,44 @@ public class SSHCmdHelper { private static final int DEFAULT_CONNECT_TIMEOUT = 180000; private static final int DEFAULT_KEX_TIMEOUT = 60000; + public static class SSHCmdResult { + private int returnCode = -1; + private String stdOut; + private String stdErr; + + public SSHCmdResult(final int returnCode, final String stdOut, final String stdErr) { + this.returnCode = returnCode; + this.stdOut = stdOut; + this.stdErr = stdErr; + } + + @Override + public String toString() { + return String.format("SSH cmd result: return code=%d, stdout=%s, stderr=%s", + getReturnCode(), getStdOut().split("-----BEGIN")[0], getStdErr()); + } + + public boolean isSuccess() { + return returnCode == 0; + } + + public int getReturnCode() { + return returnCode; + } + + public void setReturnCode(int returnCode) { + this.returnCode = returnCode; + } + + public String getStdOut() { + return stdOut; + } + + public String getStdErr() { + return stdErr; + } + } + public static com.trilead.ssh2.Connection acquireAuthorizedConnection(String ip, String username, String password) { return acquireAuthorizedConnection(ip, 22, username, password); } @@ -65,36 +105,41 @@ public class SSHCmdHelper { public static boolean sshExecuteCmd(com.trilead.ssh2.Connection sshConnection, String cmd, int nTimes) { for (int i = 0; i < nTimes; i++) { try { - if (sshExecuteCmdOneShot(sshConnection, cmd)) + final SSHCmdResult result = sshExecuteCmdOneShot(sshConnection, cmd); + if (result.isSuccess()) { return true; - } catch (SshException e) { + } + } catch (SshException ignored) { continue; } } return false; } - public static int sshExecuteCmdWithExitCode(com.trilead.ssh2.Connection sshConnection, String cmd) { - return sshExecuteCmdWithExitCode(sshConnection, cmd, 3); - } - - public static int sshExecuteCmdWithExitCode(com.trilead.ssh2.Connection sshConnection, String cmd, int nTimes) { + public static SSHCmdResult sshExecuteCmdWithResult(com.trilead.ssh2.Connection sshConnection, String cmd, int nTimes) { for (int i = 0; i < nTimes; i++) { try { - return sshExecuteCmdOneShotWithExitCode(sshConnection, cmd); - } catch (SshException e) { + final SSHCmdResult result = sshExecuteCmdOneShot(sshConnection, cmd); + if (result.isSuccess()) { + return result; + } + } catch (SshException ignored) { continue; } } - return -1; + return new SSHCmdResult(-1, null, null); } public static boolean sshExecuteCmd(com.trilead.ssh2.Connection sshConnection, String cmd) { return sshExecuteCmd(sshConnection, cmd, 3); } - public static int sshExecuteCmdOneShotWithExitCode(com.trilead.ssh2.Connection sshConnection, String cmd) throws SshException { - s_logger.debug("Executing cmd: " + cmd); + public static SSHCmdResult sshExecuteCmdWithResult(com.trilead.ssh2.Connection sshConnection, String cmd) { + return sshExecuteCmdWithResult(sshConnection, cmd, 3); + } + + public static SSHCmdResult sshExecuteCmdOneShot(com.trilead.ssh2.Connection sshConnection, String cmd) throws SshException { + s_logger.debug("Executing cmd: " + cmd.split(KeyStoreUtils.defaultKeystoreFile)[0]); Session sshSession = null; try { sshSession = sshConnection.openSession(); @@ -112,7 +157,8 @@ public class SSHCmdHelper { InputStream stderr = sshSession.getStderr(); byte[] buffer = new byte[8192]; - StringBuffer sbResult = new StringBuffer(); + StringBuffer sbStdoutResult = new StringBuffer(); + StringBuffer sbStdErrResult = new StringBuffer(); int currentReadBytes = 0; while (true) { @@ -145,27 +191,30 @@ public class SSHCmdHelper { while (stdout.available() > 0) { currentReadBytes = stdout.read(buffer); - sbResult.append(new String(buffer, 0, currentReadBytes)); + sbStdoutResult.append(new String(buffer, 0, currentReadBytes)); } while (stderr.available() > 0) { currentReadBytes = stderr.read(buffer); - sbResult.append(new String(buffer, 0, currentReadBytes)); + sbStdErrResult.append(new String(buffer, 0, currentReadBytes)); } } - String result = sbResult.toString(); - if (result != null && !result.isEmpty()) - s_logger.debug(cmd + " output:" + result); + final SSHCmdResult result = new SSHCmdResult(-1, sbStdoutResult.toString(), sbStdErrResult.toString()); + if (!Strings.isNullOrEmpty(result.getStdOut()) || !Strings.isNullOrEmpty(result.getStdErr())) { + s_logger.debug("SSH command: " + cmd.split(KeyStoreUtils.defaultKeystoreFile)[0] + "\nSSH command output:" + result.getStdOut().split("-----BEGIN")[0] + "\n" + result.getStdErr()); + } + // exit status delivery might get delayed for(int i = 0 ; i<10 ; i++ ) { Integer status = sshSession.getExitStatus(); if( status != null ) { - return status; + result.setReturnCode(status); + return result; } Thread.sleep(100); } - return -1; + return result; } catch (Exception e) { s_logger.debug("Ssh executed failed", e); throw new SshException("Ssh executed failed " + e.getMessage()); @@ -174,8 +223,4 @@ public class SSHCmdHelper { sshSession.close(); } } - - public static boolean sshExecuteCmdOneShot(com.trilead.ssh2.Connection sshConnection, String cmd) throws SshException { - return sshExecuteCmdOneShotWithExitCode(sshConnection, cmd) == 0; - } } diff --git a/utils/src/main/java/org/apache/cloudstack/utils/security/CertUtils.java b/utils/src/main/java/org/apache/cloudstack/utils/security/CertUtils.java new file mode 100644 index 00000000000..c2ef9edb8c4 --- /dev/null +++ b/utils/src/main/java/org/apache/cloudstack/utils/security/CertUtils.java @@ -0,0 +1,240 @@ +// +// 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.utils.security; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.List; + +import javax.security.auth.x500.X500Principal; + +import org.apache.log4j.Logger; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v1CertificateBuilder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.cert.jcajce.JcaX509v1CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; +import org.bouncycastle.util.io.pem.PemWriter; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import com.google.common.base.Strings; + +//import org.bouncycastle.x509.extension.SubjectKeyIdentifierStructure; + +public class CertUtils { + + private static final Logger LOG = Logger.getLogger(CertUtils.class); + + public static KeyPair generateRandomKeyPair(final int keySize) throws NoSuchProviderException, NoSuchAlgorithmException { + Security.addProvider(new BouncyCastleProvider()); + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); + keyPairGenerator.initialize(keySize, new SecureRandom()); + return keyPairGenerator.generateKeyPair(); + } + + public static KeyFactory getKeyFactory() { + KeyFactory keyFactory = null; + try { + Security.addProvider(new BouncyCastleProvider()); + keyFactory = KeyFactory.getInstance("RSA", "BC"); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + LOG.error("Unable to create KeyFactory:" + e.getMessage()); + } + return keyFactory; + } + + public static X509Certificate pemToX509Certificate(final String pem) throws CertificateException, IOException { + final PEMParser pemParser = new PEMParser(new StringReader(pem)); + return new JcaX509CertificateConverter().setProvider("BC").getCertificate((X509CertificateHolder) pemParser.readObject()); + } + + public static String x509CertificateToPem(final X509Certificate cert) throws IOException { + final StringWriter sw = new StringWriter(); + try (final JcaPEMWriter pw = new JcaPEMWriter(sw)) { + pw.writeObject(cert); + pw.flush(); + } + return sw.toString(); + } + + public static String x509CertificatesToPem(final List certificates) throws IOException { + if (certificates == null) { + return ""; + } + final StringBuilder buffer = new StringBuilder(); + for (final X509Certificate certificate: certificates) { + buffer.append(CertUtils.x509CertificateToPem(certificate)); + } + return buffer.toString(); + } + + public static PrivateKey pemToPrivateKey(final String pem) throws InvalidKeySpecException, IOException { + final PemReader pr = new PemReader(new StringReader(pem)); + final PemObject pemObject = pr.readPemObject(); + final KeyFactory keyFactory = getKeyFactory(); + return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(pemObject.getContent())); + } + + public static String privateKeyToPem(final PrivateKey key) throws IOException { + final PemObject pemObject = new PemObject("RSA PRIVATE KEY", key.getEncoded()); + final StringWriter sw = new StringWriter(); + try (final PemWriter pw = new PemWriter(sw)) { + pw.writeObject(pemObject); + } + return sw.toString(); + } + + public static PublicKey pemToPublicKey(final String pem) throws InvalidKeySpecException, IOException { + final PemReader pr = new PemReader(new StringReader(pem)); + final PemObject pemObject = pr.readPemObject(); + final KeyFactory keyFactory = getKeyFactory(); + return keyFactory.generatePublic(new X509EncodedKeySpec(pemObject.getContent())); + } + + public static String publicKeyToPem(final PublicKey key) throws IOException { + final PemObject pemObject = new PemObject("PUBLIC KEY", key.getEncoded()); + final StringWriter sw = new StringWriter(); + try (final PemWriter pw = new PemWriter(sw)) { + pw.writeObject(pemObject); + } + return sw.toString(); + } + + public static BigInteger generateRandomBigInt() { + return new BigInteger(64, new SecureRandom()); + } + + public static X509Certificate generateV1Certificate(final KeyPair keyPair, + final String subject, + final String issuer, + final int validityYears, + final String signatureAlgorithm) throws CertificateException, NoSuchAlgorithmException, NoSuchProviderException, SignatureException, InvalidKeyException, OperatorCreationException { + final DateTime now = DateTime.now(DateTimeZone.UTC); + final X509v1CertificateBuilder certBuilder = new JcaX509v1CertificateBuilder( + new X500Name(issuer), + generateRandomBigInt(), + now.minusDays(1).toDate(), + now.plusYears(validityYears).toDate(), + new X500Name(subject), + keyPair.getPublic()); + final ContentSigner signer = new JcaContentSignerBuilder(signatureAlgorithm).setProvider("BC").build(keyPair.getPrivate()); + final X509CertificateHolder certHolder = certBuilder.build(signer); + return new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder); + } + + public static X509Certificate generateV3Certificate(final X509Certificate caCert, + final PrivateKey caPrivateKey, + final PublicKey clientPublicKey, + final String subject, + final String signatureAlgorithm, + final int validityDays, + final List dnsNames, + final List publicIPAddresses) throws IOException, NoSuchAlgorithmException, CertificateException, NoSuchProviderException, InvalidKeyException, SignatureException, OperatorCreationException { + + final DateTime now = DateTime.now(DateTimeZone.UTC); + final BigInteger serial = generateRandomBigInt(); + final X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( + caCert, + serial, + now.minusHours(12).toDate(), + now.plusDays(validityDays).toDate(), + new X500Principal(subject), + clientPublicKey); + + final JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils(); + + certBuilder.addExtension( + Extension.subjectKeyIdentifier, + false, + extUtils.createSubjectKeyIdentifier(clientPublicKey)); + + certBuilder.addExtension( + Extension.authorityKeyIdentifier, + false, + extUtils.createAuthorityKeyIdentifier(caCert)); + + final List subjectAlternativeNames = new ArrayList(); + if (publicIPAddresses != null) { + for (final String publicIPAddress: publicIPAddresses) { + if (Strings.isNullOrEmpty(publicIPAddress)) { + continue; + } + subjectAlternativeNames.add(new GeneralName(GeneralName.iPAddress, publicIPAddress)); + } + } + if (dnsNames != null) { + for (final String dnsName : dnsNames) { + if (Strings.isNullOrEmpty(dnsName)) { + continue; + } + subjectAlternativeNames.add(new GeneralName(GeneralName.dNSName, dnsName)); + } + } + if (subjectAlternativeNames.size() > 0) { + final GeneralNames subjectAltNames = GeneralNames.getInstance(new DERSequence(subjectAlternativeNames.toArray(new ASN1Encodable[] {}))); + certBuilder.addExtension( + Extension.subjectAlternativeName, + false, + subjectAltNames); + } + + final ContentSigner signer = new JcaContentSignerBuilder(signatureAlgorithm).setProvider("BC").build(caPrivateKey); + final X509CertificateHolder certHolder = certBuilder.build(signer); + final X509Certificate cert = new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder); + cert.verify(caCert.getPublicKey()); + return cert; + } +} diff --git a/utils/src/main/java/org/apache/cloudstack/utils/security/KeyStoreUtils.java b/utils/src/main/java/org/apache/cloudstack/utils/security/KeyStoreUtils.java new file mode 100644 index 00000000000..e02d3b09d8f --- /dev/null +++ b/utils/src/main/java/org/apache/cloudstack/utils/security/KeyStoreUtils.java @@ -0,0 +1,70 @@ +// +// 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.utils.security; + +import java.io.File; +import java.io.IOException; + +import com.cloud.utils.script.Script; +import com.google.common.base.Strings; + +public class KeyStoreUtils { + + public static String defaultTmpKeyStoreFile = "/tmp/tmp.jks"; + public static String defaultKeystoreFile = "cloud.jks"; + public static String defaultPrivateKeyFile = "cloud.key"; + public static String defaultCsrFile = "cloud.csr"; + public static String defaultCertFile = "cloud.crt"; + public static String defaultCaCertFile = "cloud.ca.crt"; + public static char[] defaultKeystorePassphrase = "vmops.com".toCharArray(); + + public static String certNewlineEncoder = "^"; + public static String certSpaceEncoder = "~"; + + public static String keyStoreSetupScript = "keystore-setup"; + public static String keyStoreImportScript = "keystore-cert-import"; + public static String passphrasePropertyName = "keystore.passphrase"; + + public static String sshMode = "ssh"; + public static String agentMode = "agent"; + + public static void copyKeystore(final String keystorePath, final String tmpKeystorePath) throws IOException { + if (Strings.isNullOrEmpty(keystorePath) || Strings.isNullOrEmpty(tmpKeystorePath)) { + throw new IOException("Invalid keystore path provided"); + } + try { + final Script script = new Script(true, "cp", 5000, null); + script.add("-f"); + script.add(tmpKeystorePath); + script.add(keystorePath); + final String result = script.execute(); + if (result != null) { + throw new IOException("Failed to execute cp to copy keystore file to mgmt server conf location"); + } + } catch (final Exception e) { + throw new IOException("Failed to create keystore file: " + keystorePath, e); + } + try { + new File(tmpKeystorePath).delete(); + } catch (Exception ignored) { + } + } + +} diff --git a/utils/src/test/java/com/cloud/utils/StringUtilsTest.java b/utils/src/test/java/com/cloud/utils/StringUtilsTest.java index 3619ede84b2..e8e62b0a75e 100644 --- a/utils/src/test/java/com/cloud/utils/StringUtilsTest.java +++ b/utils/src/test/java/com/cloud/utils/StringUtilsTest.java @@ -20,6 +20,8 @@ package com.cloud.utils; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertNotEquals; import java.nio.charset.Charset; @@ -250,4 +252,15 @@ public class StringUtilsTest { assertEquals("a,b,c", StringUtils.listToCsvTags(Arrays.asList("a","b", "c"))); assertEquals("", StringUtils.listToCsvTags(new ArrayList())); } + + @Test + public void testShuffleCSVList() { + String input = "one,two,three,four,five,six,seven,eight,nine,ten"; + String output = StringUtils.shuffleCSVList(input); + assertFalse(input.equals(output)); + + input = "only-one"; + output = StringUtils.shuffleCSVList("only-one"); + assertTrue(input.equals(output)); + } } diff --git a/utils/src/test/java/com/cloud/utils/testcase/NioTest.java b/utils/src/test/java/com/cloud/utils/testcase/NioTest.java index 894aa1adc88..0a9deea1a9d 100644 --- a/utils/src/test/java/com/cloud/utils/testcase/NioTest.java +++ b/utils/src/test/java/com/cloud/utils/testcase/NioTest.java @@ -98,7 +98,7 @@ public class NioTest { testBytes = new byte[1000000]; randomGenerator.nextBytes(testBytes); - server = new NioServer("NioTestServer", 0, 1, new NioTestServer()); + server = new NioServer("NioTestServer", 0, 1, new NioTestServer(), null); try { server.start(); } catch (final NioConnectionException e) { diff --git a/utils/src/test/java/org/apache/cloudstack/utils/security/CertUtilsTest.java b/utils/src/test/java/org/apache/cloudstack/utils/security/CertUtilsTest.java new file mode 100644 index 00000000000..406f60449f7 --- /dev/null +++ b/utils/src/test/java/org/apache/cloudstack/utils/security/CertUtilsTest.java @@ -0,0 +1,118 @@ +// +// 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.utils.security; + +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPublicKey; +import java.util.Arrays; +import java.util.List; + +import org.bouncycastle.asn1.x509.GeneralName; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class CertUtilsTest { + KeyPair caKeyPair; + X509Certificate caCertificate; + + @Before + public void setUp() throws Exception { + caKeyPair = CertUtils.generateRandomKeyPair(1024); + caCertificate = CertUtils.generateV1Certificate(caKeyPair, "CN=test", "CN=test", 1, "SHA256WithRSAEncryption"); + } + + @Test + public void testGenerateRandomKeyPair() throws Exception { + final int size = 2048; + final KeyPair kp = CertUtils.generateRandomKeyPair(size); + Assert.assertEquals(((RSAPublicKey)kp.getPublic()).getModulus().bitLength(), size); + } + + @Test + public void testCertificateConversionMethods() throws Exception { + final X509Certificate in = caCertificate; + final String pem = CertUtils.x509CertificateToPem(in); + final X509Certificate out = CertUtils.pemToX509Certificate(pem); + Assert.assertTrue(pem.startsWith("-----BEGIN CERTIFICATE-----\n")); + Assert.assertTrue(pem.endsWith("-----END CERTIFICATE-----\n")); + Assert.assertEquals(in.getSerialNumber(), out.getSerialNumber()); + Assert.assertArrayEquals(in.getSignature(), out.getSignature()); + Assert.assertEquals(in.getSigAlgName(), out.getSigAlgName()); + Assert.assertEquals(in.getPublicKey(), out.getPublicKey()); + Assert.assertEquals(in.getNotBefore(), out.getNotBefore()); + Assert.assertEquals(in.getNotAfter(), out.getNotAfter()); + Assert.assertEquals(in.getIssuerDN().toString(), out.getIssuerDN().toString()); + } + + @Test + public void testKeysConversionMethods() throws Exception { + final KeyPair kp = CertUtils.generateRandomKeyPair(2048); + + final PrivateKey inPrivateKey = kp.getPrivate(); + final PrivateKey outPrivateKey = CertUtils.pemToPrivateKey(CertUtils.privateKeyToPem(inPrivateKey)); + Assert.assertEquals(inPrivateKey.getAlgorithm(), outPrivateKey.getAlgorithm()); + Assert.assertEquals(inPrivateKey.getFormat(), outPrivateKey.getFormat()); + Assert.assertArrayEquals(inPrivateKey.getEncoded(), outPrivateKey.getEncoded()); + + final PublicKey inPublicKey = kp.getPublic(); + final PublicKey outPublicKey = CertUtils.pemToPublicKey(CertUtils.publicKeyToPem(inPublicKey)); + Assert.assertEquals(inPublicKey.getAlgorithm(), outPublicKey.getAlgorithm()); + Assert.assertEquals(inPublicKey.getFormat(), inPublicKey.getFormat()); + Assert.assertArrayEquals(inPublicKey.getEncoded(), outPublicKey.getEncoded()); + } + + @Test + public void testGenerateRandomBigInt() throws Exception { + Assert.assertNotEquals(CertUtils.generateRandomBigInt(), CertUtils.generateRandomBigInt()); + } + + @Test + public void testGenerateCertificate() throws Exception { + final KeyPair clientKeyPair = CertUtils.generateRandomKeyPair(1024); + final List domainNames = Arrays.asList("domain1.com", "www.2.domain2.com", "3.domain3.com"); + final List addressList = Arrays.asList("1.2.3.4", "192.168.1.1", "2a02:120b:2c16:f6d0:d9df:8ebc:e44a:f181"); + + final X509Certificate clientCert = CertUtils.generateV3Certificate(caCertificate, caKeyPair.getPrivate(), clientKeyPair.getPublic(), + "CN=domain.example", "SHA256WithRSAEncryption", 10, domainNames, addressList); + + clientCert.verify(caKeyPair.getPublic()); + Assert.assertEquals(clientCert.getIssuerDN(), caCertificate.getIssuerDN()); + Assert.assertEquals(clientCert.getSigAlgName(), "SHA256WITHRSA"); + Assert.assertArrayEquals(clientCert.getPublicKey().getEncoded(), clientKeyPair.getPublic().getEncoded()); + Assert.assertNotNull(clientCert.getSubjectAlternativeNames()); + + for (final List altNames : clientCert.getSubjectAlternativeNames()) { + Assert.assertTrue(altNames.size() == 2); + final Object first = altNames.get(0); + final Object second = altNames.get(1); + if (first instanceof Integer && ((Integer) first) == GeneralName.iPAddress) { + Assert.assertTrue(addressList.contains((String) second)); + } + if (first instanceof Integer && ((Integer) first) == GeneralName.dNSName) { + Assert.assertTrue(domainNames.contains((String) second)); + } + } + } + +} \ No newline at end of file