mirror of https://github.com/apache/cloudstack.git
Merge 217fdd5168 into 5893ba5a8c
This commit is contained in:
commit
24f0e7e6e6
|
|
@ -63,6 +63,12 @@ public class ProvisionCertificateCmd extends BaseAsyncCmd {
|
|||
description = "Name of the CA service provider, otherwise the default configured provider plugin will be used")
|
||||
private String provider;
|
||||
|
||||
@Parameter(name = ApiConstants.FORCED, type = CommandType.BOOLEAN,
|
||||
description = "When true, uses SSH to re-provision the agent's certificate, bypassing the NIO agent connection. " +
|
||||
"Use this when agents are disconnected due to a CA change. Supported for KVM hosts and SystemVMs. Default is false",
|
||||
since = "4.23.0")
|
||||
private Boolean forced;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////////// Accessors ///////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
|
@ -79,6 +85,10 @@ public class ProvisionCertificateCmd extends BaseAsyncCmd {
|
|||
return provider;
|
||||
}
|
||||
|
||||
public boolean isForced() {
|
||||
return forced != null && forced;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////// API Implementation///////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
|
@ -90,7 +100,7 @@ public class ProvisionCertificateCmd extends BaseAsyncCmd {
|
|||
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to find host by ID: " + getHostId());
|
||||
}
|
||||
|
||||
boolean result = caManager.provisionCertificate(host, getReconnect(), getProvider());
|
||||
boolean result = caManager.provisionCertificate(host, getReconnect(), getProvider(), isForced());
|
||||
SuccessResponse response = new SuccessResponse(getCommandName());
|
||||
response.setSuccess(result);
|
||||
setResponseObject(response);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import java.security.cert.X509Certificate;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.trilead.ssh2.Connection;
|
||||
|
||||
import org.apache.cloudstack.framework.ca.CAProvider;
|
||||
import org.apache.cloudstack.framework.ca.CAService;
|
||||
import org.apache.cloudstack.framework.ca.Certificate;
|
||||
|
|
@ -39,7 +41,10 @@ public interface CAManager extends CAService, Configurable, PluggableService {
|
|||
ConfigKey<String> 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);
|
||||
"The CA provider plugin used for CloudStack internal certificate management (MS-agent encryption and authentication). " +
|
||||
"The default 'root' provider auto-generates a CA on first startup, but also supports user-provided custom CA material " +
|
||||
"via the ca.plugin.root.private.key, ca.plugin.root.public.key, and ca.plugin.root.ca.certificate settings. " +
|
||||
"Restart management server(s) when changed.", false);
|
||||
|
||||
ConfigKey<Integer> CertKeySize = new ConfigKey<>("Advanced", Integer.class,
|
||||
"ca.framework.cert.keysize",
|
||||
|
|
@ -85,6 +90,12 @@ public interface CAManager extends CAService, Configurable, PluggableService {
|
|||
"The actual implementation will depend on the configured CA provider.",
|
||||
false);
|
||||
|
||||
ConfigKey<Boolean> CaInjectDefaultTruststore = new ConfigKey<>("Advanced", Boolean.class,
|
||||
"ca.framework.inject.default.truststore", "true",
|
||||
"When true, injects the CA provider's certificate into the JVM default truststore on management server startup. " +
|
||||
"This allows outgoing HTTPS connections from the management server to trust servers with certificates signed by the configured CA. " +
|
||||
"Restart management server(s) when changed.", false);
|
||||
|
||||
/**
|
||||
* Returns a list of available CA provider plugins
|
||||
* @return returns list of CAProvider
|
||||
|
|
@ -130,12 +141,26 @@ public interface CAManager extends CAService, Configurable, PluggableService {
|
|||
boolean revokeCertificate(final BigInteger certSerial, final String certCn, final String provider);
|
||||
|
||||
/**
|
||||
* Provisions certificate for given active and connected agent host
|
||||
* Provisions certificate for given agent host.
|
||||
* When forced=true, uses SSH to re-provision bypassing the NIO agent connection (for disconnected agents).
|
||||
* @param host
|
||||
* @param reconnect
|
||||
* @param provider
|
||||
* @param forced when true, provisions via SSH instead of NIO; supports KVM hosts and SystemVMs
|
||||
* @return returns success/failure as boolean
|
||||
*/
|
||||
boolean provisionCertificate(final Host host, final Boolean reconnect, final String provider);
|
||||
boolean provisionCertificate(final Host host, final Boolean reconnect, final String provider, final boolean forced);
|
||||
|
||||
/**
|
||||
* Provisions certificate for a KVM host using an existing SSH connection.
|
||||
* Runs keystore-setup to generate a CSR, issues a certificate, then runs keystore-cert-import.
|
||||
* Used during host discovery and for forced re-provisioning when the NIO agent is unreachable.
|
||||
* @param sshConnection active SSH connection to the KVM host
|
||||
* @param agentIp IP address of the KVM host agent
|
||||
* @param agentHostname hostname of the KVM host agent
|
||||
* @param caProvider optional CA provider plugin name (null uses default)
|
||||
*/
|
||||
void provisionCertificateViaSsh(Connection sshConnection, String agentIp, String agentHostname, String caProvider);
|
||||
|
||||
/**
|
||||
* Setups up a new keystore and generates CSR for a host
|
||||
|
|
|
|||
|
|
@ -40,17 +40,17 @@ public final class RootCACustomTrustManager implements X509TrustManager {
|
|||
private boolean authStrictness = true;
|
||||
private boolean allowExpiredCertificate = true;
|
||||
private CrlDao crlDao;
|
||||
private X509Certificate caCertificate;
|
||||
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 X509Certificate caCertificate, final CrlDao crlDao) {
|
||||
public RootCACustomTrustManager(final String clientAddress, final boolean authStrictness, final boolean allowExpiredCertificate, 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.activeCertMap = activeCertMap;
|
||||
this.caCertificate = caCertificate;
|
||||
this.caCertificates = caCertificates;
|
||||
this.crlDao = crlDao;
|
||||
}
|
||||
|
||||
|
|
@ -151,6 +151,6 @@ public final class RootCACustomTrustManager implements X509TrustManager {
|
|||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[]{caCertificate};
|
||||
return caCertificates.toArray(new X509Certificate[0]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ import java.security.cert.X509Certificate;
|
|||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
|
@ -60,6 +59,7 @@ 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.ValidatedConfigKey;
|
||||
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
|
||||
import org.apache.cloudstack.utils.security.CertUtils;
|
||||
import org.apache.cloudstack.utils.security.KeyStoreUtils;
|
||||
|
|
@ -92,6 +92,7 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
|
|||
|
||||
private static KeyPair caKeyPair = null;
|
||||
private static X509Certificate caCertificate = null;
|
||||
private static List<X509Certificate> caCertificates = null;
|
||||
private static KeyStore managementKeyStore = null;
|
||||
|
||||
@Inject
|
||||
|
|
@ -103,20 +104,25 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
|
|||
/////////////// Root CA Settings ///////////////////
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
private static ConfigKey<String> rootCAPrivateKey = new ConfigKey<>("Hidden", String.class,
|
||||
"ca.plugin.root.private.key",
|
||||
null,
|
||||
"The ROOT CA private key.", true);
|
||||
private static ConfigKey<String> rootCAPrivateKey = new ValidatedConfigKey<>("Hidden", String.class,
|
||||
"ca.plugin.root.private.key", null,
|
||||
"The ROOT CA private key in PEM format. " +
|
||||
"When set along with the public key and certificate, CloudStack uses this custom CA instead of auto-generating one. " +
|
||||
"All three ca.plugin.root.* keys must be set together. Restart management server(s) when changed.",
|
||||
false, ConfigKey.Scope.Global, null, RootCAProvider::validatePrivateKeyPem);
|
||||
|
||||
private static ConfigKey<String> rootCAPublicKey = new ConfigKey<>("Hidden", String.class,
|
||||
"ca.plugin.root.public.key",
|
||||
null,
|
||||
"The ROOT CA public key.", true);
|
||||
private static ConfigKey<String> rootCAPublicKey = new ValidatedConfigKey<>("Hidden", String.class,
|
||||
"ca.plugin.root.public.key", null,
|
||||
"The ROOT CA public key in PEM format (X.509/SPKI: must start with '-----BEGIN PUBLIC KEY-----'). " +
|
||||
"Required when providing a custom CA. Restart management server(s) when changed.",
|
||||
false, ConfigKey.Scope.Global, null, RootCAProvider::validatePublicKeyPem);
|
||||
|
||||
private static ConfigKey<String> rootCACertificate = new ConfigKey<>("Hidden", String.class,
|
||||
"ca.plugin.root.ca.certificate",
|
||||
null,
|
||||
"The ROOT CA certificate.", true);
|
||||
private static ConfigKey<String> rootCACertificate = new ValidatedConfigKey<>("Hidden", String.class,
|
||||
"ca.plugin.root.ca.certificate", null,
|
||||
"The CA certificate(s) in PEM format (must start with '-----BEGIN CERTIFICATE-----'). " +
|
||||
"For intermediate CAs, concatenate the signing cert first, followed by intermediate(s) and root. " +
|
||||
"Required when providing a custom CA. Restart management server(s) when changed.",
|
||||
false, ConfigKey.Scope.Global, null, RootCAProvider::validateCACertificatePem);
|
||||
|
||||
private static ConfigKey<String> rootCAIssuerDN = new ConfigKey<>("Advanced", String.class,
|
||||
"ca.plugin.root.issuer.dn",
|
||||
|
|
@ -151,7 +157,7 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
|
|||
caCertificate, caKeyPair, keyPair.getPublic(),
|
||||
subject, CAManager.CertSignatureAlgorithm.value(),
|
||||
validityDays, domainNames, ipAddresses);
|
||||
return new Certificate(clientCertificate, keyPair.getPrivate(), Collections.singletonList(caCertificate));
|
||||
return new Certificate(clientCertificate, keyPair.getPrivate(), caCertificates);
|
||||
}
|
||||
|
||||
private Certificate generateCertificateUsingCsr(final String csr, final List<String> names, final List<String> ips, final int validityDays) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, CertificateException, SignatureException, IOException, OperatorCreationException {
|
||||
|
|
@ -205,7 +211,7 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
|
|||
caCertificate, caKeyPair, request.getPublicKey(),
|
||||
subject, CAManager.CertSignatureAlgorithm.value(),
|
||||
validityDays, dnsNames, ipAddresses);
|
||||
return new Certificate(clientCertificate, null, Collections.singletonList(caCertificate));
|
||||
return new Certificate(clientCertificate, null, caCertificates);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
|
|
@ -219,7 +225,7 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
|
|||
|
||||
@Override
|
||||
public List<X509Certificate> getCaCertificate() {
|
||||
return Collections.singletonList(caCertificate);
|
||||
return caCertificates;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -254,8 +260,8 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
|
|||
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(), getKeyStorePassphrase(), new X509Certificate[]{caCertificate});
|
||||
if (caKeyPair != null && CollectionUtils.isNotEmpty(caCertificates)) {
|
||||
ks.setKeyEntry(caAlias, caKeyPair.getPrivate(), getKeyStorePassphrase(), caCertificates.toArray(new X509Certificate[0]));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -274,7 +280,7 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
|
|||
final boolean authStrictness = rootCAAuthStrictness.value();
|
||||
final boolean allowExpiredCertificate = rootCAAllowExpiredCert.value();
|
||||
|
||||
TrustManager[] tms = new TrustManager[]{new RootCACustomTrustManager(remoteAddress, authStrictness, allowExpiredCertificate, certMap, caCertificate, crlDao)};
|
||||
TrustManager[] tms = new TrustManager[]{new RootCACustomTrustManager(remoteAddress, authStrictness, allowExpiredCertificate, certMap, caCertificates, crlDao)};
|
||||
|
||||
sslContext.init(kmf.getKeyManagers(), tms, new SecureRandom());
|
||||
final SSLEngine sslEngine = sslContext.createSSLEngine();
|
||||
|
|
@ -322,24 +328,26 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
|
|||
return loadRootCAKeyPair();
|
||||
}
|
||||
|
||||
private boolean saveNewRootCACertificate() {
|
||||
boolean saveNewRootCACertificate() {
|
||||
if (caKeyPair == null) {
|
||||
throw new CloudRuntimeException("Cannot issue self-signed root CA certificate as CA keypair is not initialized");
|
||||
}
|
||||
try {
|
||||
logger.debug("Generating root CA certificate");
|
||||
final X509Certificate rootCaCertificate = CertUtils.generateV3Certificate(
|
||||
final X509Certificate generatedCACert = CertUtils.generateV3Certificate(
|
||||
null, caKeyPair, caKeyPair.getPublic(),
|
||||
rootCAIssuerDN.value(), CAManager.CertSignatureAlgorithm.value(),
|
||||
getCaValidityDays(), null, null);
|
||||
if (!configDao.update(rootCACertificate.key(), rootCACertificate.category(), CertUtils.x509CertificateToPem(rootCaCertificate))) {
|
||||
if (!configDao.update(rootCACertificate.key(), rootCACertificate.category(), CertUtils.x509CertificateToPem(generatedCACert))) {
|
||||
logger.error("Failed to update RootCA public/x509 certificate");
|
||||
}
|
||||
caCertificates = new ArrayList<>(java.util.Collections.singletonList(generatedCACert));
|
||||
caCertificate = generatedCACert;
|
||||
} catch (final CertificateException | NoSuchAlgorithmException | NoSuchProviderException | SignatureException | InvalidKeyException | OperatorCreationException | IOException e) {
|
||||
logger.error("Failed to generate RootCA certificate from private/public keys due to exception:", e);
|
||||
return false;
|
||||
}
|
||||
return loadRootCACertificate();
|
||||
return caCertificate != null;
|
||||
}
|
||||
|
||||
private boolean loadRootCAKeyPair() {
|
||||
|
|
@ -355,14 +363,32 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
|
|||
return caKeyPair.getPrivate() != null && caKeyPair.getPublic() != null;
|
||||
}
|
||||
|
||||
private boolean loadRootCACertificate() {
|
||||
boolean loadRootCACertificate() {
|
||||
caCertificate = null;
|
||||
caCertificates = null;
|
||||
if (StringUtils.isEmpty(rootCACertificate.value())) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
caCertificate = CertUtils.pemToX509Certificate(rootCACertificate.value());
|
||||
caCertificate.verify(caKeyPair.getPublic());
|
||||
} catch (final IOException | CertificateException | NoSuchAlgorithmException | InvalidKeyException | SignatureException | NoSuchProviderException e) {
|
||||
final List<X509Certificate> loadedCerts = CertUtils.pemToX509Certificates(rootCACertificate.value());
|
||||
if (CollectionUtils.isEmpty(loadedCerts)) {
|
||||
logger.error("No certificates found in ca.plugin.root.ca.certificate");
|
||||
return false;
|
||||
}
|
||||
final X509Certificate loadedCACert = loadedCerts.get(0);
|
||||
|
||||
// Verify key ownership without enforcing self-signature
|
||||
if (!loadedCACert.getPublicKey().equals(caKeyPair.getPublic())) {
|
||||
logger.error("The public key in the CA certificate does not match the configured CA public key");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (loadedCerts.size() > 1) {
|
||||
logger.info("Loaded CA certificate chain with {} certificate(s)", loadedCerts.size());
|
||||
}
|
||||
caCertificates = loadedCerts;
|
||||
caCertificate = loadedCACert;
|
||||
} catch (final IOException | CertificateException e) {
|
||||
logger.error("Failed to load saved RootCA certificate due to exception:", e);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -389,9 +415,15 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
|
|||
try {
|
||||
managementKeyStore = KeyStore.getInstance("JKS");
|
||||
managementKeyStore.load(null, null);
|
||||
managementKeyStore.setCertificateEntry(caAlias, caCertificate);
|
||||
int caIndex = 0;
|
||||
for (final X509Certificate cert : caCertificates) {
|
||||
managementKeyStore.setCertificateEntry(caAlias + "-" + caIndex++, cert);
|
||||
}
|
||||
final List<X509Certificate> fullChain = new ArrayList<>();
|
||||
fullChain.add(serverCertificate.getClientCertificate());
|
||||
fullChain.addAll(caCertificates);
|
||||
managementKeyStore.setKeyEntry(managementAlias, serverCertificate.getPrivateKey(), getKeyStorePassphrase(),
|
||||
new X509Certificate[]{serverCertificate.getClientCertificate(), caCertificate});
|
||||
fullChain.toArray(new X509Certificate[0]));
|
||||
} catch (final CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException e) {
|
||||
logger.error("Failed to load root CA management-server keystore due to exception: ", e);
|
||||
return false;
|
||||
|
|
@ -421,14 +453,63 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
|
|||
}
|
||||
|
||||
|
||||
private boolean setupCA() {
|
||||
if (!loadRootCAKeyPair() && !saveNewRootCAKeypair()) {
|
||||
logger.error("Failed to save and load root CA keypair");
|
||||
return false;
|
||||
private static void validatePrivateKeyPem(String value) {
|
||||
if (StringUtils.isEmpty(value)) return;
|
||||
try {
|
||||
CertUtils.pemToPrivateKey(value);
|
||||
} catch (InvalidKeySpecException | IOException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"ca.plugin.root.private.key is not a valid PEM private key: " + e.getMessage());
|
||||
}
|
||||
if (!loadRootCACertificate() && !saveNewRootCACertificate()) {
|
||||
logger.error("Failed to save and load root CA certificate");
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void validatePublicKeyPem(String value) {
|
||||
if (StringUtils.isEmpty(value)) return;
|
||||
try {
|
||||
CertUtils.pemToPublicKey(value);
|
||||
} catch (InvalidKeySpecException | IOException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"ca.plugin.root.public.key is not a valid PEM public key: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
static void validateCACertificatePem(String value) {
|
||||
if (StringUtils.isEmpty(value)) return;
|
||||
try {
|
||||
final List<X509Certificate> certs = CertUtils.pemToX509Certificates(value);
|
||||
if (CollectionUtils.isEmpty(certs)) {
|
||||
throw new IllegalArgumentException(
|
||||
"ca.plugin.root.ca.certificate contains no certificates");
|
||||
}
|
||||
} catch (IOException | CertificateException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"ca.plugin.root.ca.certificate is not a valid PEM certificate: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean setupCA() {
|
||||
if (!loadRootCAKeyPair()) {
|
||||
if (hasUserProvidedCAKeys()) {
|
||||
logger.error("Failed to load user-provided CA keys from configuration. " +
|
||||
"Check that ca.plugin.root.private.key, ca.plugin.root.public.key, and " +
|
||||
"ca.plugin.root.ca.certificate are all set and in the correct PEM format. " +
|
||||
"Overwriting with auto-generated keys.");
|
||||
}
|
||||
if (!saveNewRootCAKeypair()) {
|
||||
logger.error("Failed to save and load root CA keypair");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!loadRootCACertificate()) {
|
||||
if (hasUserProvidedCAKeys()) {
|
||||
logger.error("Failed to load user-provided CA certificate. " +
|
||||
"Check that ca.plugin.root.ca.certificate is set and in PEM format. " +
|
||||
"Overwriting with auto-generated certificate.");
|
||||
}
|
||||
if (!saveNewRootCACertificate()) {
|
||||
logger.error("Failed to save and load root CA certificate");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!loadManagementKeyStore()) {
|
||||
logger.error("Failed to check and configure management server keystore");
|
||||
|
|
@ -437,10 +518,16 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
|
|||
return true;
|
||||
}
|
||||
|
||||
private boolean hasUserProvidedCAKeys() {
|
||||
return StringUtils.isNotEmpty(rootCAPublicKey.value())
|
||||
|| StringUtils.isNotEmpty(rootCAPrivateKey.value())
|
||||
|| StringUtils.isNotEmpty(rootCACertificate.value());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean start() {
|
||||
managementCertificateCustomSAN = CAManager.CertManagementCustomSubjectAlternativeName.value();
|
||||
return loadRootCAKeyPair() && loadRootCAKeyPair() && loadManagementKeyStore();
|
||||
return loadRootCAKeyPair() && loadRootCACertificate() && loadManagementKeyStore();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -23,9 +23,11 @@ import java.math.BigInteger;
|
|||
import java.security.KeyPair;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.cloudstack.utils.security.CertUtils;
|
||||
import org.junit.Assert;
|
||||
|
|
@ -63,14 +65,14 @@ public class RootCACustomTrustManagerTest {
|
|||
|
||||
@Test
|
||||
public void testAuthNotStrictWithInvalidCert() throws Exception {
|
||||
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao);
|
||||
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, Collections.singletonList(caCertificate), crlDao);
|
||||
trustManager.checkClientTrusted(null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthNotStrictWithRevokedCert() throws Exception {
|
||||
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(new CrlVO());
|
||||
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao);
|
||||
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, Collections.singletonList(caCertificate), crlDao);
|
||||
trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, "RSA");
|
||||
Assert.assertTrue(certMap.containsKey(clientIp));
|
||||
Assert.assertEquals(certMap.get(clientIp), caCertificate);
|
||||
|
|
@ -79,7 +81,7 @@ public class RootCACustomTrustManagerTest {
|
|||
@Test
|
||||
public void testAuthNotStrictWithInvalidCertOwnership() throws Exception {
|
||||
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null);
|
||||
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao);
|
||||
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, Collections.singletonList(caCertificate), crlDao);
|
||||
trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, "RSA");
|
||||
Assert.assertTrue(certMap.containsKey(clientIp));
|
||||
Assert.assertEquals(certMap.get(clientIp), caCertificate);
|
||||
|
|
@ -88,14 +90,14 @@ public class RootCACustomTrustManagerTest {
|
|||
@Test(expected = CertificateException.class)
|
||||
public void testAuthNotStrictWithDenyExpiredCertAndOwnership() throws Exception {
|
||||
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null);
|
||||
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, false, certMap, caCertificate, crlDao);
|
||||
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, false, certMap, Collections.singletonList(caCertificate), crlDao);
|
||||
trustManager.checkClientTrusted(new X509Certificate[]{expiredClientCertificate}, "RSA");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthNotStrictWithAllowExpiredCertAndOwnership() throws Exception {
|
||||
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null);
|
||||
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao);
|
||||
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, Collections.singletonList(caCertificate), crlDao);
|
||||
trustManager.checkClientTrusted(new X509Certificate[]{expiredClientCertificate}, "RSA");
|
||||
Assert.assertTrue(certMap.containsKey(clientIp));
|
||||
Assert.assertEquals(certMap.get(clientIp), expiredClientCertificate);
|
||||
|
|
@ -103,35 +105,50 @@ public class RootCACustomTrustManagerTest {
|
|||
|
||||
@Test(expected = CertificateException.class)
|
||||
public void testAuthStrictWithInvalidCert() throws Exception {
|
||||
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao);
|
||||
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, Collections.singletonList(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);
|
||||
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, Collections.singletonList(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);
|
||||
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, Collections.singletonList(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);
|
||||
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, false, certMap, Collections.singletonList(caCertificate), crlDao);
|
||||
trustManager.checkClientTrusted(new X509Certificate[]{expiredClientCertificate}, "RSA");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAcceptedIssuersWithChain() throws Exception {
|
||||
final KeyPair rootKeyPair = CertUtils.generateRandomKeyPair(1024);
|
||||
final X509Certificate rootCert = CertUtils.generateV3Certificate(null, rootKeyPair, rootKeyPair.getPublic(),
|
||||
"CN=root", "SHA256withRSA", 365, null, null);
|
||||
final List<X509Certificate> chain = Arrays.asList(caCertificate, rootCert);
|
||||
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(
|
||||
clientIp, false, true, certMap, chain, crlDao);
|
||||
|
||||
final X509Certificate[] issuers = trustManager.getAcceptedIssuers();
|
||||
Assert.assertEquals(2, issuers.length);
|
||||
Assert.assertEquals(caCertificate, issuers[0]);
|
||||
Assert.assertEquals(rootCert, issuers[1]);
|
||||
}
|
||||
|
||||
@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);
|
||||
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, Collections.singletonList(caCertificate), crlDao);
|
||||
Assert.assertTrue(trustManager.getAcceptedIssuers() != null);
|
||||
Assert.assertTrue(trustManager.getAcceptedIssuers().length == 1);
|
||||
Assert.assertEquals(trustManager.getAcceptedIssuers()[0], caCertificate);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import java.security.cert.X509Certificate;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
|
|
@ -38,6 +39,7 @@ import javax.net.ssl.SSLEngine;
|
|||
|
||||
import org.apache.cloudstack.framework.ca.Certificate;
|
||||
import org.apache.cloudstack.framework.config.ConfigKey;
|
||||
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
|
||||
import org.apache.cloudstack.utils.security.CertUtils;
|
||||
import org.apache.cloudstack.utils.security.SSLUtils;
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
|
|
@ -49,7 +51,6 @@ import org.junit.Test;
|
|||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
|
|
@ -75,7 +76,7 @@ public class RootCAProviderTest {
|
|||
|
||||
addField(provider, "caKeyPair", caKeyPair);
|
||||
addField(provider, "caCertificate", caCertificate);
|
||||
addField(provider, "caKeyPair", caKeyPair);
|
||||
addField(provider, "caCertificates", Collections.singletonList(caCertificate));
|
||||
}
|
||||
|
||||
@After
|
||||
|
|
@ -129,6 +130,46 @@ public class RootCAProviderTest {
|
|||
certificate.getClientCertificate().verify(caCertificate.getPublicKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCaCertificateWithChain() throws Exception {
|
||||
final KeyPair rootKeyPair = CertUtils.generateRandomKeyPair(1024);
|
||||
final X509Certificate rootCert = CertUtils.generateV3Certificate(null, rootKeyPair, rootKeyPair.getPublic(),
|
||||
"CN=root", "SHA256withRSA", 365, null, null);
|
||||
final KeyPair intermediateKeyPair = CertUtils.generateRandomKeyPair(1024);
|
||||
final X509Certificate intermediateCert = CertUtils.generateV3Certificate(rootCert, rootKeyPair,
|
||||
intermediateKeyPair.getPublic(), "CN=intermediate", "SHA256withRSA", 365, null, null);
|
||||
|
||||
final List<X509Certificate> chain = Arrays.asList(intermediateCert, rootCert);
|
||||
addField(provider, "caKeyPair", intermediateKeyPair);
|
||||
addField(provider, "caCertificate", intermediateCert);
|
||||
addField(provider, "caCertificates", chain);
|
||||
|
||||
Assert.assertEquals(2, provider.getCaCertificate().size());
|
||||
Assert.assertEquals(intermediateCert, provider.getCaCertificate().get(0));
|
||||
Assert.assertEquals(rootCert, provider.getCaCertificate().get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIssueCertificateWithoutCsrAndChain() throws Exception {
|
||||
final KeyPair rootKeyPair = CertUtils.generateRandomKeyPair(1024);
|
||||
final X509Certificate rootCert = CertUtils.generateV3Certificate(null, rootKeyPair, rootKeyPair.getPublic(),
|
||||
"CN=root", "SHA256withRSA", 365, null, null);
|
||||
final KeyPair intermediateKeyPair = CertUtils.generateRandomKeyPair(1024);
|
||||
final X509Certificate intermediateCert = CertUtils.generateV3Certificate(rootCert, rootKeyPair,
|
||||
intermediateKeyPair.getPublic(), "CN=intermediate", "SHA256withRSA", 365, null, null);
|
||||
|
||||
addField(provider, "caKeyPair", intermediateKeyPair);
|
||||
addField(provider, "caCertificate", intermediateCert);
|
||||
addField(provider, "caCertificates", Arrays.asList(intermediateCert, rootCert));
|
||||
|
||||
final Certificate certificate = provider.issueCertificate(Arrays.asList("domain1.com"), null, 1);
|
||||
Assert.assertNotNull(certificate);
|
||||
Assert.assertEquals(2, certificate.getCaCertificates().size());
|
||||
Assert.assertEquals(intermediateCert, certificate.getCaCertificates().get(0));
|
||||
Assert.assertEquals(rootCert, certificate.getCaCertificates().get(1));
|
||||
certificate.getClientCertificate().verify(intermediateKeyPair.getPublic());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRevokeCertificate() throws Exception {
|
||||
Assert.assertTrue(provider.revokeCertificate(CertUtils.generateRandomBigInt(), "anyString"));
|
||||
|
|
@ -177,8 +218,8 @@ public class RootCAProviderTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testIsManagementCertificateNoMatch() {
|
||||
ReflectionTestUtils.setField(provider, "managementCertificateCustomSAN", "cloudstack");
|
||||
public void testIsManagementCertificateNoMatch() throws Exception {
|
||||
addField(provider, "managementCertificateCustomSAN", "cloudstack");
|
||||
try {
|
||||
X509Certificate certificate = Mockito.mock(X509Certificate.class);
|
||||
List<List<?>> altNames = new ArrayList<>();
|
||||
|
|
@ -193,9 +234,9 @@ public class RootCAProviderTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testIsManagementCertificateMatch() {
|
||||
public void testIsManagementCertificateMatch() throws Exception {
|
||||
String customSAN = "cloudstack";
|
||||
ReflectionTestUtils.setField(provider, "managementCertificateCustomSAN", customSAN);
|
||||
addField(provider, "managementCertificateCustomSAN", customSAN);
|
||||
try {
|
||||
X509Certificate certificate = Mockito.mock(X509Certificate.class);
|
||||
List<List<?>> altNames = new ArrayList<>();
|
||||
|
|
@ -208,4 +249,58 @@ public class RootCAProviderTest {
|
|||
Assert.fail(String.format("Exception occurred: %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadRootCACertificateWithMismatchedCert() throws Exception {
|
||||
KeyPair otherKeyPair = CertUtils.generateRandomKeyPair(1024);
|
||||
X509Certificate mismatchedCert = CertUtils.generateV3Certificate(null, otherKeyPair, otherKeyPair.getPublic(), "CN=other", "SHA256withRSA", 365, null, null);
|
||||
String mismatchedPem = CertUtils.x509CertificateToPem(mismatchedCert);
|
||||
|
||||
ConfigKey<String> mockCertKey = Mockito.mock(ConfigKey.class);
|
||||
Mockito.when(mockCertKey.value()).thenReturn(mismatchedPem);
|
||||
addField(provider, "rootCACertificate", mockCertKey);
|
||||
|
||||
addField(provider, "caCertificate", null);
|
||||
addField(provider, "caCertificates", null);
|
||||
|
||||
Boolean result = provider.loadRootCACertificate();
|
||||
Assert.assertFalse(result);
|
||||
Assert.assertNull(provider.getCaCertificate());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveNewRootCACertificateWithStaleCache() throws Exception {
|
||||
ConfigurationDao configDao = Mockito.mock(ConfigurationDao.class);
|
||||
addField(provider, "configDao", configDao);
|
||||
|
||||
ConfigKey<String> mockCertKey = Mockito.mock(ConfigKey.class);
|
||||
Mockito.when(mockCertKey.key()).thenReturn("ca.plugin.root.ca.certificate");
|
||||
Mockito.when(mockCertKey.category()).thenReturn("Hidden");
|
||||
addField(provider, "rootCACertificate", mockCertKey);
|
||||
|
||||
ConfigKey<String> mockIssuerKey = Mockito.mock(ConfigKey.class);
|
||||
Mockito.when(mockIssuerKey.value()).thenReturn("CN=ca.cloudstack.apache.org");
|
||||
addField(provider, "rootCAIssuerDN", mockIssuerKey);
|
||||
|
||||
addField(provider, "caCertificate", null);
|
||||
addField(provider, "caCertificates", null);
|
||||
|
||||
Mockito.when(configDao.update(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(true);
|
||||
|
||||
Boolean result = provider.saveNewRootCACertificate();
|
||||
Assert.assertTrue(result);
|
||||
Assert.assertNotNull(provider.getCaCertificate());
|
||||
Assert.assertEquals(1, provider.getCaCertificate().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateCACertificatePem() throws Exception {
|
||||
String truncatedPem = "-----BEGIN CERTIFICATE-----\nMIICxTCCAa0CAQAw\n";
|
||||
try {
|
||||
RootCAProvider.validateCACertificatePem(truncatedPem);
|
||||
Assert.fail("Expected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
Assert.assertTrue(e.getMessage().contains("is not a valid PEM certificate"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ import org.apache.http.HttpHeaders;
|
|||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.apache.http.conn.ssl.NoopHostnameVerifier;
|
||||
import org.apache.http.conn.ssl.TrustAllStrategy;
|
||||
import org.apache.http.entity.ContentType;
|
||||
|
|
@ -97,7 +99,9 @@ public class WebhookDeliveryThread implements Runnable {
|
|||
|
||||
protected void setHttpClient() throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
if (webhook.isSslVerification()) {
|
||||
httpClient = HttpClients.createDefault();
|
||||
httpClient = HttpClients.custom()
|
||||
.setSSLContext(SSLContext.getDefault())
|
||||
.build();
|
||||
return;
|
||||
}
|
||||
httpClient = HttpClients
|
||||
|
|
|
|||
|
|
@ -70,8 +70,8 @@ elif [ ! -f "$CACERT_FILE" ]; then
|
|||
fi
|
||||
|
||||
# Import cacerts into the keystore
|
||||
awk '/-----BEGIN CERTIFICATE-----?/{n++}{print > "cloudca." n }' "$CACERT_FILE"
|
||||
for caChain in $(ls cloudca.*); do
|
||||
awk 'BEGIN{n=0} /-----BEGIN CERTIFICATE-----/{n++} n>0{print > "cloudca." n }' "$CACERT_FILE"
|
||||
for caChain in $(ls cloudca.* 2>/dev/null); do
|
||||
keytool -delete -noprompt -alias "$caChain" -keystore "$KS_FILE" -storepass "$KS_PASS" > /dev/null 2>&1 || true
|
||||
keytool -import -noprompt -storepass "$KS_PASS" -trustcacerts -alias "$caChain" -file "$caChain" -keystore "$KS_FILE" > /dev/null 2>&1
|
||||
done
|
||||
|
|
@ -137,6 +137,22 @@ if [ -f "$SYSTEM_FILE" ]; then
|
|||
chmod 644 /usr/local/share/ca-certificates/cloudstack/ca.crt
|
||||
update-ca-certificates > /dev/null 2>&1 || true
|
||||
|
||||
# Import CA cert(s) into realhostip.keystore so the SSVM JVM
|
||||
# (which overrides the truststore via -Djavax.net.ssl.trustStore in _run.sh)
|
||||
# can trust servers signed by the CloudStack CA
|
||||
REALHOSTIP_KS_FILE="$(dirname "$(dirname "$PROPS_FILE")")/certs/realhostip.keystore"
|
||||
REALHOSTIP_PASS="vmops.com"
|
||||
if [ -f "$REALHOSTIP_KS_FILE" ]; then
|
||||
awk 'BEGIN{n=0} /-----BEGIN CERTIFICATE-----/{n++} n>0{print > "cloudca." n }' "$CACERT_FILE"
|
||||
for caChain in $(ls cloudca.* 2>/dev/null); do
|
||||
keytool -delete -noprompt -alias "$caChain" -keystore "$REALHOSTIP_KS_FILE" \
|
||||
-storepass "$REALHOSTIP_PASS" > /dev/null 2>&1 || true
|
||||
keytool -import -noprompt -trustcacerts -alias "$caChain" -file "$caChain" \
|
||||
-keystore "$REALHOSTIP_KS_FILE" -storepass "$REALHOSTIP_PASS" > /dev/null 2>&1
|
||||
done
|
||||
rm -f cloudca.*
|
||||
fi
|
||||
|
||||
# Ensure cloud service is running in systemvm
|
||||
if [ "$MODE" == "ssh" ]; then
|
||||
systemctl start cloud > /dev/null 2>&1
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import static com.cloud.configuration.ConfigurationManagerImpl.ADD_HOST_ON_SERVI
|
|||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
|
@ -32,11 +31,8 @@ import javax.naming.ConfigurationException;
|
|||
|
||||
import org.apache.cloudstack.agent.lb.IndirectAgentLB;
|
||||
import org.apache.cloudstack.ca.CAManager;
|
||||
import org.apache.cloudstack.ca.SetupCertificateCommand;
|
||||
import org.apache.cloudstack.direct.download.DirectDownloadManager;
|
||||
import org.apache.cloudstack.framework.ca.Certificate;
|
||||
import org.apache.cloudstack.utils.cache.LazyCache;
|
||||
import org.apache.cloudstack.utils.security.KeyStoreUtils;
|
||||
|
||||
import com.cloud.agent.AgentManager;
|
||||
import com.cloud.agent.Listener;
|
||||
|
|
@ -66,7 +62,6 @@ 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.UuidUtils;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
|
|
@ -174,55 +169,7 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements
|
|||
throw new CloudRuntimeException("Cannot secure agent communication because SSH connection is invalid for host IP=" + agentIp);
|
||||
}
|
||||
|
||||
Integer validityPeriod = CAManager.CertValidityPeriod.value();
|
||||
if (validityPeriod < 1) {
|
||||
validityPeriod = 1;
|
||||
}
|
||||
|
||||
String keystorePassword = PasswordGenerator.generateRandomPassword(16);
|
||||
final SSHCmdHelper.SSHCmdResult keystoreSetupResult = SSHCmdHelper.sshExecuteCmdWithResult(sshConnection,
|
||||
String.format("sudo /usr/share/cloudstack-common/scripts/util/%s " +
|
||||
"/etc/cloudstack/agent/agent.properties " +
|
||||
"/etc/cloudstack/agent/%s " +
|
||||
"%s %d " +
|
||||
"/etc/cloudstack/agent/%s",
|
||||
KeyStoreUtils.KS_SETUP_SCRIPT,
|
||||
KeyStoreUtils.KS_FILENAME,
|
||||
keystorePassword,
|
||||
validityPeriod,
|
||||
KeyStoreUtils.CSR_FILENAME));
|
||||
|
||||
if (!keystoreSetupResult.isSuccess()) {
|
||||
throw new CloudRuntimeException("Failed to setup keystore on the KVM host: " + agentIp);
|
||||
}
|
||||
|
||||
final Certificate certificate = caManager.issueCertificate(keystoreSetupResult.getStdOut(), Arrays.asList(agentHostname, agentIp), Collections.singletonList(agentIp), null, null);
|
||||
if (certificate == null || certificate.getClientCertificate() == null) {
|
||||
throw new CloudRuntimeException("Failed to issue certificates for KVM host agent: " + agentIp);
|
||||
}
|
||||
|
||||
final SetupCertificateCommand certificateCommand = new SetupCertificateCommand(certificate);
|
||||
final SSHCmdHelper.SSHCmdResult setupCertResult = SSHCmdHelper.sshExecuteCmdWithResult(sshConnection,
|
||||
String.format("sudo /usr/share/cloudstack-common/scripts/util/%s " +
|
||||
"/etc/cloudstack/agent/agent.properties %s " +
|
||||
"/etc/cloudstack/agent/%s %s " +
|
||||
"/etc/cloudstack/agent/%s \"%s\" " +
|
||||
"/etc/cloudstack/agent/%s \"%s\" " +
|
||||
"/etc/cloudstack/agent/%s \"%s\"",
|
||||
KeyStoreUtils.KS_IMPORT_SCRIPT,
|
||||
keystorePassword,
|
||||
KeyStoreUtils.KS_FILENAME,
|
||||
KeyStoreUtils.SSH_MODE,
|
||||
KeyStoreUtils.CERT_FILENAME,
|
||||
certificateCommand.getEncodedCertificate(),
|
||||
KeyStoreUtils.CACERT_FILENAME,
|
||||
certificateCommand.getEncodedCaCertificates(),
|
||||
KeyStoreUtils.PKEY_FILENAME,
|
||||
certificateCommand.getEncodedPrivateKey()));
|
||||
|
||||
if (setupCertResult != null && !setupCertResult.isSuccess()) {
|
||||
throw new CloudRuntimeException("Failed to setup certificate in the KVM agent's keystore file, please see logs and configure manually!");
|
||||
}
|
||||
caManager.provisionCertificateViaSsh(sshConnection, agentIp, agentHostname, null);
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Succeeded to import certificate in the keystore for agent on the KVM host: " + agentIp + ". Agent secured and trusted.");
|
||||
|
|
|
|||
|
|
@ -22,12 +22,14 @@ import java.math.BigInteger;
|
|||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateNotYetValidException;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
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;
|
||||
|
|
@ -39,6 +41,21 @@ import javax.inject.Inject;
|
|||
import javax.naming.ConfigurationException;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import com.trilead.ssh2.Connection;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
|
||||
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
|
||||
import com.cloud.host.HostVO;
|
||||
import com.cloud.utils.PasswordGenerator;
|
||||
import com.cloud.utils.ssh.SSHCmdHelper;
|
||||
import com.cloud.vm.VMInstanceVO;
|
||||
import com.cloud.vm.dao.VMInstanceDao;
|
||||
import org.apache.cloudstack.utils.security.KeyStoreUtils;
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
|
||||
import org.apache.cloudstack.api.ApiErrorCode;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
|
|
@ -60,6 +77,7 @@ import org.joda.time.DateTime;
|
|||
import org.joda.time.DateTimeZone;
|
||||
|
||||
import com.cloud.agent.AgentManager;
|
||||
import com.cloud.agent.api.routing.NetworkElementCommand;
|
||||
import com.cloud.alert.AlertManager;
|
||||
import com.cloud.certificate.CrlVO;
|
||||
import com.cloud.certificate.dao.CrlDao;
|
||||
|
|
@ -81,6 +99,12 @@ public class CAManagerImpl extends ManagerBase implements CAManager {
|
|||
@Inject
|
||||
private HostDao hostDao;
|
||||
@Inject
|
||||
private VMInstanceDao vmInstanceDao;
|
||||
@Inject
|
||||
private NetworkOrchestrationService networkOrchestrationService;
|
||||
@Inject
|
||||
private ConfigurationDao configDao;
|
||||
@Inject
|
||||
private AgentManager agentManager;
|
||||
@Inject
|
||||
private BackgroundPollManager backgroundPollManager;
|
||||
|
|
@ -177,12 +201,17 @@ public class CAManagerImpl extends ManagerBase implements CAManager {
|
|||
|
||||
@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) {
|
||||
public boolean provisionCertificate(final Host host, final Boolean reconnect, final String caProvider, final boolean forced) {
|
||||
if (host == null) {
|
||||
throw new CloudRuntimeException("Unable to find valid host to renew certificate for");
|
||||
}
|
||||
CallContext.current().setEventDetails("Host ID: " + host.getUuid());
|
||||
CallContext.current().putContextParameter(Host.class, host.getUuid());
|
||||
|
||||
if (forced) {
|
||||
return provisionCertificateForced(host, reconnect, caProvider);
|
||||
}
|
||||
|
||||
String csr = null;
|
||||
|
||||
try {
|
||||
|
|
@ -200,6 +229,141 @@ public class CAManagerImpl extends ManagerBase implements CAManager {
|
|||
}
|
||||
}
|
||||
|
||||
protected boolean provisionCertificateForced(Host host, Boolean reconnect, String caProvider) {
|
||||
if (host.getType() == Host.Type.Routing && host.getHypervisorType() == com.cloud.hypervisor.Hypervisor.HypervisorType.KVM) {
|
||||
return provisionKvmHostViaSsh(host, caProvider);
|
||||
} else if (host.getType() == Host.Type.ConsoleProxy || host.getType() == Host.Type.SecondaryStorageVM) {
|
||||
return provisionSystemVmViaSsh(host, reconnect, caProvider);
|
||||
}
|
||||
throw new CloudRuntimeException("Forced certificate provisioning is only supported for KVM hosts and SystemVMs.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void provisionCertificateViaSsh(final Connection sshConnection, final String agentIp, final String agentHostname, final String caProvider) {
|
||||
Integer validityPeriod = CAManager.CertValidityPeriod.value();
|
||||
if (validityPeriod < 1) {
|
||||
validityPeriod = 1;
|
||||
}
|
||||
|
||||
String keystorePassword = PasswordGenerator.generateRandomPassword(16);
|
||||
|
||||
// 1. Setup Keystore and Generate CSR
|
||||
final SSHCmdHelper.SSHCmdResult keystoreSetupResult = SSHCmdHelper.sshExecuteCmdWithResult(sshConnection,
|
||||
String.format("sudo /usr/share/cloudstack-common/scripts/util/%s " +
|
||||
"/etc/cloudstack/agent/agent.properties " +
|
||||
"/etc/cloudstack/agent/%s " +
|
||||
"%s %d " +
|
||||
"/etc/cloudstack/agent/%s",
|
||||
KeyStoreUtils.KS_SETUP_SCRIPT,
|
||||
KeyStoreUtils.KS_FILENAME,
|
||||
keystorePassword,
|
||||
validityPeriod,
|
||||
KeyStoreUtils.CSR_FILENAME));
|
||||
|
||||
if (!keystoreSetupResult.isSuccess()) {
|
||||
throw new CloudRuntimeException("Failed to setup keystore and generate CSR via SSH on host: " + agentIp);
|
||||
}
|
||||
|
||||
// 2. Issue Certificate based on returned CSR
|
||||
final String csr = keystoreSetupResult.getStdOut();
|
||||
final Certificate certificate = issueCertificate(csr, Arrays.asList(agentHostname, agentIp),
|
||||
Collections.singletonList(agentIp), null, caProvider);
|
||||
|
||||
if (certificate == null || certificate.getClientCertificate() == null) {
|
||||
throw new CloudRuntimeException("Failed to issue certificates for host: " + agentIp);
|
||||
}
|
||||
|
||||
// 3. Import Certificate into agent keystore
|
||||
final SetupCertificateCommand certificateCommand = new SetupCertificateCommand(certificate);
|
||||
final SSHCmdHelper.SSHCmdResult setupCertResult = SSHCmdHelper.sshExecuteCmdWithResult(sshConnection,
|
||||
String.format("sudo /usr/share/cloudstack-common/scripts/util/%s " +
|
||||
"/etc/cloudstack/agent/agent.properties %s " +
|
||||
"/etc/cloudstack/agent/%s %s " +
|
||||
"/etc/cloudstack/agent/%s \"%s\" " +
|
||||
"/etc/cloudstack/agent/%s \"%s\" " +
|
||||
"/etc/cloudstack/agent/%s \"%s\"",
|
||||
KeyStoreUtils.KS_IMPORT_SCRIPT,
|
||||
keystorePassword,
|
||||
KeyStoreUtils.KS_FILENAME,
|
||||
KeyStoreUtils.SSH_MODE,
|
||||
KeyStoreUtils.CERT_FILENAME,
|
||||
certificateCommand.getEncodedCertificate(),
|
||||
KeyStoreUtils.CACERT_FILENAME,
|
||||
certificateCommand.getEncodedCaCertificates(),
|
||||
KeyStoreUtils.PKEY_FILENAME,
|
||||
certificateCommand.getEncodedPrivateKey()));
|
||||
|
||||
if (!setupCertResult.isSuccess()) {
|
||||
throw new CloudRuntimeException("Failed to import certificates into agent keystore via SSH on host: " + agentIp);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean provisionKvmHostViaSsh(Host host, String caProvider) {
|
||||
final HostVO hostVO = (HostVO) host;
|
||||
hostDao.loadDetails(hostVO);
|
||||
String username = hostVO.getDetail(ApiConstants.USERNAME);
|
||||
String password = hostVO.getDetail(ApiConstants.PASSWORD);
|
||||
String hostIp = host.getPrivateIpAddress();
|
||||
|
||||
int port = AgentManager.KVMHostDiscoverySshPort.valueIn(host.getClusterId());
|
||||
if (hostVO.getDetail(Host.HOST_SSH_PORT) != null) {
|
||||
port = NumberUtils.toInt(hostVO.getDetail(Host.HOST_SSH_PORT), port);
|
||||
}
|
||||
|
||||
Connection sshConnection = null;
|
||||
try {
|
||||
sshConnection = new Connection(hostIp, port);
|
||||
sshConnection.connect(null, 60000, 60000);
|
||||
|
||||
String privateKey = configDao.getValue("ssh.privatekey");
|
||||
if (!SSHCmdHelper.acquireAuthorizedConnectionWithPublicKey(sshConnection, username, privateKey)) {
|
||||
if (StringUtils.isEmpty(password) || !sshConnection.authenticateWithPassword(username, password)) {
|
||||
throw new CloudRuntimeException("Failed to authenticate to host via SSH for forced provisioning: " + hostIp);
|
||||
}
|
||||
}
|
||||
|
||||
provisionCertificateViaSsh(sshConnection, hostIp, host.getName(), caProvider);
|
||||
|
||||
String sudoPrefix = "root".equals(username) ? "" : "sudo ";
|
||||
SSHCmdHelper.sshExecuteCmd(sshConnection, sudoPrefix + "systemctl restart libvirtd");
|
||||
SSHCmdHelper.sshExecuteCmd(sshConnection, sudoPrefix + "systemctl restart cloudstack-agent");
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.error("Error during forced SSH provisioning for KVM host " + host.getUuid(), e);
|
||||
return false;
|
||||
} finally {
|
||||
if (sshConnection != null) {
|
||||
sshConnection.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean provisionSystemVmViaSsh(Host host, Boolean reconnect, String caProvider) {
|
||||
VMInstanceVO vm = vmInstanceDao.findVMByInstanceName(host.getName());
|
||||
if (vm == null) {
|
||||
throw new CloudRuntimeException("Cannot find underlying VM for host: " + host.getName());
|
||||
}
|
||||
|
||||
final Map<String, String> sshAccessDetails = networkOrchestrationService.getSystemVMAccessDetails(vm);
|
||||
final Map<String, String> ipAddressDetails = new HashMap<>(sshAccessDetails);
|
||||
ipAddressDetails.remove(NetworkElementCommand.ROUTER_NAME);
|
||||
|
||||
try {
|
||||
final Host hypervisorHost = hostDao.findById(vm.getHostId());
|
||||
if (hypervisorHost == null) {
|
||||
throw new CloudRuntimeException("Cannot find hypervisor host for system VM: " + host.getName());
|
||||
}
|
||||
|
||||
final Certificate certificate = issueCertificate(null, Arrays.asList(vm.getHostName(), vm.getInstanceName()),
|
||||
new ArrayList<>(ipAddressDetails.values()), CertValidityPeriod.value(), caProvider);
|
||||
return deployCertificate(hypervisorHost, certificate, reconnect, sshAccessDetails);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to provision system VM " + host.getName() + " via hypervisor SSH proxy. Ensure the hypervisor host is connected.", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateKeyStoreAndCsr(final Host host, final Map<String, String> sshAccessDetails) throws AgentUnavailableException, OperationTimedoutException {
|
||||
final SetupKeyStoreCommand cmd = new SetupKeyStoreCommand(CertValidityPeriod.value());
|
||||
|
|
@ -211,11 +375,6 @@ public class CAManagerImpl extends ManagerBase implements CAManager {
|
|||
return answer.getCsr();
|
||||
}
|
||||
|
||||
private boolean isValidSystemVMType(Host.Type type) {
|
||||
return Host.Type.SecondaryStorageVM.equals(type) ||
|
||||
Host.Type.ConsoleProxy.equals(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deployCertificate(final Host host, final Certificate certificate, final Boolean reconnect, final Map<String, String> sshAccessDetails)
|
||||
throws AgentUnavailableException, OperationTimedoutException {
|
||||
|
|
@ -340,7 +499,7 @@ public class CAManagerImpl extends ManagerBase implements CAManager {
|
|||
if (AutomaticCertRenewal.valueIn(host.getClusterId())) {
|
||||
try {
|
||||
logger.debug("Attempting certificate auto-renewal for " + hostDescription, e);
|
||||
boolean result = caManager.provisionCertificate(host, false, null);
|
||||
boolean result = caManager.provisionCertificate(host, false, null, false);
|
||||
if (result) {
|
||||
logger.debug("Succeeded in auto-renewing certificate for " + hostDescription, e);
|
||||
} else {
|
||||
|
|
@ -400,9 +559,57 @@ public class CAManagerImpl extends ManagerBase implements CAManager {
|
|||
logger.error("Failed to find valid configured CA provider, please check!");
|
||||
return false;
|
||||
}
|
||||
if (CaInjectDefaultTruststore.value()) {
|
||||
injectCaCertIntoDefaultTruststore();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void injectCaCertIntoDefaultTruststore() {
|
||||
try {
|
||||
final List<X509Certificate> caCerts = configuredCaProvider.getCaCertificate();
|
||||
if (caCerts == null || caCerts.isEmpty()) {
|
||||
logger.debug("No CA certificates found from the configured provider, skipping JVM truststore injection");
|
||||
return;
|
||||
}
|
||||
|
||||
final KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
trustStore.load(null, null);
|
||||
|
||||
// Copy existing default trusted certs
|
||||
final TrustManagerFactory defaultTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
defaultTmf.init((KeyStore) null);
|
||||
int aliasIndex = 0;
|
||||
for (final TrustManager tm : defaultTmf.getTrustManagers()) {
|
||||
if (tm instanceof X509TrustManager) {
|
||||
for (final X509Certificate cert : ((X509TrustManager) tm).getAcceptedIssuers()) {
|
||||
trustStore.setCertificateEntry("default-ca-" + aliasIndex++, cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add CA provider's certificates
|
||||
int count = 0;
|
||||
for (final X509Certificate caCert : caCerts) {
|
||||
final String alias = "cloudstack-ca-" + count;
|
||||
trustStore.setCertificateEntry(alias, caCert);
|
||||
count++;
|
||||
logger.info("Injected CA certificate into JVM default truststore: subject={}, alias={}",
|
||||
caCert.getSubjectX500Principal().getName(), alias);
|
||||
}
|
||||
|
||||
// Reinitialize default SSLContext with the updated truststore
|
||||
final TrustManagerFactory updatedTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
updatedTmf.init(trustStore);
|
||||
final SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, updatedTmf.getTrustManagers(), new SecureRandom());
|
||||
SSLContext.setDefault(sslContext);
|
||||
logger.info("Successfully injected {} CA certificate(s) into JVM default truststore", count);
|
||||
} catch (final GeneralSecurityException | IOException e) {
|
||||
logger.error("Failed to inject CA certificate into JVM default truststore", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configure(final String name, final Map<String, Object> params) throws ConfigurationException {
|
||||
backgroundPollManager.submitTask(new CABackgroundTask(this, hostDao));
|
||||
|
|
@ -433,7 +640,7 @@ public class CAManagerImpl extends ManagerBase implements CAManager {
|
|||
public ConfigKey<?>[] getConfigKeys() {
|
||||
return new ConfigKey<?>[] {CAProviderPlugin, CertKeySize, CertSignatureAlgorithm, CertValidityPeriod,
|
||||
AutomaticCertRenewal, AllowHostIPInSysVMAgentCert, CABackgroundJobDelay, CertExpiryAlertPeriod,
|
||||
CertManagementCustomSubjectAlternativeName
|
||||
CertManagementCustomSubjectAlternativeName, CaInjectDefaultTruststore
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -115,19 +115,19 @@ public class CABackgroundTaskTest {
|
|||
certMap.put(hostIp, expiredCertificate);
|
||||
Assume.assumeThat(certMap.size() == 1, is(true));
|
||||
task.runInContext();
|
||||
Mockito.verify(caManager, Mockito.times(1)).provisionCertificate(host, false, null);
|
||||
Mockito.verify(caManager, Mockito.times(1)).provisionCertificate(host, false, null, false);
|
||||
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(any(Host.class), anyBoolean(), nullable(String.class))).thenThrow(new CloudRuntimeException("some error"));
|
||||
Mockito.when(caManager.provisionCertificate(any(Host.class), anyBoolean(), nullable(String.class), anyBoolean())).thenThrow(new CloudRuntimeException("some error"));
|
||||
host.setManagementServerId(ManagementServerNode.getManagementServerId());
|
||||
certMap.put(hostIp, expiredCertificate);
|
||||
Assume.assumeThat(certMap.size() == 1, is(true));
|
||||
task.runInContext();
|
||||
Mockito.verify(caManager, Mockito.times(1)).provisionCertificate(host, false, null);
|
||||
Mockito.verify(caManager, Mockito.times(1)).provisionCertificate(host, false, null, false);
|
||||
Mockito.verify(caManager, Mockito.times(1)).sendAlert(Mockito.any(Host.class), Mockito.anyString(), Mockito.anyString());
|
||||
}
|
||||
|
||||
|
|
@ -138,12 +138,12 @@ public class CABackgroundTaskTest {
|
|||
Assume.assumeThat(certMap.size() == 1, is(true));
|
||||
// First round
|
||||
task.runInContext();
|
||||
Mockito.verify(caManager, Mockito.times(0)).provisionCertificate(Mockito.any(Host.class), anyBoolean(), Mockito.anyString());
|
||||
Mockito.verify(caManager, Mockito.times(0)).provisionCertificate(Mockito.any(Host.class), anyBoolean(), Mockito.anyString(), Mockito.anyBoolean());
|
||||
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), anyBoolean(), Mockito.anyString());
|
||||
Mockito.verify(caManager, Mockito.times(0)).provisionCertificate(Mockito.any(Host.class), anyBoolean(), Mockito.anyString(), Mockito.anyBoolean());
|
||||
Mockito.verify(caManager, Mockito.times(0)).sendAlert(Mockito.any(Host.class), Mockito.anyString(), Mockito.anyString());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import com.cloud.certificate.CrlVO;
|
|||
import com.cloud.certificate.dao.CrlDao;
|
||||
import com.cloud.host.Host;
|
||||
import com.cloud.host.dao.HostDao;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.framework.ca.CAProvider;
|
||||
import org.apache.cloudstack.framework.ca.Certificate;
|
||||
|
|
@ -33,16 +34,34 @@ import org.junit.Assert;
|
|||
import org.junit.Before;
|
||||
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.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyPair;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
|
||||
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.MockedConstruction;
|
||||
import com.cloud.utils.ssh.SSHCmdHelper;
|
||||
import com.cloud.host.HostVO;
|
||||
import com.cloud.vm.VMInstanceVO;
|
||||
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
|
||||
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
|
||||
import com.cloud.vm.dao.VMInstanceDao;
|
||||
import com.trilead.ssh2.Connection;
|
||||
import com.cloud.agent.api.routing.NetworkElementCommand;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
|
|
@ -62,8 +81,15 @@ public class CAManagerImplTest {
|
|||
private AgentManager agentManager;
|
||||
@Mock
|
||||
private CAProvider caProvider;
|
||||
|
||||
private CAManagerImpl caManager;
|
||||
@Mock
|
||||
private VMInstanceDao vmInstanceDao;
|
||||
@Mock
|
||||
private NetworkOrchestrationService networkOrchestrationService;
|
||||
@Mock
|
||||
private ConfigurationDao configDao;
|
||||
@InjectMocks
|
||||
@Spy
|
||||
private CAManagerImpl caManager = new CAManagerImpl();
|
||||
|
||||
private void addField(final CAManagerImpl provider, final String name, final Object o) throws IllegalAccessException, NoSuchFieldException {
|
||||
Field f = CAManagerImpl.class.getDeclaredField(name);
|
||||
|
|
@ -73,10 +99,6 @@ public class CAManagerImplTest {
|
|||
|
||||
@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");
|
||||
|
|
@ -91,19 +113,19 @@ public class CAManagerImplTest {
|
|||
}
|
||||
|
||||
@Test(expected = ServerApiException.class)
|
||||
public void testIssueCertificateThrowsException() throws Exception {
|
||||
public void testIssueCertificateThrowsException() {
|
||||
caManager.issueCertificate(null, null, null, 1, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIssueCertificate() throws Exception {
|
||||
public void testIssueCertificate() {
|
||||
caManager.issueCertificate(null, Collections.singletonList("domain.example"), null, 1, null);
|
||||
Mockito.verify(caProvider, Mockito.times(1)).issueCertificate(anyList(), nullable(List.class), anyInt());
|
||||
Mockito.verify(caProvider, Mockito.times(0)).issueCertificate(anyString(), anyList(), anyList(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRevokeCertificate() throws Exception {
|
||||
public void testRevokeCertificate() {
|
||||
final CrlVO crl = new CrlVO(CertUtils.generateRandomBigInt(), "some.domain", "some-uuid");
|
||||
Mockito.when(crlDao.revokeCertificate(Mockito.any(BigInteger.class), anyString())).thenReturn(crl);
|
||||
Mockito.when(caProvider.revokeCertificate(Mockito.any(BigInteger.class), anyString())).thenReturn(true);
|
||||
|
|
@ -121,9 +143,190 @@ public class CAManagerImplTest {
|
|||
Mockito.when(agentManager.send(anyLong(), any(SetupCertificateCommand.class))).thenReturn(new SetupCertificateAnswer(true));
|
||||
Mockito.when(agentManager.send(anyLong(), any(SetupKeyStoreCommand.class))).thenReturn(new SetupKeystoreAnswer("someCsr"));
|
||||
Mockito.doNothing().when(agentManager).reconnect(Mockito.anyLong());
|
||||
Assert.assertTrue(caManager.provisionCertificate(host, true, null));
|
||||
Assert.assertTrue(caManager.provisionCertificate(host, true, null, false));
|
||||
Mockito.verify(agentManager, Mockito.times(1)).send(Mockito.anyLong(), any(SetupKeyStoreCommand.class));
|
||||
Mockito.verify(agentManager, Mockito.times(1)).send(Mockito.anyLong(), any(SetupCertificateCommand.class));
|
||||
Mockito.verify(agentManager, Mockito.times(1)).reconnect(Mockito.anyLong());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testProvisionCertificateForced() throws Exception {
|
||||
final Host host = Mockito.mock(Host.class);
|
||||
Mockito.doReturn(true).when(caManager).provisionCertificateForced(host, true, null);
|
||||
Assert.assertTrue(caManager.provisionCertificate(host, true, null, true));
|
||||
Mockito.verify(caManager, Mockito.times(1)).provisionCertificateForced(host, true, null);
|
||||
Mockito.verify(agentManager, Mockito.never()).send(Mockito.anyLong(), any(SetupKeyStoreCommand.class));
|
||||
Mockito.verify(agentManager, Mockito.never()).send(Mockito.anyLong(), any(SetupCertificateCommand.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIssueCertificateWithCsr() throws Exception {
|
||||
final KeyPair keyPair = CertUtils.generateRandomKeyPair(1024);
|
||||
final X509Certificate x509 = CertUtils.generateV3Certificate(null, keyPair, keyPair.getPublic(), "CN=ca", "SHA256withRSA", 365, null, null);
|
||||
Mockito.when(caProvider.issueCertificate(anyString(), anyList(), anyList(), anyInt()))
|
||||
.thenReturn(new Certificate(x509, null, Collections.singletonList(x509)));
|
||||
final Certificate result = caManager.issueCertificate("someCsr", Collections.singletonList("domain.example"), Collections.singletonList("1.2.3.4"), 365, null);
|
||||
Assert.assertNotNull(result);
|
||||
Mockito.verify(caProvider, Mockito.times(1)).issueCertificate(anyString(), anyList(), anyList(), anyInt());
|
||||
Mockito.verify(caProvider, Mockito.never()).issueCertificate(anyList(), nullable(List.class), anyInt());
|
||||
}
|
||||
|
||||
@Test(expected = CloudRuntimeException.class)
|
||||
public void testProvisionCertificateNullHost() {
|
||||
caManager.provisionCertificate(null, true, null, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProvisionCertificateForSystemVm() throws Exception {
|
||||
final Host host = Mockito.mock(Host.class);
|
||||
Mockito.when(host.getType()).thenReturn(Host.Type.ConsoleProxy);
|
||||
Mockito.when(host.getPrivateIpAddress()).thenReturn("1.2.3.4");
|
||||
final KeyPair keyPair = CertUtils.generateRandomKeyPair(1024);
|
||||
final X509Certificate x509 = CertUtils.generateV3Certificate(null, keyPair, keyPair.getPublic(), "CN=ca", "SHA256withRSA", 365, null, null);
|
||||
Mockito.when(caProvider.issueCertificate(anyList(), anyList(), anyInt()))
|
||||
.thenReturn(new Certificate(x509, null, Collections.singletonList(x509)));
|
||||
Mockito.when(agentManager.send(anyLong(), any(SetupCertificateCommand.class))).thenReturn(new SetupCertificateAnswer(true));
|
||||
Assert.assertTrue(caManager.provisionCertificate(host, false, null, false));
|
||||
Mockito.verify(agentManager, Mockito.never()).send(Mockito.anyLong(), any(SetupKeyStoreCommand.class));
|
||||
Mockito.verify(agentManager, Mockito.times(1)).send(Mockito.anyLong(), any(SetupCertificateCommand.class));
|
||||
Mockito.verify(agentManager, Mockito.never()).reconnect(Mockito.anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProvisionCertificateWithoutReconnect() throws Exception {
|
||||
final Host host = Mockito.mock(Host.class);
|
||||
Mockito.when(host.getPrivateIpAddress()).thenReturn("1.2.3.4");
|
||||
final KeyPair keyPair = CertUtils.generateRandomKeyPair(1024);
|
||||
final X509Certificate x509 = CertUtils.generateV3Certificate(null, keyPair, keyPair.getPublic(), "CN=ca", "SHA256withRSA", 365, null, null);
|
||||
Mockito.when(caProvider.issueCertificate(anyString(), anyList(), anyList(), anyInt()))
|
||||
.thenReturn(new Certificate(x509, null, Collections.singletonList(x509)));
|
||||
Mockito.when(agentManager.send(anyLong(), any(SetupCertificateCommand.class))).thenReturn(new SetupCertificateAnswer(true));
|
||||
Mockito.when(agentManager.send(anyLong(), any(SetupKeyStoreCommand.class))).thenReturn(new SetupKeystoreAnswer("someCsr"));
|
||||
Assert.assertTrue(caManager.provisionCertificate(host, false, null, false));
|
||||
Mockito.verify(agentManager, Mockito.never()).reconnect(Mockito.anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRevokeCertificateReturnsFalseWhenCrlIsNull() {
|
||||
Mockito.when(crlDao.revokeCertificate(Mockito.any(BigInteger.class), anyString())).thenReturn(null);
|
||||
Assert.assertFalse(caManager.revokeCertificate(BigInteger.ONE, "some.domain", null));
|
||||
Mockito.verify(caProvider, Mockito.never()).revokeCertificate(Mockito.any(BigInteger.class), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRevokeCertificateReturnsFalseWhenSerialMismatch() {
|
||||
final CrlVO crl = new CrlVO(BigInteger.ONE, "some.domain", "some-uuid");
|
||||
Mockito.when(crlDao.revokeCertificate(Mockito.any(BigInteger.class), anyString())).thenReturn(crl);
|
||||
Assert.assertFalse(caManager.revokeCertificate(BigInteger.TWO, "some.domain", null));
|
||||
Mockito.verify(caProvider, Mockito.never()).revokeCertificate(Mockito.any(BigInteger.class), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPurgeHostCertificate() throws Exception {
|
||||
final Host host = Mockito.mock(Host.class);
|
||||
Mockito.when(host.getPrivateIpAddress()).thenReturn("10.0.0.1");
|
||||
Mockito.when(host.getPublicIpAddress()).thenReturn("192.168.0.1");
|
||||
final KeyPair keyPair = CertUtils.generateRandomKeyPair(1024);
|
||||
final X509Certificate x509 = CertUtils.generateV3Certificate(null, keyPair,
|
||||
keyPair.getPublic(), "CN=ca", "SHA256withRSA",
|
||||
365, null, null);
|
||||
caManager.getActiveCertificatesMap().put("10.0.0.1", x509);
|
||||
caManager.getActiveCertificatesMap().put("192.168.0.1", x509);
|
||||
caManager.purgeHostCertificate(host);
|
||||
Assert.assertFalse(caManager.getActiveCertificatesMap().containsKey("10.0.0.1"));
|
||||
Assert.assertFalse(caManager.getActiveCertificatesMap().containsKey("192.168.0.1"));
|
||||
}
|
||||
@Test
|
||||
public void testProvisionCertificateViaSsh() throws Exception {
|
||||
Connection sshConnection = Mockito.mock(Connection.class);
|
||||
final String agentIp = "192.168.1.1";
|
||||
final String agentHostname = "host1";
|
||||
final String caProviderStr = "root";
|
||||
|
||||
try (MockedStatic<SSHCmdHelper> sshCmdHelperMock = Mockito.mockStatic(SSHCmdHelper.class)) {
|
||||
SSHCmdHelper.SSHCmdResult successResult = new SSHCmdHelper.SSHCmdResult(0, "someCsr", "");
|
||||
sshCmdHelperMock.when(() -> SSHCmdHelper.sshExecuteCmdWithResult(Mockito.eq(sshConnection), Mockito.anyString()))
|
||||
.thenReturn(successResult);
|
||||
|
||||
final KeyPair keyPair = CertUtils.generateRandomKeyPair(1024);
|
||||
final X509Certificate x509 = CertUtils.generateV3Certificate(null, keyPair, keyPair.getPublic(), "CN=ca", "SHA256withRSA", 365, null, null);
|
||||
Mockito.doReturn(new Certificate(x509, null, Collections.singletonList(x509)))
|
||||
.when(caManager).issueCertificate(Mockito.anyString(), Mockito.anyList(), Mockito.anyList(), Mockito.nullable(Integer.class), Mockito.anyString());
|
||||
|
||||
caManager.provisionCertificateViaSsh(sshConnection, agentIp, agentHostname, caProviderStr);
|
||||
|
||||
sshCmdHelperMock.verify(() -> SSHCmdHelper.sshExecuteCmdWithResult(Mockito.eq(sshConnection), Mockito.contains("keystore-setup")), Mockito.times(1));
|
||||
sshCmdHelperMock.verify(() -> SSHCmdHelper.sshExecuteCmdWithResult(Mockito.eq(sshConnection), Mockito.contains("keystore-cert-import")), Mockito.times(1));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProvisionKvmHostViaSsh() throws Exception {
|
||||
HostVO host = Mockito.mock(HostVO.class);
|
||||
Mockito.when(host.getPrivateIpAddress()).thenReturn("192.168.1.1");
|
||||
Mockito.when(host.getName()).thenReturn("host1");
|
||||
Mockito.when(host.getClusterId()).thenReturn(1L);
|
||||
|
||||
Mockito.doNothing().when(hostDao).loadDetails(host);
|
||||
Mockito.when(host.getDetail(ApiConstants.USERNAME)).thenReturn("root");
|
||||
Mockito.when(host.getDetail(ApiConstants.PASSWORD)).thenReturn("password");
|
||||
|
||||
Mockito.when(configDao.getValue("ssh.privatekey")).thenReturn("privatekey");
|
||||
|
||||
try (MockedConstruction<Connection> ignored = Mockito.mockConstruction(Connection.class,
|
||||
(mock, context) -> {
|
||||
// Do nothing on connect
|
||||
});
|
||||
MockedStatic<SSHCmdHelper> sshCmdHelperMock = Mockito.mockStatic(SSHCmdHelper.class)) {
|
||||
sshCmdHelperMock.when(() -> SSHCmdHelper.acquireAuthorizedConnectionWithPublicKey(Mockito.any(Connection.class), Mockito.anyString(), Mockito.anyString()))
|
||||
.thenReturn(true);
|
||||
|
||||
Mockito.doNothing().when(caManager).provisionCertificateViaSsh(Mockito.any(Connection.class), Mockito.anyString(), Mockito.anyString(), Mockito.anyString());
|
||||
|
||||
Method method = CAManagerImpl.class.getDeclaredMethod("provisionKvmHostViaSsh", Host.class, String.class);
|
||||
method.setAccessible(true);
|
||||
boolean result = (Boolean) method.invoke(caManager, host, "root");
|
||||
|
||||
Assert.assertTrue(result);
|
||||
Mockito.verify(caManager, Mockito.times(1)).provisionCertificateViaSsh(Mockito.any(Connection.class), Mockito.eq("192.168.1.1"), Mockito.eq("host1"), Mockito.eq("root"));
|
||||
sshCmdHelperMock.verify(() -> SSHCmdHelper.sshExecuteCmd(Mockito.any(Connection.class), Mockito.eq("systemctl restart libvirtd")), Mockito.times(1));
|
||||
sshCmdHelperMock.verify(() -> SSHCmdHelper.sshExecuteCmd(Mockito.any(Connection.class), Mockito.eq("systemctl restart cloudstack-agent")), Mockito.times(1));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProvisionSystemVmViaSsh() throws Exception {
|
||||
Host host = Mockito.mock(Host.class);
|
||||
Mockito.when(host.getName()).thenReturn("v-1-VM");
|
||||
|
||||
VMInstanceVO vm = Mockito.mock(VMInstanceVO.class);
|
||||
Mockito.when(vm.getHostId()).thenReturn(1L);
|
||||
Mockito.when(vm.getHostName()).thenReturn("host1");
|
||||
Mockito.when(vm.getInstanceName()).thenReturn("v-1-VM");
|
||||
Mockito.when(vmInstanceDao.findVMByInstanceName("v-1-VM")).thenReturn(vm);
|
||||
|
||||
Map<String, String> accessDetails = new HashMap<>();
|
||||
accessDetails.put(NetworkElementCommand.ROUTER_IP, "192.168.1.2");
|
||||
Mockito.when(networkOrchestrationService.getSystemVMAccessDetails(vm)).thenReturn(accessDetails);
|
||||
|
||||
HostVO hypervisorHost = Mockito.mock(HostVO.class);
|
||||
Mockito.when(hostDao.findById(1L)).thenReturn(hypervisorHost);
|
||||
|
||||
final KeyPair keyPair = CertUtils.generateRandomKeyPair(1024);
|
||||
final X509Certificate x509 = CertUtils.generateV3Certificate(null, keyPair, keyPair.getPublic(), "CN=ca", "SHA256withRSA", 365, null, null);
|
||||
Certificate cert = new Certificate(x509, null, Collections.singletonList(x509));
|
||||
Mockito.doReturn(cert)
|
||||
.when(caManager).issueCertificate(Mockito.nullable(String.class), Mockito.anyList(), Mockito.anyList(), Mockito.nullable(Integer.class), Mockito.anyString());
|
||||
|
||||
Mockito.doReturn(true)
|
||||
.when(caManager).deployCertificate(Mockito.eq(hypervisorHost), Mockito.eq(cert), Mockito.anyBoolean(), Mockito.eq(accessDetails));
|
||||
|
||||
Method method = CAManagerImpl.class.getDeclaredMethod("provisionSystemVmViaSsh", Host.class, Boolean.class, String.class);
|
||||
method.setAccessible(true);
|
||||
boolean result = (Boolean) method.invoke(caManager, host, true, "root");
|
||||
|
||||
Assert.assertTrue(result);
|
||||
Mockito.verify(caManager, Mockito.times(1)).deployCertificate(Mockito.eq(hypervisorHost), Mockito.eq(cert), Mockito.eq(true), Mockito.eq(accessDetails));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,7 +126,28 @@ patch_systemvm() {
|
|||
|
||||
if [ "$TYPE" = "consoleproxy" ] || [ "$TYPE" = "secstorage" ]; then
|
||||
# Import global cacerts into 'cloud' service's keystore
|
||||
keytool -importkeystore -srckeystore /etc/ssl/certs/java/cacerts -destkeystore /usr/local/cloud/systemvm/certs/realhostip.keystore -srcstorepass changeit -deststorepass vmops.com -noprompt 2>/dev/null || true
|
||||
REALHOSTIP_KS_FILE="/usr/local/cloud/systemvm/certs/realhostip.keystore"
|
||||
REALHOSTIP_PASS="vmops.com"
|
||||
|
||||
keytool -importkeystore -srckeystore /etc/ssl/certs/java/cacerts \
|
||||
-destkeystore "$REALHOSTIP_KS_FILE" -srcstorepass changeit -deststorepass \
|
||||
"$REALHOSTIP_PASS" -noprompt 2>/dev/null || true
|
||||
|
||||
# Import CA cert(s) into realhostip.keystore so the SSVM JVM
|
||||
# (which overrides the truststore via -Djavax.net.ssl.trustStore in _run.sh)
|
||||
# can trust servers signed by the CloudStack CA
|
||||
CACERT_FILE="/usr/local/share/ca-certificates/cloudstack/ca.crt"
|
||||
|
||||
if [ -f "$CACERT_FILE" ] && [ -f "$REALHOSTIP_KS_FILE" ]; then
|
||||
awk 'BEGIN{n=0} /-----BEGIN CERTIFICATE-----/{n++} n>0{print > "cloudca." n }' "$CACERT_FILE"
|
||||
for caChain in $(ls cloudca.* 2>/dev/null); do
|
||||
keytool -delete -noprompt -alias "$caChain" -keystore "$REALHOSTIP_KS_FILE" \
|
||||
-storepass "$REALHOSTIP_PASS" > /dev/null 2>&1 || true
|
||||
keytool -import -noprompt -trustcacerts -alias "$caChain" -file "$caChain" \
|
||||
-keystore "$REALHOSTIP_KS_FILE" -storepass "$REALHOSTIP_PASS" > /dev/null 2>&1
|
||||
done
|
||||
rm -f cloudca.*
|
||||
fi
|
||||
fi
|
||||
|
||||
update_checksum $newpath/cloud-scripts.tgz
|
||||
|
|
|
|||
|
|
@ -15,9 +15,12 @@
|
|||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from nose.plugins.attrib import attr
|
||||
from marvin.cloudstackTestCase import cloudstackTestCase
|
||||
from marvin.lib.utils import cleanup_resources
|
||||
from marvin.lib.utils import cleanup_resources, wait_until
|
||||
from marvin.lib.base import *
|
||||
from marvin.lib.common import list_hosts
|
||||
|
||||
|
|
@ -60,6 +63,29 @@ class TestCARootProvider(cloudstackTestCase):
|
|||
except Exception as e:
|
||||
print(f"Certificate verification failed: {e}")
|
||||
|
||||
|
||||
def parseCertificateChain(self, pem):
|
||||
"""Split a PEM blob containing one or more certificates into a list of x509 objects."""
|
||||
certs = []
|
||||
matches = re.findall(
|
||||
r'-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----',
|
||||
pem,
|
||||
re.DOTALL
|
||||
)
|
||||
for match in matches:
|
||||
certs.append(x509.load_pem_x509_certificate(match.encode(), default_backend()))
|
||||
return certs
|
||||
|
||||
|
||||
def assertSignatureValid(self, issuerCert, cert):
|
||||
"""Verify cert is signed by issuerCert; raise on failure."""
|
||||
issuerCert.public_key().verify(
|
||||
cert.signature,
|
||||
cert.tbs_certificate_bytes,
|
||||
padding.PKCS1v15(),
|
||||
cert.signature_hash_algorithm,
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.apiclient = self.testClient.getApiClient()
|
||||
self.dbclient = self.testClient.getDbConnection()
|
||||
|
|
@ -224,3 +250,152 @@ class TestCARootProvider(cloudstackTestCase):
|
|||
self.assertTrue(len(hosts) == 1)
|
||||
else:
|
||||
self.fail("Failed to have systemvm host in Up state after cert provisioning")
|
||||
|
||||
|
||||
@attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
|
||||
def test_ca_certificate_chain_validity(self):
|
||||
"""
|
||||
Tests that listCaCertificate returns a valid certificate chain.
|
||||
When an intermediate CA is configured, the response is a PEM blob
|
||||
containing multiple certificates. Each non-root cert must be signed
|
||||
by the next cert in the chain, and the final cert must be self-signed.
|
||||
"""
|
||||
pem = self.getCaCertificate()
|
||||
self.assertTrue(len(pem) > 0)
|
||||
|
||||
chain = self.parseCertificateChain(pem)
|
||||
self.assertTrue(len(chain) >= 1, "Expected at least one certificate in CA chain")
|
||||
|
||||
# Each non-root cert must be signed by the next cert in the chain
|
||||
for i in range(len(chain) - 1):
|
||||
child = chain[i]
|
||||
parent = chain[i + 1]
|
||||
self.assertEqual(
|
||||
child.issuer, parent.subject,
|
||||
f"Chain break: cert[{i}] issuer does not match cert[{i + 1}] subject"
|
||||
)
|
||||
try:
|
||||
self.assertSignatureValid(parent, child)
|
||||
except Exception as e:
|
||||
self.fail(f"Signature verification failed for chain link {i} -> {i + 1}: {e}")
|
||||
|
||||
# The last cert in the chain must be self-signed (root CA)
|
||||
root = chain[-1]
|
||||
self.assertEqual(
|
||||
root.issuer, root.subject,
|
||||
"Final cert in CA chain is not self-signed"
|
||||
)
|
||||
try:
|
||||
self.assertSignatureValid(root, root)
|
||||
except Exception as e:
|
||||
self.fail(f"Root CA self-signature verification failed: {e}")
|
||||
|
||||
|
||||
@attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
|
||||
def test_issue_certificate_issuer_matches_ca(self):
|
||||
"""
|
||||
Tests that an issued certificate's issuer DN matches the subject DN
|
||||
of the first cert in the returned CA chain, and that the signature
|
||||
verifies against that cert's public key.
|
||||
"""
|
||||
cmd = issueCertificate.issueCertificateCmd()
|
||||
cmd.domain = 'apache.org'
|
||||
cmd.ipaddress = '10.1.1.1'
|
||||
cmd.provider = 'root'
|
||||
|
||||
response = self.apiclient.issueCertificate(cmd)
|
||||
self.assertTrue(len(response.certificate) > 0)
|
||||
self.assertTrue(len(response.cacertificates) > 0)
|
||||
|
||||
leaf = x509.load_pem_x509_certificate(response.certificate.encode(), default_backend())
|
||||
caChain = self.parseCertificateChain(response.cacertificates)
|
||||
self.assertTrue(len(caChain) >= 1, "Expected at least one CA certificate in response")
|
||||
|
||||
# The issuing CA is the first cert in the returned chain (intermediate
|
||||
# if an intermediate CA is configured, otherwise the root).
|
||||
issuingCa = caChain[0]
|
||||
self.assertEqual(
|
||||
leaf.issuer, issuingCa.subject,
|
||||
"Leaf certificate issuer does not match issuing CA subject"
|
||||
)
|
||||
try:
|
||||
self.assertSignatureValid(issuingCa, leaf)
|
||||
except Exception as e:
|
||||
self.fail(f"Leaf certificate signature does not verify against issuing CA: {e}")
|
||||
|
||||
|
||||
@attr(tags=['advanced', 'simulator', 'basic', 'sg'], required_hardware=False)
|
||||
def test_certificate_validity_period(self):
|
||||
"""
|
||||
Tests that an issued certificate has sensible validity bounds:
|
||||
not_valid_before <= now <= not_valid_after, and validity duration
|
||||
is at least 300 days (CloudStack default is 1 year).
|
||||
"""
|
||||
cmd = issueCertificate.issueCertificateCmd()
|
||||
cmd.domain = 'apache.org'
|
||||
cmd.provider = 'root'
|
||||
|
||||
response = self.apiclient.issueCertificate(cmd)
|
||||
self.assertTrue(len(response.certificate) > 0)
|
||||
|
||||
cert = x509.load_pem_x509_certificate(response.certificate.encode(), default_backend())
|
||||
|
||||
# cryptography >= 42 prefers the *_utc variants; fall back for older versions.
|
||||
notBefore = getattr(cert, 'not_valid_before_utc', None) or cert.not_valid_before
|
||||
notAfter = getattr(cert, 'not_valid_after_utc', None) or cert.not_valid_after
|
||||
|
||||
now = datetime.now(notBefore.tzinfo) if notBefore.tzinfo else datetime.utcnow()
|
||||
self.assertTrue(notBefore <= now, f"Certificate not_valid_before {notBefore} is in the future")
|
||||
self.assertTrue(now <= notAfter, f"Certificate not_valid_after {notAfter} is in the past")
|
||||
|
||||
duration = notAfter - notBefore
|
||||
self.assertTrue(
|
||||
duration >= timedelta(days=300),
|
||||
f"Certificate validity duration {duration} is less than expected minimum of 300 days"
|
||||
)
|
||||
|
||||
|
||||
def getUpKVMHosts(self, hostId=None):
|
||||
hosts = list_hosts(
|
||||
self.apiclient,
|
||||
type='Routing',
|
||||
hypervisor='KVM',
|
||||
state='Up',
|
||||
resourcestate='Enabled',
|
||||
id=hostId
|
||||
)
|
||||
return hosts
|
||||
|
||||
|
||||
@attr(tags=['advanced'], required_hardware=True)
|
||||
def test_provision_certificate_kvm(self):
|
||||
"""
|
||||
Tests certificate provisioning on a KVM host.
|
||||
Exercises the keystore-cert-import + cloud.jks provisioning flow
|
||||
against a real agent. Skipped when no KVM hosts are available.
|
||||
"""
|
||||
if self.hypervisor.lower() != 'kvm':
|
||||
raise self.skipTest("Hypervisor is not KVM, skipping test")
|
||||
|
||||
hosts = self.getUpKVMHosts()
|
||||
if not hosts or len(hosts) < 1:
|
||||
raise self.skipTest("No Up KVM hosts found, skipping test")
|
||||
|
||||
host = hosts[0]
|
||||
|
||||
cmd = provisionCertificate.provisionCertificateCmd()
|
||||
cmd.hostid = host.id
|
||||
cmd.reconnect = True
|
||||
cmd.provider = 'root'
|
||||
|
||||
response = self.apiclient.provisionCertificate(cmd)
|
||||
self.assertTrue(response.success)
|
||||
|
||||
def checkHostIsUp(hostId):
|
||||
hosts = self.getUpKVMHosts(hostId)
|
||||
return (hosts is not None and len(hosts) > 0), hosts
|
||||
|
||||
result, hosts = wait_until(2, 30, checkHostIsUp, host.id)
|
||||
if not result:
|
||||
self.fail("KVM host did not return to Up state after certificate provisioning")
|
||||
self.assertEqual(len(hosts), 1)
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ export default {
|
|||
show: (record) => {
|
||||
return record.hypervisor === 'KVM' || record.hypervisor === store.getters.customHypervisorName
|
||||
},
|
||||
args: ['hostid'],
|
||||
args: ['hostid', 'forced'],
|
||||
mapping: {
|
||||
hostid: {
|
||||
value: (record) => { return record.id }
|
||||
|
|
|
|||
|
|
@ -552,7 +552,7 @@ public class Link {
|
|||
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 new HandshakeHolder(myAppData, myNetData, true);
|
||||
return new HandshakeHolder(myAppData, myNetData, false);
|
||||
}
|
||||
if (result == null) {
|
||||
return new HandshakeHolder(myAppData, myNetData, false);
|
||||
|
|
|
|||
|
|
@ -98,9 +98,18 @@ public class CertUtils {
|
|||
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 List<X509Certificate> pemToX509Certificates(final String pem) throws CertificateException, IOException {
|
||||
final List<X509Certificate> certs = new ArrayList<>();
|
||||
try (final PEMParser pemParser = new PEMParser(new StringReader(pem))) {
|
||||
final JcaX509CertificateConverter certConverter = new JcaX509CertificateConverter().setProvider("BC");
|
||||
Object parsedObj;
|
||||
while ((parsedObj = pemParser.readObject()) != null) {
|
||||
if (parsedObj instanceof X509CertificateHolder) {
|
||||
certs.add(certConverter.getCertificate((X509CertificateHolder) parsedObj));
|
||||
}
|
||||
}
|
||||
}
|
||||
return certs;
|
||||
}
|
||||
|
||||
public static String x509CertificateToPem(final X509Certificate cert) throws IOException {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import com.cloud.utils.PropertiesUtil;
|
|||
public class KeyStoreUtils {
|
||||
public static final String KS_SETUP_SCRIPT = "keystore-setup";
|
||||
public static final String KS_IMPORT_SCRIPT = "keystore-cert-import";
|
||||
public static final String KS_SYSTEMVM_IMPORT_SCRIPT = "keystore-cert-import-sysvm";
|
||||
|
||||
public static final String AGENT_PROPSFILE = "agent.properties";
|
||||
public static final String KS_PASSPHRASE_PROPERTY = "keystore.passphrase";
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ public class CertUtilsTest {
|
|||
public void testCertificateConversionMethods() throws Exception {
|
||||
final X509Certificate in = caCertificate;
|
||||
final String pem = CertUtils.x509CertificateToPem(in);
|
||||
final X509Certificate out = CertUtils.pemToX509Certificate(pem);
|
||||
final X509Certificate out = CertUtils.pemToX509Certificates(pem).get(0);
|
||||
Assert.assertTrue(pem.startsWith("-----BEGIN CERTIFICATE-----\n"));
|
||||
Assert.assertTrue(pem.endsWith("-----END CERTIFICATE-----\n"));
|
||||
Assert.assertEquals(in.getSerialNumber(), out.getSerialNumber());
|
||||
|
|
@ -87,6 +87,21 @@ public class CertUtilsTest {
|
|||
Assert.assertNotEquals(CertUtils.generateRandomBigInt(), CertUtils.generateRandomBigInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPemToX509CertificatesWithChain() throws Exception {
|
||||
final KeyPair intermediateKeyPair = CertUtils.generateRandomKeyPair(1024);
|
||||
final X509Certificate intermediateCert = CertUtils.generateV3Certificate(caCertificate, caKeyPair,
|
||||
intermediateKeyPair.getPublic(), "CN=intermediate", "SHA256withRSA", 365, null, null);
|
||||
|
||||
final String chainPem = CertUtils.x509CertificateToPem(intermediateCert)
|
||||
+ CertUtils.x509CertificateToPem(caCertificate);
|
||||
final List<X509Certificate> parsed = CertUtils.pemToX509Certificates(chainPem);
|
||||
|
||||
Assert.assertEquals(2, parsed.size());
|
||||
Assert.assertEquals(intermediateCert.getSerialNumber(), parsed.get(0).getSerialNumber());
|
||||
Assert.assertEquals(caCertificate.getSerialNumber(), parsed.get(1).getSerialNumber());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateCertificate() throws Exception {
|
||||
final KeyPair clientKeyPair = CertUtils.generateRandomKeyPair(1024);
|
||||
|
|
|
|||
Loading…
Reference in New Issue