blob: 928e58a4f25c34465e50d40f310624c8d618c27c [file] [log] [blame]
// 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.network.ssl;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
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.cert.CertPathBuilder;
import java.security.cert.CertPathBuilderException;
import java.security.cert.CertStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.inject.Inject;
import org.apache.cloudstack.acl.SecurityChecker;
import org.apache.cloudstack.api.command.user.loadbalancer.DeleteSslCertCmd;
import org.apache.cloudstack.api.command.user.loadbalancer.ListSslCertsCmd;
import org.apache.cloudstack.api.command.user.loadbalancer.UploadSslCertCmd;
import org.apache.cloudstack.api.response.SslCertResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.network.tls.CertService;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import com.cloud.domain.DomainVO;
import com.cloud.domain.dao.DomainDao;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.network.dao.LoadBalancerCertMapDao;
import com.cloud.network.dao.LoadBalancerCertMapVO;
import com.cloud.network.dao.LoadBalancerVO;
import com.cloud.network.dao.SslCertDao;
import com.cloud.network.dao.SslCertVO;
import com.cloud.network.rules.LoadBalancer;
import com.cloud.projects.Project;
import com.cloud.projects.ProjectService;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.dao.AccountDao;
import com.cloud.utils.db.DB;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.security.CertificateHelper;
import com.google.common.base.Preconditions;
import org.apache.commons.lang3.StringUtils;
public class CertServiceImpl implements CertService {
protected Logger logger = LogManager.getLogger(getClass());
@Inject
AccountManager _accountMgr;
@Inject
AccountDao _accountDao;
@Inject
ProjectService _projectMgr;
@Inject
DomainDao _domainDao;
@Inject
SslCertDao _sslCertDao;
@Inject
LoadBalancerCertMapDao _lbCertDao;
@Inject
EntityManager _entityMgr;
public CertServiceImpl() {
Security.addProvider(new BouncyCastleProvider());
}
@DB
@Override
@ActionEvent(eventType = EventTypes.EVENT_LB_CERT_UPLOAD, eventDescription = "Uploading a certificate to cloudstack", async = false)
public SslCertResponse uploadSslCert(final UploadSslCertCmd certCmd) {
Preconditions.checkNotNull(certCmd);
final String cert = certCmd.getCert();
final String key = certCmd.getKey();
final String password = certCmd.getPassword();
final String chain = certCmd.getChain();
final String name = certCmd.getName();
validate(cert, key, password, chain, certCmd.getEnabledRevocationCheck());
logger.debug("Certificate Validation succeeded");
final String fingerPrint = CertificateHelper.generateFingerPrint(parseCertificate(cert));
final CallContext ctx = CallContext.current();
final Account caller = ctx.getCallingAccount();
Account owner = null;
if (StringUtils.isNotEmpty(certCmd.getAccountName()) && certCmd.getDomainId() != null || certCmd.getProjectId() != null) {
owner = _accountMgr.finalizeOwner(caller, certCmd.getAccountName(), certCmd.getDomainId(), certCmd.getProjectId());
} else {
owner = caller;
}
final Long accountId = owner.getId();
final Long domainId = owner.getDomainId();
final SslCertVO certVO = new SslCertVO(cert, key, password, chain, accountId, domainId, fingerPrint, name);
_sslCertDao.persist(certVO);
return createCertResponse(certVO, null);
}
@DB
@Override
@ActionEvent(eventType = EventTypes.EVENT_LB_CERT_DELETE, eventDescription = "Deleting a certificate to cloudstack", async = false)
public void deleteSslCert(final DeleteSslCertCmd deleteSslCertCmd) {
Preconditions.checkNotNull(deleteSslCertCmd);
final CallContext ctx = CallContext.current();
final Account caller = ctx.getCallingAccount();
final Long certId = deleteSslCertCmd.getId();
final SslCertVO certVO = _sslCertDao.findById(certId);
if (certVO == null) {
throw new InvalidParameterValueException("Invalid certificate id: " + certId);
}
_accountMgr.checkAccess(caller, SecurityChecker.AccessType.OperateEntry, true, certVO);
final List<LoadBalancerCertMapVO> lbCertRule = _lbCertDao.listByCertId(certId);
if (lbCertRule != null && !lbCertRule.isEmpty()) {
StringBuilder lbNames = new StringBuilder();
for (final LoadBalancerCertMapVO rule : lbCertRule) {
final LoadBalancerVO lb = _entityMgr.findById(LoadBalancerVO.class, rule.getLbId());
lbNames.append(lb.getName()).append(" ");
}
throw new CloudRuntimeException("Certificate in use by a loadbalancer(s) " + lbNames.toString());
}
_sslCertDao.remove(certId);
}
@Override
public List<SslCertResponse> listSslCerts(final ListSslCertsCmd listSslCertCmd) {
Preconditions.checkNotNull(listSslCertCmd);
final CallContext ctx = CallContext.current();
final Account caller = ctx.getCallingAccount();
final Long certId = listSslCertCmd.getCertId();
final Long accountId = listSslCertCmd.getAccountId();
final Long lbRuleId = listSslCertCmd.getLbId();
final Long projectId = listSslCertCmd.getProjectId();
final List<SslCertResponse> certResponseList = new ArrayList<SslCertResponse>();
if (certId == null && accountId == null && lbRuleId == null && projectId == null) {
throw new InvalidParameterValueException("Invalid parameters either certificate ID or Account ID or Loadbalancer ID or Project ID required");
}
List<LoadBalancerCertMapVO> certLbMap = null;
SslCertVO certVO = null;
if (certId != null) {
certVO = _sslCertDao.findById(certId);
if (certVO == null) {
throw new InvalidParameterValueException("Invalid certificate id: " + certId);
}
_accountMgr.checkAccess(caller, SecurityChecker.AccessType.UseEntry, true, certVO);
certLbMap = _lbCertDao.listByCertId(certId);
certResponseList.add(createCertResponse(certVO, certLbMap));
return certResponseList;
}
if (lbRuleId != null) {
final LoadBalancer lb = _entityMgr.findById(LoadBalancerVO.class, lbRuleId);
if (lb == null) {
throw new InvalidParameterValueException("Found no loadbalancer with id: " + lbRuleId);
}
_accountMgr.checkAccess(caller, SecurityChecker.AccessType.UseEntry, true, lb);
// get the cert id
LoadBalancerCertMapVO lbCertMapRule;
lbCertMapRule = _lbCertDao.findByLbRuleId(lbRuleId);
if (lbCertMapRule == null) {
logger.debug("No certificate bound to loadbalancer id: " + lbRuleId);
return certResponseList;
}
certVO = _sslCertDao.findById(lbCertMapRule.getCertId());
certLbMap = _lbCertDao.listByCertId(lbCertMapRule.getCertId());
certResponseList.add(createCertResponse(certVO, certLbMap));
return certResponseList;
}
if (projectId != null) {
final Project project = _projectMgr.getProject(projectId);
if (project == null) {
throw new InvalidParameterValueException("Found no project with id: " + projectId);
}
final List<SslCertVO> projectCertVOList = _sslCertDao.listByAccountId(project.getProjectAccountId());
if (projectCertVOList == null || projectCertVOList.isEmpty()) {
return certResponseList;
}
_accountMgr.checkAccess(caller, SecurityChecker.AccessType.UseEntry, true, projectCertVOList.get(0));
for (final SslCertVO cert : projectCertVOList) {
certLbMap = _lbCertDao.listByCertId(cert.getId());
certResponseList.add(createCertResponse(cert, certLbMap));
}
return certResponseList;
}
//reached here look by accountId
final List<SslCertVO> certVOList = _sslCertDao.listByAccountId(accountId);
if (certVOList == null || certVOList.isEmpty()) {
return certResponseList;
}
_accountMgr.checkAccess(caller, SecurityChecker.AccessType.UseEntry, true, certVOList.get(0));
for (final SslCertVO cert : certVOList) {
certLbMap = _lbCertDao.listByCertId(cert.getId());
certResponseList.add(createCertResponse(cert, certLbMap));
}
return certResponseList;
}
private void validate(final String certInput, final String keyInput, final String password, final String chainInput, boolean revocationEnabled) {
try {
List<Certificate> chain = null;
final Certificate cert = parseCertificate(certInput);
final PrivateKey key = parsePrivateKey(keyInput);
if (chainInput != null) {
chain = CertificateHelper.parseChain(chainInput);
}
validateCert(cert);
validateKeys(cert.getPublicKey(), key);
if (chainInput != null) {
validateChain(chain, cert, revocationEnabled);
}
} catch (final IOException | CertificateException e) {
throw new IllegalStateException("Parsing certificate/key failed: " + e.getMessage(), e);
}
}
public SslCertResponse createCertResponse(final SslCertVO cert, final List<LoadBalancerCertMapVO> lbCertMap) {
Preconditions.checkNotNull(cert);
final SslCertResponse response = new SslCertResponse();
final Account account = _accountDao.findByIdIncludingRemoved(cert.getAccountId());
if (account.getType() == Account.Type.PROJECT) {
// find the project
final Project project = _projectMgr.findByProjectAccountIdIncludingRemoved(account.getId());
if (project != null)
{
response.setProjectId(project.getUuid());
response.setProjectName(project.getName());
}
response.setAccountName(account.getAccountName());
} else {
response.setAccountName(account.getAccountName());
}
final DomainVO domain = _domainDao.findByIdIncludingRemoved(cert.getDomainId());
response.setDomainId(domain.getUuid());
response.setDomainName(domain.getName());
response.setObjectName("sslcert");
response.setId(cert.getUuid());
response.setCertificate(cert.getCertificate());
response.setFingerprint(cert.getFingerPrint());
response.setName(cert.getName());
if (cert.getChain() != null) {
response.setCertchain(cert.getChain());
}
if (lbCertMap != null && !lbCertMap.isEmpty()) {
final List<String> lbIds = new ArrayList<String>();
for (final LoadBalancerCertMapVO mapVO : lbCertMap) {
final LoadBalancer lb = _entityMgr.findById(LoadBalancerVO.class, mapVO.getLbId());
if (lb != null) {
lbIds.add(lb.getUuid());
}
}
response.setLbIds(lbIds);
}
return response;
}
private void validateCert(final Certificate cert) throws CertificateNotYetValidException, CertificateExpiredException {
Preconditions.checkNotNull(cert);
if (!(cert instanceof X509Certificate)) {
throw new IllegalArgumentException("Invalid certificate format. Expected X509 certificate");
}
((X509Certificate)cert).checkValidity();
}
private void validateKeys(final PublicKey pubKey, final PrivateKey privKey) {
Preconditions.checkNotNull(pubKey);
Preconditions.checkNotNull(privKey);
if (!pubKey.getAlgorithm().equals(privKey.getAlgorithm())) {
throw new IllegalArgumentException("Public and private key have different algorithms");
}
// No encryption for DSA
if (pubKey.getAlgorithm() != "RSA") {
return;
}
try {
final String data = "ENCRYPT_DATA";
final SecureRandom random = new SecureRandom();
final Cipher cipher = Cipher.getInstance(pubKey.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, privKey, random);
final byte[] encryptedData = cipher.doFinal(data.getBytes());
cipher.init(Cipher.DECRYPT_MODE, pubKey, random);
final String decreptedData = new String(cipher.doFinal(encryptedData));
if (!decreptedData.equals(data)) {
throw new IllegalStateException("Bad public-private key");
}
} catch (final BadPaddingException | IllegalBlockSizeException | InvalidKeyException | NoSuchPaddingException e) {
throw new IllegalStateException("Bad public-private key", e);
} catch (final NoSuchAlgorithmException e) {
throw new IllegalStateException("Invalid algorithm for public-private key", e);
}
}
private void validateChain(final List<Certificate> chain, final Certificate cert, boolean revocationEnabled) {
final List<Certificate> certs = new ArrayList<Certificate>();
final Set<TrustAnchor> anchors = new HashSet<TrustAnchor>();
certs.add(cert); // adding for self signed certs
certs.addAll(chain);
for (final Certificate c : certs) {
if (!(c instanceof X509Certificate)) {
throw new IllegalArgumentException("Invalid chain format. Expected X509 certificate");
}
final X509Certificate xCert = (X509Certificate)c;
anchors.add(new TrustAnchor(xCert, null));
}
final X509CertSelector target = new X509CertSelector();
target.setCertificate((X509Certificate)cert);
PKIXBuilderParameters params = null;
try {
params = new PKIXBuilderParameters(anchors, target);
params.setRevocationEnabled(revocationEnabled);
params.addCertStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(certs)));
final CertPathBuilder builder = CertPathBuilder.getInstance("PKIX", "BC");
builder.build(params);
} catch (final InvalidAlgorithmParameterException | CertPathBuilderException | NoSuchAlgorithmException e) {
throw new IllegalStateException("Invalid certificate chain", e);
} catch (final NoSuchProviderException e) {
throw new CloudRuntimeException("No provider for certificate validation", e);
}
}
public PrivateKey parsePrivateKey(final String key) throws IOException {
Preconditions.checkArgument(StringUtils.isNotEmpty(key));
try (final PemReader pemReader = new PemReader(new StringReader(key));) {
final PemObject pemObject = pemReader.readPemObject();
final byte[] content = pemObject.getContent();
final PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(content);
final KeyFactory factory = KeyFactory.getInstance("RSA", "BC");
return factory.generatePrivate(privKeySpec);
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
throw new IOException("No encryption provider available.", e);
} catch (final InvalidKeySpecException e) {
throw new IOException("Invalid Key format.", e);
}
}
@Override
public Certificate parseCertificate(final String cert) {
Preconditions.checkArgument(StringUtils.isNotEmpty(cert));
final PemReader certPem = new PemReader(new StringReader(cert));
try {
return readCertificateFromPemObject(certPem.readPemObject());
} catch (final CertificateException | IOException e) {
throw new InvalidParameterValueException("Invalid Certificate format. Expected X509 certificate. Failed due to " + e.getMessage());
} finally {
IOUtils.closeQuietly(certPem);
}
}
private Certificate readCertificateFromPemObject(final PemObject pemObject) throws CertificateException {
Preconditions.checkNotNull(pemObject);
final ByteArrayInputStream bais = new ByteArrayInputStream(pemObject.getContent());
final CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
return certificateFactory.generateCertificate(bais);
}
}