This is an automated email from the ASF dual-hosted git repository.
davsclaus 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 e46f19fcb860 [backport camel-4.18.x] CAMEL-25992: camel-pqc - sign and
verify message bodies over their raw bytes (#28329)
e46f19fcb860 is described below
commit e46f19fcb8605206ed3493e3e47b80f5e2ff1c17
Author: Guillaume Nodet <[email protected]>
AuthorDate: Fri Oct 23 13:47:21 2026 +0200
[backport camel-4.18.x] CAMEL-25992: camel-pqc - sign and verify message
bodies over their raw bytes (#28329)
* [backport camel-4.18.x] CAMEL-25992: camel-pqc - sign and verify message
bodies over their raw bytes
* CAMEL-25992: camel-pqc - adapt the backported tests and add the 4.18
upgrade note
The cherry-picked PQCBinaryBodySignatureTest and PQCFileBodySignatureTest
used
the hybrid signature API (hybridSign/hybridVerify, HybridSignature,
PQCClassicalSignatureAlgorithms) and camel-test-junit6, neither of which
exists
on camel-4.18.x: this line has only the sign and verify operations and uses
camel-test-junit5. The tests now cover the single-algorithm sign and verify
operations over byte[], ByteBuffer, ByteArrayOutputStream, File, Path,
GenericFile and String bodies, and the rejection of a Map or POJO body, and
import camel-test-junit5.
Also add the camel-pqc entry to the 4.18.6 upgrade guide (the cherry-pick's
entry targeted the 4.24 guide, which does not exist on this branch).
Signed-off-by: Claus Ibsen <[email protected]>
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* CAMEL-25992: camel-pqc - fix source formatting in PQCProducer
The cherry-pick to camel-4.18.x left two consecutive blank lines before
the updateSignatureFromBody javadoc. The source formatter collapses that
to a single blank line, which failed the build's "fail if there are
uncommitted changes" check. Remove the extra blank line.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
.../camel-pqc/src/main/docs/pqc-component.adoc | 2 +
.../apache/camel/component/pqc/PQCProducer.java | 121 +++++++++++-
.../component/pqc/PQCBinaryBodySignatureTest.java | 183 ++++++++++++++++++
.../component/pqc/PQCFileBodySignatureTest.java | 206 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_18.adoc | 14 ++
5 files changed, 516 insertions(+), 10 deletions(-)
diff --git a/components/camel-pqc/src/main/docs/pqc-component.adoc
b/components/camel-pqc/src/main/docs/pqc-component.adoc
index db4056014ec6..c78492a469bb 100644
--- a/components/camel-pqc/src/main/docs/pqc-component.adoc
+++ b/components/camel-pqc/src/main/docs/pqc-component.adoc
@@ -99,6 +99,8 @@ In case the KeyPair and the Signature Objects are not in the
registry, it will p
This will be true for standardized algorithms and for experimental ones.
+NOTE: The sign and verify operations stream the message body. A `byte[]` or
`InputStream` body is fed to the signature in chunks without being materialised
as a String, so large payloads (files, large messages) can be signed and
verified without loading them fully into memory. A `java.io.File` or
`java.nio.file.Path` body, and a file from a file consumer (a `GenericFile`
body), is read the same way from the file, so the signature covers the bytes of
the file. When the body is a re-readab [...]
+
== Examples
- ML-DSA
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 f5b1098b1e2b..c87e88d94c75 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,7 +16,12 @@
*/
package org.apache.camel.component.pqc;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.security.*;
import java.security.cert.Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
@@ -29,6 +34,10 @@ import javax.crypto.spec.SecretKeySpec;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.Message;
+import org.apache.camel.StreamCache;
+import org.apache.camel.TypeConverter;
+import org.apache.camel.WrappedFile;
import org.apache.camel.support.DefaultProducer;
import org.apache.camel.util.ObjectHelper;
import org.bouncycastle.jcajce.SecretKeyWithEncapsulation;
@@ -46,6 +55,8 @@ public class PQCProducer extends DefaultProducer {
private static final Logger LOG =
LoggerFactory.getLogger(PQCProducer.class);
+ private static final int STREAM_BUFFER_SIZE = 8192;
+
private Signature signer;
// Set only when this producer created the Signature itself, so it knows
how to create another one.
// Left null when the user configured an instance, which then has to be
shared and locked instead.
@@ -155,15 +166,12 @@ public class PQCProducer extends DefaultProducer {
}
private void signature(Exchange exchange)
- throws InvalidPayloadException, InvalidKeyException,
SignatureException, NoSuchAlgorithmException,
- NoSuchProviderException {
- String payload = exchange.getMessage().getMandatoryBody(String.class);
-
+ throws Exception {
Signature signerForExchange = signerForExchange();
byte[] signature;
synchronized (signerForExchange) {
signerForExchange.initSign(keyPair.getPrivate());
- signerForExchange.update(payload.getBytes(StandardCharsets.UTF_8));
+ updateSignatureFromBody(signerForExchange, exchange.getMessage());
signature = signerForExchange.sign();
}
exchange.getMessage().setHeader(PQCConstants.SIGNATURE, signature);
@@ -182,20 +190,113 @@ public class PQCProducer extends DefaultProducer {
}
private void verification(Exchange exchange)
- throws InvalidPayloadException, InvalidKeyException,
SignatureException, NoSuchAlgorithmException,
- NoSuchProviderException {
- String payload = exchange.getMessage().getMandatoryBody(String.class);
-
+ throws InvalidPayloadException, InvalidKeyException,
SignatureException, IOException,
+ NoSuchAlgorithmException, NoSuchProviderException {
Signature signerForExchange = signerForExchange();
boolean verified;
synchronized (signerForExchange) {
signerForExchange.initVerify(keyPair.getPublic());
- signerForExchange.update(payload.getBytes(StandardCharsets.UTF_8));
+ updateSignatureFromBody(signerForExchange, exchange.getMessage());
verified =
signerForExchange.verify(exchange.getMessage().getHeader(PQCConstants.SIGNATURE,
byte[].class));
}
exchange.getMessage().setHeader(PQCConstants.VERIFY, verified);
}
+ /**
+ * Feeds the message body into the given {@link Signature} without
materialising the whole payload as a String. A
+ * {@code byte[]} body is used directly, an {@link InputStream} body is
read in chunks (and reset afterwards when it
+ * is a re-readable {@link StreamCache} so downstream processors still see
the payload), and a file body is read in
+ * chunks from the file (see {@link #fileContent(Object)}). A {@code
String} or {@link CharSequence} body is encoded
+ * as UTF-8, and any other body is read through {@link #rawBytes(Message,
Object)}, which either gets its bytes or
+ * rejects it.
+ */
+ private static void updateSignatureFromBody(Signature signature, Message
message)
+ throws InvalidPayloadException, SignatureException, IOException {
+ Object body = fileContent(message.getBody());
+ if (body instanceof byte[] bytes) {
+ signature.update(bytes);
+ } else if (body instanceof Path path) {
+ try (InputStream stream = Files.newInputStream(path)) {
+ updateSignatureFromStream(signature, stream);
+ }
+ } else if (body instanceof InputStream stream) {
+ try {
+ updateSignatureFromStream(signature, stream);
+ } finally {
+ if (body instanceof StreamCache streamCache) {
+ streamCache.reset();
+ }
+ }
+ } else if (body instanceof CharSequence text) {
+ signature.update(textToBytes(text));
+ } else {
+ signature.update(rawBytes(message, body));
+ }
+ }
+
+ /**
+ * Encodes a text body as UTF-8. The JVM default charset is platform
dependent, so signing and verifying on JVMs
+ * with a different default would disagree on the bytes for a non-ASCII
payload; UTF-8 keeps them in step.
+ */
+ private static byte[] textToBytes(CharSequence text) {
+ return text.toString().getBytes(StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Returns the bytes of a body that is neither a {@code byte[]}, an {@link
InputStream}, a file nor a
+ * {@link CharSequence}, by asking the type converter registry for a
registered converter to {@code byte[]} - which
+ * covers bodies with a binary form such as a {@link java.nio.ByteBuffer},
a {@link java.io.ByteArrayOutputStream},
+ * a Netty {@code ByteBuf} or a Vert.x {@code Buffer}, so they are signed
over their bytes rather than over a text
+ * rendering. A body with no such converter (for example a {@code Map} or
a POJO) has no well-defined bytes to sign,
+ * so it is rejected with {@link InvalidPayloadException} rather than
signed over an incidental text form. Fallback
+ * converters are not consulted, so the result does not depend on which
components are on the classpath.
+ */
+ private static byte[] rawBytes(Message message, Object body) throws
InvalidPayloadException {
+ Exchange exchange = message.getExchange();
+ if (body != null) {
+ // lookup finds only a registered converter, so a body with a
binary form (ByteBuffer, ByteArrayOutputStream,
+ // Netty ByteBuf, Vert.x Buffer, ...) is converted, while a Map or
POJO - which would only convert through a
+ // fallback converter, if any component provides one - is left for
the InvalidPayloadException below, so the
+ // outcome does not depend on the classpath. tryConvertTo returns
null instead of throwing on a miss.
+ TypeConverter converter =
exchange.getContext().getTypeConverterRegistry().lookup(byte[].class,
body.getClass());
+ if (converter != null) {
+ byte[] bytes = converter.tryConvertTo(byte[].class, exchange,
body);
+ if (bytes != null) {
+ return bytes;
+ }
+ }
+ }
+ throw new InvalidPayloadException(exchange, byte[].class, message);
+ }
+
+ private static void updateSignatureFromStream(Signature signature,
InputStream stream)
+ throws SignatureException, IOException {
+ byte[] buffer = new byte[STREAM_BUFFER_SIZE];
+ int read;
+ while ((read = stream.read(buffer)) != -1) {
+ signature.update(buffer, 0, read);
+ }
+ }
+
+ /**
+ * Resolves a file body to the content to sign: a {@link WrappedFile} (the
body of the file based consumers) gives
+ * its content, and a {@link File} is turned into its {@link Path}, so
that the bytes of the file are read as they
+ * are. The content of a {@link WrappedFile} is a {@link File} for the
local file system, otherwise the content the
+ * consumer has already loaded, such as a {@code byte[]} or an {@link
InputStream}. Other bodies are not changed.
+ */
+ private static Object fileContent(Object body) {
+ if (body instanceof WrappedFile<?> wrappedFile) {
+ Object content = wrappedFile.getBody();
+ if (content != null) {
+ body = content;
+ }
+ }
+ if (body instanceof File file) {
+ return file.toPath();
+ }
+ return body;
+ }
+
/**
* Returns the KEM {@link KeyGenerator} to use for a single exchange.
* <p>
diff --git
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCBinaryBodySignatureTest.java
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCBinaryBodySignatureTest.java
new file mode 100644
index 000000000000..3b416da298b4
--- /dev/null
+++
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCBinaryBodySignatureTest.java
@@ -0,0 +1,183 @@
+/*
+ * 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.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.Security;
+import java.security.Signature;
+import java.util.Map;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.Registry;
+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.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Bodies that are not a {@code byte[]}, an {@code InputStream} or a file are
signed and verified over their bytes when
+ * they have a binary type ({@link ByteBuffer}, {@link ByteArrayOutputStream},
and, through their own type converters,
+ * Netty {@code ByteBuf} and Vert.x {@code Buffer}), a {@code String} is
signed as UTF-8, and a body with no binary
+ * representation (a {@link Map} or a POJO) is rejected rather than signed
over a text rendering.
+ */
+public class PQCBinaryBodySignatureTest extends CamelTestSupport {
+
+ private static final String PQC_KEY =
"&signatureAlgorithm=MLDSA&keyPair=#pqcKeyPair";
+ private static final String SIGN = "pqc:sign?operation=sign" + PQC_KEY;
+ private static final String VERIFY = "pqc:verify?operation=verify" +
PQC_KEY;
+
+ private static final byte[] CONTENT = binaryContent();
+
+ private static KeyPair pqcKeyPair;
+
+ @BeforeAll
+ public static void startup() throws Exception {
+ if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+ if (Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME) ==
null) {
+ Security.addProvider(new BouncyCastlePQCProvider());
+ }
+
+ KeyPairGenerator pqc =
KeyPairGenerator.getInstance(PQCSignatureAlgorithms.MLDSA.getAlgorithm(),
+ PQCSignatureAlgorithms.MLDSA.getBcProvider());
+ pqc.initialize(MLDSAParameterSpec.ml_dsa_65);
+ pqcKeyPair = pqc.generateKeyPair();
+ }
+
+ @Override
+ protected void bindToRegistry(Registry registry) {
+ registry.bind("pqcKeyPair", pqcKeyPair);
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:sign").to(SIGN);
+ from("direct:verify").to(VERIFY);
+ }
+ };
+ }
+
+ @Test
+ void byteBufferBodyIsSignedOverItsBytes() throws Exception {
+ assertTrue(verifiesContent(sign(ByteBuffer.wrap(CONTENT.clone()))),
+ "the signature must cover the bytes of the ByteBuffer");
+ }
+
+ @Test
+ void byteBufferBodyIsVerifiedOverItsBytes() throws Exception {
+ assertTrue(verify(ByteBuffer.wrap(CONTENT.clone()), signContent()),
+ "a signature over the bytes must verify a ByteBuffer body");
+ }
+
+ @Test
+ void byteArrayOutputStreamBodyIsSignedOverItsBytes() throws Exception {
+ ByteArrayOutputStream body = new ByteArrayOutputStream();
+ body.write(CONTENT);
+
+ assertTrue(verifiesContent(sign(body)), "the signature must cover the
bytes of the ByteArrayOutputStream");
+ }
+
+ @Test
+ void stringBodyIsSignedAsUtf8() throws Exception {
+ String body = "post-quantum café éè";
+
+ Signature verifier = pqcSignature();
+ verifier.initVerify(pqcKeyPair.getPublic());
+ verifier.update(body.getBytes(StandardCharsets.UTF_8));
+ assertTrue(verifier.verify(sign(body)), "a String body must be signed
as UTF-8");
+ }
+
+ @Test
+ void mapBodyIsRejected() {
+ // a Map has no binary representation, so there is no well-defined set
of bytes to sign
+ assertThrows(InvalidPayloadException.class, () ->
sign(Map.of("amount", "10", "currency", "EUR")));
+ }
+
+ @Test
+ void pojoBodyIsRejected() {
+ assertThrows(InvalidPayloadException.class, () -> sign(new
Document("secret.pdf", 10)));
+ }
+
+ private static byte[] binaryContent() {
+ // every byte value, so the content is binary data rather than text
+ byte[] content = new byte[1024];
+ for (int i = 0; i < content.length; i++) {
+ content[i] = (byte) i;
+ }
+ return content;
+ }
+
+ private byte[] sign(Object body) throws Exception {
+ Exchange out = request("direct:sign", body, null, null);
+ return out.getMessage().getHeader(PQCConstants.SIGNATURE,
byte[].class);
+ }
+
+ private boolean verify(Object body, byte[] signature) throws Exception {
+ Exchange out = request("direct:verify", body, PQCConstants.SIGNATURE,
signature);
+ return out.getMessage().getHeader(PQCConstants.VERIFY, Boolean.class);
+ }
+
+ private Exchange request(String uri, Object body, String header, Object
value) throws Exception {
+ Exchange out = template.request(uri, e -> {
+ e.getMessage().setBody(body);
+ if (header != null) {
+ e.getMessage().setHeader(header, value);
+ }
+ });
+ if (out.getException() != null) {
+ throw out.getException();
+ }
+ return out;
+ }
+
+ private static boolean verifiesContent(byte[] signature) throws Exception {
+ Signature verifier = pqcSignature();
+ verifier.initVerify(pqcKeyPair.getPublic());
+ verifier.update(CONTENT);
+ return verifier.verify(signature);
+ }
+
+ private static byte[] signContent() throws Exception {
+ Signature signer = pqcSignature();
+ signer.initSign(pqcKeyPair.getPrivate());
+ signer.update(CONTENT);
+ return signer.sign();
+ }
+
+ private static Signature pqcSignature() throws Exception {
+ return
Signature.getInstance(PQCSignatureAlgorithms.MLDSA.getAlgorithm(),
+ PQCSignatureAlgorithms.MLDSA.getBcProvider());
+ }
+
+ private record Document(String name, int size) {
+ }
+}
diff --git
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCFileBodySignatureTest.java
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCFileBodySignatureTest.java
new file mode 100644
index 000000000000..866716614162
--- /dev/null
+++
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCFileBodySignatureTest.java
@@ -0,0 +1,206 @@
+/*
+ * 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.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.Security;
+import java.security.Signature;
+
+import org.apache.camel.EndpointInject;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.file.GenericFile;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.spi.Registry;
+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 org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A {@link File}, a {@link Path} and a file from a file consumer ({@link
GenericFile}) are signed and verified over the
+ * bytes of the file.
+ */
+public class PQCFileBodySignatureTest extends CamelTestSupport {
+
+ private static final String PQC_KEY =
"&signatureAlgorithm=MLDSA&keyPair=#pqcKeyPair";
+ private static final String SIGN = "pqc:sign?operation=sign" + PQC_KEY;
+ private static final String VERIFY = "pqc:verify?operation=verify" +
PQC_KEY;
+
+ private static final byte[] CONTENT = binaryContent();
+
+ private static KeyPair pqcKeyPair;
+
+ @TempDir
+ Path folder;
+
+ @EndpointInject("mock:consumed")
+ MockEndpoint consumed;
+
+ @BeforeAll
+ public static void startup() throws Exception {
+ if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+ if (Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME) ==
null) {
+ Security.addProvider(new BouncyCastlePQCProvider());
+ }
+
+ KeyPairGenerator pqc =
KeyPairGenerator.getInstance(PQCSignatureAlgorithms.MLDSA.getAlgorithm(),
+ PQCSignatureAlgorithms.MLDSA.getBcProvider());
+ pqc.initialize(MLDSAParameterSpec.ml_dsa_65);
+ pqcKeyPair = pqc.generateKeyPair();
+ }
+
+ @Override
+ protected void bindToRegistry(Registry registry) {
+ registry.bind("pqcKeyPair", pqcKeyPair);
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:sign").to(SIGN);
+ from("direct:verify").to(VERIFY);
+
+ from("file:" + folder +
"?noop=true&initialDelay=0&delay=10").routeId("fileConsumer").autoStartup(false)
+ .to(SIGN)
+ .to(VERIFY)
+ .to("mock:consumed");
+ }
+ };
+ }
+
+ @Test
+ void fileBodyIsSignedOverItsBytes() throws Exception {
+ File file = write("signed.dat").toFile();
+
+ assertTrue(verifiesContent(sign(file)), "the signature must cover the
bytes of the file");
+ }
+
+ @Test
+ void pathBodyIsSignedOverItsBytes() throws Exception {
+ Path path = write("signed.dat");
+
+ assertTrue(verifiesContent(sign(path)), "the signature must cover the
bytes of the file");
+ }
+
+ @Test
+ void fileBodyIsVerifiedOverItsBytes() throws Exception {
+ File file = write("verified.dat").toFile();
+
+ assertTrue(verify(file, signContent()), "a signature over the bytes of
the file must verify");
+ }
+
+ @Test
+ void pathBodyIsVerifiedOverItsBytes() throws Exception {
+ Path path = write("verified.dat");
+
+ assertTrue(verify(path, signContent()), "a signature over the bytes of
the file must verify");
+ }
+
+ @Test
+ void fileFromFileConsumerIsSignedAndVerifiedOverItsBytes() throws
Exception {
+ write("consumed.dat");
+ consumed.expectedMessageCount(1);
+
+ context.getRouteController().startRoute("fileConsumer");
+
+ consumed.assertIsSatisfied();
+ Exchange exchange = consumed.getReceivedExchanges().get(0);
+ assertTrue(exchange.getMessage().getHeader(PQCConstants.VERIFY,
Boolean.class));
+
assertTrue(verifiesContent(exchange.getMessage().getHeader(PQCConstants.SIGNATURE,
byte[].class)),
+ "the signature must cover the bytes of the consumed file");
+ }
+
+ @Test
+ void genericFileWithLoadedContentIsSignedOverItsBytes() throws Exception {
+ // a file whose content the consumer has already loaded into memory
+ GenericFile<Object> file = new GenericFile<>();
+ file.setFileName("loaded.dat");
+ file.setBody(CONTENT.clone());
+
+ assertTrue(verifiesContent(sign(file)), "the signature must cover the
loaded content of the file");
+ }
+
+ private static byte[] binaryContent() {
+ // every byte value, so the content is binary data rather than text
+ byte[] content = new byte[1024];
+ for (int i = 0; i < content.length; i++) {
+ content[i] = (byte) i;
+ }
+ return content;
+ }
+
+ private Path write(String name) throws IOException {
+ return Files.write(folder.resolve(name), CONTENT);
+ }
+
+ private byte[] sign(Object body) throws Exception {
+ Exchange out = request("direct:sign", body, null, null);
+ return out.getMessage().getHeader(PQCConstants.SIGNATURE,
byte[].class);
+ }
+
+ private boolean verify(Object body, byte[] signature) throws Exception {
+ Exchange out = request("direct:verify", body, PQCConstants.SIGNATURE,
signature);
+ return out.getMessage().getHeader(PQCConstants.VERIFY, Boolean.class);
+ }
+
+ private Exchange request(String uri, Object body, String header, Object
value) throws Exception {
+ Exchange out = template.request(uri, e -> {
+ e.getMessage().setBody(body);
+ if (header != null) {
+ e.getMessage().setHeader(header, value);
+ }
+ });
+ if (out.getException() != null) {
+ throw out.getException();
+ }
+ return out;
+ }
+
+ private static boolean verifiesContent(byte[] signature) throws Exception {
+ Signature verifier = pqcSignature();
+ verifier.initVerify(pqcKeyPair.getPublic());
+ verifier.update(CONTENT);
+ return verifier.verify(signature);
+ }
+
+ private static byte[] signContent() throws Exception {
+ Signature signer = pqcSignature();
+ signer.initSign(pqcKeyPair.getPrivate());
+ signer.update(CONTENT);
+ return signer.sign();
+ }
+
+ private static Signature pqcSignature() throws Exception {
+ return
Signature.getInstance(PQCSignatureAlgorithms.MLDSA.getAlgorithm(),
+ PQCSignatureAlgorithms.MLDSA.getBcProvider());
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
index c9cb359db72a..b6a7f2c348c5 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
@@ -34,6 +34,20 @@ partition stayed quiet it was delivered again after a
restart. Routes that read
Checkpoint failures from the timer and from the batch-size trigger are now
logged at `WARN` level with the full stack trace instead of
at `DEBUG` level with the message only.
+=== camel-pqc - message bodies are signed and verified over their bytes
+
+The `sign` and `verify` operations now sign and verify the message body over
its bytes for every body type that has a
+well-defined binary form. A `byte[]` or `InputStream` body, a `java.io.File`
or `java.nio.file.Path` body, and a file
+from a file consumer (a `GenericFile` body), are read as bytes. A `String` (or
any `CharSequence`) body is encoded as
+UTF-8, and any other body that has a binary type converter (for example a
`java.nio.ByteBuffer`, a
+`java.io.ByteArrayOutputStream`, a Netty `ByteBuf` or a Vert.x `Buffer`) is
signed over those bytes.
+
+A body with no binary representation (for example a `Map` or a POJO) is no
longer signed over its `toString()`: it is
+now rejected with an `InvalidPayloadException`. Convert such a body to a
`String`, `byte[]` or other binary type
+earlier in the route.
+
+Signatures created by earlier versions may not verify and need to be created
again.
+
== Upgrading from 4.18.4 to 4.18.5
=== camel-docling - output path handling