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..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 @@ -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..bef20c0f91f13
--- /dev/null
+++ b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCEndpointProviderLifecycleTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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 stoppingOneEndpointLeavesTheProviderForTheOther() throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ 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 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*");
+
+ context.createProducerTemplate().sendBody("direct:b", "Hello");
+
+ assertNotNull(getProvider(BouncyCastleProvider.PROVIDER_NAME),
+ "a sibling PQC endpoint must still see the BC provider");
+ }
+ }
+}
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