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

davsclaus pushed a commit to branch CAMEL-26201-crypto-stream-body
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 0d778fe13c58ef906214533d9d6c8511102f1ae7
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 3 00:15:16 2026 +0100

    CAMEL-26201: camel-crypto - sign and verify keep a stream body, verify 
fails without body, and the key header always wins
    
    With stream caching off, sign and verify read and closed a stream body, so 
the
    next step of the route got an empty body. A stream body is now cached and 
put
    back as the body, and a StreamCache body is reset before and after it is 
read.
    
    verify now fails with a SignatureException when the message has no body (it
    verified a null body as an empty payload). The CamelSignaturePublicKeyOrCert
    header now always wins over the key, certificate or keystore of the 
endpoint (a
    PublicKey in the header was ignored when the endpoint had a certificate). 
The
    doc, the header description and the body types say so.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../apache/camel/catalog/components/crypto.json    |   4 +-
 .../camel/catalog/docs/crypto-component.adoc       |   9 ++
 .../org/apache/camel/component/crypto/crypto.json  |   4 +-
 .../src/main/docs/crypto-component.adoc            |   9 ++
 .../crypto/DigitalSignatureConstants.java          |   3 +-
 .../component/crypto/DigitalSignatureEndpoint.java |   5 +-
 .../processor/DigitalSignatureProcessor.java       |  23 ++-
 .../crypto/processor/VerifyingProcessor.java       |  43 +++---
 .../component/crypto/SignatureStreamBodyTest.java  | 169 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  13 ++
 .../DigitalSignatureEndpointBuilderFactory.java    |   3 +-
 11 files changed, 254 insertions(+), 31 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/crypto.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/crypto.json
index c490e9e89045..3dea8343914d 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/crypto.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/crypto.json
@@ -48,13 +48,13 @@
   },
   "headers": {
     "CamelSignaturePrivateKey": { "index": 0, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": 
"java.security.PrivateKey", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "The PrivateKey that should 
be used to sign the message", "constantName": 
"org.apache.camel.component.crypto.DigitalSignatureConstants#SIGNATURE_PRIVATE_KEY"
 },
-    "CamelSignaturePublicKeyOrCert": { "index": 1, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Certificate or PublicKey", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "The Certificate or 
PublicKey that should be used to verify the signature", "constantName": 
"org.apache.camel.component.crypto.DigitalSignatureConstants#SIGNATURE_PUBLIC_KEY_OR_CERT"
 },
+    "CamelSignaturePublicKeyOrCert": { "index": 1, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Certificate or PublicKey", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "The Certificate or 
PublicKey that should be used to verify the signature. When set, it is used 
instead of the publicKey, certificate or keystore of the endpoint", 
"constantName": "org.apache.camel.component.crypto. [...]
     "CamelDigitalSignature": { "index": 2, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "byte[] 
(Base64 encoded)", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The signature of the message, set by 
the sign operation and read by the verify operation (the signatureHeaderName 
option changes the header name)", "constantName": 
"org.apache.camel.component.crypto.DigitalSignatureConstant [...]
     "CamelSignatureKeyStoreAlias": { "index": 3, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The alias used to query the KeyStore 
for keys and Certificates to be used in signing and verifying exchanges", 
"constantName": 
"org.apache.camel.component.crypto.DigitalSignatureConstants#KEYSTORE_ALIAS" },
     "CamelSignatureKeyStorePassword": { "index": 4, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "char[]", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The password used to access an aliased 
PrivateKey in the KeyStore.", "constantName": 
"org.apache.camel.component.crypto.DigitalSignatureConstants#KEYSTORE_PASSWORD" 
}
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "text", "bytes", "stream", "file", "null" ], 
"description": "The payload to sign or verify, read as a stream: text, bytes, a 
stream or a file; no body is an empty payload. A Map, a POJO or a number fails: 
marshal it first. Without stream caching a stream body is used up. The 
signature is not in the body: sign sets it, Base64 encoded, in the 
CamelDigitalSignature header (signatureHeaderName), which verify reads" },
+    "producerIn": { "carriers": [ "text", "bytes", "stream", "file", "null" ], 
"description": "The payload to sign or verify, read as a stream: text, bytes, a 
stream or a file. A Map, a POJO or a number fails: marshal it first. A stream 
body is cached, so the next step still gets it. No body: sign signs an empty 
payload, and verify fails with a SignatureException. The signature is not in 
the body: sign sets it, Base64 encoded, in the CamelDigitalSignature header 
(signatureHeaderName), wh [...]
     "producerOut": { "carriers": [ "unchanged" ], "description": "The body is 
unchanged: a signature that does not verify fails the exchange with a 
SignatureException" }
   },
   "properties": {
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/crypto-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/crypto-component.adoc
index 394c3e016e68..71cb79dfbef7 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/crypto-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/crypto-component.adoc
@@ -72,6 +72,11 @@ crypto:verify:name[?options]
 * `crypto:verify` will read in the contents of this header and do the
 verification calculation.
 
+The message body is the payload that is signed or verified, and it is not 
changed.
+A stream body is cached, so the next step of the route still gets the body, 
also when stream caching is turned off.
+A message without body is signed as an empty payload by `crypto:sign`, and 
`crypto:verify` fails it with a
+`SignatureException`.
+
 To correctly function, the sign and verify process needs a pair
 of keys to be shared, signing requiring a `PrivateKey` and verifying a
 `PublicKey` (or a `Certificate` containing one). Using the JCE it is
@@ -289,6 +294,10 @@ to be supplied dynamically via the message headers below
 * `CamelSignaturePrivateKey`
 * `CamelSignaturePublicKeyOrCert`
 
+The `CamelSignaturePublicKeyOrCert` header, when set, wins over the 
`publicKey`, `certificate` or `keystore` of the
+`crypto:verify` endpoint. The `CamelSignaturePrivateKey` header is used only 
when the `crypto:sign` endpoint has no
+private key (no `privateKey`, and no `keystore` with a key for the alias).
+
 Even better would be to dynamically supply a keystore alias. Again, the
 alias can be supplied in a message header
 
diff --git 
a/components/camel-crypto/src/generated/resources/META-INF/org/apache/camel/component/crypto/crypto.json
 
b/components/camel-crypto/src/generated/resources/META-INF/org/apache/camel/component/crypto/crypto.json
index c490e9e89045..3dea8343914d 100644
--- 
a/components/camel-crypto/src/generated/resources/META-INF/org/apache/camel/component/crypto/crypto.json
+++ 
b/components/camel-crypto/src/generated/resources/META-INF/org/apache/camel/component/crypto/crypto.json
@@ -48,13 +48,13 @@
   },
   "headers": {
     "CamelSignaturePrivateKey": { "index": 0, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": 
"java.security.PrivateKey", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "The PrivateKey that should 
be used to sign the message", "constantName": 
"org.apache.camel.component.crypto.DigitalSignatureConstants#SIGNATURE_PRIVATE_KEY"
 },
-    "CamelSignaturePublicKeyOrCert": { "index": 1, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Certificate or PublicKey", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "The Certificate or 
PublicKey that should be used to verify the signature", "constantName": 
"org.apache.camel.component.crypto.DigitalSignatureConstants#SIGNATURE_PUBLIC_KEY_OR_CERT"
 },
+    "CamelSignaturePublicKeyOrCert": { "index": 1, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Certificate or PublicKey", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "The Certificate or 
PublicKey that should be used to verify the signature. When set, it is used 
instead of the publicKey, certificate or keystore of the endpoint", 
"constantName": "org.apache.camel.component.crypto. [...]
     "CamelDigitalSignature": { "index": 2, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "byte[] 
(Base64 encoded)", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The signature of the message, set by 
the sign operation and read by the verify operation (the signatureHeaderName 
option changes the header name)", "constantName": 
"org.apache.camel.component.crypto.DigitalSignatureConstant [...]
     "CamelSignatureKeyStoreAlias": { "index": 3, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The alias used to query the KeyStore 
for keys and Certificates to be used in signing and verifying exchanges", 
"constantName": 
"org.apache.camel.component.crypto.DigitalSignatureConstants#KEYSTORE_ALIAS" },
     "CamelSignatureKeyStorePassword": { "index": 4, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "char[]", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The password used to access an aliased 
PrivateKey in the KeyStore.", "constantName": 
"org.apache.camel.component.crypto.DigitalSignatureConstants#KEYSTORE_PASSWORD" 
}
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "text", "bytes", "stream", "file", "null" ], 
"description": "The payload to sign or verify, read as a stream: text, bytes, a 
stream or a file; no body is an empty payload. A Map, a POJO or a number fails: 
marshal it first. Without stream caching a stream body is used up. The 
signature is not in the body: sign sets it, Base64 encoded, in the 
CamelDigitalSignature header (signatureHeaderName), which verify reads" },
+    "producerIn": { "carriers": [ "text", "bytes", "stream", "file", "null" ], 
"description": "The payload to sign or verify, read as a stream: text, bytes, a 
stream or a file. A Map, a POJO or a number fails: marshal it first. A stream 
body is cached, so the next step still gets it. No body: sign signs an empty 
payload, and verify fails with a SignatureException. The signature is not in 
the body: sign sets it, Base64 encoded, in the CamelDigitalSignature header 
(signatureHeaderName), wh [...]
     "producerOut": { "carriers": [ "unchanged" ], "description": "The body is 
unchanged: a signature that does not verify fails the exchange with a 
SignatureException" }
   },
   "properties": {
diff --git a/components/camel-crypto/src/main/docs/crypto-component.adoc 
b/components/camel-crypto/src/main/docs/crypto-component.adoc
index 394c3e016e68..71cb79dfbef7 100644
--- a/components/camel-crypto/src/main/docs/crypto-component.adoc
+++ b/components/camel-crypto/src/main/docs/crypto-component.adoc
@@ -72,6 +72,11 @@ crypto:verify:name[?options]
 * `crypto:verify` will read in the contents of this header and do the
 verification calculation.
 
+The message body is the payload that is signed or verified, and it is not 
changed.
+A stream body is cached, so the next step of the route still gets the body, 
also when stream caching is turned off.
+A message without body is signed as an empty payload by `crypto:sign`, and 
`crypto:verify` fails it with a
+`SignatureException`.
+
 To correctly function, the sign and verify process needs a pair
 of keys to be shared, signing requiring a `PrivateKey` and verifying a
 `PublicKey` (or a `Certificate` containing one). Using the JCE it is
@@ -289,6 +294,10 @@ to be supplied dynamically via the message headers below
 * `CamelSignaturePrivateKey`
 * `CamelSignaturePublicKeyOrCert`
 
+The `CamelSignaturePublicKeyOrCert` header, when set, wins over the 
`publicKey`, `certificate` or `keystore` of the
+`crypto:verify` endpoint. The `CamelSignaturePrivateKey` header is used only 
when the `crypto:sign` endpoint has no
+private key (no `privateKey`, and no `keystore` with a key for the alias).
+
 Even better would be to dynamically supply a keystore alias. Again, the
 alias can be supplied in a message header
 
diff --git 
a/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/DigitalSignatureConstants.java
 
b/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/DigitalSignatureConstants.java
index 8dea9fda1d68..18afc6a9408e 100644
--- 
a/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/DigitalSignatureConstants.java
+++ 
b/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/DigitalSignatureConstants.java
@@ -24,7 +24,8 @@ import org.apache.camel.spi.Metadata;
 public final class DigitalSignatureConstants {
     @Metadata(description = "The PrivateKey that should be used to sign the 
message", javaType = "java.security.PrivateKey")
     public static final String SIGNATURE_PRIVATE_KEY = 
"CamelSignaturePrivateKey";
-    @Metadata(description = "The Certificate or PublicKey that should be used 
to verify the signature",
+    @Metadata(description = "The Certificate or PublicKey that should be used 
to verify the signature. When set, it is"
+                            + " used instead of the publicKey, certificate or 
keystore of the endpoint",
               javaType = "Certificate or PublicKey")
     public static final String SIGNATURE_PUBLIC_KEY_OR_CERT = 
"CamelSignaturePublicKeyOrCert";
     @Metadata(description = "The signature of the message, set by the sign 
operation and read by the verify operation"
diff --git 
a/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/DigitalSignatureEndpoint.java
 
b/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/DigitalSignatureEndpoint.java
index 9ea33ce2ce24..45546f78516f 100644
--- 
a/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/DigitalSignatureEndpoint.java
+++ 
b/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/DigitalSignatureEndpoint.java
@@ -43,8 +43,9 @@ import org.apache.camel.support.DefaultEndpoint;
              headersClass = DigitalSignatureConstants.class)
 @BodyTypes(producerIn = @BodyType(value = { Carrier.TEXT, Carrier.BYTES, 
Carrier.STREAM, Carrier.FILE, Carrier.NULL },
                                   description = "The payload to sign or 
verify, read as a stream: text, bytes, a stream or a"
-                                                + " file; no body is an empty 
payload. A Map, a POJO or a number fails: marshal"
-                                                + " it first. Without stream 
caching a stream body is used up. The signature"
+                                                + " file. A Map, a POJO or a 
number fails: marshal it first. A stream body is"
+                                                + " cached, so the next step 
still gets it. No body: sign signs an empty"
+                                                + " payload, and verify fails 
with a SignatureException. The signature"
                                                 + " is not in the body: sign 
sets it, Base64 encoded, in the"
                                                 + " CamelDigitalSignature 
header (signatureHeaderName), which verify reads"),
            producerOut = @BodyType(value = Carrier.UNCHANGED,
diff --git 
a/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/processor/DigitalSignatureProcessor.java
 
b/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/processor/DigitalSignatureProcessor.java
index 8598f770bb2c..c95c951607cf 100644
--- 
a/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/processor/DigitalSignatureProcessor.java
+++ 
b/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/processor/DigitalSignatureProcessor.java
@@ -17,6 +17,7 @@
 package org.apache.camel.component.crypto.processor;
 
 import java.io.InputStream;
+import java.io.Reader;
 import java.security.KeyStore;
 import java.security.NoSuchAlgorithmException;
 import java.security.NoSuchProviderException;
@@ -25,6 +26,7 @@ import java.security.Signature;
 import org.apache.camel.Exchange;
 import org.apache.camel.Message;
 import org.apache.camel.Processor;
+import org.apache.camel.StreamCache;
 import org.apache.camel.component.crypto.DigitalSignatureConfiguration;
 import org.apache.camel.component.crypto.DigitalSignatureConstants;
 import org.apache.camel.support.ExchangeHelper;
@@ -41,6 +43,19 @@ public abstract class DigitalSignatureProcessor implements 
Processor {
     public void calculateSignature(Exchange exchange, Signature signer) throws 
Exception {
         Object payload = exchange.getIn().getBody();
         if (payload != null) {
+            StreamCache cache = payload instanceof StreamCache sc ? sc : null;
+            if (cache == null && (payload instanceof InputStream || payload 
instanceof Reader)) {
+                // a stream can be read only once: cache it so the next step 
of the route still gets the body
+                cache = 
exchange.getContext().getTypeConverter().convertTo(StreamCache.class, exchange, 
payload);
+                if (cache != null) {
+                    exchange.getIn().setBody(cache);
+                    payload = cache;
+                }
+            }
+            if (cache != null) {
+                // sign or verify the whole payload, also when the cache has 
been read before
+                cache.reset();
+            }
             InputStream payloadStream = 
ExchangeHelper.convertToMandatoryType(exchange, InputStream.class, payload);
             try {
                 byte[] buffer = new byte[config.getBufferSize()];
@@ -49,7 +64,13 @@ public abstract class DigitalSignatureProcessor implements 
Processor {
                     signer.update(buffer, 0, read);
                 }
             } finally {
-                IOHelper.close(payloadStream);
+                if (payloadStream != cache) {
+                    IOHelper.close(payloadStream);
+                }
+                if (cache != null) {
+                    // keep the cached body readable for the next step of the 
route
+                    cache.reset();
+                }
             }
         }
     }
diff --git 
a/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/processor/VerifyingProcessor.java
 
b/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/processor/VerifyingProcessor.java
index 0e30c5398730..3487159bf82f 100644
--- 
a/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/processor/VerifyingProcessor.java
+++ 
b/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/processor/VerifyingProcessor.java
@@ -39,20 +39,33 @@ public class VerifyingProcessor extends 
DigitalSignatureProcessor {
     @Override
     public void process(Exchange exchange) throws Exception {
         Signature signer = createSignatureService();
-        Certificate cert = getCertificate(exchange);
+        // the key or certificate in the message header wins over the one 
configured on the endpoint
+        Certificate cert
+                = 
exchange.getIn().getHeader(DigitalSignatureConstants.SIGNATURE_PUBLIC_KEY_OR_CERT,
 Certificate.class);
+        PublicKey pk = null;
         if (cert == null) {
-            PublicKey pk = getPublicKeyOrCertificateFromHeader(exchange, 
PublicKey.class, config.getPublicKey());
-            if (pk == null) {
-                throw new IllegalStateException(
-                        String.format("Cannot verify signature as no Public 
Key or Certificate has been supplied."
-                                      + " Either supply one in the route 
definition or via the message header '%s'",
-                                
DigitalSignatureConstants.SIGNATURE_PUBLIC_KEY_OR_CERT));
+            pk = 
exchange.getIn().getHeader(DigitalSignatureConstants.SIGNATURE_PUBLIC_KEY_OR_CERT,
 PublicKey.class);
+        }
+        if (cert == null && pk == null) {
+            cert = config.getCertificate(getAlias(exchange));
+            if (cert == null) {
+                pk = config.getPublicKey();
             }
+        }
+        if (cert != null) {
+            signer.initVerify(cert);
+        } else if (pk != null) {
             signer.initVerify(pk);
         } else {
-            signer.initVerify(cert);
+            throw new IllegalStateException(
+                    String.format("Cannot verify signature as no Public Key or 
Certificate has been supplied."
+                                  + " Either supply one in the route 
definition or via the message header '%s'",
+                            
DigitalSignatureConstants.SIGNATURE_PUBLIC_KEY_OR_CERT));
         }
 
+        if (exchange.getIn().getBody() == null) {
+            throw new SignatureException("Cannot verify signature of exchange 
as the message has no body");
+        }
         calculateSignature(exchange, signer);
 
         byte[] signature = getSignatureFromExchange(exchange);
@@ -70,18 +83,4 @@ public class VerifyingProcessor extends 
DigitalSignatureProcessor {
         }
         return new Base64().decode(encodedSignature);
     }
-
-    private Certificate getCertificate(Exchange exchange) throws Exception {
-        Certificate cert = config.getCertificate(getAlias(exchange));
-        return getPublicKeyOrCertificateFromHeader(exchange, 
Certificate.class, cert);
-    }
-
-    private <T> T getPublicKeyOrCertificateFromHeader(Exchange exchange, 
Class<? extends T> verificationType, T defaultsTo) {
-        T pkOrCert = 
exchange.getIn().getHeader(DigitalSignatureConstants.SIGNATURE_PUBLIC_KEY_OR_CERT,
 verificationType);
-        if (pkOrCert == null) {
-            pkOrCert = defaultsTo;
-        }
-        return pkOrCert;
-    }
-
 }
diff --git 
a/components/camel-crypto/src/test/java/org/apache/camel/component/crypto/SignatureStreamBodyTest.java
 
b/components/camel-crypto/src/test/java/org/apache/camel/component/crypto/SignatureStreamBodyTest.java
new file mode 100644
index 000000000000..f59cacf6a4b8
--- /dev/null
+++ 
b/components/camel-crypto/src/test/java/org/apache/camel/component/crypto/SignatureStreamBodyTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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.crypto;
+
+import java.io.ByteArrayInputStream;
+import java.io.FilterInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.Certificate;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.converter.stream.InputStreamCache;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static 
org.apache.camel.component.crypto.DigitalSignatureConstants.SIGNATURE;
+import static 
org.apache.camel.component.crypto.DigitalSignatureConstants.SIGNATURE_PUBLIC_KEY_OR_CERT;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Sign and verify with stream caching turned off: a stream body is still 
there for the next step of the route.
+ */
+public class SignatureStreamBodyTest extends CamelTestSupport {
+
+    private static final String PAYLOAD = "Dear Alice, Rest assured it's me, 
signed Bob";
+
+    private KeyPair keyPair;
+
+    @Override
+    public void doPreSetup() throws Exception {
+        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+        keyGen.initialize(2048);
+        keyPair = keyGen.generateKeyPair();
+    }
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+        context.setStreamCaching(false);
+        return context;
+    }
+
+    @BindToRegistry("myPrivateKey")
+    public PrivateKey getPrivateKey() {
+        return keyPair.getPrivate();
+    }
+
+    @BindToRegistry("myPublicKey")
+    public PublicKey getPublicKey() {
+        return keyPair.getPublic();
+    }
+
+    @BindToRegistry("bobCert")
+    public Certificate getBobCertificate() throws Exception {
+        KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
+        try (InputStream in = 
SignatureStreamBodyTest.class.getResourceAsStream("/ks.keystore")) {
+            keystore.load(in, "letmein".toCharArray());
+        }
+        return keystore.getCertificate("bob");
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("direct:signature").to("crypto:sign:signature?privateKey=#myPrivateKey");
+                
from("direct:sign").to("crypto:sign:stream?privateKey=#myPrivateKey", 
"mock:signed");
+                
from("direct:verify").to("crypto:verify:stream?publicKey=#myPublicKey", 
"mock:verified");
+                
from("direct:verify-cert").to("crypto:verify:cert?certificate=#bobCert", 
"mock:verified");
+            }
+        };
+    }
+
+    @Test
+    void signKeepsStreamBody() throws Exception {
+        MockEndpoint signed = getMockEndpoint("mock:signed");
+        signed.expectedBodiesReceived(PAYLOAD);
+
+        template.sendBody("direct:sign", streamOf(PAYLOAD));
+
+        MockEndpoint.assertIsSatisfied(context);
+        
assertNotNull(signed.getReceivedExchanges().get(0).getMessage().getHeader(SIGNATURE));
+    }
+
+    @Test
+    void verifyKeepsStreamBody() throws Exception {
+        getMockEndpoint("mock:verified").expectedBodiesReceived(PAYLOAD);
+
+        template.sendBodyAndHeader("direct:verify", streamOf(PAYLOAD), 
SIGNATURE, signatureOf(PAYLOAD));
+
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    @Test
+    void verifyKeepsStreamCacheBody() throws Exception {
+        getMockEndpoint("mock:verified").expectedBodiesReceived(PAYLOAD);
+
+        InputStreamCache body = new 
InputStreamCache(PAYLOAD.getBytes(StandardCharsets.UTF_8));
+        template.sendBodyAndHeader("direct:verify", body, SIGNATURE, 
signatureOf(PAYLOAD));
+
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    @Test
+    void verifyFailsWithoutBody() throws Exception {
+        getMockEndpoint("mock:verified").expectedMessageCount(0);
+
+        // a signature of an empty payload
+        String signature = signatureOf(null);
+        CamelExecutionException e = assertThrows(CamelExecutionException.class,
+                () -> template.sendBodyAndHeader("direct:verify", null, 
SIGNATURE, signature));
+        assertInstanceOf(SignatureException.class, e.getCause());
+
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    @Test
+    void headerPublicKeyWinsOverEndpointCertificate() throws Exception {
+        getMockEndpoint("mock:verified").expectedBodiesReceived(PAYLOAD);
+
+        Map<String, Object> headers = new HashMap<>();
+        headers.put(SIGNATURE, signatureOf(PAYLOAD));
+        headers.put(SIGNATURE_PUBLIC_KEY_OR_CERT, keyPair.getPublic());
+        template.sendBodyAndHeaders("direct:verify-cert", PAYLOAD, headers);
+
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    private String signatureOf(String body) {
+        Exchange signed = template.request("direct:signature", e -> 
e.getIn().setBody(body));
+        return signed.getMessage().getHeader(SIGNATURE, String.class);
+    }
+
+    private static InputStream streamOf(String text) {
+        // a stream that is not a StreamCache, so it can be read only once
+        return new FilterInputStream(new 
ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))) {
+        };
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 10f5a47c1b89..dd9e8c272b2b 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -2221,6 +2221,19 @@ file is written from the column names, not from the 
first map). Every map of the
 `ignoreFirstRecord` only applies to unmarshal. A route that set 
`ignoreFirstRecord=false` to work around this is not
 affected; a route that added a dummy first map now gets it in the output too.
 
+=== camel-crypto - verify fails on a message without body, and the key header 
always wins
+
+`crypto:verify` now fails with a `SignatureException` when the message has no 
body. Before, a null body was verified
+as an empty payload, so a signature of an empty payload passed. `crypto:sign` 
still signs a null body as an empty
+payload.
+
+The `CamelSignaturePublicKeyOrCert` header now always wins over the 
`publicKey`, `certificate` or `keystore` of the
+`crypto:verify` endpoint. Before, a `PublicKey` in the header was ignored when 
the endpoint had a certificate (or a
+keystore with a certificate for the alias); a `Certificate` in the header 
already won.
+
+A stream body is now cached by `crypto:sign` and `crypto:verify`, so the next 
step of the route gets the body also
+when stream caching is turned off. Before, the stream was read and closed, and 
the next step got an empty body.
+
 === camel-csv - the maps of a list are written in the columns of their keys
 
 When no `header` is set (or `headerDisabled=true`), marshalling a 
`List<Map<String, Object>>` wrote the values of
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DigitalSignatureEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DigitalSignatureEndpointBuilderFactory.java
index 18a8da68f095..18c753f2dac8 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DigitalSignatureEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DigitalSignatureEndpointBuilderFactory.java
@@ -619,7 +619,8 @@ public interface DigitalSignatureEndpointBuilderFactory {
         }
         /**
          * The Certificate or PublicKey that should be used to verify the
-         * signature.
+         * signature. When set, it is used instead of the publicKey, 
certificate
+         * or keystore of the endpoint.
          * 
          * The option is a: {@code Certificate or PublicKey} type.
          * 

Reply via email to