This is an automated email from the ASF dual-hosted git repository. gnodet pushed a commit to branch florentine-november in repository https://gitbox.apache.org/repos/asf/camel.git
commit c333b35603ed5a0946cacf5ebfc835ee4d86f620 Author: Guillaume Nodet <[email protected]> AuthorDate: Mon Mar 23 14:07:14 2026 +0100 CAMEL-22497: Make HTTPS easier for camel.server - Move SSL configuration before HTTP server configuration so global SSL context is available when the server is created - Auto-enable useGlobalSslContextParameters on HTTP server and management server when camel.ssl.enabled=true - Generate self-signed certificate when SSL is enabled but no keystore is configured, for easy development use Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../org/apache/camel/main/BaseMainSupport.java | 74 +++++-- .../camel/main/SelfSignedCertificateGenerator.java | 240 +++++++++++++++++++++ .../java/org/apache/camel/main/MainSSLTest.java | 66 ++++++ 3 files changed, 360 insertions(+), 20 deletions(-) diff --git a/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java b/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java index e73e1565752c..7a71216f850b 100644 --- a/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java +++ b/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java @@ -1526,6 +1526,13 @@ public abstract class BaseMainSupport extends BaseService { mainConfigurationProperties.isAutoConfigurationFailFast(), true, autoConfiguredProperties); camelContext.setRestConfiguration(rest); } + // SSL must be configured before HTTP servers, so global SSL context is available + if (!sslProperties.isEmpty() || mainConfigurationProperties.hasSslConfiguration()) { + LOG.debug("Auto-configuring SSL from loaded properties: {}", sslProperties.size()); + setSslProperties(camelContext, sslProperties, + mainConfigurationProperties.isAutoConfigurationFailFast(), + autoConfiguredProperties); + } if (!httpServerProperties.isEmpty() || mainConfigurationProperties.hasHttpServerConfiguration()) { LOG.debug("Auto-configuring HTTP Server from loaded properties: {}", httpServerProperties.size()); setHttpServerProperties(camelContext, httpServerProperties, @@ -1594,12 +1601,6 @@ public abstract class BaseMainSupport extends BaseService { mainConfigurationProperties.isAutoConfigurationFailFast(), autoConfiguredProperties); } - if (!sslProperties.isEmpty() || mainConfigurationProperties.hasSslConfiguration()) { - LOG.debug("Auto-configuring SSL from loaded properties: {}", sslProperties.size()); - setSslProperties(camelContext, sslProperties, - mainConfigurationProperties.isAutoConfigurationFailFast(), - autoConfiguredProperties); - } if (!debuggerProperties.isEmpty() || mainConfigurationProperties.hasDebuggerConfiguration()) { LOG.debug("Auto-configuring Debugger from loaded properties: {}", debuggerProperties.size()); setDebuggerProperties(camelContext, debuggerProperties, @@ -2023,6 +2024,12 @@ public abstract class BaseMainSupport extends BaseService { return; } + // when global SSL is enabled, automatically use it for the HTTP server + // (unless the user has explicitly configured useGlobalSslContextParameters) + if (!server.isUseGlobalSslContextParameters() && camelContext.getSSLContextParameters() != null) { + server.setUseGlobalSslContextParameters(true); + } + // auto-detect camel-platform-http-main on classpath MainHttpServerFactory sf = resolveMainHttpServerFactory(camelContext); // create http server as a service managed by camel context @@ -2047,6 +2054,11 @@ public abstract class BaseMainSupport extends BaseService { return; } + // when global SSL is enabled, automatically use it for the HTTP management server + if (!server.isUseGlobalSslContextParameters() && camelContext.getSSLContextParameters() != null) { + server.setUseGlobalSslContextParameters(true); + } + // auto-detect camel-platform-http-main on classpath MainHttpServerFactory sf = resolveMainHttpServerFactory(camelContext); // create http management server as a service managed by camel context @@ -2106,7 +2118,8 @@ public abstract class BaseMainSupport extends BaseService { private void setSslProperties( CamelContext camelContext, OrderedLocationProperties properties, - boolean failIfNotSet, OrderedLocationProperties autoConfiguredProperties) { + boolean failIfNotSet, OrderedLocationProperties autoConfiguredProperties) + throws Exception { SSLConfigurationProperties sslConfig = mainConfigurationProperties.sslConfig(); setPropertiesOnTarget(camelContext, sslConfig, properties, "camel.ssl.", @@ -2116,19 +2129,40 @@ public abstract class BaseMainSupport extends BaseService { return; } - KeyStoreParameters ksp = new KeyStoreParameters(); - ksp.setCamelContext(camelContext); - ksp.setResource(sslConfig.getKeyStore()); - ksp.setType(sslConfig.getKeyStoreType()); - ksp.setPassword(sslConfig.getKeystorePassword()); - ksp.setProvider(sslConfig.getKeyStoreProvider()); - - KeyManagersParameters kmp = new KeyManagersParameters(); - kmp.setCamelContext(camelContext); - kmp.setKeyPassword(sslConfig.getKeystorePassword()); - kmp.setKeyStore(ksp); - kmp.setAlgorithm(sslConfig.getKeyManagerAlgorithm()); - kmp.setProvider(sslConfig.getKeyManagerProvider()); + KeyManagersParameters kmp; + if (sslConfig.getKeyStore() != null) { + // use the configured keystore + KeyStoreParameters ksp = new KeyStoreParameters(); + ksp.setCamelContext(camelContext); + ksp.setResource(sslConfig.getKeyStore()); + ksp.setType(sslConfig.getKeyStoreType()); + ksp.setPassword(sslConfig.getKeystorePassword()); + ksp.setProvider(sslConfig.getKeyStoreProvider()); + + kmp = new KeyManagersParameters(); + kmp.setCamelContext(camelContext); + kmp.setKeyPassword(sslConfig.getKeystorePassword()); + kmp.setKeyStore(ksp); + kmp.setAlgorithm(sslConfig.getKeyManagerAlgorithm()); + kmp.setProvider(sslConfig.getKeyManagerProvider()); + } else { + // no keystore configured, generate a self-signed certificate for development use + LOG.info("No SSL keystore configured - generating self-signed certificate for development use." + + " Do NOT use this in production."); + String password = "camel-self-signed"; + KeyStore ks = SelfSignedCertificateGenerator.generateKeyStore(password); + + KeyStoreParameters ksp = new KeyStoreParameters(); + ksp.setCamelContext(camelContext); + ksp.setKeyStore(ks); + ksp.setType("PKCS12"); + ksp.setPassword(password); + + kmp = new KeyManagersParameters(); + kmp.setCamelContext(camelContext); + kmp.setKeyPassword(password); + kmp.setKeyStore(ksp); + } final SSLContextParameters sslContextParameters = createSSLContextParameters(camelContext, sslConfig, kmp); camelContext.setSSLContextParameters(sslContextParameters); diff --git a/core/camel-main/src/main/java/org/apache/camel/main/SelfSignedCertificateGenerator.java b/core/camel-main/src/main/java/org/apache/camel/main/SelfSignedCertificateGenerator.java new file mode 100644 index 000000000000..dc1dd0ac8d21 --- /dev/null +++ b/core/camel-main/src/main/java/org/apache/camel/main/SelfSignedCertificateGenerator.java @@ -0,0 +1,240 @@ +/* + * 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.main; + +import java.io.ByteArrayInputStream; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; + +/** + * Generates a self-signed certificate for development use. This allows enabling HTTPS with minimal configuration when + * no keystore is provided. + * + * The generated certificate is NOT suitable for production use. + */ +final class SelfSignedCertificateGenerator { + + private SelfSignedCertificateGenerator() { + } + + /** + * Generates a PKCS12 KeyStore containing a self-signed certificate. + * + * @param password the password for the keystore and key entry + * @return a KeyStore containing the self-signed certificate + * @throws Exception if certificate generation fails + */ + static KeyStore generateKeyStore(String password) throws Exception { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048, new SecureRandom()); + KeyPair keyPair = keyGen.generateKeyPair(); + + X509Certificate cert = generateCertificate(keyPair); + + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(null, password.toCharArray()); + ks.setKeyEntry("camel-self-signed", keyPair.getPrivate(), password.toCharArray(), + new X509Certificate[] { cert }); + + return ks; + } + + @SuppressWarnings("restriction") + private static X509Certificate generateCertificate(KeyPair keyPair) throws Exception { + PublicKey publicKey = keyPair.getPublic(); + PrivateKey privateKey = keyPair.getPrivate(); + + Instant now = Instant.now(); + Date notBefore = Date.from(now); + Date notAfter = Date.from(now.plus(365, ChronoUnit.DAYS)); + + // Build self-signed X.509 certificate using DER encoding + byte[] encoded = buildSelfSignedCertificateDer(publicKey, privateKey, notBefore, notAfter); + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(encoded)); + } + + private static byte[] buildSelfSignedCertificateDer( + PublicKey publicKey, PrivateKey privateKey, + Date notBefore, Date notAfter) + throws Exception { + + // DN: CN=localhost, O=Apache Camel (self-signed) + byte[] issuerDn = buildDn(); + + // TBS Certificate + byte[] tbsCertificate = buildTbsCertificate(publicKey, issuerDn, notBefore, notAfter); + + // Sign the TBS certificate + Signature sig = Signature.getInstance("SHA256withRSA"); + sig.initSign(privateKey); + sig.update(tbsCertificate); + byte[] signature = sig.sign(); + + // Build the full certificate: SEQUENCE { tbsCertificate, signatureAlgorithm, signature } + byte[] signatureAlgorithm = sha256WithRsaAlgorithmIdentifier(); + byte[] signatureBitString = wrapBitString(signature); + + return wrapSequence(concat(tbsCertificate, signatureAlgorithm, signatureBitString)); + } + + private static byte[] buildTbsCertificate( + PublicKey publicKey, byte[] dn, + Date notBefore, Date notAfter) + throws Exception { + + // Version: v3 (2) + byte[] version = wrapExplicitTag(0, wrapInteger(new byte[] { 2 })); + + // Serial number + byte[] serialBytes = new byte[16]; + new SecureRandom().nextBytes(serialBytes); + serialBytes[0] &= 0x7F; // ensure positive + byte[] serial = wrapInteger(serialBytes); + + // Signature algorithm + byte[] signatureAlgorithm = sha256WithRsaAlgorithmIdentifier(); + + // Issuer DN + byte[] issuer = dn; + + // Validity + byte[] validity = wrapSequence(concat(encodeUtcTime(notBefore), encodeUtcTime(notAfter))); + + // Subject DN (same as issuer for self-signed) + byte[] subject = dn; + + // Subject Public Key Info (from the encoded public key) + byte[] subjectPublicKeyInfo = publicKey.getEncoded(); + + return wrapSequence(concat(version, serial, signatureAlgorithm, issuer, validity, subject, subjectPublicKeyInfo)); + } + + // Builds DN: CN=localhost, O=Apache Camel (self-signed) + private static byte[] buildDn() { + byte[] cn = buildRdn(new byte[] { 0x55, 0x04, 0x03 }, "localhost"); + byte[] o = buildRdn(new byte[] { 0x55, 0x04, 0x0A }, "Apache Camel (self-signed)"); + return wrapSequence(concat(wrapSet(cn), wrapSet(o))); + } + + private static byte[] buildRdn(byte[] oidBytes, String value) { + byte[] oid = new byte[2 + oidBytes.length]; + oid[0] = 0x06; // OID tag + oid[1] = (byte) oidBytes.length; + System.arraycopy(oidBytes, 0, oid, 2, oidBytes.length); + + byte[] valueBytes = value.getBytes(java.nio.charset.StandardCharsets.UTF_8); + byte[] utf8String = new byte[2 + valueBytes.length]; + utf8String[0] = 0x0C; // UTF8String tag + utf8String[1] = (byte) valueBytes.length; + System.arraycopy(valueBytes, 0, utf8String, 2, valueBytes.length); + + return wrapSequence(concat(oid, utf8String)); + } + + private static byte[] sha256WithRsaAlgorithmIdentifier() { + // OID 1.2.840.113549.1.1.11 (sha256WithRSAEncryption) + NULL parameters + byte[] oid = new byte[] { + 0x06, 0x09, 0x2A, (byte) 0x86, 0x48, (byte) 0x86, (byte) 0xF7, 0x0D, 0x01, 0x01, 0x0B }; + byte[] nullParam = new byte[] { 0x05, 0x00 }; + return wrapSequence(concat(oid, nullParam)); + } + + @SuppressWarnings("deprecation") + private static byte[] encodeUtcTime(Date date) { + // UTCTime format: YYMMDDHHmmSSZ + String utc = String.format("%02d%02d%02d%02d%02d%02dZ", + date.getYear() % 100, date.getMonth() + 1, date.getDate(), + date.getHours(), date.getMinutes(), date.getSeconds()); + byte[] timeBytes = utc.getBytes(java.nio.charset.StandardCharsets.US_ASCII); + byte[] result = new byte[2 + timeBytes.length]; + result[0] = 0x17; // UTCTime tag + result[1] = (byte) timeBytes.length; + System.arraycopy(timeBytes, 0, result, 2, timeBytes.length); + return result; + } + + private static byte[] wrapSequence(byte[] content) { + return wrapTag(0x30, content); + } + + private static byte[] wrapSet(byte[] content) { + return wrapTag(0x31, content); + } + + private static byte[] wrapInteger(byte[] value) { + return wrapTag(0x02, value); + } + + private static byte[] wrapBitString(byte[] content) { + // BitString: tag + length + 0x00 (no unused bits) + content + byte[] padded = new byte[1 + content.length]; + padded[0] = 0x00; + System.arraycopy(content, 0, padded, 1, content.length); + return wrapTag(0x03, padded); + } + + private static byte[] wrapExplicitTag(int tagNumber, byte[] content) { + return wrapTag(0xA0 | tagNumber, content); + } + + private static byte[] wrapTag(int tag, byte[] content) { + byte[] lengthBytes = encodeLength(content.length); + byte[] result = new byte[1 + lengthBytes.length + content.length]; + result[0] = (byte) tag; + System.arraycopy(lengthBytes, 0, result, 1, lengthBytes.length); + System.arraycopy(content, 0, result, 1 + lengthBytes.length, content.length); + return result; + } + + private static byte[] encodeLength(int length) { + if (length < 128) { + return new byte[] { (byte) length }; + } else if (length < 256) { + return new byte[] { (byte) 0x81, (byte) length }; + } else if (length < 65536) { + return new byte[] { (byte) 0x82, (byte) (length >> 8), (byte) length }; + } else { + return new byte[] { (byte) 0x83, (byte) (length >> 16), (byte) (length >> 8), (byte) length }; + } + } + + private static byte[] concat(byte[]... arrays) { + int totalLength = 0; + for (byte[] array : arrays) { + totalLength += array.length; + } + byte[] result = new byte[totalLength]; + int offset = 0; + for (byte[] array : arrays) { + System.arraycopy(array, 0, result, offset, array.length); + offset += array.length; + } + return result; + } +} diff --git a/core/camel-main/src/test/java/org/apache/camel/main/MainSSLTest.java b/core/camel-main/src/test/java/org/apache/camel/main/MainSSLTest.java index 21f22e7f1cb6..e83d62744b2e 100644 --- a/core/camel-main/src/test/java/org/apache/camel/main/MainSSLTest.java +++ b/core/camel-main/src/test/java/org/apache/camel/main/MainSSLTest.java @@ -16,8 +16,11 @@ */ package org.apache.camel.main; +import java.security.KeyStore; import java.util.List; +import javax.net.ssl.SSLContext; + import org.apache.camel.CamelContext; import org.apache.camel.support.jsse.ClientAuthentication; import org.apache.camel.support.jsse.FilterParameters; @@ -387,6 +390,69 @@ public class MainSSLTest { main.stop(); } + @Test + public void testMainSSLSelfSigned() { + Main main = new Main(); + + // just enabling SSL without a keystore should generate a self-signed certificate + main.addInitialProperty("camel.ssl.enabled", "true"); + + main.start(); + + CamelContext context = main.getCamelContext(); + assertNotNull(context); + + SSLContextParameters sslParams = context.getSSLContextParameters(); + assertNotNull(sslParams); + + // should have key managers with a self-signed certificate + KeyManagersParameters kmp = sslParams.getKeyManagers(); + assertNotNull(kmp); + + KeyStoreParameters ksp = kmp.getKeyStore(); + assertNotNull(ksp); + // the keystore should be set directly (not via resource) + assertNull(ksp.getResource()); + + // verify that an SSLContext can be created from the parameters + try { + SSLContext sslContext = sslParams.createSSLContext(context); + assertNotNull(sslContext); + } catch (Exception e) { + Assertions.fail("Should be able to create SSLContext from self-signed certificate: " + e.getMessage()); + } + + main.stop(); + } + + @Test + public void testMainSSLSelfSignedFluent() { + Main main = new Main(); + + main.configure().sslConfig() + .withEnabled(true); + + main.start(); + + CamelContext context = main.getCamelContext(); + assertNotNull(context); + + SSLContextParameters sslParams = context.getSSLContextParameters(); + assertNotNull(sslParams); + assertNotNull(sslParams.getKeyManagers()); + + main.stop(); + } + + @Test + public void testSelfSignedCertificateGenerator() throws Exception { + KeyStore ks = SelfSignedCertificateGenerator.generateKeyStore("test-password"); + assertNotNull(ks); + Assertions.assertTrue(ks.containsAlias("camel-self-signed")); + assertNotNull(ks.getKey("camel-self-signed", "test-password".toCharArray())); + assertNotNull(ks.getCertificate("camel-self-signed")); + } + @Test public void testMainSSLSignatureSchemesFilterFluent() { Main main = new Main();
