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. *
