4040import java .security .spec .InvalidKeySpecException ;
4141import java .util .ArrayList ;
4242import java .util .Collection ;
43- import java .util .Collections ;
4443import java .util .Enumeration ;
4544import java .util .HashSet ;
4645import 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 ()) {
0 commit comments