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 --output=

` now writes one normalized file per input file into the