diff --git a/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java b/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java index d018d488c64a..307f0b8dfde9 100644 --- a/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java +++ b/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java @@ -18,6 +18,7 @@ package org.apache.cloudstack.ca.provider; import java.math.BigInteger; +import java.security.GeneralSecurityException; import java.security.cert.CertificateException; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateNotYetValidException; @@ -27,6 +28,7 @@ import javax.net.ssl.X509TrustManager; +import org.apache.commons.collections.CollectionUtils; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; @@ -39,21 +41,36 @@ public final class RootCACustomTrustManager implements X509TrustManager { private String clientAddress = "Unknown"; private boolean authStrictness = true; private boolean allowExpiredCertificate = true; + private boolean certSignatureVerification = false; private CrlDao crlDao; private List caCertificates; private Map activeCertMap; - public RootCACustomTrustManager(final String clientAddress, final boolean authStrictness, final boolean allowExpiredCertificate, final Map activeCertMap, final List caCertificates, final CrlDao crlDao) { + public RootCACustomTrustManager(final String clientAddress, final boolean authStrictness, final boolean allowExpiredCertificate, final boolean certSignatureVerification, + final Map activeCertMap, final List caCertificates, final CrlDao crlDao) { if (StringUtils.isNotEmpty(clientAddress)) { this.clientAddress = clientAddress.replace("/", "").split(":")[0]; } this.authStrictness = authStrictness; this.allowExpiredCertificate = allowExpiredCertificate; + this.certSignatureVerification = certSignatureVerification; this.activeCertMap = activeCertMap; this.caCertificates = caCertificates; this.crlDao = crlDao; } + private boolean isSignedByAnyRootCA(final X509Certificate certificate) { + for (final X509Certificate ca : caCertificates) { + try { + certificate.verify(ca.getPublicKey()); + return true; + } catch (final GeneralSecurityException e) { + // try the next CA certificate + } + } + return false; + } + private void printCertificateChain(final X509Certificate[] certificates, final String s) throws CertificateException { if (certificates == null) { return; @@ -91,6 +108,22 @@ public void checkClientTrusted(final X509Certificate[] certificates, final Strin return; } + // CA signature check: confirm the cert was actually issued by one of our root CAs + if (certSignatureVerification) { + if (CollectionUtils.isEmpty(caCertificates)) { + final String errorMsg = "Cannot verify client certificate signature because no root CA certificate is available, from address=" + clientAddress; + if (authStrictness) { + throw new CertificateException(errorMsg); + } + logger.warn(errorMsg + "; continuing since strict auth mode is disabled"); + } else if (!isSignedByAnyRootCA(primaryClientCertificate)) { + final String errorMsg = String.format("Client certificate is not signed by the root CA, serial=%x, subject=%s from address=%s", + primaryClientCertificate.getSerialNumber(), primaryClientCertificate.getSubjectDN(), clientAddress); + logger.error(errorMsg); + exceptionMsg = (StringUtils.isEmpty(exceptionMsg)) ? errorMsg : (exceptionMsg + ". " + errorMsg); + } + } + // Revocation check final BigInteger serialNumber = primaryClientCertificate.getSerialNumber(); if (serialNumber == null || crlDao.findBySerial(serialNumber) != null) { @@ -146,7 +179,37 @@ public void checkClientTrusted(final X509Certificate[] certificates, final Strin } @Override - public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + public void checkServerTrusted(final X509Certificate[] certificates, final String s) throws CertificateException { + if (logger.isDebugEnabled()) { + printCertificateChain(certificates, s); + } + if (!certSignatureVerification) { + return; + } + if (CollectionUtils.isEmpty(caCertificates)) { + final String errorMsg = "Cannot verify server certificate signature because no root CA certificate is available, from address=" + clientAddress; + if (authStrictness) { + throw new CertificateException(errorMsg); + } + logger.warn(errorMsg + "; continuing since strict auth mode is disabled"); + return; + } + final X509Certificate primaryServerCertificate = (certificates != null && certificates.length > 0 && certificates[0] != null) ? certificates[0] : null; + if (primaryServerCertificate == null) { + final String errorMsg = "No certificate was presented by the server from address=" + clientAddress; + logger.error(errorMsg); + if (authStrictness) { + throw new CertificateException(errorMsg); + } + return; + } + if (!isSignedByAnyRootCA(primaryServerCertificate)) { + final String errorMsg = "Server certificate is not signed by the root CA from address=" + clientAddress; + logger.error(errorMsg); + if (authStrictness) { + throw new CertificateException(errorMsg); + } + } } @Override diff --git a/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCAProvider.java b/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCAProvider.java index afb4f561160e..dda154e159f5 100644 --- a/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCAProvider.java +++ b/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCAProvider.java @@ -23,6 +23,7 @@ import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; +import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.KeyManagementException; import java.security.KeyPair; @@ -139,6 +140,12 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con "true", "When set to true, it will allow expired client certificate during SSL handshake.", true); + protected static ConfigKey rootCACertSignatureVerification = new ConfigKey<>("Advanced", Boolean.class, + "ca.plugin.root.ca.signature.verification", + "false", + "Verify that agent, server and peer management certificates are signed by the CloudStack root CA. Enforced only when ca.plugin.root.auth.strictness is true; " + + "otherwise signature failures are only logged and the connection is allowed. Enable only after all agents use CA-signed certificates.", true); + private static String managementCertificateCustomSAN; @@ -279,8 +286,9 @@ public SSLEngine createSSLEngine(final SSLContext sslContext, final String remot final boolean authStrictness = rootCAAuthStrictness.value(); final boolean allowExpiredCertificate = rootCAAllowExpiredCert.value(); + final boolean certSignatureVerification = rootCACertSignatureVerification.value(); - TrustManager[] tms = new TrustManager[]{new RootCACustomTrustManager(remoteAddress, authStrictness, allowExpiredCertificate, certMap, caCertificates, crlDao)}; + TrustManager[] tms = new TrustManager[]{new RootCACustomTrustManager(remoteAddress, authStrictness, allowExpiredCertificate, certSignatureVerification, certMap, caCertificates, crlDao)}; sslContext.init(kmf.getKeyManagers(), tms, new SecureRandom()); final SSLEngine sslEngine = sslContext.createSSLEngine(); @@ -575,7 +583,8 @@ public ConfigKey[] getConfigKeys() { rootCACertificate, rootCAIssuerDN, rootCAAuthStrictness, - rootCAAllowExpiredCert + rootCAAllowExpiredCert, + rootCACertSignatureVerification }; } @@ -596,6 +605,30 @@ public boolean isManagementCertificate(java.security.cert.Certificate certificat } X509Certificate x509Certificate = (X509Certificate) certificate; + // When signature verification is enabled, confirm the certificate was issued by one of our root CAs + // before trusting its SAN. Otherwise any self-signed certificate carrying the management SAN + // would qualify as a peer management node. + if (rootCACertSignatureVerification.value()) { + if (CollectionUtils.isEmpty(caCertificates)) { + logger.warn("Cannot verify management certificate signature because no root CA certificate is available"); + return false; + } + boolean signedByCA = false; + for (final X509Certificate ca : caCertificates) { + try { + x509Certificate.verify(ca.getPublicKey()); + signedByCA = true; + break; + } catch (final GeneralSecurityException e) { + // try the next CA certificate + } + } + if (!signedByCA) { + logger.warn("Management certificate is not signed by the root CA"); + return false; + } + } + // Check for alternative names Collection> altNames = x509Certificate.getSubjectAlternativeNames(); if (CollectionUtils.isEmpty(altNames)) { diff --git a/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java b/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java index 714e18c3449f..d7e125e4f606 100644 --- a/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java +++ b/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java @@ -23,6 +23,7 @@ import java.security.KeyPair; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -50,6 +51,8 @@ public class RootCACustomTrustManagerTest { private KeyPair clientKeypair; private X509Certificate caCertificate; private X509Certificate expiredClientCertificate; + private X509Certificate validClientCertificate; + private X509Certificate foreignClientCertificate; private String clientIp = "1.2.3.4"; private Map certMap = new HashMap<>(); @@ -61,18 +64,29 @@ public void setUp() throws Exception { caCertificate = CertUtils.generateV3Certificate(null, caKeypair, caKeypair.getPublic(), "CN=ca", "SHA256withRSA", 365, null, null); expiredClientCertificate = CertUtils.generateV3Certificate(caCertificate, caKeypair, clientKeypair.getPublic(), "CN=cloudstack.apache.org", "SHA256withRSA", 0, Collections.singletonList("cloudstack.apache.org"), Collections.singletonList(clientIp)); + validClientCertificate = CertUtils.generateV3Certificate(caCertificate, caKeypair, clientKeypair.getPublic(), + "CN=cloudstack.apache.org", "SHA256withRSA", 365, Collections.singletonList("cloudstack.apache.org"), Collections.singletonList(clientIp)); + + // A certificate signed by a rogue CA but crafted with a SAN matching the client IP and a serial absent + // from the CRL. It passes every check except the CA signature verification. + final KeyPair rogueCaKeypair = CertUtils.generateRandomKeyPair(1024); + final KeyPair rogueClientKeypair = CertUtils.generateRandomKeyPair(1024); + final X509Certificate rogueCaCertificate = CertUtils.generateV3Certificate(null, rogueCaKeypair, rogueCaKeypair.getPublic(), + "CN=rogue-ca", "SHA256withRSA", 365, null, null); + foreignClientCertificate = CertUtils.generateV3Certificate(rogueCaCertificate, rogueCaKeypair, rogueClientKeypair.getPublic(), + "CN=cloudstack.apache.org", "SHA256withRSA", 365, Collections.singletonList("cloudstack.apache.org"), Collections.singletonList(clientIp)); } @Test public void testAuthNotStrictWithInvalidCert() throws Exception { - final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, Collections.singletonList(caCertificate), crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, false, 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, Collections.singletonList(caCertificate), crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, false, certMap, Collections.singletonList(caCertificate), crlDao); trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, "RSA"); Assert.assertTrue(certMap.containsKey(clientIp)); Assert.assertEquals(certMap.get(clientIp), caCertificate); @@ -81,7 +95,7 @@ public void testAuthNotStrictWithRevokedCert() throws Exception { @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, Collections.singletonList(caCertificate), crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, false, certMap, Collections.singletonList(caCertificate), crlDao); trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, "RSA"); Assert.assertTrue(certMap.containsKey(clientIp)); Assert.assertEquals(certMap.get(clientIp), caCertificate); @@ -90,14 +104,14 @@ public void testAuthNotStrictWithInvalidCertOwnership() throws Exception { @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, Collections.singletonList(caCertificate), crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, 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, Collections.singletonList(caCertificate), crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, false, certMap, Collections.singletonList(caCertificate), crlDao); trustManager.checkClientTrusted(new X509Certificate[]{expiredClientCertificate}, "RSA"); Assert.assertTrue(certMap.containsKey(clientIp)); Assert.assertEquals(certMap.get(clientIp), expiredClientCertificate); @@ -105,28 +119,28 @@ public void testAuthNotStrictWithAllowExpiredCertAndOwnership() throws Exception @Test(expected = CertificateException.class) public void testAuthStrictWithInvalidCert() throws Exception { - final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, Collections.singletonList(caCertificate), crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, false, 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, Collections.singletonList(caCertificate), crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, false, 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, Collections.singletonList(caCertificate), crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, false, 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, Collections.singletonList(caCertificate), crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, false, false, certMap, Collections.singletonList(caCertificate), crlDao); trustManager.checkClientTrusted(new X509Certificate[]{expiredClientCertificate}, "RSA"); } @@ -137,7 +151,7 @@ public void testGetAcceptedIssuersWithChain() throws Exception { "CN=root", "SHA256withRSA", 365, null, null); final List chain = Arrays.asList(caCertificate, rootCert); final RootCACustomTrustManager trustManager = new RootCACustomTrustManager( - clientIp, false, true, certMap, chain, crlDao); + clientIp, false, true, false, certMap, chain, crlDao); final X509Certificate[] issuers = trustManager.getAcceptedIssuers(); Assert.assertEquals(2, issuers.length); @@ -148,7 +162,7 @@ public void testGetAcceptedIssuersWithChain() throws Exception { @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, Collections.singletonList(caCertificate), crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, false, certMap, Collections.singletonList(caCertificate), crlDao); Assert.assertTrue(trustManager.getAcceptedIssuers() != null); Assert.assertTrue(trustManager.getAcceptedIssuers().length == 1); Assert.assertEquals(trustManager.getAcceptedIssuers()[0], caCertificate); @@ -157,4 +171,104 @@ public void testAuthStrictWithAllowExpiredCertAndOwnership() throws Exception { Assert.assertEquals(certMap.get(clientIp), expiredClientCertificate); } + @Test + public void testSignatureVerificationEnabledWithValidCert() throws Exception { + Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, false, true, certMap, Collections.singletonList(caCertificate), crlDao); + trustManager.checkClientTrusted(new X509Certificate[]{validClientCertificate}, "RSA"); + Assert.assertTrue(certMap.containsKey(clientIp)); + Assert.assertEquals(certMap.get(clientIp), validClientCertificate); + } + + @Test + public void testSignatureVerificationEnabledWithCertSignedByRotatedCA() throws Exception { + // The trusted CA list contains a decoy plus the actual signer; verification must + // succeed as long as any entry in the list signed the certificate. + final KeyPair otherCaKeyPair = CertUtils.generateRandomKeyPair(1024); + final X509Certificate otherCaCertificate = CertUtils.generateV3Certificate(null, otherCaKeyPair, otherCaKeyPair.getPublic(), + "CN=other-ca", "SHA256withRSA", 365, null, null); + Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null); + final List caCertificates = Arrays.asList(otherCaCertificate, caCertificate); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, false, true, certMap, caCertificates, crlDao); + trustManager.checkClientTrusted(new X509Certificate[]{validClientCertificate}, "RSA"); + Assert.assertTrue(certMap.containsKey(clientIp)); + Assert.assertEquals(certMap.get(clientIp), validClientCertificate); + } + + @Test(expected = CertificateException.class) + public void testSignatureVerificationEnabledStrictWithForeignCert() throws Exception { + Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, true, certMap, Collections.singletonList(caCertificate), crlDao); + trustManager.checkClientTrusted(new X509Certificate[]{foreignClientCertificate}, "RSA"); + } + + @Test + public void testSignatureVerificationDisabledStrictWithForeignCert() throws Exception { + Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, false, certMap, Collections.singletonList(caCertificate), crlDao); + trustManager.checkClientTrusted(new X509Certificate[]{foreignClientCertificate}, "RSA"); + Assert.assertTrue(certMap.containsKey(clientIp)); + Assert.assertEquals(certMap.get(clientIp), foreignClientCertificate); + } + + @Test + public void testSignatureVerificationEnabledNotStrictWithForeignCert() throws Exception { + Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, true, certMap, Collections.singletonList(caCertificate), crlDao); + trustManager.checkClientTrusted(new X509Certificate[]{foreignClientCertificate}, "RSA"); + Assert.assertTrue(certMap.containsKey(clientIp)); + Assert.assertEquals(certMap.get(clientIp), foreignClientCertificate); + } + + @Test(expected = CertificateException.class) + public void testSignatureVerificationEnabledStrictWithNoCaCertificates() throws Exception { + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, true, certMap, new ArrayList<>(), crlDao); + trustManager.checkClientTrusted(new X509Certificate[]{validClientCertificate}, "RSA"); + } + + @Test + public void testSignatureVerificationEnabledNotStrictWithNoCaCertificates() throws Exception { + Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, true, certMap, new ArrayList<>(), crlDao); + trustManager.checkClientTrusted(new X509Certificate[]{validClientCertificate}, "RSA"); + Assert.assertTrue(certMap.containsKey(clientIp)); + } + + @Test + public void testCheckServerTrustedVerificationDisabledIsNoop() throws Exception { + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, false, certMap, Collections.singletonList(caCertificate), crlDao); + trustManager.checkServerTrusted(new X509Certificate[]{foreignClientCertificate}, "RSA"); + trustManager.checkServerTrusted(null, "RSA"); + } + + @Test + public void testCheckServerTrustedVerificationEnabledWithCaSignedCert() throws Exception { + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, true, certMap, Collections.singletonList(caCertificate), crlDao); + trustManager.checkServerTrusted(new X509Certificate[]{validClientCertificate}, "RSA"); + } + + @Test(expected = CertificateException.class) + public void testCheckServerTrustedVerificationEnabledWithForeignCert() throws Exception { + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, true, certMap, Collections.singletonList(caCertificate), crlDao); + trustManager.checkServerTrusted(new X509Certificate[]{foreignClientCertificate}, "RSA"); + } + + @Test(expected = CertificateException.class) + public void testCheckServerTrustedVerificationEnabledWithNoCert() throws Exception { + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, true, certMap, Collections.singletonList(caCertificate), crlDao); + trustManager.checkServerTrusted(null, "RSA"); + } + + @Test(expected = CertificateException.class) + public void testCheckServerTrustedVerificationEnabledStrictWithNoCaCertificates() throws Exception { + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, true, certMap, new ArrayList<>(), crlDao); + trustManager.checkServerTrusted(new X509Certificate[]{validClientCertificate}, "RSA"); + } + + @Test + public void testCheckServerTrustedVerificationEnabledNotStrictWithNoCaCertificates() throws Exception { + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, true, certMap, new ArrayList<>(), crlDao); + trustManager.checkServerTrusted(new X509Certificate[]{validClientCertificate}, "RSA"); + } + } diff --git a/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCAProviderTest.java b/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCAProviderTest.java index 21f00c66a1df..e7065a876750 100644 --- a/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCAProviderTest.java +++ b/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCAProviderTest.java @@ -77,6 +77,10 @@ public void setUp() throws Exception { addField(provider, "caKeyPair", caKeyPair); addField(provider, "caCertificate", caCertificate); addField(provider, "caCertificates", Collections.singletonList(caCertificate)); + + // Signature verification is off by default; individual tests enable it as needed + provider.rootCACertSignatureVerification = Mockito.mock(ConfigKey.class); + Mockito.lenient().when(provider.rootCACertSignatureVerification.value()).thenReturn(Boolean.FALSE); } @After @@ -250,6 +254,66 @@ public void testIsManagementCertificateMatch() throws Exception { } } + @Test + public void testIsManagementCertificateSignatureVerificationRejectsForeignCert() throws Exception { + String customSAN = "cloudstack.internal"; + addField(provider, "managementCertificateCustomSAN", customSAN); + Mockito.when(provider.rootCACertSignatureVerification.value()).thenReturn(Boolean.TRUE); + + // Cert with the management SAN but signed by a rogue CA must be rejected + KeyPair rogueCaKeyPair = CertUtils.generateRandomKeyPair(1024); + X509Certificate rogueCa = CertUtils.generateV3Certificate(null, rogueCaKeyPair, rogueCaKeyPair.getPublic(), "CN=rogue", "SHA256withRSA", 365, null, null); + KeyPair clientKeyPair = CertUtils.generateRandomKeyPair(1024); + X509Certificate foreignCert = CertUtils.generateV3Certificate(rogueCa, rogueCaKeyPair, clientKeyPair.getPublic(), + "CN=" + customSAN, "SHA256withRSA", 365, List.of(customSAN), null); + + Assert.assertFalse(provider.isManagementCertificate(foreignCert)); + } + + @Test + public void testIsManagementCertificateSignatureVerificationAcceptsCaSignedCert() throws Exception { + String customSAN = "cloudstack.internal"; + addField(provider, "managementCertificateCustomSAN", customSAN); + Mockito.when(provider.rootCACertSignatureVerification.value()).thenReturn(Boolean.TRUE); + + KeyPair clientKeyPair = CertUtils.generateRandomKeyPair(1024); + X509Certificate caSignedCert = CertUtils.generateV3Certificate(caCertificate, caKeyPair, clientKeyPair.getPublic(), + "CN=" + customSAN, "SHA256withRSA", 365, List.of(customSAN), null); + + Assert.assertTrue(provider.isManagementCertificate(caSignedCert)); + } + + @Test + public void testIsManagementCertificateSignatureVerificationAcceptsCertSignedByRotatedCA() throws Exception { + String customSAN = "cloudstack.internal"; + addField(provider, "managementCertificateCustomSAN", customSAN); + Mockito.when(provider.rootCACertSignatureVerification.value()).thenReturn(Boolean.TRUE); + + KeyPair otherCaKeyPair = CertUtils.generateRandomKeyPair(1024); + X509Certificate otherCaCertificate = CertUtils.generateV3Certificate(null, otherCaKeyPair, otherCaKeyPair.getPublic(), "CN=other-ca", "SHA256withRSA", 365, null, null); + addField(provider, "caCertificates", Arrays.asList(otherCaCertificate, caCertificate)); + + KeyPair clientKeyPair = CertUtils.generateRandomKeyPair(1024); + X509Certificate caSignedCert = CertUtils.generateV3Certificate(caCertificate, caKeyPair, clientKeyPair.getPublic(), + "CN=" + customSAN, "SHA256withRSA", 365, List.of(customSAN), null); + + Assert.assertTrue(provider.isManagementCertificate(caSignedCert)); + } + + @Test + public void testIsManagementCertificateSignatureVerificationRejectsWhenNoCaCertificates() throws Exception { + String customSAN = "cloudstack.internal"; + addField(provider, "managementCertificateCustomSAN", customSAN); + Mockito.when(provider.rootCACertSignatureVerification.value()).thenReturn(Boolean.TRUE); + addField(provider, "caCertificates", null); + + KeyPair clientKeyPair = CertUtils.generateRandomKeyPair(1024); + X509Certificate caSignedCert = CertUtils.generateV3Certificate(caCertificate, caKeyPair, clientKeyPair.getPublic(), + "CN=" + customSAN, "SHA256withRSA", 365, List.of(customSAN), null); + + Assert.assertFalse(provider.isManagementCertificate(caSignedCert)); + } + @Test public void testLoadRootCACertificateWithMismatchedCert() throws Exception { KeyPair otherKeyPair = CertUtils.generateRandomKeyPair(1024);