This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch backport/28296-to-camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 39d65712bd29add6342bd90027e6bef9b240d0c1
Author: Guillaume Nodet <[email protected]>
AuthorDate: Fri Oct 23 05:35:22 2026 +0000

    [backport camel-4.18.x] CAMEL-25992: camel-pqc - sign and verify message 
bodies over their raw bytes
---
 .../camel-pqc/src/main/docs/pqc-component.adoc     |   2 +
 .../apache/camel/component/pqc/PQCProducer.java    | 122 +++++++++-
 .../component/pqc/PQCBinaryBodySignatureTest.java  | 210 ++++++++++++++++++
 .../component/pqc/PQCFileBodySignatureTest.java    | 247 +++++++++++++++++++++
 4 files changed, 571 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..b2378082027c 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,114 @@ 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..16c79be472a0
--- /dev/null
+++ 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCBinaryBodySignatureTest.java
@@ -0,0 +1,210 @@
+/*
+ * 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.security.spec.ECGenParameterSpec;
+import java.util.Map;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.pqc.crypto.hybrid.HybridSignature;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.test.junit6.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.assertNotNull;
+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 CLASSICAL_KEY = 
"&classicalSignatureAlgorithm=ECDSA_P256&classicalKeyPair=#classicalKeyPair";
+    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;
+    private static KeyPair classicalKeyPair;
+
+    @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();
+
+        KeyPairGenerator classical
+                = 
KeyPairGenerator.getInstance(PQCClassicalSignatureAlgorithms.ECDSA_P256.getKeyAlgorithm());
+        classical.initialize(new 
ECGenParameterSpec(PQCClassicalSignatureAlgorithms.ECDSA_P256.getParameterSpec()));
+        classicalKeyPair = classical.generateKeyPair();
+    }
+
+    @Override
+    protected void bindToRegistry(Registry registry) {
+        registry.bind("pqcKeyPair", pqcKeyPair);
+        registry.bind("classicalKeyPair", classicalKeyPair);
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:sign").to(SIGN);
+                from("direct:verify").to(VERIFY);
+                
from("direct:hybridSign").to("pqc:hybridSign?operation=hybridSign" + PQC_KEY + 
CLASSICAL_KEY);
+                
from("direct:hybridVerify").to("pqc:hybridVerify?operation=hybridVerify" + 
PQC_KEY + CLASSICAL_KEY);
+            }
+        };
+    }
+
+    @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 hybridSignatureOfByteBufferBodyCoversItsBytes() throws Exception {
+        Exchange out = request("direct:hybridSign", 
ByteBuffer.wrap(CONTENT.clone()), null, null);
+        byte[] hybridSignature = 
out.getMessage().getHeader(PQCConstants.HYBRID_SIGNATURE, byte[].class);
+
+        assertNotNull(hybridSignature);
+        assertTrue(HybridSignature.verify(CONTENT, hybridSignature, 
classicalKeyPair.getPublic(), classicalSignature(),
+                pqcKeyPair.getPublic(), pqcSignature()), "the hybrid signature 
must cover the bytes of the ByteBuffer");
+    }
+
+    @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 static Signature classicalSignature() throws Exception {
+        return 
Signature.getInstance(PQCClassicalSignatureAlgorithms.ECDSA_P256.getAlgorithm());
+    }
+
+    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..80607df87e17
--- /dev/null
+++ 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCFileBodySignatureTest.java
@@ -0,0 +1,247 @@
+/*
+ * 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 java.security.spec.ECGenParameterSpec;
+
+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.component.pqc.crypto.hybrid.HybridSignature;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.test.junit6.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.assertNotNull;
+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, by the single-algorithm and by the hybrid signature 
operations.
+ */
+public class PQCFileBodySignatureTest extends CamelTestSupport {
+
+    private static final String PQC_KEY = 
"&signatureAlgorithm=MLDSA&keyPair=#pqcKeyPair";
+    private static final String CLASSICAL_KEY = 
"&classicalSignatureAlgorithm=ECDSA_P256&classicalKeyPair=#classicalKeyPair";
+    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;
+    private static KeyPair classicalKeyPair;
+
+    @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();
+
+        KeyPairGenerator classical
+                = 
KeyPairGenerator.getInstance(PQCClassicalSignatureAlgorithms.ECDSA_P256.getKeyAlgorithm());
+        classical.initialize(new 
ECGenParameterSpec(PQCClassicalSignatureAlgorithms.ECDSA_P256.getParameterSpec()));
+        classicalKeyPair = classical.generateKeyPair();
+    }
+
+    @Override
+    protected void bindToRegistry(Registry registry) {
+        registry.bind("pqcKeyPair", pqcKeyPair);
+        registry.bind("classicalKeyPair", classicalKeyPair);
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:sign").to(SIGN);
+                from("direct:verify").to(VERIFY);
+                
from("direct:hybridSign").to("pqc:hybridSign?operation=hybridSign" + PQC_KEY + 
CLASSICAL_KEY);
+                
from("direct:hybridVerify").to("pqc:hybridVerify?operation=hybridVerify" + 
PQC_KEY + CLASSICAL_KEY);
+
+                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");
+    }
+
+    @Test
+    void hybridSignatureOfFileBodyCoversItsBytes() throws Exception {
+        Path path = write("hybrid.dat");
+
+        Exchange out = request("direct:hybridSign", path, null, null);
+        byte[] hybridSignature = 
out.getMessage().getHeader(PQCConstants.HYBRID_SIGNATURE, byte[].class);
+
+        assertNotNull(hybridSignature);
+        assertTrue(HybridSignature.verify(CONTENT, hybridSignature, 
classicalKeyPair.getPublic(), classicalSignature(),
+                pqcKeyPair.getPublic(), pqcSignature()), "the hybrid signature 
must cover the bytes of the file");
+    }
+
+    @Test
+    void hybridVerificationOfFileBodyCoversItsBytes() throws Exception {
+        File file = write("hybrid.dat").toFile();
+        byte[] hybridSignature = HybridSignature.sign(CONTENT, 
classicalKeyPair.getPrivate(), classicalSignature(),
+                pqcKeyPair.getPrivate(), pqcSignature());
+
+        Exchange out = request("direct:hybridVerify", file, 
PQCConstants.HYBRID_SIGNATURE, hybridSignature);
+
+        assertTrue(out.getMessage().getHeader(PQCConstants.HYBRID_VERIFY, 
Boolean.class),
+                "a hybrid signature over the bytes of the file must verify");
+    }
+
+    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());
+    }
+
+    private static Signature classicalSignature() throws Exception {
+        return 
Signature.getInstance(PQCClassicalSignatureAlgorithms.ECDSA_P256.getAlgorithm());
+    }
+}

Reply via email to