This is an automated email from the ASF dual-hosted git repository. acosentino pushed a commit to branch CAMEL-22940 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 5a54ae7b30c51908e94270204d112427b5041740 Author: Andrea Cosentino <[email protected]> AuthorDate: Mon Feb 2 10:17:07 2026 +0100 CAMEL-22940 - [camel-milo] Cannot configure certificate chain Signed-off-by: Andrea Cosentino <[email protected]> --- .../camel/component/milo/KeyStoreLoader.java | 23 ++++- .../milo/client/MiloClientConfiguration.java | 1 + .../component/milo/server/MiloServerComponent.java | 12 ++- .../camel/component/milo/KeyStoreLoaderTest.java | 105 +++++++++++++++++++++ 4 files changed, 135 insertions(+), 6 deletions(-) diff --git a/components/camel-milo/src/main/java/org/apache/camel/component/milo/KeyStoreLoader.java b/components/camel-milo/src/main/java/org/apache/camel/component/milo/KeyStoreLoader.java index 61ddf47e9577..ab2ae48eed98 100644 --- a/components/camel-milo/src/main/java/org/apache/camel/component/milo/KeyStoreLoader.java +++ b/components/camel-milo/src/main/java/org/apache/camel/component/milo/KeyStoreLoader.java @@ -43,10 +43,12 @@ public class KeyStoreLoader { public static class Result { private final X509Certificate certificate; + private final X509Certificate[] certificateChain; private final KeyPair keyPair; - public Result(final X509Certificate certificate, final KeyPair keyPair) { + public Result(final X509Certificate certificate, final X509Certificate[] certificateChain, final KeyPair keyPair) { this.certificate = certificate; + this.certificateChain = certificateChain; this.keyPair = keyPair; } @@ -54,6 +56,10 @@ public class KeyStoreLoader { return this.certificate; } + public X509Certificate[] getCertificateChain() { + return this.certificateChain; + } + public KeyPair getKeyPair() { return this.keyPair; } @@ -132,14 +138,23 @@ public class KeyStoreLoader { = keyStore.getKey(effectiveKeyAlias, this.keyPassword != null ? this.keyPassword.toCharArray() : null); if (privateKey instanceof PrivateKey pk) { - final X509Certificate certificate = (X509Certificate) keyStore.getCertificate(effectiveKeyAlias); - if (certificate == null) { + // Load the full certificate chain + final java.security.cert.Certificate[] chain = keyStore.getCertificateChain(effectiveKeyAlias); + if (chain == null || chain.length == 0) { return null; } + // Convert to X509Certificate array + final X509Certificate[] certificateChain = new X509Certificate[chain.length]; + for (int i = 0; i < chain.length; i++) { + certificateChain[i] = (X509Certificate) chain[i]; + } + + // The first certificate in the chain is the end-entity certificate + final X509Certificate certificate = certificateChain[0]; final PublicKey publicKey = certificate.getPublicKey(); final KeyPair keyPair = new KeyPair(publicKey, pk); - return new Result(certificate, keyPair); + return new Result(certificate, certificateChain, keyPair); } return null; diff --git a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConfiguration.java b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConfiguration.java index 4c61fd2e3812..b345ad53b2db 100644 --- a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConfiguration.java +++ b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConfiguration.java @@ -480,6 +480,7 @@ public class MiloClientConfiguration implements Cloneable { } builder.setCertificate(result.getCertificate()); + builder.setCertificateChain(result.getCertificateChain()); builder.setKeyPair(result.getKeyPair()); } diff --git a/components/camel-milo/src/main/java/org/apache/camel/component/milo/server/MiloServerComponent.java b/components/camel-milo/src/main/java/org/apache/camel/component/milo/server/MiloServerComponent.java index f3fad98863a9..8c4d33179da6 100644 --- a/components/camel-milo/src/main/java/org/apache/camel/component/milo/server/MiloServerComponent.java +++ b/components/camel-milo/src/main/java/org/apache/camel/component/milo/server/MiloServerComponent.java @@ -440,13 +440,21 @@ public class MiloServerComponent extends DefaultComponent { * desired, do it explicitly. */ Objects.requireNonNull(result, "Setting a null is not supported. call setCertificateManager(null) instead.)"); - loadServerCertificate(result.getKeyPair(), result.getCertificate()); + loadServerCertificate(result.getKeyPair(), result.getCertificate(), result.getCertificateChain()); } /** * Server certificate */ public void loadServerCertificate(final KeyPair keyPair, final X509Certificate certificate) { + loadServerCertificate(keyPair, certificate, new X509Certificate[] { certificate }); + } + + /** + * Server certificate with full certificate chain + */ + public void loadServerCertificate( + final KeyPair keyPair, final X509Certificate certificate, final X509Certificate[] certificateChain) { this.certificate = certificate; // TODO evaluate migration to CertificateGroup // setCertificateManager(new DefaultCertificateManager(keyPair, certificate)); @@ -455,7 +463,7 @@ public class MiloServerComponent extends DefaultComponent { this.certificateGroup.updateCertificate( NodeIds.ServerConfiguration_CertificateGroups_DefaultApplicationGroup, keyPair, - new X509Certificate[] { certificate }); + certificateChain); } catch (Exception e) { throw new RuntimeCamelException(e); } diff --git a/components/camel-milo/src/test/java/org/apache/camel/component/milo/KeyStoreLoaderTest.java b/components/camel-milo/src/test/java/org/apache/camel/component/milo/KeyStoreLoaderTest.java new file mode 100644 index 000000000000..8c2f8c5da0c7 --- /dev/null +++ b/components/camel-milo/src/test/java/org/apache/camel/component/milo/KeyStoreLoaderTest.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.milo; + +import java.security.cert.X509Certificate; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for {@link KeyStoreLoader} certificate chain loading functionality. + */ +public class KeyStoreLoaderTest { + + @Test + public void testLoadCertificateChain() throws Exception { + final KeyStoreLoader loader = new KeyStoreLoader(); + loader.setUrl("file:src/test/resources/keystore"); + loader.setKeyStorePassword("testtest"); + loader.setKeyPassword("test"); + + final KeyStoreLoader.Result result = loader.load(); + + assertNotNull(result, "Result should not be null"); + assertNotNull(result.getCertificate(), "Certificate should not be null"); + assertNotNull(result.getCertificateChain(), "Certificate chain should not be null"); + assertNotNull(result.getKeyPair(), "KeyPair should not be null"); + + // The certificate chain should contain at least the end-entity certificate + assertTrue(result.getCertificateChain().length >= 1, + "Certificate chain should contain at least one certificate"); + + // The first certificate in the chain should be the same as getCertificate() + assertEquals(result.getCertificate(), result.getCertificateChain()[0], + "First certificate in chain should match getCertificate()"); + } + + @Test + public void testLoadCertificateChainWithAlias() throws Exception { + final KeyStoreLoader loader = new KeyStoreLoader(); + loader.setUrl("file:src/test/resources/keystore"); + loader.setKeyStorePassword("testtest"); + loader.setKeyPassword("test"); + loader.setKeyAlias("test"); + + final KeyStoreLoader.Result result = loader.load(); + + assertNotNull(result, "Result should not be null"); + assertNotNull(result.getCertificateChain(), "Certificate chain should not be null"); + + // Verify the chain is properly loaded + X509Certificate[] chain = result.getCertificateChain(); + assertTrue(chain.length >= 1, "Certificate chain should have at least one certificate"); + + // Verify all certificates in the chain are valid X509Certificates + for (X509Certificate cert : chain) { + assertNotNull(cert, "Each certificate in chain should not be null"); + assertNotNull(cert.getSubjectX500Principal(), "Certificate should have a subject"); + } + } + + @Test + public void testCertificateChainOrder() throws Exception { + final KeyStoreLoader loader = new KeyStoreLoader(); + loader.setUrl("file:src/test/resources/keystore"); + loader.setKeyStorePassword("testtest"); + loader.setKeyPassword("test"); + + final KeyStoreLoader.Result result = loader.load(); + + assertNotNull(result, "Result should not be null"); + + X509Certificate[] chain = result.getCertificateChain(); + + // If the chain has more than one certificate, verify proper chain order + // Each certificate should be issued by the next one in the chain + if (chain.length > 1) { + for (int i = 0; i < chain.length - 1; i++) { + X509Certificate subject = chain[i]; + X509Certificate issuer = chain[i + 1]; + + // The issuer of chain[i] should match the subject of chain[i+1] + assertEquals(subject.getIssuerX500Principal(), issuer.getSubjectX500Principal(), + "Certificate at index " + i + " should be issued by certificate at index " + (i + 1)); + } + } + } +}
