Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.apache.cloudstack.ca.provider;

import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
Expand All @@ -27,6 +28,7 @@

import javax.net.ssl.X509TrustManager;

import org.apache.commons.collections.CollectionUtils;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

Expand All @@ -39,21 +41,36 @@ public final class RootCACustomTrustManager implements X509TrustManager {
private String clientAddress = "Unknown";
private boolean authStrictness = true;
private boolean allowExpiredCertificate = true;
private boolean certSignatureVerification = false;
private CrlDao crlDao;
private List<X509Certificate> caCertificates;
private Map<String, X509Certificate> activeCertMap;

public RootCACustomTrustManager(final String clientAddress, final boolean authStrictness, final boolean allowExpiredCertificate, final Map<String, X509Certificate> activeCertMap, final List<X509Certificate> caCertificates, final CrlDao crlDao) {
public RootCACustomTrustManager(final String clientAddress, final boolean authStrictness, final boolean allowExpiredCertificate, final boolean certSignatureVerification,
final Map<String, X509Certificate> activeCertMap, final List<X509Certificate> caCertificates, final CrlDao crlDao) {
if (StringUtils.isNotEmpty(clientAddress)) {
this.clientAddress = clientAddress.replace("/", "").split(":")[0];
}
this.authStrictness = authStrictness;
this.allowExpiredCertificate = allowExpiredCertificate;
this.certSignatureVerification = certSignatureVerification;
this.activeCertMap = activeCertMap;
this.caCertificates = caCertificates;
this.crlDao = crlDao;
}

private boolean isSignedByAnyRootCA(final X509Certificate certificate) {
for (final X509Certificate ca : caCertificates) {
try {
certificate.verify(ca.getPublicKey());
return true;
} catch (final GeneralSecurityException e) {
// try the next CA certificate
}
}
return false;
}

private void printCertificateChain(final X509Certificate[] certificates, final String s) throws CertificateException {
if (certificates == null) {
return;
Expand Down Expand Up @@ -91,6 +108,22 @@ public void checkClientTrusted(final X509Certificate[] certificates, final Strin
return;
}

// CA signature check: confirm the cert was actually issued by one of our root CAs
if (certSignatureVerification) {
if (CollectionUtils.isEmpty(caCertificates)) {
final String errorMsg = "Cannot verify client certificate signature because no root CA certificate is available, from address=" + clientAddress;
if (authStrictness) {
throw new CertificateException(errorMsg);
}
logger.warn(errorMsg + "; continuing since strict auth mode is disabled");
} else if (!isSignedByAnyRootCA(primaryClientCertificate)) {
final String errorMsg = String.format("Client certificate is not signed by the root CA, serial=%x, subject=%s from address=%s",
primaryClientCertificate.getSerialNumber(), primaryClientCertificate.getSubjectDN(), clientAddress);
logger.error(errorMsg);
exceptionMsg = (StringUtils.isEmpty(exceptionMsg)) ? errorMsg : (exceptionMsg + ". " + errorMsg);
}
}

// Revocation check
final BigInteger serialNumber = primaryClientCertificate.getSerialNumber();
if (serialNumber == null || crlDao.findBySerial(serialNumber) != null) {
Expand Down Expand Up @@ -146,7 +179,37 @@ public void checkClientTrusted(final X509Certificate[] certificates, final Strin
}

@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
public void checkServerTrusted(final X509Certificate[] certificates, final String s) throws CertificateException {
if (logger.isDebugEnabled()) {
printCertificateChain(certificates, s);
}
if (!certSignatureVerification) {
return;
}
if (CollectionUtils.isEmpty(caCertificates)) {
final String errorMsg = "Cannot verify server certificate signature because no root CA certificate is available, from address=" + clientAddress;
if (authStrictness) {
throw new CertificateException(errorMsg);
}
logger.warn(errorMsg + "; continuing since strict auth mode is disabled");
return;
}
final X509Certificate primaryServerCertificate = (certificates != null && certificates.length > 0 && certificates[0] != null) ? certificates[0] : null;
if (primaryServerCertificate == null) {
final String errorMsg = "No certificate was presented by the server from address=" + clientAddress;
logger.error(errorMsg);
if (authStrictness) {
throw new CertificateException(errorMsg);
}
return;
}
if (!isSignedByAnyRootCA(primaryServerCertificate)) {
final String errorMsg = "Server certificate is not signed by the root CA from address=" + clientAddress;
logger.error(errorMsg);
if (authStrictness) {
throw new CertificateException(errorMsg);
}
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyManagementException;
import java.security.KeyPair;
Expand Down Expand Up @@ -139,6 +140,12 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
"true",
"When set to true, it will allow expired client certificate during SSL handshake.", true);

protected static ConfigKey<Boolean> rootCACertSignatureVerification = new ConfigKey<>("Advanced", Boolean.class,
"ca.plugin.root.ca.signature.verification",
"false",
"Verify that agent, server and peer management certificates are signed by the CloudStack root CA. Enforced only when ca.plugin.root.auth.strictness is true; " +
"otherwise signature failures are only logged and the connection is allowed. Enable only after all agents use CA-signed certificates.", true);

private static String managementCertificateCustomSAN;


Expand Down Expand Up @@ -279,8 +286,9 @@ public SSLEngine createSSLEngine(final SSLContext sslContext, final String remot

final boolean authStrictness = rootCAAuthStrictness.value();
final boolean allowExpiredCertificate = rootCAAllowExpiredCert.value();
final boolean certSignatureVerification = rootCACertSignatureVerification.value();

TrustManager[] tms = new TrustManager[]{new RootCACustomTrustManager(remoteAddress, authStrictness, allowExpiredCertificate, certMap, caCertificates, crlDao)};
TrustManager[] tms = new TrustManager[]{new RootCACustomTrustManager(remoteAddress, authStrictness, allowExpiredCertificate, certSignatureVerification, certMap, caCertificates, crlDao)};

sslContext.init(kmf.getKeyManagers(), tms, new SecureRandom());
final SSLEngine sslEngine = sslContext.createSSLEngine();
Expand Down Expand Up @@ -575,7 +583,8 @@ public ConfigKey<?>[] getConfigKeys() {
rootCACertificate,
rootCAIssuerDN,
rootCAAuthStrictness,
rootCAAllowExpiredCert
rootCAAllowExpiredCert,
rootCACertSignatureVerification
};
}

Expand All @@ -596,6 +605,30 @@ public boolean isManagementCertificate(java.security.cert.Certificate certificat
}
X509Certificate x509Certificate = (X509Certificate) certificate;

// When signature verification is enabled, confirm the certificate was issued by one of our root CAs
// before trusting its SAN. Otherwise any self-signed certificate carrying the management SAN
// would qualify as a peer management node.
if (rootCACertSignatureVerification.value()) {
if (CollectionUtils.isEmpty(caCertificates)) {
logger.warn("Cannot verify management certificate signature because no root CA certificate is available");
return false;
}
boolean signedByCA = false;
for (final X509Certificate ca : caCertificates) {
try {
x509Certificate.verify(ca.getPublicKey());
signedByCA = true;
break;
} catch (final GeneralSecurityException e) {
// try the next CA certificate
}
}
if (!signedByCA) {
logger.warn("Management certificate is not signed by the root CA");
return false;
}
}

// Check for alternative names
Collection<List<?>> altNames = x509Certificate.getSubjectAlternativeNames();
if (CollectionUtils.isEmpty(altNames)) {
Expand Down
Loading
Loading