Skip to content

Commit 6c7fffb

Browse files
committed
Add support for intermediate certificate chain in RootCAProvider
1 parent d3b0026 commit 6c7fffb

6 files changed

Lines changed: 64 additions & 35 deletions

File tree

plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,17 @@ public final class RootCACustomTrustManager implements X509TrustManager {
4040
private boolean authStrictness = true;
4141
private boolean allowExpiredCertificate = true;
4242
private CrlDao crlDao;
43-
private X509Certificate caCertificate;
43+
private List<X509Certificate> caCertificates;
4444
private Map<String, X509Certificate> activeCertMap;
4545

46-
public RootCACustomTrustManager(final String clientAddress, final boolean authStrictness, final boolean allowExpiredCertificate, final Map<String, X509Certificate> activeCertMap, final X509Certificate caCertificate, final CrlDao crlDao) {
46+
public RootCACustomTrustManager(final String clientAddress, final boolean authStrictness, final boolean allowExpiredCertificate, final Map<String, X509Certificate> activeCertMap, final List<X509Certificate> caCertificates, final CrlDao crlDao) {
4747
if (StringUtils.isNotEmpty(clientAddress)) {
4848
this.clientAddress = clientAddress.replace("/", "").split(":")[0];
4949
}
5050
this.authStrictness = authStrictness;
5151
this.allowExpiredCertificate = allowExpiredCertificate;
5252
this.activeCertMap = activeCertMap;
53-
this.caCertificate = caCertificate;
53+
this.caCertificates = caCertificates;
5454
this.crlDao = crlDao;
5555
}
5656

@@ -151,6 +151,6 @@ public void checkServerTrusted(X509Certificate[] x509Certificates, String s) thr
151151

152152
@Override
153153
public X509Certificate[] getAcceptedIssuers() {
154-
return new X509Certificate[]{caCertificate};
154+
return caCertificates.toArray(new X509Certificate[0]);
155155
}
156156
}

plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCAProvider.java

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
import java.security.spec.InvalidKeySpecException;
4141
import java.util.ArrayList;
4242
import java.util.Collection;
43-
import java.util.Collections;
4443
import java.util.Enumeration;
4544
import java.util.HashSet;
4645
import java.util.List;
@@ -92,6 +91,7 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
9291

9392
private static KeyPair caKeyPair = null;
9493
private static X509Certificate caCertificate = null;
94+
private static List<X509Certificate> caCertificates = null;
9595
private static KeyStore managementKeyStore = null;
9696

9797
@Inject
@@ -106,20 +106,21 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
106106
private static ConfigKey<String> rootCAPrivateKey = new ConfigKey<>("Hidden", String.class,
107107
"ca.plugin.root.private.key",
108108
null,
109-
"The ROOT CA private key in PEM format (PKCS#8: must start with 'BEGIN PRIVATE KEY'). " +
109+
"The ROOT CA private key in PEM format. " +
110110
"When set along with the public key and certificate, CloudStack uses this custom CA instead of auto-generating one. " +
111111
"All three ca.plugin.root.* keys must be set together. Restart management server(s) when changed.", true);
112112

113113
private static ConfigKey<String> rootCAPublicKey = new ConfigKey<>("Hidden", String.class,
114114
"ca.plugin.root.public.key",
115115
null,
116-
"The ROOT CA public key in PEM format (X.509/SPKI: must start with 'BEGIN PUBLIC KEY'). " +
116+
"The ROOT CA public key in PEM format (X.509/SPKI: must start with '-----BEGIN PUBLIC KEY-----'). " +
117117
"Required when providing a custom CA. Restart management server(s) when changed.", true);
118118

119119
private static ConfigKey<String> rootCACertificate = new ConfigKey<>("Hidden", String.class,
120120
"ca.plugin.root.ca.certificate",
121121
null,
122-
"The ROOT CA X.509 certificate in PEM format (must start with 'BEGIN CERTIFICATE'). " +
122+
"The CA certificate(s) in PEM format (must start with '-----BEGIN CERTIFICATE-----'). " +
123+
"For intermediate CAs, concatenate the signing cert first, followed by intermediate(s) and root. " +
123124
"Required when providing a custom CA. Restart management server(s) when changed.", true);
124125

125126
private static ConfigKey<String> rootCAIssuerDN = new ConfigKey<>("Advanced", String.class,
@@ -155,7 +156,7 @@ private Certificate generateCertificate(final List<String> domainNames, final Li
155156
caCertificate, caKeyPair, keyPair.getPublic(),
156157
subject, CAManager.CertSignatureAlgorithm.value(),
157158
validityDays, domainNames, ipAddresses);
158-
return new Certificate(clientCertificate, keyPair.getPrivate(), Collections.singletonList(caCertificate));
159+
return new Certificate(clientCertificate, keyPair.getPrivate(), caCertificates);
159160
}
160161

161162
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 {
@@ -209,7 +210,7 @@ private Certificate generateCertificateUsingCsr(final String csr, final List<Str
209210
caCertificate, caKeyPair, request.getPublicKey(),
210211
subject, CAManager.CertSignatureAlgorithm.value(),
211212
validityDays, dnsNames, ipAddresses);
212-
return new Certificate(clientCertificate, null, Collections.singletonList(caCertificate));
213+
return new Certificate(clientCertificate, null, caCertificates);
213214
}
214215

215216
////////////////////////////////////////////////////////
@@ -223,7 +224,7 @@ public boolean canProvisionCertificates() {
223224

224225
@Override
225226
public List<X509Certificate> getCaCertificate() {
226-
return Collections.singletonList(caCertificate);
227+
return caCertificates;
227228
}
228229

229230
@Override
@@ -258,8 +259,8 @@ public boolean revokeCertificate(final BigInteger certSerial, final String certC
258259
private KeyStore getCaKeyStore() throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException {
259260
final KeyStore ks = KeyStore.getInstance("JKS");
260261
ks.load(null, null);
261-
if (caKeyPair != null && caCertificate != null) {
262-
ks.setKeyEntry(caAlias, caKeyPair.getPrivate(), getKeyStorePassphrase(), new X509Certificate[]{caCertificate});
262+
if (caKeyPair != null && CollectionUtils.isNotEmpty(caCertificates)) {
263+
ks.setKeyEntry(caAlias, caKeyPair.getPrivate(), getKeyStorePassphrase(), caCertificates.toArray(new X509Certificate[0]));
263264
} else {
264265
return null;
265266
}
@@ -278,7 +279,7 @@ public SSLEngine createSSLEngine(final SSLContext sslContext, final String remot
278279
final boolean authStrictness = rootCAAuthStrictness.value();
279280
final boolean allowExpiredCertificate = rootCAAllowExpiredCert.value();
280281

281-
TrustManager[] tms = new TrustManager[]{new RootCACustomTrustManager(remoteAddress, authStrictness, allowExpiredCertificate, certMap, caCertificate, crlDao)};
282+
TrustManager[] tms = new TrustManager[]{new RootCACustomTrustManager(remoteAddress, authStrictness, allowExpiredCertificate, certMap, caCertificates, crlDao)};
282283

283284
sslContext.init(kmf.getKeyManagers(), tms, new SecureRandom());
284285
final SSLEngine sslEngine = sslContext.createSSLEngine();
@@ -364,9 +365,23 @@ private boolean loadRootCACertificate() {
364365
return false;
365366
}
366367
try {
367-
caCertificate = CertUtils.pemToX509Certificate(rootCACertificate.value());
368-
caCertificate.verify(caKeyPair.getPublic());
369-
} catch (final IOException | CertificateException | NoSuchAlgorithmException | InvalidKeyException | SignatureException | NoSuchProviderException e) {
368+
caCertificates = CertUtils.pemToX509Certificates(rootCACertificate.value());
369+
if (CollectionUtils.isEmpty(caCertificates)) {
370+
logger.error("No certificates found in ca.plugin.root.ca.certificate");
371+
return false;
372+
}
373+
caCertificate = caCertificates.get(0);
374+
375+
// Verify key ownership without enforcing self-signature
376+
if (!caCertificate.getPublicKey().equals(caKeyPair.getPublic())) {
377+
logger.error("The public key in the CA certificate does not match the configured CA public key");
378+
return false;
379+
}
380+
381+
if (caCertificates.size() > 1) {
382+
logger.info("Loaded CA certificate chain with {} certificate(s)", caCertificates.size());
383+
}
384+
} catch (final IOException | CertificateException e) {
370385
logger.error("Failed to load saved RootCA certificate due to exception:", e);
371386
return false;
372387
}
@@ -393,9 +408,15 @@ private boolean loadManagementKeyStore() {
393408
try {
394409
managementKeyStore = KeyStore.getInstance("JKS");
395410
managementKeyStore.load(null, null);
396-
managementKeyStore.setCertificateEntry(caAlias, caCertificate);
411+
int caIndex = 0;
412+
for (final X509Certificate cert : caCertificates) {
413+
managementKeyStore.setCertificateEntry(caAlias + "-" + caIndex++, cert);
414+
}
415+
final List<X509Certificate> fullChain = new ArrayList<>();
416+
fullChain.add(serverCertificate.getClientCertificate());
417+
fullChain.addAll(caCertificates);
397418
managementKeyStore.setKeyEntry(managementAlias, serverCertificate.getPrivateKey(), getKeyStorePassphrase(),
398-
new X509Certificate[]{serverCertificate.getClientCertificate(), caCertificate});
419+
fullChain.toArray(new X509Certificate[0]));
399420
} catch (final CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException e) {
400421
logger.error("Failed to load root CA management-server keystore due to exception: ", e);
401422
return false;
@@ -430,8 +451,7 @@ private boolean setupCA() {
430451
if (hasUserProvidedCAKeys()) {
431452
logger.error("Failed to load user-provided CA keys from configuration. " +
432453
"Check that ca.plugin.root.private.key, ca.plugin.root.public.key, and " +
433-
"ca.plugin.root.ca.certificate are all set and in the correct PEM format " +
434-
"(private key must be PKCS#8: 'BEGIN PRIVATE KEY'). " +
454+
"ca.plugin.root.ca.certificate are all set and in the correct PEM format. " +
435455
"Overwriting with auto-generated keys.");
436456
}
437457
if (!saveNewRootCAKeypair()) {

plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,14 @@ public void setUp() throws Exception {
6363

6464
@Test
6565
public void testAuthNotStrictWithInvalidCert() throws Exception {
66-
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao);
66+
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, Collections.singletonList(caCertificate), crlDao);
6767
trustManager.checkClientTrusted(null, null);
6868
}
6969

7070
@Test
7171
public void testAuthNotStrictWithRevokedCert() throws Exception {
7272
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(new CrlVO());
73-
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao);
73+
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, Collections.singletonList(caCertificate), crlDao);
7474
trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, "RSA");
7575
Assert.assertTrue(certMap.containsKey(clientIp));
7676
Assert.assertEquals(certMap.get(clientIp), caCertificate);
@@ -79,7 +79,7 @@ public void testAuthNotStrictWithRevokedCert() throws Exception {
7979
@Test
8080
public void testAuthNotStrictWithInvalidCertOwnership() throws Exception {
8181
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null);
82-
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao);
82+
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, Collections.singletonList(caCertificate), crlDao);
8383
trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, "RSA");
8484
Assert.assertTrue(certMap.containsKey(clientIp));
8585
Assert.assertEquals(certMap.get(clientIp), caCertificate);
@@ -88,50 +88,50 @@ public void testAuthNotStrictWithInvalidCertOwnership() throws Exception {
8888
@Test(expected = CertificateException.class)
8989
public void testAuthNotStrictWithDenyExpiredCertAndOwnership() throws Exception {
9090
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null);
91-
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, false, certMap, caCertificate, crlDao);
91+
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, false, certMap, Collections.singletonList(caCertificate), crlDao);
9292
trustManager.checkClientTrusted(new X509Certificate[]{expiredClientCertificate}, "RSA");
9393
}
9494

9595
@Test
9696
public void testAuthNotStrictWithAllowExpiredCertAndOwnership() throws Exception {
9797
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null);
98-
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao);
98+
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, Collections.singletonList(caCertificate), crlDao);
9999
trustManager.checkClientTrusted(new X509Certificate[]{expiredClientCertificate}, "RSA");
100100
Assert.assertTrue(certMap.containsKey(clientIp));
101101
Assert.assertEquals(certMap.get(clientIp), expiredClientCertificate);
102102
}
103103

104104
@Test(expected = CertificateException.class)
105105
public void testAuthStrictWithInvalidCert() throws Exception {
106-
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao);
106+
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, Collections.singletonList(caCertificate), crlDao);
107107
trustManager.checkClientTrusted(null, null);
108108
}
109109

110110
@Test(expected = CertificateException.class)
111111
public void testAuthStrictWithRevokedCert() throws Exception {
112112
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(new CrlVO());
113-
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao);
113+
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, Collections.singletonList(caCertificate), crlDao);
114114
trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, "RSA");
115115
}
116116

117117
@Test(expected = CertificateException.class)
118118
public void testAuthStrictWithInvalidCertOwnership() throws Exception {
119119
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null);
120-
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao);
120+
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, Collections.singletonList(caCertificate), crlDao);
121121
trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, "RSA");
122122
}
123123

124124
@Test(expected = CertificateException.class)
125125
public void testAuthStrictWithDenyExpiredCertAndOwnership() throws Exception {
126126
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null);
127-
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, false, certMap, caCertificate, crlDao);
127+
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, false, certMap, Collections.singletonList(caCertificate), crlDao);
128128
trustManager.checkClientTrusted(new X509Certificate[]{expiredClientCertificate}, "RSA");
129129
}
130130

131131
@Test
132132
public void testAuthStrictWithAllowExpiredCertAndOwnership() throws Exception {
133133
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null);
134-
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao);
134+
final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, Collections.singletonList(caCertificate), crlDao);
135135
Assert.assertTrue(trustManager.getAcceptedIssuers() != null);
136136
Assert.assertTrue(trustManager.getAcceptedIssuers().length == 1);
137137
Assert.assertEquals(trustManager.getAcceptedIssuers()[0], caCertificate);

plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCAProviderTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.util.ArrayList;
3232
import java.util.Arrays;
3333
import java.util.Collection;
34+
import java.util.Collections;
3435
import java.util.List;
3536
import java.util.UUID;
3637

@@ -75,7 +76,7 @@ public void setUp() throws Exception {
7576

7677
addField(provider, "caKeyPair", caKeyPair);
7778
addField(provider, "caCertificate", caCertificate);
78-
addField(provider, "caKeyPair", caKeyPair);
79+
addField(provider, "caCertificates", Collections.singletonList(caCertificate));
7980
}
8081

8182
@After

utils/src/main/java/org/apache/cloudstack/utils/security/CertUtils.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,17 @@ public static KeyFactory getKeyFactory() {
9898
return keyFactory;
9999
}
100100

101-
public static X509Certificate pemToX509Certificate(final String pem) throws CertificateException, IOException {
101+
public static List<X509Certificate> pemToX509Certificates(final String pem) throws CertificateException, IOException {
102+
final List<X509Certificate> certs = new ArrayList<>();
102103
final PEMParser pemParser = new PEMParser(new StringReader(pem));
103-
return new JcaX509CertificateConverter().setProvider("BC").getCertificate((X509CertificateHolder) pemParser.readObject());
104+
final JcaX509CertificateConverter certConverter = new JcaX509CertificateConverter().setProvider("BC");
105+
Object parsedObj;
106+
while ((parsedObj = pemParser.readObject()) != null) {
107+
if (parsedObj instanceof X509CertificateHolder) {
108+
certs.add(certConverter.getCertificate((X509CertificateHolder) parsedObj));
109+
}
110+
}
111+
return certs;
104112
}
105113

106114
public static String x509CertificateToPem(final X509Certificate cert) throws IOException {

utils/src/test/java/org/apache/cloudstack/utils/security/CertUtilsTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public void testGenerateRandomKeyPair() throws Exception {
5353
public void testCertificateConversionMethods() throws Exception {
5454
final X509Certificate in = caCertificate;
5555
final String pem = CertUtils.x509CertificateToPem(in);
56-
final X509Certificate out = CertUtils.pemToX509Certificate(pem);
56+
final X509Certificate out = CertUtils.pemToX509Certificates(pem).get(0);
5757
Assert.assertTrue(pem.startsWith("-----BEGIN CERTIFICATE-----\n"));
5858
Assert.assertTrue(pem.endsWith("-----END CERTIFICATE-----\n"));
5959
Assert.assertEquals(in.getSerialNumber(), out.getSerialNumber());

0 commit comments

Comments
 (0)