This is an automated email from the ASF dual-hosted git repository.
Croway pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 9d3108def6e8 CAMEL-24441: camel-crypto-pgp - require integrity
protection when decrypting (#25848)
9d3108def6e8 is described below
commit 9d3108def6e8344f65a70ee86832b8f5237bc82d
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Aug 28 12:22:10 2026 +0200
CAMEL-24441: camel-crypto-pgp - require integrity protection when
decrypting (#25848)
unmarshal() verified the modification detection code only when the message
was an
OpenPGP symmetrically encrypted integrity protected data packet:
if (pbe.isIntegrityProtected()) {
if (!pbe.verify()) {
throw new PGPException("Message failed integrity check");
}
}
The older symmetrically encrypted data packet carries no such code, and
OpenPGP's CFB
mode is malleable without one, so a message using that packet skipped the
check
altogether. The packet type is chosen by whoever produced the message,
which left the
sender - or anyone able to rewrite the message in transit - deciding
whether the check
applied. The existing integrity option governs marshalling only and has no
decrypt-side
counterpart.
Add requireIntegrityProtection, defaulting to true, which rejects a message
that is not
integrity protected. Routes interoperating with a sender that still emits
the legacy
packet must set it to false.
signatureVerificationOption still defaults to optional, so a message
carrying no
signature is accepted. Flipping that would reject every unsigned message
and is a
separate decision; the upgrade guide points at it, since the two options
together are
what give a decrypted message authenticity and not only confidentiality.
PGPDataFormatTest sets encryptor.setIntegrity(false) for its whole class,
so its
decryptor now opts out explicitly - the same change an affected deployment
makes.
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
Co-authored-by: Federico Mariani <[email protected]>
---
.../org/apache/camel/catalog/dataformats/pgp.json | 3 +-
.../org/apache/camel/catalog/models/pgp.json | 3 +-
.../apache/camel/catalog/schemas/camel-spring.xsd | 12 +++
.../apache/camel/catalog/schemas/camel-xml-io.xsd | 12 +++
.../camel/catalog/schemas/camelYamlDsl-model.json | 6 ++
.../converter/crypto/PGPDataFormatConfigurer.java | 7 ++
.../org/apache/camel/converter/crypto/pgp.json | 3 +-
.../converter/crypto/PGPKeyAccessDataFormat.java | 29 +++++++
.../camel/converter/crypto/PGPDataFormatTest.java | 3 +
.../crypto/PGPRequireIntegrityProtectionTest.java | 97 ++++++++++++++++++++++
.../org/apache/camel/model/dataformat/pgp.json | 3 +-
.../camel/model/dataformat/PGPDataFormat.java | 38 +++++++++
.../reifier/dataformat/PGPDataFormatReifier.java | 1 +
.../apache/camel/java/out/JavaDslModelWriter.java | 1 +
.../java/org/apache/camel/xml/in/ModelParser.java | 1 +
.../java/org/apache/camel/xml/out/ModelWriter.java | 1 +
.../org/apache/camel/yaml/out/YamlModelWriter.java | 1 +
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 27 ++++++
.../dsl/yaml/deserializers/ModelDeserializers.java | 6 ++
.../resources/schema/camelYamlDsl-canonical.json | 6 ++
.../resources/schema/camelYamlDsl-model.json | 6 ++
.../generated/resources/schema/camelYamlDsl.json | 6 ++
22 files changed, 268 insertions(+), 4 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dataformats/pgp.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dataformats/pgp.json
index 5fe1000427b8..182755fb8e4a 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dataformats/pgp.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dataformats/pgp.json
@@ -30,6 +30,7 @@
"algorithm": { "index": 11, "kind": "attribute", "displayName":
"Algorithm", "group": "common", "required": false, "type": "integer",
"javaType": "java.lang.Integer", "deprecated": false, "autowired": false,
"secret": false, "description": "Symmetric key encryption algorithm; possible
values are defined in org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags. Only
relevant for encrypting." },
"compressionAlgorithm": { "index": 12, "kind": "attribute", "displayName":
"Compression Algorithm", "group": "common", "required": false, "type":
"integer", "javaType": "java.lang.Integer", "deprecated": false, "autowired":
false, "secret": false, "description": "Compression algorithm; possible values
are defined in org.bouncycastle.bcpg.CompressionAlgorithmTags. Only relevant
for encrypting." },
"hashAlgorithm": { "index": 13, "kind": "attribute", "displayName": "Hash
Algorithm", "group": "common", "required": false, "type": "integer",
"javaType": "java.lang.Integer", "deprecated": false, "autowired": false,
"secret": false, "description": "Signature hash algorithm; possible values are
defined in org.bouncycastle.bcpg.HashAlgorithmTags. Only relevant for signing."
},
- "signatureVerificationOption": { "index": 14, "kind": "attribute",
"displayName": "Signature Verification Option", "group": "common", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "description": "Controls the behavior for
verifying the signature during unmarshaling. Possible values: optional,
required, ignore, no_signature_allowed." }
+ "signatureVerificationOption": { "index": 14, "kind": "attribute",
"displayName": "Signature Verification Option", "group": "common", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "description": "Controls the behavior for
verifying the signature during unmarshaling. Possible values: optional,
required, ignore, no_signature_allowed." },
+ "requireIntegrityProtection": { "index": 15, "kind": "attribute",
"displayName": "Require Integrity Protection", "group": "common", "required":
false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether a message must be integrity protected in order to be decrypted. The
legacy symmetrically encrypted data packet carries no modification detection
code, and the packet type is chosen [...]
}
}
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/pgp.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/pgp.json
index 183f64016406..977fcad645a1 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/pgp.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/pgp.json
@@ -27,6 +27,7 @@
"algorithm": { "index": 11, "kind": "attribute", "displayName":
"Algorithm", "group": "common", "required": false, "type": "integer",
"javaType": "java.lang.Integer", "deprecated": false, "autowired": false,
"secret": false, "description": "Symmetric key encryption algorithm; possible
values are defined in org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags. Only
relevant for encrypting." },
"compressionAlgorithm": { "index": 12, "kind": "attribute", "displayName":
"Compression Algorithm", "group": "common", "required": false, "type":
"integer", "javaType": "java.lang.Integer", "deprecated": false, "autowired":
false, "secret": false, "description": "Compression algorithm; possible values
are defined in org.bouncycastle.bcpg.CompressionAlgorithmTags. Only relevant
for encrypting." },
"hashAlgorithm": { "index": 13, "kind": "attribute", "displayName": "Hash
Algorithm", "group": "common", "required": false, "type": "integer",
"javaType": "java.lang.Integer", "deprecated": false, "autowired": false,
"secret": false, "description": "Signature hash algorithm; possible values are
defined in org.bouncycastle.bcpg.HashAlgorithmTags. Only relevant for signing."
},
- "signatureVerificationOption": { "index": 14, "kind": "attribute",
"displayName": "Signature Verification Option", "group": "common", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "description": "Controls the behavior for
verifying the signature during unmarshaling. Possible values: optional,
required, ignore, no_signature_allowed." }
+ "signatureVerificationOption": { "index": 14, "kind": "attribute",
"displayName": "Signature Verification Option", "group": "common", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "description": "Controls the behavior for
verifying the signature during unmarshaling. Possible values: optional,
required, ignore, no_signature_allowed." },
+ "requireIntegrityProtection": { "index": 15, "kind": "attribute",
"displayName": "Require Integrity Protection", "group": "common", "required":
false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether a message must be integrity protected in order to be decrypted. The
legacy symmetrically encrypted data packet carries no modification detection
code, and the packet type is chosen [...]
}
}
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd
index e0e99a0501d1..be72acc1fe6c 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd
@@ -9544,6 +9544,18 @@ signing.
<![CDATA[
Controls the behavior for verifying the signature during unmarshaling.
Possible values: optional, required, ignore,
no_signature_allowed.
+]]>
+ </xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="requireIntegrityProtection" type="xs:string">
+ <xs:annotation>
+ <xs:documentation xml:lang="en">
+<![CDATA[
+Whether a message must be integrity protected in order to be decrypted. The
legacy symmetrically encrypted data packet
+carries no modification detection code, and the packet type is chosen by
whoever produced the message, so accepting it
+lets the sender decide whether the integrity check applies. Set to false only
to interoperate with a sender that still
+emits the legacy packet. Default value: true
]]>
</xs:documentation>
</xs:annotation>
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd
index 70a97d463f6d..3fff6f740797 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd
@@ -8666,6 +8666,18 @@ signing.
<![CDATA[
Controls the behavior for verifying the signature during unmarshaling.
Possible values: optional, required, ignore,
no_signature_allowed.
+]]>
+ </xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="requireIntegrityProtection" type="xs:string">
+ <xs:annotation>
+ <xs:documentation xml:lang="en">
+<![CDATA[
+Whether a message must be integrity protected in order to be decrypted. The
legacy symmetrically encrypted data packet
+carries no modification detection code, and the packet type is chosen by
whoever produced the message, so accepting it
+lets the sender decide whether the integrity check applies. Set to false only
to interoperate with a sender that still
+emits the legacy packet. Default value: true
]]>
</xs:documentation>
</xs:annotation>
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
index 918fa669f3a7..2a19bbe7cccc 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
@@ -12742,6 +12742,12 @@
"type" : "string",
"description" : "Java Cryptography Extension (JCE) provider, default
is Bouncy Castle (BC). Alternatively you can use, for example, the IAIK JCE
provider.",
"title" : "Provider"
+ }, {
+ "name" : "requireIntegrityProtection",
+ "type" : "boolean",
+ "description" : "Whether a message must be integrity protected in
order to be decrypted. The legacy symmetrically encrypted data packet carries
no modification detection code, and the packet type is chosen by whoever
produced the message, so accepting it lets the sender decide whether the
integrity check applies. Set to false only to interoperate with a sender that
still emits the legacy packet.",
+ "title" : "Require Integrity Protection",
+ "default" : "true"
}, {
"name" : "signatureKeyFileName",
"type" : "string",
diff --git
a/components/camel-crypto-pgp/src/generated/java/org/apache/camel/converter/crypto/PGPDataFormatConfigurer.java
b/components/camel-crypto-pgp/src/generated/java/org/apache/camel/converter/crypto/PGPDataFormatConfigurer.java
index 97f991c429d4..1e0bb26c42aa 100644
---
a/components/camel-crypto-pgp/src/generated/java/org/apache/camel/converter/crypto/PGPDataFormatConfigurer.java
+++
b/components/camel-crypto-pgp/src/generated/java/org/apache/camel/converter/crypto/PGPDataFormatConfigurer.java
@@ -31,6 +31,7 @@ public class PGPDataFormatConfigurer extends
org.apache.camel.support.component.
map.put("KeyUserid", java.lang.String.class);
map.put("Password", java.lang.String.class);
map.put("Provider", java.lang.String.class);
+ map.put("RequireIntegrityProtection", boolean.class);
map.put("SignatureKeyFileName", java.lang.String.class);
map.put("SignatureKeyRing", byte[].class);
map.put("SignatureKeyUserid", java.lang.String.class);
@@ -56,6 +57,8 @@ public class PGPDataFormatConfigurer extends
org.apache.camel.support.component.
case "keyUserid": target.setKeyUserid(property(camelContext,
java.lang.String.class, value)); return true;
case "password": target.setPassword(property(camelContext,
java.lang.String.class, value)); return true;
case "provider": target.setProvider(property(camelContext,
java.lang.String.class, value)); return true;
+ case "requireintegrityprotection":
+ case "requireIntegrityProtection":
target.setRequireIntegrityProtection(property(camelContext, boolean.class,
value)); return true;
case "signaturekeyfilename":
case "signatureKeyFileName":
target.setSignatureKeyFileName(property(camelContext, java.lang.String.class,
value)); return true;
case "signaturekeyring":
@@ -91,6 +94,8 @@ public class PGPDataFormatConfigurer extends
org.apache.camel.support.component.
case "keyUserid": return java.lang.String.class;
case "password": return java.lang.String.class;
case "provider": return java.lang.String.class;
+ case "requireintegrityprotection":
+ case "requireIntegrityProtection": return boolean.class;
case "signaturekeyfilename":
case "signatureKeyFileName": return java.lang.String.class;
case "signaturekeyring":
@@ -122,6 +127,8 @@ public class PGPDataFormatConfigurer extends
org.apache.camel.support.component.
case "keyUserid": return target.getKeyUserid();
case "password": return target.getPassword();
case "provider": return target.getProvider();
+ case "requireintegrityprotection":
+ case "requireIntegrityProtection": return
target.isRequireIntegrityProtection();
case "signaturekeyfilename":
case "signatureKeyFileName": return target.getSignatureKeyFileName();
case "signaturekeyring":
diff --git
a/components/camel-crypto-pgp/src/generated/resources/META-INF/org/apache/camel/converter/crypto/pgp.json
b/components/camel-crypto-pgp/src/generated/resources/META-INF/org/apache/camel/converter/crypto/pgp.json
index 5fe1000427b8..182755fb8e4a 100644
---
a/components/camel-crypto-pgp/src/generated/resources/META-INF/org/apache/camel/converter/crypto/pgp.json
+++
b/components/camel-crypto-pgp/src/generated/resources/META-INF/org/apache/camel/converter/crypto/pgp.json
@@ -30,6 +30,7 @@
"algorithm": { "index": 11, "kind": "attribute", "displayName":
"Algorithm", "group": "common", "required": false, "type": "integer",
"javaType": "java.lang.Integer", "deprecated": false, "autowired": false,
"secret": false, "description": "Symmetric key encryption algorithm; possible
values are defined in org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags. Only
relevant for encrypting." },
"compressionAlgorithm": { "index": 12, "kind": "attribute", "displayName":
"Compression Algorithm", "group": "common", "required": false, "type":
"integer", "javaType": "java.lang.Integer", "deprecated": false, "autowired":
false, "secret": false, "description": "Compression algorithm; possible values
are defined in org.bouncycastle.bcpg.CompressionAlgorithmTags. Only relevant
for encrypting." },
"hashAlgorithm": { "index": 13, "kind": "attribute", "displayName": "Hash
Algorithm", "group": "common", "required": false, "type": "integer",
"javaType": "java.lang.Integer", "deprecated": false, "autowired": false,
"secret": false, "description": "Signature hash algorithm; possible values are
defined in org.bouncycastle.bcpg.HashAlgorithmTags. Only relevant for signing."
},
- "signatureVerificationOption": { "index": 14, "kind": "attribute",
"displayName": "Signature Verification Option", "group": "common", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "description": "Controls the behavior for
verifying the signature during unmarshaling. Possible values: optional,
required, ignore, no_signature_allowed." }
+ "signatureVerificationOption": { "index": 14, "kind": "attribute",
"displayName": "Signature Verification Option", "group": "common", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "description": "Controls the behavior for
verifying the signature during unmarshaling. Possible values: optional,
required, ignore, no_signature_allowed." },
+ "requireIntegrityProtection": { "index": 15, "kind": "attribute",
"displayName": "Require Integrity Protection", "group": "common", "required":
false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether a message must be integrity protected in order to be decrypted. The
legacy symmetrically encrypted data packet carries no modification detection
code, and the packet type is chosen [...]
}
}
diff --git
a/components/camel-crypto-pgp/src/main/java/org/apache/camel/converter/crypto/PGPKeyAccessDataFormat.java
b/components/camel-crypto-pgp/src/main/java/org/apache/camel/converter/crypto/PGPKeyAccessDataFormat.java
index 36fca4cee3b2..3f51111a400c 100644
---
a/components/camel-crypto-pgp/src/main/java/org/apache/camel/converter/crypto/PGPKeyAccessDataFormat.java
+++
b/components/camel-crypto-pgp/src/main/java/org/apache/camel/converter/crypto/PGPKeyAccessDataFormat.java
@@ -163,6 +163,8 @@ public class PGPKeyAccessDataFormat extends ServiceSupport
implements DataFormat
private String signatureVerificationOption = "optional";
+ private boolean requireIntegrityProtection = true;
+
/*
* The default value "_CONSOLE" marks the file as For Your Eyes Only... may
* cause problems for the receiver if they use an automated process to
@@ -418,6 +420,14 @@ public class PGPKeyAccessDataFormat extends ServiceSupport
implements DataFormat
if (!pbe.verify()) {
throw new PGPException("Message failed integrity check");
}
+ } else if (requireIntegrityProtection) {
+ // Without a modification detection code the ciphertext is
malleable, and whether the check runs
+ // would otherwise be decided by the packet type the sender
chose - so a sender or an intermediary
+ // could drop the protection simply by sending the legacy
packet.
+ throw new PGPException(
+ "PGP message is not integrity protected. Either send
an integrity protected message, or set"
+ + " requireIntegrityProtection=false on
the PGP decryptor to accept the legacy"
+ + " symmetrically encrypted data
packet.");
}
} finally {
IOHelper.close(osb, litData, uncompressedData, encData, in,
encryptedStream);
@@ -690,6 +700,25 @@ public class PGPKeyAccessDataFormat extends ServiceSupport
implements DataFormat
this.secretKeyAccessor = secretKeyAccessor;
}
+ public boolean isRequireIntegrityProtection() {
+ return requireIntegrityProtection;
+ }
+
+ /**
+ * Whether a message must be integrity protected in order to be decrypted.
+ * <p>
+ * An OpenPGP symmetrically encrypted integrity protected data packet
carries a modification detection code, which
+ * {@code unmarshal} verifies. The older symmetrically encrypted data
packet carries none, and OpenPGP's CFB mode is
+ * malleable without it, so accepting such a message means accepting
ciphertext that may have been altered. Since
+ * the packet type is chosen by whoever produced the message, leaving this
off lets the sender - or anyone who can
+ * rewrite the message in transit - decide whether the check applies at
all.
+ * <p>
+ * Defaults to true. Set it to false only to interoperate with a sender
that still emits the legacy packet.
+ */
+ public void setRequireIntegrityProtection(boolean
requireIntegrityProtection) {
+ this.requireIntegrityProtection = requireIntegrityProtection;
+ }
+
public String getSignatureVerificationOption() {
return signatureVerificationOption;
}
diff --git
a/components/camel-crypto-pgp/src/test/java/org/apache/camel/converter/crypto/PGPDataFormatTest.java
b/components/camel-crypto-pgp/src/test/java/org/apache/camel/converter/crypto/PGPDataFormatTest.java
index 261c24f8c16e..a9b137b3e923 100644
---
a/components/camel-crypto-pgp/src/test/java/org/apache/camel/converter/crypto/PGPDataFormatTest.java
+++
b/components/camel-crypto-pgp/src/test/java/org/apache/camel/converter/crypto/PGPDataFormatTest.java
@@ -98,6 +98,9 @@ public class PGPDataFormatTest extends
AbstractPGPDataFormatTest {
decryptor.setSignatureKeyFileName(PUB_KEY_RING_SUBKEYS_FILE_NAME);
decryptor.setPassword("Abcd1234");
decryptor.setSignatureKeyUserid("keyflag");
+ // the encryptor above deliberately emits the legacy packet, which
carries no modification detection code,
+ // so the decryptor has to accept messages that are not integrity
protected
+ decryptor.setRequireIntegrityProtection(false);
}
protected String getKeyFileName() {
diff --git
a/components/camel-crypto-pgp/src/test/java/org/apache/camel/converter/crypto/PGPRequireIntegrityProtectionTest.java
b/components/camel-crypto-pgp/src/test/java/org/apache/camel/converter/crypto/PGPRequireIntegrityProtectionTest.java
new file mode 100644
index 000000000000..fa1f5053f7dd
--- /dev/null
+++
b/components/camel-crypto-pgp/src/test/java/org/apache/camel/converter/crypto/PGPRequireIntegrityProtectionTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.converter.crypto;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultExchange;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openpgp.PGPException;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The legacy symmetrically encrypted data packet carries no modification
detection code, and OpenPGP's CFB mode is
+ * malleable without one. Because the packet type is chosen by whoever
produced the message, a decryptor that only
+ * checks integrity when the message claims to be protected lets the sender
decide whether the check runs at all.
+ */
+class PGPRequireIntegrityProtectionTest {
+
+ private static final String PUB_KEY_RING =
"org/apache/camel/component/crypto/pubring.gpg";
+ private static final String SEC_KEY_RING =
"org/apache/camel/component/crypto/secring.gpg";
+ private static final String PAYLOAD = "Hello PGP";
+
+ @BeforeAll
+ static void installProvider() {
+ java.security.Security.addProvider(new BouncyCastleProvider());
+ }
+
+ @Test
+ void aMessageWithoutIntegrityProtectionIsRejectedByDefault() throws
Exception {
+ PGPDataFormat decryptor = decryptor();
+ assertTrue(decryptor.isRequireIntegrityProtection(), "expected the
secure default");
+
+ Exception e = assertThrows(PGPException.class, () ->
roundTrip(decryptor));
+ assertTrue(e.getMessage().contains("not integrity protected"),
"unexpected message: " + e.getMessage());
+ }
+
+ @Test
+ void theLegacyPacketCanStillBeAcceptedOnPurpose() throws Exception {
+ PGPDataFormat decryptor = decryptor();
+ decryptor.setRequireIntegrityProtection(false);
+
+ assertEquals(PAYLOAD, roundTrip(decryptor));
+ }
+
+ /**
+ * Encrypts with {@code integrity=false}, so the message carries no
modification detection code, then decrypts it
+ * with the given decryptor.
+ */
+ private static String roundTrip(PGPDataFormat decryptor) throws Exception {
+ PGPDataFormat encryptor = new PGPDataFormat();
+ encryptor.setKeyFileName(PUB_KEY_RING);
+ encryptor.setKeyUserid("sdude");
+ encryptor.setIntegrity(false);
+
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ context.start();
+ encryptor.start();
+ decryptor.start();
+
+ Exchange exchange = new DefaultExchange(context);
+ ByteArrayOutputStream encrypted = new ByteArrayOutputStream();
+ encryptor.marshal(exchange,
PAYLOAD.getBytes(StandardCharsets.UTF_8), encrypted);
+
+ Object body = decryptor.unmarshal(exchange,
encrypted.toByteArray());
+ return context.getTypeConverter().convertTo(String.class,
exchange, body);
+ }
+ }
+
+ private static PGPDataFormat decryptor() {
+ PGPDataFormat decryptor = new PGPDataFormat();
+ decryptor.setKeyFileName(SEC_KEY_RING);
+ decryptor.setPassword("sdude");
+ return decryptor;
+ }
+}
diff --git
a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/dataformat/pgp.json
b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/dataformat/pgp.json
index 183f64016406..977fcad645a1 100644
---
a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/dataformat/pgp.json
+++
b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/dataformat/pgp.json
@@ -27,6 +27,7 @@
"algorithm": { "index": 11, "kind": "attribute", "displayName":
"Algorithm", "group": "common", "required": false, "type": "integer",
"javaType": "java.lang.Integer", "deprecated": false, "autowired": false,
"secret": false, "description": "Symmetric key encryption algorithm; possible
values are defined in org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags. Only
relevant for encrypting." },
"compressionAlgorithm": { "index": 12, "kind": "attribute", "displayName":
"Compression Algorithm", "group": "common", "required": false, "type":
"integer", "javaType": "java.lang.Integer", "deprecated": false, "autowired":
false, "secret": false, "description": "Compression algorithm; possible values
are defined in org.bouncycastle.bcpg.CompressionAlgorithmTags. Only relevant
for encrypting." },
"hashAlgorithm": { "index": 13, "kind": "attribute", "displayName": "Hash
Algorithm", "group": "common", "required": false, "type": "integer",
"javaType": "java.lang.Integer", "deprecated": false, "autowired": false,
"secret": false, "description": "Signature hash algorithm; possible values are
defined in org.bouncycastle.bcpg.HashAlgorithmTags. Only relevant for signing."
},
- "signatureVerificationOption": { "index": 14, "kind": "attribute",
"displayName": "Signature Verification Option", "group": "common", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "description": "Controls the behavior for
verifying the signature during unmarshaling. Possible values: optional,
required, ignore, no_signature_allowed." }
+ "signatureVerificationOption": { "index": 14, "kind": "attribute",
"displayName": "Signature Verification Option", "group": "common", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "description": "Controls the behavior for
verifying the signature during unmarshaling. Possible values: optional,
required, ignore, no_signature_allowed." },
+ "requireIntegrityProtection": { "index": 15, "kind": "attribute",
"displayName": "Require Integrity Protection", "group": "common", "required":
false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether a message must be integrity protected in order to be decrypted. The
legacy symmetrically encrypted data packet carries no modification detection
code, and the packet type is chosen [...]
}
}
diff --git
a/core/camel-core-model/src/main/java/org/apache/camel/model/dataformat/PGPDataFormat.java
b/core/camel-core-model/src/main/java/org/apache/camel/model/dataformat/PGPDataFormat.java
index ccad08ae3eec..0ae219d316a3 100644
---
a/core/camel-core-model/src/main/java/org/apache/camel/model/dataformat/PGPDataFormat.java
+++
b/core/camel-core-model/src/main/java/org/apache/camel/model/dataformat/PGPDataFormat.java
@@ -86,6 +86,14 @@ public class PGPDataFormat extends DataFormatDefinition {
@XmlAttribute
@Metadata(description = "Controls the behavior for verifying the signature
during unmarshaling. Possible values: optional, required, ignore,
no_signature_allowed.")
private String signatureVerificationOption;
+ @XmlAttribute
+ @Metadata(javaType = "java.lang.Boolean", defaultValue = "true",
+ description = "Whether a message must be integrity protected in
order to be decrypted. The legacy"
+ + " symmetrically encrypted data packet carries no
modification detection code, and the"
+ + " packet type is chosen by whoever produced the
message, so accepting it lets the sender"
+ + " decide whether the integrity check applies.
Set to false only to interoperate with a"
+ + " sender that still emits the legacy packet.")
+ private String requireIntegrityProtection;
public PGPDataFormat() {
super("pgp");
@@ -107,6 +115,7 @@ public class PGPDataFormat extends DataFormatDefinition {
this.compressionAlgorithm = source.compressionAlgorithm;
this.hashAlgorithm = source.hashAlgorithm;
this.signatureVerificationOption = source.signatureVerificationOption;
+ this.requireIntegrityProtection = source.requireIntegrityProtection;
}
private PGPDataFormat(Builder builder) {
@@ -125,6 +134,7 @@ public class PGPDataFormat extends DataFormatDefinition {
this.compressionAlgorithm = builder.compressionAlgorithm;
this.hashAlgorithm = builder.hashAlgorithm;
this.signatureVerificationOption = builder.signatureVerificationOption;
+ this.requireIntegrityProtection = builder.requireIntegrityProtection;
}
@Override
@@ -244,6 +254,17 @@ public class PGPDataFormat extends DataFormatDefinition {
this.signatureVerificationOption = signatureVerificationOption;
}
+ public String getRequireIntegrityProtection() {
+ return requireIntegrityProtection;
+ }
+
+ /**
+ * Whether a message must be integrity protected in order to be decrypted.
Defaults to true.
+ */
+ public void setRequireIntegrityProtection(String
requireIntegrityProtection) {
+ this.requireIntegrityProtection = requireIntegrityProtection;
+ }
+
/**
* {@code Builder} is a specific builder for {@link PGPDataFormat}.
*/
@@ -264,6 +285,7 @@ public class PGPDataFormat extends DataFormatDefinition {
private String compressionAlgorithm;
private String hashAlgorithm;
private String signatureVerificationOption;
+ private String requireIntegrityProtection;
/**
* User ID of the key in the PGP keyring used for signing (during
encryption) or signature verification (during
@@ -448,6 +470,22 @@ public class PGPDataFormat extends DataFormatDefinition {
return this;
}
+ /**
+ * Whether a message must be integrity protected in order to be
decrypted. Defaults to true.
+ */
+ public Builder requireIntegrityProtection(String
requireIntegrityProtection) {
+ this.requireIntegrityProtection = requireIntegrityProtection;
+ return this;
+ }
+
+ /**
+ * Whether a message must be integrity protected in order to be
decrypted. Defaults to true.
+ */
+ public Builder requireIntegrityProtection(boolean
requireIntegrityProtection) {
+ this.requireIntegrityProtection =
Boolean.toString(requireIntegrityProtection);
+ return this;
+ }
+
@Override
public PGPDataFormat end() {
return new PGPDataFormat(this);
diff --git
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/dataformat/PGPDataFormatReifier.java
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/dataformat/PGPDataFormatReifier.java
index 0b97d0f9ebcc..de05a23a4b5b 100644
---
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/dataformat/PGPDataFormatReifier.java
+++
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/dataformat/PGPDataFormatReifier.java
@@ -44,6 +44,7 @@ public class PGPDataFormatReifier extends
DataFormatReifier<PGPDataFormat> {
properties.put("compressionAlgorithm",
definition.getCompressionAlgorithm());
properties.put("hashAlgorithm", definition.getHashAlgorithm());
properties.put("signatureVerificationOption",
definition.getSignatureVerificationOption());
+ properties.put("requireIntegrityProtection",
definition.getRequireIntegrityProtection());
}
}
diff --git
a/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java
b/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java
index eca0ed5407e4..b1d7fdf89b82 100644
---
a/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java
+++
b/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java
@@ -3222,6 +3222,7 @@ public class JavaDslModelWriter extends
JavaDslModelWriterSupport {
doWriteAttribute(sb, "compressionAlgorithm",
def.getCompressionAlgorithm(), null);
doWriteAttribute(sb, "hashAlgorithm", def.getHashAlgorithm(), null);
doWriteAttribute(sb, "signatureVerificationOption",
def.getSignatureVerificationOption(), null);
+ doWriteAttribute(sb, "requireIntegrityProtection",
def.getRequireIntegrityProtection(), "true");
}
protected void doWritePQCDataFormat(StringBuilder sb, PQCDataFormat def) {
doWriteIdentifiedTypeAttributes(sb, def);
diff --git
a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java
b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java
index ba4dfbc2f3f0..d2f9e108d076 100644
---
a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java
+++
b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java
@@ -1956,6 +1956,7 @@ public class ModelParser extends BaseParser {
case "keyUserid": def.setKeyUserid(val); yield true;
case "password": def.setPassword(val); yield true;
case "provider": def.setProvider(val); yield true;
+ case "requireIntegrityProtection":
def.setRequireIntegrityProtection(val); yield true;
case "signatureKeyFileName": def.setSignatureKeyFileName(val);
yield true;
case "signatureKeyRing": def.setSignatureKeyRing(val); yield
true;
case "signatureKeyUserid": def.setSignatureKeyUserid(val);
yield true;
diff --git
a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java
b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java
index efc0ae42a38a..3e120b4988b6 100644
---
a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java
+++
b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java
@@ -2587,6 +2587,7 @@ public class ModelWriter extends BaseWriter {
doWriteAttribute("compressionAlgorithm",
def.getCompressionAlgorithm(), null);
doWriteAttribute("hashAlgorithm", def.getHashAlgorithm(), null);
doWriteAttribute("signatureVerificationOption",
def.getSignatureVerificationOption(), null);
+ doWriteAttribute("requireIntegrityProtection",
def.getRequireIntegrityProtection(), "true");
endElement(name);
}
protected void doWritePQCDataFormat(String name, PQCDataFormat def) throws
IOException {
diff --git
a/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java
b/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java
index 5537ddca5f24..fc56d46bb007 100644
---
a/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java
+++
b/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java
@@ -2585,6 +2585,7 @@ public class YamlModelWriter extends
YamlModelWriterSupport {
doWriteAttribute(jo, "compressionAlgorithm",
def.getCompressionAlgorithm(), null);
doWriteAttribute(jo, "hashAlgorithm", def.getHashAlgorithm(), null);
doWriteAttribute(jo, "signatureVerificationOption",
def.getSignatureVerificationOption(), null);
+ doWriteAttribute(jo, "requireIntegrityProtection",
def.getRequireIntegrityProtection(), "true");
return jo;
}
protected JsonObject doWritePQCDataFormat(PQCDataFormat def) {
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index aad1d4a1b9b0..e072b59ab56a 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -564,6 +564,33 @@ is truncated rather than recreated and would otherwise
keep its original permiss
Deployments where another account legitimately reads these files — a sidecar
or a backup agent running as
a different user — need to run as the owner, or use a group-aware key store
instead.
+=== camel-crypto-pgp
+
+The `pgp` data format verifies a message's modification detection code only
when the message is an
+OpenPGP symmetrically encrypted *integrity protected* data packet:
+
+[source,java]
+----
+if (pbe.isIntegrityProtected()) {
+ if (!pbe.verify()) {
+ throw new PGPException("Message failed integrity check");
+ }
+}
+----
+
+The older symmetrically encrypted data packet carries no such code, so a
message using it skipped the
+check entirely. Because the packet type is chosen by whoever produced the
message, that left the sender
+— or anyone able to rewrite the message in transit — deciding whether the
check applied. The existing
+`integrity` option governs marshalling only and has no decrypt-side
counterpart.
+
+A new `requireIntegrityProtection` option, **defaulting to `true`**, now
rejects a message that is not
+integrity protected. Routes that must interoperate with a sender still
emitting the legacy packet have to
+set `requireIntegrityProtection=false` explicitly.
+
+Note that `signatureVerificationOption` still defaults to `optional`, which
accepts a message carrying no
+signature at all. Set it to `required` where the sender is expected to sign;
the two options together are
+what give a decrypted message authenticity as well as confidentiality.
+
=== camel-http
Credentials are no longer sent to an authority the endpoint was not configured
with. Two paths reached that
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java
index e7f5ca05d1b7..5a40d6b83956 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java
@@ -11188,6 +11188,7 @@ public final class ModelDeserializers extends
YamlDeserializerSupport {
@YamlProperty(name = "keyUserid", type = "string",
description = "The user ID of the key in the PGP keyring used during
encryption. Can also be only a part of a user ID.", displayName = "Key Userid"),
@YamlProperty(name = "password", type = "string",
description = "Password used when opening the private key (not used for
encryption).", displayName = "Password"),
@YamlProperty(name = "provider", type = "string",
description = "Java Cryptography Extension (JCE) provider, default is Bouncy
Castle (BC). Alternatively you can use, for example, the IAIK JCE provider.",
displayName = "Provider"),
+ @YamlProperty(name = "requireIntegrityProtection", type =
"boolean", defaultValue = "true", description = "Whether a message must be
integrity protected in order to be decrypted. The legacy symmetrically
encrypted data packet carries no modification detection code, and the packet
type is chosen by whoever produced the message, so accepting it lets the sender
decide whether the integrity check applies. Set to false only to interoperate
with a sender that still emits th [...]
@YamlProperty(name = "signatureKeyFileName", type =
"string", description = "Filename of the keyring to use for signing (during
encryption) or for signature verification (during decryption); must be
accessible as a classpath resource (but you can specify a location in the file
system by using the file: prefix).", displayName = "Signature Key File Name"),
@YamlProperty(name = "signatureKeyRing", type = "string",
description = "Keyring used for signing/verifying as byte array. You cannot set
the signatureKeyFileName and signatureKeyRing at the same time.", displayName =
"Signature Key Ring"),
@YamlProperty(name = "signatureKeyUserid", type =
"string", description = "User ID of the key in the PGP keyring used for signing
(during encryption) or signature verification (during decryption).",
displayName = "Signature Key Userid"),
@@ -11260,6 +11261,11 @@ public final class ModelDeserializers extends
YamlDeserializerSupport {
target.setProvider(val);
break;
}
+ case "requireIntegrityProtection": {
+ String val = asText(node);
+ target.setRequireIntegrityProtection(val);
+ break;
+ }
case "signatureKeyFileName": {
String val = asText(node);
target.setSignatureKeyFileName(val);
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
index 84756762cd55..5c915ce5da31 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
@@ -8065,6 +8065,12 @@
"title" : "Provider",
"description" : "Java Cryptography Extension (JCE) provider,
default is Bouncy Castle (BC). Alternatively you can use, for example, the IAIK
JCE provider."
},
+ "requireIntegrityProtection" : {
+ "type" : "boolean",
+ "title" : "Require Integrity Protection",
+ "description" : "Whether a message must be integrity protected in
order to be decrypted. The legacy symmetrically encrypted data packet carries
no modification detection code, and the packet type is chosen by whoever
produced the message, so accepting it lets the sender decide whether the
integrity check applies. Set to false only to interoperate with a sender that
still emits the legacy packet.",
+ "default" : true
+ },
"signatureKeyFileName" : {
"type" : "string",
"title" : "Signature Key File Name",
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
index 918fa669f3a7..2a19bbe7cccc 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
@@ -12742,6 +12742,12 @@
"type" : "string",
"description" : "Java Cryptography Extension (JCE) provider, default
is Bouncy Castle (BC). Alternatively you can use, for example, the IAIK JCE
provider.",
"title" : "Provider"
+ }, {
+ "name" : "requireIntegrityProtection",
+ "type" : "boolean",
+ "description" : "Whether a message must be integrity protected in
order to be decrypted. The legacy symmetrically encrypted data packet carries
no modification detection code, and the packet type is chosen by whoever
produced the message, so accepting it lets the sender decide whether the
integrity check applies. Set to false only to interoperate with a sender that
still emits the legacy packet.",
+ "title" : "Require Integrity Protection",
+ "default" : "true"
}, {
"name" : "signatureKeyFileName",
"type" : "string",
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
index 9818897680b4..9324a3669b58 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
@@ -11576,6 +11576,12 @@
"title" : "Provider",
"description" : "Java Cryptography Extension (JCE) provider,
default is Bouncy Castle (BC). Alternatively you can use, for example, the IAIK
JCE provider."
},
+ "requireIntegrityProtection" : {
+ "type" : "boolean",
+ "title" : "Require Integrity Protection",
+ "description" : "Whether a message must be integrity protected in
order to be decrypted. The legacy symmetrically encrypted data packet carries
no modification detection code, and the packet type is chosen by whoever
produced the message, so accepting it lets the sender decide whether the
integrity check applies. Set to false only to interoperate with a sender that
still emits the legacy packet.",
+ "default" : true
+ },
"signatureKeyFileName" : {
"type" : "string",
"title" : "Signature Key File Name",