This is an automated email from the ASF dual-hosted git repository.
oscerd pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.18.x by this push:
new abd25389d04a [backport camel-4.18.x] CAMEL-23843: Camel-PQC - sign and
verify String payloads as UTF-8 (#24723)
abd25389d04a is described below
commit abd25389d04a0c26f70e534db6803ccaea3dfd96
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Jul 16 11:49:01 2026 +0200
[backport camel-4.18.x] CAMEL-23843: Camel-PQC - sign and verify String
payloads as UTF-8 (#24723)
[backport camel-4.18.x] CAMEL-23843: Camel-PQC sign and verify String
payloads as UTF-8
sign/verify encoded a String message body with String.getBytes(), which
uses the JVM
default charset. The default charset is platform dependent, so signing on
one JVM and
verifying on another with a different default could disagree on the bytes
of a
non-ASCII payload and fail verification. Pin StandardCharsets.UTF_8.
Charset-only backport of #24716. The native byte[]/InputStream handling from
CAMEL-23847 is an improvement that stays on main only, so it is
intentionally not
part of this backport.
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
.../apache/camel/component/pqc/PQCProducer.java | 5 +-
.../component/pqc/PQCSignatureCharsetTest.java | 122 +++++++++++++++++++++
2 files changed, 125 insertions(+), 2 deletions(-)
diff --git
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java
index a008b38966dd..df67f451af21 100644
---
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java
+++
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.component.pqc;
+import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.Certificate;
@@ -129,7 +130,7 @@ public class PQCProducer extends DefaultProducer {
String payload = exchange.getMessage().getMandatoryBody(String.class);
signer.initSign(keyPair.getPrivate());
- signer.update(payload.getBytes());
+ signer.update(payload.getBytes(StandardCharsets.UTF_8));
byte[] signature = signer.sign();
exchange.getMessage().setHeader(PQCConstants.SIGNATURE, signature);
@@ -140,7 +141,7 @@ public class PQCProducer extends DefaultProducer {
String payload = exchange.getMessage().getMandatoryBody(String.class);
signer.initVerify(keyPair.getPublic());
- signer.update(payload.getBytes());
+ signer.update(payload.getBytes(StandardCharsets.UTF_8));
if
(signer.verify(exchange.getMessage().getHeader(PQCConstants.SIGNATURE,
byte[].class))) {
exchange.getMessage().setHeader(PQCConstants.VERIFY, true);
} else {
diff --git
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCSignatureCharsetTest.java
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCSignatureCharsetTest.java
new file mode 100644
index 000000000000..8b62de609185
--- /dev/null
+++
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCSignatureCharsetTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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 java.nio.charset.StandardCharsets;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.Security;
+import java.security.Signature;
+
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.EndpointInject;
+import org.apache.camel.Produce;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.bouncycastle.jcajce.spec.MLDSAParameterSpec;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A String body must be signed as UTF-8, not with the JVM default charset, so
that a signature produced on one JVM
+ * verifies on another with a different default. This pins the UTF-8 contract
by verifying the produced signature
+ * independently over the UTF-8 bytes of the payload (and confirming it does
not match the ISO-8859-1 bytes).
+ */
+public class PQCSignatureCharsetTest extends CamelTestSupport {
+
+ private static final String NON_ASCII = "héllo wörld — ünïcode";
+
+ private KeyPair keyPair;
+
+ @EndpointInject("mock:sign")
+ protected MockEndpoint resultSign;
+
+ @Produce("direct:sign")
+ protected ProducerTemplate templateSign;
+
+ @BeforeAll
+ public static void startup() {
+ if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+ if (Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME) ==
null) {
+ Security.addProvider(new BouncyCastlePQCProvider());
+ }
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("direct:sign").to("pqc:sign?operation=sign").to("mock:sign");
+ }
+ };
+ }
+
+ @Test
+ void testStringBodyIsSignedAsUtf8() throws Exception {
+ resultSign.expectedMessageCount(1);
+ templateSign.sendBody(NON_ASCII);
+ resultSign.assertIsSatisfied();
+
+ byte[] signature =
resultSign.getExchanges().get(0).getMessage().getHeader(PQCConstants.SIGNATURE,
byte[].class);
+ assertNotNull(signature);
+
+ // The signature must validate against the UTF-8 bytes of the payload
+ assertTrue(verifyOver(NON_ASCII.getBytes(StandardCharsets.UTF_8),
signature),
+ "a String body must be signed as UTF-8");
+ // ... and must not validate against a different encoding of the same
non-ASCII text
+
assertFalse(verifyOver(NON_ASCII.getBytes(StandardCharsets.ISO_8859_1),
signature),
+ "a String body must not be signed with the platform charset
(ISO-8859-1 here)");
+ }
+
+ private boolean verifyOver(byte[] data, byte[] signature) throws Exception
{
+ Signature verifier =
Signature.getInstance(PQCSignatureAlgorithms.MLDSA.getAlgorithm(),
+ PQCSignatureAlgorithms.MLDSA.getBcProvider());
+ verifier.initVerify(keyPair.getPublic());
+ verifier.update(data);
+ return verifier.verify(signature);
+ }
+
+ @BindToRegistry("Keypair")
+ public KeyPair setKeyPair()
+ throws NoSuchAlgorithmException, NoSuchProviderException,
InvalidAlgorithmParameterException {
+ KeyPairGenerator kpGen =
KeyPairGenerator.getInstance(PQCSignatureAlgorithms.MLDSA.getAlgorithm(),
+ PQCSignatureAlgorithms.MLDSA.getBcProvider());
+ kpGen.initialize(MLDSAParameterSpec.ml_dsa_65);
+ keyPair = kpGen.generateKeyPair();
+ return keyPair;
+ }
+
+ @BindToRegistry("Signer")
+ public Signature getSigner() throws NoSuchAlgorithmException,
NoSuchProviderException {
+ return
Signature.getInstance(PQCSignatureAlgorithms.MLDSA.getAlgorithm(),
+ PQCSignatureAlgorithms.MLDSA.getBcProvider());
+ }
+}