This is an automated email from the ASF dual-hosted git repository. jiriondrusek pushed a commit to branch camel-main in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
commit bea8328614fb979cd038ef0308601e7af7cdb26a Author: Jiri Ondrusek <[email protected]> AuthorDate: Thu Mar 26 15:44:26 2026 +0100 Pqc native support --- docs/modules/ROOT/examples/components/pqc.yml | 6 +- docs/modules/ROOT/examples/dataformats/pqc.yml | 6 +- .../ROOT/pages/reference/extensions/pqc.adoc | 14 +- extensions-jvm/pom.xml | 1 - .../component/pqc/deployment/PqcProcessor.java | 46 ----- .../BouncyCastleAdditionalProviderBuildItem.java | 26 +-- .../deployment/BouncyCastleSupportProcessor.java | 14 +- .../support/bouncycastle/BouncyCastleRecorder.java | 43 +++-- extensions/pom.xml | 1 + .../pqc/deployment/pom.xml | 4 + .../component/pqc/deployment/PqcProcessor.java | 81 +++++++++ {extensions-jvm => extensions}/pqc/pom.xml | 2 +- {extensions-jvm => extensions}/pqc/runtime/pom.xml | 5 + .../main/resources/META-INF/quarkus-extension.yaml | 1 - integration-tests-jvm/pom.xml | 1 - .../quarkus/component/pqc/it/PqcResource.java | 50 ------ integration-tests/pom.xml | 1 + .../pqc/pom.xml | 36 ++++ .../quarkus/component/pqc/it/PqcProducers.java | 89 ++++++++++ .../quarkus/component/pqc/it/PqcResource.java | 187 +++++++++++++++++++++ .../camel/quarkus/component/pqc/it/PqcIT.java | 17 +- .../camel/quarkus/component/pqc/it/PqcTest.java | 159 ++++++++++++++++++ tooling/scripts/test-categories.yaml | 1 + 23 files changed, 636 insertions(+), 155 deletions(-) diff --git a/docs/modules/ROOT/examples/components/pqc.yml b/docs/modules/ROOT/examples/components/pqc.yml index 5d107de78c..723e0663d8 100644 --- a/docs/modules/ROOT/examples/components/pqc.yml +++ b/docs/modules/ROOT/examples/components/pqc.yml @@ -2,11 +2,11 @@ # This file was generated by camel-quarkus-maven-plugin:update-extension-doc-page cqArtifactId: camel-quarkus-pqc cqArtifactIdBase: pqc -cqNativeSupported: false -cqStatus: Preview +cqNativeSupported: true +cqStatus: Stable cqDeprecated: false cqJvmSince: 3.24.0 -cqNativeSince: n/a +cqNativeSince: 3.35.0 cqCamelPartName: pqc cqCamelPartTitle: PQC Algorithms cqCamelPartDescription: Post Quantum Cryptography Signature and Verification component. diff --git a/docs/modules/ROOT/examples/dataformats/pqc.yml b/docs/modules/ROOT/examples/dataformats/pqc.yml index 57e5f19113..62643f1ab3 100644 --- a/docs/modules/ROOT/examples/dataformats/pqc.yml +++ b/docs/modules/ROOT/examples/dataformats/pqc.yml @@ -2,11 +2,11 @@ # This file was generated by camel-quarkus-maven-plugin:update-extension-doc-page cqArtifactId: camel-quarkus-pqc cqArtifactIdBase: pqc -cqNativeSupported: false -cqStatus: Preview +cqNativeSupported: true +cqStatus: Stable cqDeprecated: false cqJvmSince: 3.24.0 -cqNativeSince: n/a +cqNativeSince: 3.35.0 cqCamelPartName: pqc cqCamelPartTitle: PQC (Post-Quantum Cryptography) cqCamelPartDescription: Encrypt and decrypt messages using Post-Quantum Cryptography Key Encapsulation Mechanisms (KEM). diff --git a/docs/modules/ROOT/pages/reference/extensions/pqc.adoc b/docs/modules/ROOT/pages/reference/extensions/pqc.adoc index 2e95efcd0c..1c17849e83 100644 --- a/docs/modules/ROOT/pages/reference/extensions/pqc.adoc +++ b/docs/modules/ROOT/pages/reference/extensions/pqc.adoc @@ -4,17 +4,17 @@ = PQC Algorithms :linkattrs: :cq-artifact-id: camel-quarkus-pqc -:cq-native-supported: false -:cq-status: Preview -:cq-status-deprecation: Preview +:cq-native-supported: true +:cq-status: Stable +:cq-status-deprecation: Stable :cq-description: Post Quantum Computing Signature and Verification component. :cq-deprecated: false :cq-jvm-since: 3.24.0 -:cq-native-since: n/a +:cq-native-since: 3.35.0 ifeval::[{doc-show-badges} == true] [.badges] -[.badge-key]##JVM since##[.badge-supported]##3.24.0## [.badge-key]##Native##[.badge-unsupported]##unsupported## +[.badge-key]##JVM since##[.badge-supported]##3.24.0## [.badge-key]##Native since##[.badge-supported]##3.35.0## endif::[] Post Quantum Computing Signature and Verification component. @@ -30,6 +30,10 @@ Please refer to the above links for usage and configuration details. [id="extensions-pqc-maven-coordinates"] == Maven coordinates +https://{link-quarkus-code-generator}/?extension-search=camel-quarkus-pqc[Create a new project with this extension on {link-quarkus-code-generator}, window="_blank"] + +Or add the coordinates to your existing project: + [source,xml] ---- <dependency> diff --git a/extensions-jvm/pom.xml b/extensions-jvm/pom.xml index 7a000433fb..0251dc69e6 100644 --- a/extensions-jvm/pom.xml +++ b/extensions-jvm/pom.xml @@ -80,7 +80,6 @@ <module>lucene</module> <module>mvel</module> <module>opensearch</module> - <module>pqc</module> <module>printer</module> <module>pulsar</module> <module>python</module> diff --git a/extensions-jvm/pqc/deployment/src/main/java/org/apache/camel/quarkus/component/pqc/deployment/PqcProcessor.java b/extensions-jvm/pqc/deployment/src/main/java/org/apache/camel/quarkus/component/pqc/deployment/PqcProcessor.java deleted file mode 100644 index b4826f3f13..0000000000 --- a/extensions-jvm/pqc/deployment/src/main/java/org/apache/camel/quarkus/component/pqc/deployment/PqcProcessor.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.quarkus.component.pqc.deployment; - -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.annotations.ExecutionTime; -import io.quarkus.deployment.annotations.Record; -import io.quarkus.deployment.builditem.FeatureBuildItem; -import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; -import org.apache.camel.quarkus.core.JvmOnlyRecorder; -import org.jboss.logging.Logger; - -class PqcProcessor { - - private static final Logger LOG = Logger.getLogger(PqcProcessor.class); - private static final String FEATURE = "camel-pqc"; - - @BuildStep - FeatureBuildItem feature() { - return new FeatureBuildItem(FEATURE); - } - - /** - * Remove this once this extension starts supporting the native mode. - */ - @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) - @Record(value = ExecutionTime.RUNTIME_INIT) - void warnJvmInNative(JvmOnlyRecorder recorder) { - JvmOnlyRecorder.warnJvmInNative(LOG, FEATURE); // warn at build time - recorder.warnJvmInNative(FEATURE); // warn at runtime - } -} diff --git a/integration-tests-jvm/pqc/src/test/java/org/apache/camel/quarkus/component/pqc/it/PqcTest.java b/extensions-support/bouncycastle/deployment/src/main/java/org/apache/camel/quarkus/support/bouncycastle/deployment/BouncyCastleAdditionalProviderBuildItem.java similarity index 59% copy from integration-tests-jvm/pqc/src/test/java/org/apache/camel/quarkus/component/pqc/it/PqcTest.java copy to extensions-support/bouncycastle/deployment/src/main/java/org/apache/camel/quarkus/support/bouncycastle/deployment/BouncyCastleAdditionalProviderBuildItem.java index ff955411d3..c004eca4cf 100644 --- a/integration-tests-jvm/pqc/src/test/java/org/apache/camel/quarkus/component/pqc/it/PqcTest.java +++ b/extensions-support/bouncycastle/deployment/src/main/java/org/apache/camel/quarkus/support/bouncycastle/deployment/BouncyCastleAdditionalProviderBuildItem.java @@ -14,21 +14,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.camel.quarkus.component.pqc.it; +package org.apache.camel.quarkus.support.bouncycastle.deployment; -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import org.junit.jupiter.api.Test; +import io.quarkus.builder.item.MultiBuildItem; -@QuarkusTest -class PqcTest { +/** + * In case that non-default BC provider has to be registered, use this buildItem. + * (provider available for registration is `BCPQC`) + */ +public final class BouncyCastleAdditionalProviderBuildItem extends MultiBuildItem { + + private final String proivderName; - @Test - public void loadComponentPqc() { - /* A simple autogenerated test */ - RestAssured.get("/pqc/load/component/pqc") - .then() - .statusCode(200); + public BouncyCastleAdditionalProviderBuildItem(String providerNme) { + this.proivderName = providerNme; } + public String getProviderName() { + return proivderName; + } } diff --git a/extensions-support/bouncycastle/deployment/src/main/java/org/apache/camel/quarkus/support/bouncycastle/deployment/BouncyCastleSupportProcessor.java b/extensions-support/bouncycastle/deployment/src/main/java/org/apache/camel/quarkus/support/bouncycastle/deployment/BouncyCastleSupportProcessor.java index a6e47238aa..012b6e73b2 100644 --- a/extensions-support/bouncycastle/deployment/src/main/java/org/apache/camel/quarkus/support/bouncycastle/deployment/BouncyCastleSupportProcessor.java +++ b/extensions-support/bouncycastle/deployment/src/main/java/org/apache/camel/quarkus/support/bouncycastle/deployment/BouncyCastleSupportProcessor.java @@ -26,7 +26,6 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; -import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.security.deployment.BouncyCastleProviderBuildItem; @@ -46,12 +45,17 @@ public class BouncyCastleSupportProcessor { @BuildStep() @Record(ExecutionTime.STATIC_INIT) - public void registerBouncyCastleProvider(List<CipherTransformationBuildItem> cipherTransformations, - BouncyCastleRecorder recorder, - ShutdownContextBuildItem shutdownContextBuildItem) { + public void registerBouncyCastleProvider( + List<BouncyCastleAdditionalProviderBuildItem> additionalProviderBuildItems, + List<CipherTransformationBuildItem> cipherTransformations, + BouncyCastleRecorder recorder) { + List<String> allCipherTransformations = cipherTransformations.stream() .flatMap(c -> c.getCipherTransformations().stream()).collect(Collectors.toList()); - recorder.registerBouncyCastleProvider(allCipherTransformations, shutdownContextBuildItem); + + recorder.registerBouncyCastleProvider( + additionalProviderBuildItems.stream().map(BouncyCastleAdditionalProviderBuildItem::getProviderName).toList(), + allCipherTransformations); } @BuildStep() diff --git a/extensions-support/bouncycastle/runtime/src/main/java/org/apache/camel/quarkus/support/bouncycastle/BouncyCastleRecorder.java b/extensions-support/bouncycastle/runtime/src/main/java/org/apache/camel/quarkus/support/bouncycastle/BouncyCastleRecorder.java index e559d1c496..b53a64f717 100644 --- a/extensions-support/bouncycastle/runtime/src/main/java/org/apache/camel/quarkus/support/bouncycastle/BouncyCastleRecorder.java +++ b/extensions-support/bouncycastle/runtime/src/main/java/org/apache/camel/quarkus/support/bouncycastle/BouncyCastleRecorder.java @@ -18,25 +18,33 @@ package org.apache.camel.quarkus.support.bouncycastle; import java.lang.reflect.InvocationTargetException; import java.security.Provider; -import java.security.Security; +import java.util.ArrayList; import java.util.List; import javax.crypto.Cipher; -import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.security.runtime.SecurityProviderUtils; +import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; import org.jboss.logging.Logger; +import static java.security.Security.addProvider; +import static java.security.Security.getProvider; + @Recorder public class BouncyCastleRecorder { private static final Logger LOG = Logger.getLogger(BouncyCastleRecorder.class); - public void registerBouncyCastleProvider(List<String> cipherTransformations, ShutdownContext shutdownContext) { - Provider provider = Security.getProvider(SecurityProviderUtils.BOUNCYCASTLE_PROVIDER_NAME); + public static final String BOUNCYCASTLE_PCQ_PROVIDER_NAME = "BCPCQ"; + + public void registerBouncyCastleProvider( + List<String> additionalProviders, + List<String> cipherTransformations) { + List<Provider> registeredProviders = new ArrayList<>(); + Provider provider = getProvider(SecurityProviderUtils.BOUNCYCASTLE_PROVIDER_NAME); if (provider == null) { - provider = Security.getProvider(SecurityProviderUtils.BOUNCYCASTLE_FIPS_PROVIDER_NAME); + provider = getProvider(SecurityProviderUtils.BOUNCYCASTLE_FIPS_PROVIDER_NAME); } if (provider == null) { // TODO: Fix BuildStep execution order so that this is not required @@ -44,7 +52,7 @@ public class BouncyCastleRecorder { try { provider = (Provider) Thread.currentThread().getContextClassLoader() .loadClass(SecurityProviderUtils.BOUNCYCASTLE_PROVIDER_CLASS_NAME).getConstructor().newInstance(); - Security.addProvider(provider); + addProvider(provider); } catch (ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) { try { @@ -52,7 +60,7 @@ public class BouncyCastleRecorder { provider = (Provider) Thread.currentThread().getContextClassLoader() .loadClass(SecurityProviderUtils.BOUNCYCASTLE_FIPS_PROVIDER_CLASS_NAME).getConstructor() .newInstance(); - Security.addProvider(provider); + addProvider(provider); } catch (ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e2) { throw new RuntimeException("Neither BC nor BCFIPS provider can be registered. \nBC: " + e.getMessage() @@ -60,6 +68,7 @@ public class BouncyCastleRecorder { } } } + registeredProviders.add(provider); // Make it explicit to the static analysis that below security services should be registered as they are reachable at runtime for (String cipherTransformation : cipherTransformations) { @@ -73,12 +82,20 @@ public class BouncyCastleRecorder { } } - shutdownContext.addShutdownTask(new Runnable() { - @Override - public void run() { - Security.removeProvider(SecurityProviderUtils.BOUNCYCASTLE_PROVIDER_NAME); - LOG.debug("Removed Bouncy Castle security provider"); + if (additionalProviders.contains(BOUNCYCASTLE_PCQ_PROVIDER_NAME)) { + Provider pqcProvider = getProvider("BCPQC"); + if (pqcProvider == null) { + pqcProvider = new BouncyCastlePQCProvider(); + try { + addProvider(pqcProvider); + } catch (SecurityException e) { + throw new RuntimeException(e); + } + LOG.debugf("Registered BouncyCastlePQCProvider"); } - }); + registeredProviders.add(pqcProvider); + + } + } } diff --git a/extensions/pom.xml b/extensions/pom.xml index edcc530261..c5956445c9 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -233,6 +233,7 @@ <module>pgevent</module> <module>pinecone</module> <module>platform-http</module> + <module>pqc</module> <module>protobuf</module> <module>pubnub</module> <module>qdrant</module> diff --git a/extensions-jvm/pqc/deployment/pom.xml b/extensions/pqc/deployment/pom.xml similarity index 93% rename from extensions-jvm/pqc/deployment/pom.xml rename to extensions/pqc/deployment/pom.xml index 1c046ba848..0c70adc24d 100644 --- a/extensions-jvm/pqc/deployment/pom.xml +++ b/extensions/pqc/deployment/pom.xml @@ -38,6 +38,10 @@ <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-pqc</artifactId> </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-support-bouncycastle-deployment</artifactId> + </dependency> </dependencies> <build> diff --git a/extensions/pqc/deployment/src/main/java/org/apache/camel/quarkus/component/pqc/deployment/PqcProcessor.java b/extensions/pqc/deployment/src/main/java/org/apache/camel/quarkus/component/pqc/deployment/PqcProcessor.java new file mode 100644 index 0000000000..425d492964 --- /dev/null +++ b/extensions/pqc/deployment/src/main/java/org/apache/camel/quarkus/component/pqc/deployment/PqcProcessor.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.quarkus.component.pqc.deployment; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.IndexDependencyBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageSecurityProviderBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import org.apache.camel.quarkus.support.bouncycastle.BouncyCastleRecorder; +import org.apache.camel.quarkus.support.bouncycastle.deployment.BouncyCastleAdditionalProviderBuildItem; +import org.jboss.jandex.IndexView; +import org.jboss.logging.Logger; + +class PqcProcessor { + + private static final Logger LOG = Logger.getLogger(PqcProcessor.class); + private static final String FEATURE = "camel-pqc"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + NativeImageSecurityProviderBuildItem registerBcpqcSecurityProvider() { + return new NativeImageSecurityProviderBuildItem("org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider"); + } + + @BuildStep + IndexDependencyBuildItem indexBouncyCastlePQC() { + return new IndexDependencyBuildItem("org.bouncycastle", "bcprov-jdk18on"); + } + + @BuildStep + ReflectiveClassBuildItem registerBouncyCastlePQCClasses(CombinedIndexBuildItem combinedIndex) { + IndexView index = combinedIndex.getIndex(); + + String[] pqcClasses = index.getKnownClasses().stream() + .map(ci -> ci.name().toString()) + .filter( + n -> (n.startsWith("org.bouncycastle.pqc.jcajce.provider") + && (n.endsWith("Spi") || n.contains("Spi$")))) + .toArray(String[]::new); + + return ReflectiveClassBuildItem.builder(pqcClasses).methods().fields().build(); + } + + @BuildStep + void registerCryptoClasses(BuildProducer<ReflectiveClassBuildItem> reflectiveClasses) { + reflectiveClasses.produce( + ReflectiveClassBuildItem.builder( + java.security.KeyPairGenerator.class, + java.security.Signature.class, + java.security.KeyFactory.class, + javax.crypto.KeyGenerator.class, + javax.crypto.SecretKey.class).methods().build()); + } + + @BuildStep + BouncyCastleAdditionalProviderBuildItem registerBouncyCastlePQCProvider() { + return new BouncyCastleAdditionalProviderBuildItem(BouncyCastleRecorder.BOUNCYCASTLE_PCQ_PROVIDER_NAME); + } + +} diff --git a/extensions-jvm/pqc/pom.xml b/extensions/pqc/pom.xml similarity index 96% rename from extensions-jvm/pqc/pom.xml rename to extensions/pqc/pom.xml index 8b2520b670..7290384ab2 100644 --- a/extensions-jvm/pqc/pom.xml +++ b/extensions/pqc/pom.xml @@ -21,7 +21,7 @@ <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.apache.camel.quarkus</groupId> - <artifactId>camel-quarkus-extensions-jvm</artifactId> + <artifactId>camel-quarkus-extensions</artifactId> <version>3.35.0-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> diff --git a/extensions-jvm/pqc/runtime/pom.xml b/extensions/pqc/runtime/pom.xml similarity index 93% rename from extensions-jvm/pqc/runtime/pom.xml rename to extensions/pqc/runtime/pom.xml index 191cfbef83..9c46a0a174 100644 --- a/extensions-jvm/pqc/runtime/pom.xml +++ b/extensions/pqc/runtime/pom.xml @@ -32,6 +32,7 @@ <properties> <camel.quarkus.jvmSince>3.24.0</camel.quarkus.jvmSince> + <camel.quarkus.nativeSince>3.35.0</camel.quarkus.nativeSince> </properties> <dependencies> @@ -43,6 +44,10 @@ <groupId>org.apache.camel</groupId> <artifactId>camel-pqc</artifactId> </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-support-bouncycastle</artifactId> + </dependency> </dependencies> <build> diff --git a/extensions-jvm/pqc/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/pqc/runtime/src/main/resources/META-INF/quarkus-extension.yaml similarity index 98% rename from extensions-jvm/pqc/runtime/src/main/resources/META-INF/quarkus-extension.yaml rename to extensions/pqc/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 8dba5652fd..00f74799d4 100644 --- a/extensions-jvm/pqc/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/pqc/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -26,7 +26,6 @@ description: "Post Quantum Computing Signature and Verification component" metadata: icon-url: "https://raw.githubusercontent.com/apache/camel-website/main/antora-ui-camel/src/img/logo-d.svg" sponsor: "Apache Software Foundation" - unlisted: true guide: "https://camel.apache.org/camel-quarkus/latest/reference/extensions/pqc.html" categories: - "integration" diff --git a/integration-tests-jvm/pom.xml b/integration-tests-jvm/pom.xml index a1f1237b89..33ae81867c 100644 --- a/integration-tests-jvm/pom.xml +++ b/integration-tests-jvm/pom.xml @@ -83,7 +83,6 @@ <module>main-devmode</module> <module>mvel</module> <module>opensearch</module> - <module>pqc</module> <module>printer</module> <module>pulsar</module> <module>python</module> diff --git a/integration-tests-jvm/pqc/src/main/java/org/apache/camel/quarkus/component/pqc/it/PqcResource.java b/integration-tests-jvm/pqc/src/main/java/org/apache/camel/quarkus/component/pqc/it/PqcResource.java deleted file mode 100644 index 3824d6ceba..0000000000 --- a/integration-tests-jvm/pqc/src/main/java/org/apache/camel/quarkus/component/pqc/it/PqcResource.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.quarkus.component.pqc.it; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import org.apache.camel.CamelContext; -import org.jboss.logging.Logger; - -@Path("/pqc") -@ApplicationScoped -public class PqcResource { - - private static final Logger LOG = Logger.getLogger(PqcResource.class); - - private static final String COMPONENT_PQC = "pqc"; - @Inject - CamelContext context; - - @Path("/load/component/pqc") - @GET - @Produces(MediaType.TEXT_PLAIN) - public Response loadComponentPqc() throws Exception { - /* This is an autogenerated test */ - if (context.getComponent(COMPONENT_PQC) != null) { - return Response.ok().build(); - } - LOG.warnf("Could not load [%s] from the Camel context", COMPONENT_PQC); - return Response.status(500, COMPONENT_PQC + " could not be loaded from the Camel context").build(); - } -} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 7f277bbdef..b9d959a23a 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -206,6 +206,7 @@ <module>platform-http</module> <module>platform-http-proxy</module> <module>platform-http-proxy-ssl</module> + <module>pqc</module> <module>protobuf</module> <module>pubnub</module> <module>qdrant</module> diff --git a/integration-tests-jvm/pqc/pom.xml b/integration-tests/pqc/pom.xml similarity index 72% rename from integration-tests-jvm/pqc/pom.xml rename to integration-tests/pqc/pom.xml index eb554db9df..aa6451218f 100644 --- a/integration-tests-jvm/pqc/pom.xml +++ b/integration-tests/pqc/pom.xml @@ -39,6 +39,10 @@ <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy</artifactId> </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-resteasy-jackson</artifactId> + </dependency> <!-- test dependencies --> <dependency> @@ -51,6 +55,11 @@ <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <scope>test</scope> + </dependency> </dependencies> <profiles> @@ -78,5 +87,32 @@ </dependency> </dependencies> </profile> + <profile> + <id>native</id> + <activation> + <property> + <name>native</name> + </property> + </activation> + <properties> + <quarkus.native.enabled>true</quarkus.native.enabled> + </properties> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-failsafe-plugin</artifactId> + <executions> + <execution> + <goals> + <goal>integration-test</goal> + <goal>verify</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> </profiles> </project> diff --git a/integration-tests/pqc/src/main/java/org/apache/camel/quarkus/component/pqc/it/PqcProducers.java b/integration-tests/pqc/src/main/java/org/apache/camel/quarkus/component/pqc/it/PqcProducers.java new file mode 100644 index 0000000000..076b329a68 --- /dev/null +++ b/integration-tests/pqc/src/main/java/org/apache/camel/quarkus/component/pqc/it/PqcProducers.java @@ -0,0 +1,89 @@ +/* + * 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.quarkus.component.pqc.it; + +import java.security.*; +import java.security.spec.AlgorithmParameterSpec; + +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import org.bouncycastle.pqc.crypto.lms.LMOtsParameters; +import org.bouncycastle.pqc.crypto.lms.LMSigParameters; +import org.bouncycastle.pqc.jcajce.spec.*; + +@Singleton +public class PqcProducers { + + //not-static to avoid buildtime initialization + private final SecureRandom secureRandom = new SecureRandom(); + + @Produces + @Singleton + @Named("dilithiumKeyPair") + KeyPair dilithiumKeyPair() throws Exception { + return generateKeyPair("Dilithium", DilithiumParameterSpec.dilithium2); + } + + @Produces + @Singleton + @Named("falconKeyPair") + public KeyPair falconKeyPair() throws Exception { + return generateKeyPair("Falcon", FalconParameterSpec.falcon_512); + } + + @Produces + @Singleton + @Named("sphincsKeyPair") + public KeyPair sphincsKeyPair() throws Exception { + return generateKeyPair("SPHINCSPlus", SPHINCSPlusParameterSpec.sha2_128f); + } + + @Produces + @Singleton + @Named("lmsKeyPair") + public KeyPair lmsKeyPair() throws Exception { + return generateKeyPair("LMS", new LMSParameterSpec(LMSigParameters.lms_sha256_n32_h5, LMOtsParameters.sha256_n32_w4)); + } + + @Produces + @Singleton + @Named("xmssKeyPair") + public KeyPair xmssKeyPair() throws Exception { + return generateKeyPair("XMSS", XMSSParameterSpec.SHA2_10_256); + } + + @Produces + @Singleton + @Named("kyberKeyPair") + public KeyPair kyberKeyPair() throws Exception { + return generateKeyPair("Kyber", KyberParameterSpec.kyber512); + } + + @Produces + @Singleton + @Named("kyberWrongKeyPair") //second keypair for negative scenario + public KeyPair kyberWrongKeyPair() throws Exception { + return generateKeyPair("Kyber", KyberParameterSpec.kyber512); + } + + private KeyPair generateKeyPair(String algorithm, AlgorithmParameterSpec spec) throws Exception { + KeyPairGenerator gen = KeyPairGenerator.getInstance(algorithm, "BCPQC"); + gen.initialize(spec, secureRandom); + return gen.generateKeyPair(); + } +} diff --git a/integration-tests/pqc/src/main/java/org/apache/camel/quarkus/component/pqc/it/PqcResource.java b/integration-tests/pqc/src/main/java/org/apache/camel/quarkus/component/pqc/it/PqcResource.java new file mode 100644 index 0000000000..5fdc95c8c0 --- /dev/null +++ b/integration-tests/pqc/src/main/java/org/apache/camel/quarkus/component/pqc/it/PqcResource.java @@ -0,0 +1,187 @@ +/* + * 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.quarkus.component.pqc.it; + +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.Security; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.spec.SecretKeySpec; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.MediaType; +import org.apache.camel.Exchange; +import org.apache.camel.ProducerTemplate; +import org.bouncycastle.jcajce.SecretKeyWithEncapsulation; +import org.jboss.logging.Logger; + +@Path("/pqc") +@ApplicationScoped +public class PqcResource { + + private static final Logger LOG = Logger.getLogger(PqcResource.class);; + + @Inject + @Named("kyberKeyPair") + KeyPair kyberKeyPair; + + @Inject + ProducerTemplate producerTemplate; + + @Path("/sign/{algorithm}") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @jakarta.ws.rs.Produces(MediaType.TEXT_PLAIN) + public String sign(String message, @PathParam("algorithm") String algorithm) { + Exchange exchange = producerTemplate.request( + "pqc:sign?operation=sign&signatureAlgorithm=%s&keyPair=%s".formatted(algorithm, toKeyPair(algorithm)), + ex -> ex.getIn().setBody(message.getBytes(StandardCharsets.UTF_8))); + + // The sign operation outputs signature in the HEADER, not the body + byte[] signature = exchange.getMessage().getHeader("CamelPQCSignature", byte[].class); + return Base64.getEncoder().encodeToString(signature); + } + + @Path("/verify/{algorithm}/{message}") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @jakarta.ws.rs.Produces(MediaType.TEXT_PLAIN) + public boolean verify(String signature, + @PathParam("algorithm") String algorithm, + @PathParam("message") String message) { + byte[] signatureBytes = Base64.getDecoder().decode(signature); + + Map<String, Object> headers = new HashMap<>(); + headers.put("CamelPQCSignature", signatureBytes); + + Exchange exchange = producerTemplate.request( + "pqc:verify?operation=verify&signatureAlgorithm=%s&keyPair=%s".formatted(algorithm, toKeyPair(algorithm)), + ex -> { + ex.getIn().setBody(message.getBytes(StandardCharsets.UTF_8)); + ex.getIn().setHeaders(headers); + }); + + Object verification = exchange.getMessage().getHeader("CamelPQCVerification"); + return Boolean.TRUE.equals(verification); + } + + @Path("/kem/encapsulate/{algorithm}/{keyAlgorithm}/{length}") + @POST + @jakarta.ws.rs.Produces(MediaType.APPLICATION_JSON) + public Map<String, String> encapsulate(@PathParam("algorithm") String algorithm, + @PathParam("keyAlgorithm") String keyAlgorithm, + @PathParam("length") int length) { + SecretKeyWithEncapsulation secretKeyWithEncapsulation = producerTemplate.requestBody( + "pqc:encapsulate?operation=generateSecretKeyEncapsulation&keyEncapsulationAlgorithm=%s&symmetricKeyAlgorithm=%s&symmetricKeyLength=%s&keyPair=%s" + .formatted(algorithm, keyAlgorithm, length, toKeyPair(algorithm)), + null, + SecretKeyWithEncapsulation.class); + String enc = Base64.getEncoder().encodeToString(secretKeyWithEncapsulation.getEncapsulation()); + String secret = Base64.getEncoder().encodeToString(secretKeyWithEncapsulation.getEncoded()); + return Map.of("enc", enc, "secret", secret); + } + + @Path("/kem/extract/{algorithm}/{keyAlgorithm}/{length}") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @jakarta.ws.rs.Produces(MediaType.TEXT_PLAIN) + public String extract(String enc, + @PathParam("algorithm") String algorithm, + @PathParam("keyAlgorithm") String keyAlgorithm, + @PathParam("length") int length) { + + byte[] encapsulation = Base64.getDecoder().decode(enc); + //create SecretKeyWithEncapsulation from the private key + SecretKeyWithEncapsulation privateKeyWithEncapsulation = new SecretKeyWithEncapsulation( + new SecretKeySpec(kyberKeyPair.getPrivate().getEncoded(), algorithm), encapsulation); + + SecretKeyWithEncapsulation result = producerTemplate.requestBody( + "pqc:extract?operation=extractSecretKeyEncapsulation&keyEncapsulationAlgorithm=%s&symmetricKeyAlgorithm=%s&symmetricKeyLength=%s&keyPair=%s" + .formatted(algorithm, keyAlgorithm, length, toKeyPair(algorithm)), + privateKeyWithEncapsulation, + SecretKeyWithEncapsulation.class); + return Base64.getEncoder().encodeToString(result.getEncoded()); + } + + private String toKeyPair(String algorithm) { + return "SPHINCSPLUS".equals(algorithm) ? "#sphincsKeyPair" : "#" + algorithm.toLowerCase() + "KeyPair"; + } + + // Body tests: binary data + @Path("/signBinaryData") + @POST + @jakarta.ws.rs.Produces(MediaType.TEXT_PLAIN) + public String signWithBinaryData(String message) { + byte[] binaryData = message.getBytes(StandardCharsets.UTF_8); + + Exchange exchange = producerTemplate.request( + "pqc:sign?operation=sign&signatureAlgorithm=DILITHIUM&keyPair=#dilithiumKeyPair", + ex -> ex.getIn().setBody(binaryData)); + + byte[] signature = exchange.getMessage().getHeader("CamelPQCSignature", byte[].class); + // Store binary data for verification + exchange.getIn().setHeader("CamelTestBinaryData", binaryData); + return Base64.getEncoder().encodeToString(signature); + } + + @Path("/verifyBinaryData/{message}") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @jakarta.ws.rs.Produces(MediaType.TEXT_PLAIN) + public boolean verifyWithBinaryData(String signature, + @PathParam("message") String message) { + byte[] signatureBytes = Base64.getDecoder().decode(signature); + byte[] binaryData = message.getBytes(StandardCharsets.UTF_8); + + Map<String, Object> headers = new HashMap<>(); + headers.put("CamelPQCSignature", signatureBytes); + + Exchange exchange = producerTemplate.request( + "pqc:verify?operation=verify&signatureAlgorithm=DILITHIUM&keyPair=#dilithiumKeyPair", + ex -> { + ex.getIn().setBody(binaryData); + ex.getIn().setHeaders(headers); + }); + + Object verification = exchange.getMessage().getHeader("CamelPQCVerification"); + return Boolean.TRUE.equals(verification); + } + + // Native mode test: check provider + @Path("/provider/check") + @POST + @jakarta.ws.rs.Produces(MediaType.TEXT_PLAIN) + public String checkProvider() { + java.security.Provider[] providers = Security.getProviders(); + for (java.security.Provider provider : providers) { + if ("BCPQC".equals(provider.getName())) { + return "available"; + } + } + return "unavailable"; + } + +} diff --git a/integration-tests-jvm/pqc/src/test/java/org/apache/camel/quarkus/component/pqc/it/PqcTest.java b/integration-tests/pqc/src/test/java/org/apache/camel/quarkus/component/pqc/it/PqcIT.java similarity index 71% rename from integration-tests-jvm/pqc/src/test/java/org/apache/camel/quarkus/component/pqc/it/PqcTest.java rename to integration-tests/pqc/src/test/java/org/apache/camel/quarkus/component/pqc/it/PqcIT.java index ff955411d3..e156d72964 100644 --- a/integration-tests-jvm/pqc/src/test/java/org/apache/camel/quarkus/component/pqc/it/PqcTest.java +++ b/integration-tests/pqc/src/test/java/org/apache/camel/quarkus/component/pqc/it/PqcIT.java @@ -16,19 +16,8 @@ */ package org.apache.camel.quarkus.component.pqc.it; -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import org.junit.jupiter.api.Test; - -@QuarkusTest -class PqcTest { - - @Test - public void loadComponentPqc() { - /* A simple autogenerated test */ - RestAssured.get("/pqc/load/component/pqc") - .then() - .statusCode(200); - } +import io.quarkus.test.junit.QuarkusIntegrationTest; +@QuarkusIntegrationTest +class PqcIT extends PqcTest { } diff --git a/integration-tests/pqc/src/test/java/org/apache/camel/quarkus/component/pqc/it/PqcTest.java b/integration-tests/pqc/src/test/java/org/apache/camel/quarkus/component/pqc/it/PqcTest.java new file mode 100644 index 0000000000..3acece8d4d --- /dev/null +++ b/integration-tests/pqc/src/test/java/org/apache/camel/quarkus/component/pqc/it/PqcTest.java @@ -0,0 +1,159 @@ +/* + * 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.quarkus.component.pqc.it; + +import java.util.Map; +import java.util.stream.Stream; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.apache.camel.component.pqc.PQCKeyEncapsulationAlgorithms; +import org.apache.camel.component.pqc.PQCSignatureAlgorithms; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@QuarkusTest +class PqcTest { + + @ParameterizedTest + @EnumSource(value = PQCSignatureAlgorithms.class, names = { "FALCON", "DILITHIUM", "SPHINCSPLUS", "XMSS", "LMS" }) + public void testSignAndVerify(PQCSignatureAlgorithms signatureAlgorithm) { + // Sign operation using Falcon algorithm + String signature = RestAssured + .given() + .contentType(ContentType.TEXT) + .body("Hello") + .post("/pqc/sign/" + signatureAlgorithm.getAlgorithm()) + .then() + .statusCode(200) + .extract() + .asString(); + + assertNotNull(signature); + assertFalse(signature.isEmpty()); + + // Verify operation using Falcon algorithm + RestAssured.given() + .contentType("text/plain") + .body(signature) + .post("/pqc/verify/" + signatureAlgorithm.getAlgorithm() + "/Hello") + .then() + .statusCode(200) + .body(equalTo("true")); + } + + @Test + public void testNegativeSignAndVerify() { + // Sign operation using Falcon algorithm + String signature = RestAssured + .given() + .contentType(ContentType.TEXT) + .body("Hello") + .post("/pqc/sign/" + PQCSignatureAlgorithms.DILITHIUM.getAlgorithm()) + .then() + .statusCode(200) + .extract() + .asString(); + + assertThat(signature).isNotBlank(); + + // Verify operation using Falcon algorithm + RestAssured.given() + .contentType("text/plain") + .body(signature) + .post("/pqc/verify/" + PQCSignatureAlgorithms.DILITHIUM.getAlgorithm() + "/Wrong_text") + .then() + .statusCode(200) + .body(equalTo("false")); + } + + static Stream<Arguments> encapsulationAlgorithms() { + return Stream.of( + Arguments.of(PQCKeyEncapsulationAlgorithms.KYBER, "AES", 128), + Arguments.of(PQCKeyEncapsulationAlgorithms.KYBER, "CHACHA7539", 256)); + } + + @ParameterizedTest + @MethodSource("encapsulationAlgorithms") + public void testEncapsulationAndExtract(PQCKeyEncapsulationAlgorithms encapsulationAlgorithm, String keyAlgorithm, + int length) { + + // Generate encapsulation using Camel PQC component + Map<?, ?> map = RestAssured.given() + .contentType(ContentType.JSON) + .post("/pqc/kem/encapsulate/" + encapsulationAlgorithm.getAlgorithm() + "/" + keyAlgorithm + "/" + length) + .then() + .statusCode(200) + .body(notNullValue()) + .body("secret", notNullValue()) + .body("enc", notNullValue()) + .extract() + .as(Map.class); + + // Extract secret key from encapsulation using Camel PQC component + RestAssured.given() + .contentType("text/plain") + .body(map.get("enc")) + .post("/pqc/kem/extract/" + encapsulationAlgorithm.getAlgorithm() + "/" + keyAlgorithm + "/" + length) + .then() + .statusCode(200) + .body(equalTo(map.get("secret"))); + } + + @Test + public void testSignVerifyWithBinaryData() { + // Sign and verify with binary data + String signature = RestAssured.given() + .body("hello") + .post("/pqc/signBinaryData") + .then() + .statusCode(200) + .extract() + .asString(); + + assertNotNull(signature); + + // Verify with same binary data + RestAssured.given() + .contentType("text/plain") + .body(signature) + .post("/pqc/verifyBinaryData/hello") + .then() + .statusCode(200) + .body(equalTo("true")); + } + + @Test + public void testBouncyCastleProviderAvailable() { + // Verify BouncyCastlePQCProvider is registered in Security providers + RestAssured.post("/pqc/provider/check") + .then() + .statusCode(200) + .body(equalTo("available")); + } + +} diff --git a/tooling/scripts/test-categories.yaml b/tooling/scripts/test-categories.yaml index 46575624b7..790327c7a6 100644 --- a/tooling/scripts/test-categories.yaml +++ b/tooling/scripts/test-categories.yaml @@ -153,6 +153,7 @@ group-08: - master-file - pdf - pinecone + - pqc - saxon - syndication - telegram
