From 1093b6494644774883bb443100fa20f572c40459 Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Mon, 19 Oct 2026 11:11:34 +0200 Subject: [PATCH 1/5] CAMEL-25795: camel-pqc - stopping an endpoint no longer removes the Bouncy Castle providers from the JVM PQCEndpoint.doStop() unconditionally removed the BC and BCPQC providers from java.security.Security, which is JVM-global. doStart() only adds a provider when it is absent, so an endpoint routinely removed one it had not installed - tearing it out from under the sibling pqc endpoint of the same route, other CamelContexts, camel-crypto/camel-ssh/camel-xmlsecurity, and all non-Camel code in the same JVM. Per-endpoint ownership tracking does not fix this: endpoint A adds the provider, endpoint B sees it present and skips, then A stops and removes it, breaking B. Since Security records no ownership, the providers are now simply left registered, which is what camel-as2, camel-crypto-pgp and this component's own statically initialised PQCDefault*Material classes already do. Co-Authored-By: Claude Opus 5 Signed-off-by: Andrea Cosentino --- .../camel/component/pqc/PQCEndpoint.java | 20 +++-- .../pqc/PQCEndpointProviderLifecycleTest.java | 80 +++++++++++++++++++ 2 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCEndpointProviderLifecycleTest.java diff --git a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCEndpoint.java b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCEndpoint.java index 42dc26432838e..9254816f19252 100644 --- a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCEndpoint.java +++ b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCEndpoint.java @@ -60,6 +60,15 @@ public Producer createProducer() throws Exception { return new PQCProducer(this); } + /** + * Registers the BouncyCastle providers if they are not installed yet. + *

+ * The providers are deliberately never removed again. {@link Security} is JVM-global and shared with every other + * endpoint, every other {@code CamelContext} and all non-Camel code in the same JVM, and nothing records which + * component installed a given provider. Removing one on stop therefore tears it out from under unrelated users - + * including the sibling {@code pqc} endpoint of the same route and the statically initialised + * {@code PQCDefault*Material} classes of this very component, which also register BouncyCastle and never remove it. + */ @Override public void doStart() throws Exception { super.doStart(); @@ -71,17 +80,6 @@ public void doStart() throws Exception { } } - @Override - public void doStop() throws Exception { - if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) != null) { - Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); - } - if (Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME) != null) { - Security.removeProvider(BouncyCastlePQCProvider.PROVIDER_NAME); - } - super.doStop(); - } - public PQCConfiguration getConfiguration() { return configuration; } diff --git a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCEndpointProviderLifecycleTest.java b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCEndpointProviderLifecycleTest.java new file mode 100644 index 0000000000000..5257a27be2dad --- /dev/null +++ b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCEndpointProviderLifecycleTest.java @@ -0,0 +1,80 @@ +/* + * 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.pqc; + +import org.apache.camel.CamelContext; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.impl.DefaultCamelContext; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; +import org.junit.jupiter.api.Test; + +import static java.security.Security.getProvider; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * The BouncyCastle providers live in the JVM-global {@link java.security.Security} registry, so a PQC endpoint must + * never remove them on stop - other endpoints, other CamelContexts and unrelated code in the same JVM rely on them. + */ +public class PQCEndpointProviderLifecycleTest { + + @Test + void stoppingTheContextKeepsTheProvidersRegistered() throws Exception { + try (CamelContext context = new DefaultCamelContext()) { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("direct:sign").to("pqc:sign?operation=sign&signatureAlgorithm=MLDSA"); + } + }); + context.start(); + + assertNotNull(getProvider(BouncyCastleProvider.PROVIDER_NAME), "BC must be registered while running"); + assertNotNull(getProvider(BouncyCastlePQCProvider.PROVIDER_NAME), "BCPQC must be registered while running"); + + context.stop(); + + assertNotNull(getProvider(BouncyCastleProvider.PROVIDER_NAME), + "stopping a PQC endpoint must not de-register the JVM-global BC provider"); + assertNotNull(getProvider(BouncyCastlePQCProvider.PROVIDER_NAME), + "stopping a PQC endpoint must not de-register the JVM-global BCPQC provider"); + } + } + + @Test + void stoppingOneEndpointLeavesTheOtherUsable() throws Exception { + try (CamelContext context = new DefaultCamelContext()) { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("direct:a").to("pqc:a?operation=sign&signatureAlgorithm=MLDSA"); + from("direct:b").to("pqc:b?operation=sign&signatureAlgorithm=MLDSA"); + } + }); + context.start(); + + // stopping the first route disposes its endpoint; the second one must keep working + context.getRouteController().stopRoute("route1"); + context.removeEndpoints("pqc:a*"); + + context.createProducerTemplate().sendBody("direct:b", "Hello"); + + assertNotNull(getProvider(BouncyCastleProvider.PROVIDER_NAME), + "a sibling PQC endpoint must still see the BC provider"); + } + } +} From 4e6c2ee6f7c5a0fa7ab84ab828470c87e826b41d Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Mon, 19 Oct 2026 11:12:26 +0200 Subject: [PATCH 2/5] CAMEL-25795: upgrade guide - camel-pqc note for 4.24 Co-Authored-By: Claude Opus 5 Signed-off-by: Andrea Cosentino --- .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc index 6872b03448562..e0657bf277b48 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc @@ -263,6 +263,18 @@ values such as `AES`, which are pure ASCII. A deployment running on a JVM whose name identically to UTF-8 would, however, derive a different key than the previous version, and could not decrypt material produced by it. +=== camel-pqc - the BouncyCastle providers stay registered when an endpoint stops + +Stopping a `pqc` endpoint no longer removes the `BC` and `BCPQC` providers from `java.security.Security`. + +`java.security.Security` is JVM-global, and the endpoint only added a provider when it was absent, so stopping one +endpoint could remove a provider it had not installed - breaking the other `pqc` endpoint of the same route, other +`CamelContext` instances, components such as camel-crypto, camel-ssh and camel-xmlsecurity, and any non-Camel code in +the same JVM. + +An application that relied on the providers being de-registered on shutdown must now remove them itself with +`Security.removeProvider("BC")` and `Security.removeProvider("BCPQC")`. + === camel-jbang - camel validate normalize writes one file per input file to an output directory `camel validate normalize --output=

` now writes one normalized file per input file into the From ac6c0c63e1362768fc3711a6e3d0dc2df6a13a5c Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Mon, 19 Oct 2026 13:33:49 +0200 Subject: [PATCH 3/5] CAMEL-25795: Address review feedback - pin the route ids in the lifecycle test Relying on the auto-generated "route1" id would break silently if Camel's default naming scheme ever changed. The routes now carry explicit ids, which also reads better at the stopRoute call. Co-Authored-By: Claude Opus 5 Signed-off-by: Andrea Cosentino --- .../component/pqc/PQCEndpointProviderLifecycleTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCEndpointProviderLifecycleTest.java b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCEndpointProviderLifecycleTest.java index 5257a27be2dad..59aeaef2f0d65 100644 --- a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCEndpointProviderLifecycleTest.java +++ b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCEndpointProviderLifecycleTest.java @@ -61,14 +61,14 @@ void stoppingOneEndpointLeavesTheOtherUsable() throws Exception { context.addRoutes(new RouteBuilder() { @Override public void configure() { - from("direct:a").to("pqc:a?operation=sign&signatureAlgorithm=MLDSA"); - from("direct:b").to("pqc:b?operation=sign&signatureAlgorithm=MLDSA"); + from("direct:a").routeId("a").to("pqc:a?operation=sign&signatureAlgorithm=MLDSA"); + from("direct:b").routeId("b").to("pqc:b?operation=sign&signatureAlgorithm=MLDSA"); } }); context.start(); // stopping the first route disposes its endpoint; the second one must keep working - context.getRouteController().stopRoute("route1"); + context.getRouteController().stopRoute("a"); context.removeEndpoints("pqc:a*"); context.createProducerTemplate().sendBody("direct:b", "Hello"); From 3275e1cbdf7548e05703061db9cbd49891951ccc Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Mon, 19 Oct 2026 15:27:15 +0200 Subject: [PATCH 4/5] CAMEL-25795: Address review feedback - name the second test for what it pins The send to the surviving route succeeds on main too, since that endpoint already holds its own Signature. What the test actually guards is the provider still being registered, so the name and a comment now say that. Co-Authored-By: Claude Opus 5 Signed-off-by: Andrea Cosentino --- .../component/pqc/PQCEndpointProviderLifecycleTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCEndpointProviderLifecycleTest.java b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCEndpointProviderLifecycleTest.java index 59aeaef2f0d65..bef20c0f91f13 100644 --- a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCEndpointProviderLifecycleTest.java +++ b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCEndpointProviderLifecycleTest.java @@ -56,7 +56,7 @@ public void configure() { } @Test - void stoppingOneEndpointLeavesTheOtherUsable() throws Exception { + void stoppingOneEndpointLeavesTheProviderForTheOther() throws Exception { try (CamelContext context = new DefaultCamelContext()) { context.addRoutes(new RouteBuilder() { @Override @@ -67,7 +67,8 @@ public void configure() { }); context.start(); - // stopping the first route disposes its endpoint; the second one must keep working + // stopping the first route disposes its endpoint. The second route keeps its own Signature either way, + // so what this pins is the provider: on main the stop de-registers it for the whole JVM. context.getRouteController().stopRoute("a"); context.removeEndpoints("pqc:a*"); From 333577d843bbc3426c683f7be202ff2a6925e2a0 Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Tue, 20 Oct 2026 12:59:53 +0200 Subject: [PATCH 5/5] CAMEL-25795: Address review feedback -

instead of the self-closing

in the PQCEndpoint javadoc javadoc doclint rejects the self-closing form. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Andrea Cosentino --- .../main/java/org/apache/camel/component/pqc/PQCEndpoint.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCEndpoint.java b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCEndpoint.java index 9254816f19252..3a78b651d698d 100644 --- a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCEndpoint.java +++ b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCEndpoint.java @@ -62,7 +62,7 @@ public Producer createProducer() throws Exception { /** * Registers the BouncyCastle providers if they are not installed yet. - *

+ *

* The providers are deliberately never removed again. {@link Security} is JVM-global and shared with every other * endpoint, every other {@code CamelContext} and all non-Camel code in the same JVM, and nothing records which * component installed a given provider. Removing one on stop therefore tears it out from under unrelated users -