CLOUDSTACK-5296: Add certificate chain support for netscaler

This patch adds support for trust chains in the netscaler.

I initially planned on using the 10.1 API's "bundle" feature but during
my testing I found that was not working. So I am doing the chain linking
myself. Also NS can have only one entity of a certificate ie lets say
two different users try to add the same certificate on the netscaler
only one of them will go through. The other one says resouce already
exists even though they have different files.

This can be a problem in trust chains where the chain can be shared
between multiple accounts/certificates. So, I am using the figerprint as
an identifier of a certificate and making sure that we delete it only
when no one references it.
This commit is contained in:
Syed Ahmed 2013-12-05 15:32:19 +05:30 committed by Murali Reddy
parent 04b48ae04e
commit ee7380ace2
7 changed files with 272 additions and 50 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
G# Licensed to the Apache Software Foundation (ASF) under one
# 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

View File

@ -437,13 +437,15 @@ public class LoadBalancingRule {
private String key;
private String password = null;
private String chain = null;
private String fingerprint;
private boolean revoked;
public LbSslCert(String cert, String key, String password, String chain, boolean revoked) {
public LbSslCert(String cert, String key, String password, String chain, String fingerprint, boolean revoked) {
this.cert = cert;
this.key = key;
this.password = password;
this.chain = chain;
this.fingerprint = fingerprint;
this.revoked = revoked;
}
@ -464,6 +466,10 @@ public class LoadBalancingRule {
return chain;
}
public String getFingerprint() {
return fingerprint;
}
public boolean isRevoked() {
return revoked;
}

View File

@ -33,7 +33,7 @@ public class SslCertDaoImpl extends GenericDaoBase<SslCertVO, Long> implements S
listByAccountId = createSearchBuilder();
listByAccountId.and("accountId", listByAccountId.entity().getAccountId(), SearchCriteria.Op.EQ);
listByAccountId.done();
}
}
@Override
public List<SslCertVO> listByAccountId(Long accountId) {

View File

@ -16,6 +16,9 @@
// under the License.
package com.cloud.network.resource;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.HashMap;
@ -25,6 +28,15 @@ import java.util.Map;
import javax.naming.ConfigurationException;
import com.citrix.netscaler.nitro.resource.config.ssl.sslcertkey;
import com.citrix.netscaler.nitro.resource.config.ssl.sslcertkey_sslvserver_binding;
import com.citrix.netscaler.nitro.resource.config.ssl.sslcertlink;
import com.citrix.netscaler.nitro.resource.config.ssl.sslvserver_sslcertkey_binding;
import com.cloud.network.lb.LoadBalancingRule.LbSslCert;
import com.cloud.utils.security.CertificateHelper;
import com.cloud.utils.ssh.SshHelper;
import com.google.common.collect.Lists;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.log4j.Logger;
import com.citrix.netscaler.nitro.exception.nitro_exception;
@ -61,8 +73,6 @@ import com.citrix.netscaler.nitro.resource.config.ns.nshardware;
import com.citrix.netscaler.nitro.resource.config.ns.nsip;
import com.citrix.netscaler.nitro.resource.config.ns.nstimer;
import com.citrix.netscaler.nitro.resource.config.ns.nstimer_autoscalepolicy_binding;
import com.citrix.netscaler.nitro.resource.config.ssl.sslcertkey;
import com.citrix.netscaler.nitro.resource.config.ssl.sslvserver_sslcertkey_binding;
import com.citrix.netscaler.nitro.resource.stat.lb.lbvserver_stats;
import com.citrix.netscaler.nitro.service.nitro_service;
import com.citrix.netscaler.nitro.util.filtervalue;
@ -111,7 +121,6 @@ import com.cloud.agent.api.to.LoadBalancerTO.StickinessPolicyTO;
import com.cloud.agent.api.to.StaticNatRuleTO;
import com.cloud.host.Host;
import com.cloud.host.Host.Type;
import com.cloud.network.lb.LoadBalancingRule.LbSslCert;
import com.cloud.network.rules.LbStickinessMethod.StickinessMethodType;
import com.cloud.resource.ServerResource;
import com.cloud.serializer.GsonHelper;
@ -119,7 +128,7 @@ import com.cloud.utils.NumbersUtil;
import com.cloud.utils.Pair;
import com.cloud.utils.exception.ExecutionException;
import com.cloud.utils.net.NetUtils;
import com.cloud.utils.ssh.SshHelper;
import org.bouncycastle.openssl.PEMWriter;
class NitroError {
static final int NS_RESOURCE_EXISTS = 273;
@ -665,23 +674,61 @@ public class NetscalerResource implements ServerResource {
}
if (sslCert != null && lbProtocol.equals(NetUtils.SSL_PROTO)) {
if (sslCert != null && lbProtocol.equalsIgnoreCase(NetUtils.SSL_PROTO)) {
if (sslCert.isRevoked()) {
deleteCert = true;
} else {
String certName = generateSslCertName(srcIp, srcPort);
String keyName = generateSslKeyName(srcIp, srcPort);
String certKeyName = generateSslCertKeyName(srcIp, srcPort);
// If there is a chain, that should go first to the NS
if (SSL.isSslCertKeyPresent(_netscalerService, certKeyName)) {
SSL.deleteSslCertKey(_netscalerService, certKeyName);
String previousCertKeyName = null;
if ( sslCert.getChain() != null ) {
List<Certificate> chainList = CertificateHelper.parseChain(sslCert.getChain());
// go from ROOT to intermediate CAs
for ( Certificate intermediateCert : Lists.reverse(chainList)){
String fingerPrint=CertificateHelper.generateFingerPrint(intermediateCert);
String intermediateCertKeyName = generateSslCertKeyName(fingerPrint);
String intermediateCertFileName = intermediateCertKeyName + ".pem";
if (! SSL.isSslCertKeyPresent(_netscalerService, intermediateCertKeyName)) {
byte[] certData= intermediateCert.getEncoded();
StringWriter textWriter = new StringWriter();
PEMWriter pemWriter = new PEMWriter(textWriter);
pemWriter.writeObject(intermediateCert);
pemWriter.flush();
SSL.uploadCert(_ip, _username, _password, intermediateCertFileName, textWriter.toString().getBytes());
SSL.createSslCertKey(_netscalerService, intermediateCertFileName, null, intermediateCertKeyName, null);
}
if ( previousCertKeyName != null && ! SSL.certLinkExists(_netscalerService, intermediateCertKeyName, previousCertKeyName)){
SSL.linkCerts(_netscalerService, intermediateCertKeyName, previousCertKeyName);
}
previousCertKeyName = intermediateCertKeyName;
}
}
SSL.uploadCert(_ip, _username, _password, certName, sslCert.getCert().getBytes());
SSL.uploadKey(_ip, _username, _password, keyName, sslCert.getKey().getBytes());
String certFilename = generateSslCertName(sslCert.getFingerprint()) + ".pem"; //netscaler uses ".pem" format for "bundle" files
String keyFilename = generateSslKeyName(sslCert.getFingerprint()) + ".pem"; //netscaler uses ".pem" format for "bundle" files
String certKeyName = generateSslCertKeyName(sslCert.getFingerprint());
ByteArrayOutputStream certDataStream = new ByteArrayOutputStream( );
certDataStream.write(sslCert.getCert().getBytes());
if (! SSL.isSslCertKeyPresent(_netscalerService, certKeyName)) {
SSL.uploadCert(_ip, _username, _password, certFilename, certDataStream.toByteArray());
SSL.uploadKey(_ip, _username, _password, keyFilename, sslCert.getKey().getBytes());
SSL.createSslCertKey(_netscalerService, certFilename, keyFilename, certKeyName, sslCert.getPassword());
}
if (previousCertKeyName != null && ! SSL.certLinkExists(_netscalerService, certKeyName, previousCertKeyName)){
SSL.linkCerts(_netscalerService, certKeyName, previousCertKeyName);
}
SSL.createSslCertKey(_netscalerService, certName, keyName, certKeyName, sslCert.getPassword());
SSL.bindCertKeyToVserver(_netscalerService, certKeyName, nsVirtualServerName);
}
@ -778,18 +825,50 @@ public class NetscalerResource implements ServerResource {
}
if (sslCert != null && deleteCert) {
String certName = generateSslCertName(srcIp, srcPort);
String keyName = generateSslKeyName(srcIp, srcPort);
String certKeyName = generateSslCertKeyName(srcIp, srcPort);
String certFilename = generateSslCertName(sslCert.getFingerprint()) + ".pem"; //netscaler uses ".pem" format for "bundle" files
String keyFilename = generateSslKeyName(sslCert.getFingerprint()) + ".pem"; //netscaler uses ".pem" format for "bundle" files
String certKeyName = generateSslCertKeyName(sslCert.getFingerprint());
// unbind before deleting
if (nsVirtualServerExists(nsVirtualServerName)) {
if (nsVirtualServerExists(nsVirtualServerName) &&
SSL.isSslCertKeyPresent(_netscalerService, certKeyName) &&
SSL.isBoundToVserver(_netscalerService, certKeyName, nsVirtualServerName)) {
SSL.unbindCertKeyFromVserver(_netscalerService, certKeyName, nsVirtualServerName);
}
SSL.deleteSslCertKey(_netscalerService, certKeyName);
SSL.deleteCertFile(_ip, _username, _password, certName);
SSL.deleteKeyFile(_ip, _username, _password, keyName);
if (SSL.isSslCertKeyPresent(_netscalerService, certKeyName)) {
SSL.deleteSslCertKey(_netscalerService, certKeyName);
SSL.deleteCertFile(_ip, _username, _password, certFilename);
SSL.deleteKeyFile(_ip, _username, _password, keyFilename);
}
/*
* Check and delete intermediate certs:
* we can delete an intermediate cert if no other
* cert references it as the athority
*/
if ( sslCert.getChain() != null ) {
List<Certificate> chainList = CertificateHelper.parseChain(sslCert.getChain());
//go from intermediate CAs to ROOT
for ( Certificate intermediateCert : chainList){
String fingerPrint=CertificateHelper.generateFingerPrint(intermediateCert);
String intermediateCertKeyName = generateSslCertKeyName(fingerPrint);
String intermediateCertFileName = intermediateCertKeyName + ".pem";
if (SSL.isSslCertKeyPresent(_netscalerService, intermediateCertKeyName) &&
! SSL.isCaforCerts(_netscalerService, intermediateCertKeyName)) {
SSL.deleteSslCertKey(_netscalerService, intermediateCertKeyName);
SSL.deleteCertFile(_ip, _username, _password, intermediateCertFileName);
}else {
break;// if this cert has another certificate as a child then stop at this point because we need the whole chain
}
}
}
}
}
@ -1718,7 +1797,7 @@ public class NetscalerResource implements ServerResource {
return false;
}
private static void deleteSslCertKey(nitro_service ns, String certKeyName) throws ExecutionException {
private static void deleteSslCertKey(nitro_service ns, String certKeyName) throws ExecutionException {
try {
sslcertkey certkey = new sslcertkey();
@ -1733,21 +1812,23 @@ public class NetscalerResource implements ServerResource {
}
private static void deleteCertFile(String nsIp, String username, String password, String certName) throws Exception {
SshHelper.sshExecute(nsIp, SSH_PORT, username, null, password, "shell rm " + SSL_CERT_PATH + certName);
private static void deleteCertFile(String nsIp, String username, String password, String certFilename) throws Exception {
SshHelper.sshExecute(nsIp,SSH_PORT,username,null,password,"shell rm " + SSL_CERT_PATH + certFilename);
}
private static void deleteKeyFile(String nsIp, String username, String password, String keyName) throws Exception {
SshHelper.sshExecute(nsIp, SSH_PORT, username, null, password, "shell rm " + SSL_CERT_PATH + keyName);
private static void deleteKeyFile(String nsIp, String username, String password, String keyFilename) throws Exception {
SshHelper.sshExecute(nsIp, SSH_PORT, username, null, password, "shell rm " + SSL_CERT_PATH + keyFilename);
}
private static void createSslCertKey(nitro_service ns, String certName, String keyName, String certKeyName, String password) throws ExecutionException {
private static void createSslCertKey(nitro_service ns, String certFilename, String keyFilename, String certKeyName, String password) throws ExecutionException {
s_logger.debug("Adding cert to netscaler");
try {
sslcertkey certkey = new sslcertkey();
certkey.set_certkey(certKeyName);
certkey.set_cert(SSL_CERT_PATH + certName);
certkey.set_key(SSL_CERT_PATH + keyName);
certkey.set_cert(SSL_CERT_PATH + certFilename);
if ( keyFilename != null )
certkey.set_key(SSL_CERT_PATH + keyFilename);
if (password != null) {
certkey.set_passplain(password);
@ -1813,17 +1894,17 @@ public class NetscalerResource implements ServerResource {
}
private static void uploadCert(String nsIp, String user, String password, String certName, byte[] certData) throws ExecutionException {
private static void uploadCert(String nsIp, String user, String password, String certFilename, byte[] certData) throws ExecutionException {
try {
SshHelper.scpTo(nsIp, SSH_PORT, user, null, password, SSL_CERT_PATH, certData, certName, null);
} catch (Exception e) {
SshHelper.scpTo(nsIp,SSH_PORT,user,null,password, SSL_CERT_PATH, certData, certFilename, null);
} catch (Exception e){
throw new ExecutionException("Failed to copy private key to device " + e.getMessage());
}
}
private static void uploadKey(String nsIp, String user, String password, String keyName, byte[] keyData) throws ExecutionException {
private static void uploadKey(String nsIp, String user, String password, String keyFilename, byte[] keyData) throws ExecutionException {
try {
SshHelper.scpTo(nsIp, SSH_PORT, user, null, password, SSL_CERT_PATH, keyData, keyName, null);
SshHelper.scpTo(nsIp, SSH_PORT, user, null, password, SSL_CERT_PATH, keyData, keyFilename, null);
} catch (Exception e) {
throw new ExecutionException("Failed to copy private key to device " + e.getMessage());
}
@ -1831,7 +1912,7 @@ public class NetscalerResource implements ServerResource {
private static void enableSslFeature(nitro_service ns) throws ExecutionException {
try {
base_response result = ns.enable_features(new String[] {"SSL"});
base_response result = ns.enable_features(new String[]{"SSL"});
if (result.errorcode != 0)
throw new ExecutionException("Unable to enable SSL on LB");
} catch (nitro_exception e) {
@ -1859,6 +1940,80 @@ public class NetscalerResource implements ServerResource {
}
}
public static boolean certLinkExists(nitro_service ns, String userCertName, String caCertName) throws ExecutionException {
try {
// check if there is a link from userCertName to caCertName
sslcertkey userCert = sslcertkey.get(ns,userCertName);
String nsCaCert = userCert.get_linkcertkeyname();
if (nsCaCert != null && nsCaCert.equals(caCertName))
return true;
} catch (nitro_exception e) {
throw new ExecutionException("Failed to check cert link on load balancer to " + e.getMessage());
} catch (Exception e) {
throw new ExecutionException("Failed to check cert link on load balancer due to " + e.getMessage());
}
return false;
}
public static void linkCerts(nitro_service ns, String userCertName, String caCertName) throws ExecutionException {
try {
// the assumption is that that both userCertName and caCertName are present on NS
sslcertkey caCert = sslcertkey.get(ns, caCertName);
sslcertkey userCert = sslcertkey.get(ns, userCertName);
sslcertkey linkResource = new sslcertkey();
// link user cert to CA cert
linkResource.set_certkey(userCert.get_certkey());
linkResource.set_linkcertkeyname(caCert.get_certkey());
sslcertkey.link(ns, linkResource);
} catch (nitro_exception e) {
throw new ExecutionException("Failed to check cert link on load balancer to " + e.getMessage());
} catch (Exception e) {
throw new ExecutionException("Failed to check cert link on load balancer due to " + e.getMessage());
}
}
public static boolean isCaforCerts(nitro_service ns, String caCertName) throws ExecutionException {
// check if this certificate serves as a CA for other certificates
try {
sslcertlink[] childLinks = sslcertlink.get_filtered(ns,"linkcertkeyname:" + caCertName);
if(childLinks != null && childLinks.length > 0){
return true;
}
} catch (nitro_exception e) {
throw new ExecutionException("Failed to check cert link on load balancer to " + e.getMessage());
} catch (Exception e) {
throw new ExecutionException("Failed to check cert link on load balancer due to " + e.getMessage());
}
return false;
}
public static boolean isBoundToVserver(nitro_service ns, String certKeyName, String nsVirtualServerName) throws ExecutionException {
try {
sslcertkey_sslvserver_binding[] cert_vs_binding = sslcertkey_sslvserver_binding.get_filtered(ns, certKeyName, "vservername:" + nsVirtualServerName);
if(cert_vs_binding != null && cert_vs_binding.length > 0){
return true;
}
} catch (nitro_exception e) {
throw new ExecutionException("Failed to check cert link on load balancer to " + e.getMessage());
} catch (Exception e) {
throw new ExecutionException("Failed to check cert link on load balancer due to " + e.getMessage());
}
return false;
}
}
private void enableVPXInterfaces(String publicIf, String privateIf, ns ns_obj) {
@ -3588,17 +3743,22 @@ public class NetscalerResource implements ServerResource {
return counterName.replace(' ', '_');
}
private String generateSslCertName(String srcIp, long srcPort) {
private String generateSslCertName(String fingerPrint) {
// maximum length supported by NS is 31
return genObjectName("Cloud-Cert", srcIp, srcPort);
// the first 20 characters of the SHA-1 checksum are the unique id
String uniqueId = fingerPrint.replace(":","").substring(0,20);
return genObjectName("Cloud-Cert", uniqueId);
}
private String generateSslKeyName(String srcIp, long srcPort) {
return genObjectName("Cloud-Key", srcIp, srcPort);
private String generateSslKeyName(String fingerPrint) {
String uniqueId = fingerPrint.replace(":","").substring(0,20);
return genObjectName("Cloud-Key", uniqueId);
}
private String generateSslCertKeyName(String srcIp, long srcPort) {
return genObjectName("Cloud-CertKey", srcIp, srcPort);
private String generateSslCertKeyName(String fingerPrint){
String uniqueId = fingerPrint.replace(":","").substring(0,20);
return genObjectName("Cloud-Cert", uniqueId);
}
private String genObjectName(Object... args) {

View File

@ -1074,7 +1074,7 @@ public class LoadBalancingRulesManagerImpl<Type> extends ManagerBase implements
return null;
}
return new LbSslCert(certVO.getCertificate(), certVO.getKey(), certVO.getChain(), certVO.getPassword(), lbCertMap.isRevoke());
return new LbSslCert(certVO.getCertificate(), certVO.getKey(), certVO.getPassword(), certVO.getChain(), certVO.getFingerPrint(), lbCertMap.isRevoke());
}
@Override
@ -1124,11 +1124,6 @@ public class LoadBalancingRulesManagerImpl<Type> extends ManagerBase implements
LoadBalancerCertMapVO certMap = new LoadBalancerCertMapVO(lbRuleId, certId, false);
_lbCertMapDao.persist(certMap);
applyLoadBalancerConfig(loadBalancer.getId());
/*s_logger.warn("Failed to apply Ssl Cert to LB " + loadBalancer.getId());
CloudRuntimeException ex = new CloudRuntimeException(
"Failed to apply Ssl Cert to LB " + loadBalancer.getId());
ex.addProxyObject(loadBalancer.getUuid(), "loadBalancerId");
throw ex;*/
success = true;
} catch (ResourceUnavailableException e) {
if (isRollBackAllowedForProvider(loadBalancer)) {

View File

@ -25,6 +25,7 @@ import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.PublicKey;
@ -115,6 +116,7 @@ public class CertServiceImpl implements CertService {
validate(cert, key, password, chain);
s_logger.debug("Certificate Validation succeeded");
String fingerPrint = generateFingerPrint(parseCertificate(cert));
Long accountId = CallContext.current().getCallingAccount().getId();
@ -379,14 +381,17 @@ public class CertServiceImpl implements CertService {
params = new PKIXBuilderParameters(anchors, target);
params.setRevocationEnabled(false);
params.addCertStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(certs)));
CertPathBuilder builder = CertPathBuilder.getInstance("PKIX");
CertPathBuilder builder = CertPathBuilder.getInstance("PKIX", "BC");
builder.build(params);
} catch (InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException("Invalid certificate chain", e);
} catch (CertPathBuilderException e) {
throw new IllegalArgumentException("Invalid certificate chain", e);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("Invalid certificate chain", e);
} catch (NoSuchProviderException e) {
throw new CloudRuntimeException("No provider for certificate validation", e);
}
}

View File

@ -20,21 +20,28 @@ import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.List;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.commons.codec.binary.Base64;
import com.cloud.utils.Ternary;
import org.bouncycastle.openssl.PEMReader;
public class CertificateHelper {
public static byte[] buildAndSaveKeystore(String alias, String cert, String privateKey, String storePassword) throws KeyStoreException, CertificateException,
@ -106,4 +113,53 @@ public class CertificateHelper {
PKCS8EncodedKeySpec keysp = new PKCS8EncodedKeySpec(Base64.decodeBase64(base64EncodedKeyContent));
return kf.generatePrivate(keysp);
}
public static List<Certificate> parseChain(String chain) throws IOException {
List<Certificate> certs = new ArrayList<Certificate>();
PEMReader reader = new PEMReader(new StringReader(chain));
Certificate crt = null;
while ((crt = (Certificate)reader.readObject()) != null) {
if (crt instanceof X509Certificate) {
certs.add(crt);
}
}
if (certs.size() == 0)
throw new IllegalArgumentException("Unable to decode certificate chain");
return certs;
}
public static String generateFingerPrint(Certificate cert) {
final char[] HEX = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
StringBuilder buffer = new StringBuilder(60);
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] data = md.digest(cert.getEncoded());
for (int i = 0; i < data.length; i++) {
if (buffer.length() > 0) {
buffer.append(":");
}
buffer.append(HEX[(0xF0 & data[i]) >>> 4]);
buffer.append(HEX[0x0F & data[i]]);
}
} catch (CertificateEncodingException e) {
throw new CloudRuntimeException("Bad certificate encoding");
} catch (NoSuchAlgorithmException e) {
throw new CloudRuntimeException("Bad certificate algorithm");
}
return buffer.toString();
}
}