From 62d96f9734da00a943256a43ab91c3491e8ef90d Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Mon, 31 Aug 2026 10:36:57 +0200 Subject: [PATCH 1/5] CAMEL-24454: camel-mllp - default logPhi to false and honour it on the paths that ignored it (#25832) MllpComponent.logPhi defaulted to true, so message content reached the log at the default INFO/WARN levels with no configuration at all. For a protocol whose payload is patient data by definition, the safe default is the other way round. Two paths logged content regardless of the flag, because MllpSocketBuffer has no logPhi of its own: the partial-payload warning in readFrom(), which logs the content of a legitimate in-flight message from a slow sender rather than only unexpected bytes, and the bytes-before-START_OF_BLOCK warning in readSocketInputStream(). Both now go through a helper that honours the setting, printing when it is off. The suppression is applied at the log statements, via a new Hl7Util.convertToLoggableString, rather than inside convertToPrintFriendlyString. That method is not a logging helper despite the name: generateAcknowledgementPayload() uses it to extract the MSH-9 field, so redacting inside it corrupts the acknowledgement rather than the log - which is what the MLLP suite showed when it was tried that way. Both methods now carry a javadoc saying so. LogPhiTest.testLogPhiDefault asserted that the default includes the payload; it now asserts the opposite. testLogPhiFalse and testLogPhiTrue are unchanged and still pass, so the flag still works in both directions - only the default moved. Signed-off-by: Andrea Cosentino (cherry picked from commit dd2bf57190c12ff7f1e97f121738867a680992ce) MllpSocketBuffer keeps this branch's synchronized methods rather than main's ReentrantLock form, and its existing readFrom structure; the substantive change, convertToLoggableString in the partial-payload warning plus the new toLoggableStringAndReset, is carried across. Co-authored-by: Claude Opus 5 (1M context) --- .../apache/camel/catalog/components/mllp.json | 2 +- .../org/apache/camel/component/mllp/mllp.json | 2 +- .../camel/component/mllp/MllpComponent.java | 4 +-- .../component/mllp/MllpTcpServerConsumer.java | 4 +-- .../component/mllp/internal/Hl7Util.java | 30 +++++++++++++++++++ .../mllp/internal/MllpSocketBuffer.java | 16 ++++++++-- .../internal/TcpSocketConsumerRunnable.java | 10 +++---- .../camel/component/mllp/LogPhiTest.java | 6 +++- .../component/mllp/internal/Hl7UtilTest.java | 9 ++++++ .../dsl/MllpComponentBuilderFactory.java | 2 +- 10 files changed, 70 insertions(+), 15 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/mllp.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/mllp.json index d52dd544be9cb..637ac1d3bb852 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/mllp.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/mllp.json @@ -47,7 +47,7 @@ "tcpNoDelay": { "index": 20, "kind": "property", "displayName": "Tcp No Delay", "group": "producer", "label": "advanced,producer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "configurationClass": "org.apache.camel.component.mllp.MllpConfiguration", "configurationField": "configuration", "description": "Enable\/disable the TCP_NODELAY socket option." }, "autowiredEnabled": { "index": 21, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching type, which then gets configured on the component. This can be used for automatic configuring JDBC data sources, JMS connection factories, AWS Clients, etc." }, "defaultCharset": { "index": 22, "kind": "property", "displayName": "Default Charset", "group": "advanced", "label": "advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "ISO-8859-1", "description": "Set the default character set to use for byte to\/from String conversions." }, - "logPhi": { "index": 23, "kind": "property", "displayName": "Log Phi", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether to log PHI" }, + "logPhi": { "index": 23, "kind": "property", "displayName": "Log Phi", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether to log PHI" }, "logPhiMaxBytes": { "index": 24, "kind": "property", "displayName": "Log Phi Max Bytes", "group": "advanced", "label": "advanced", "required": false, "type": "integer", "javaType": "java.lang.Integer", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 5120, "description": "Set the maximum number of bytes of PHI that will be logged in a log entry." }, "maxBufferSize": { "index": 25, "kind": "property", "displayName": "Max Buffer Size", "group": "advanced", "label": "advanced", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 1073741824, "configurationClass": "org.apache.camel.component.mllp.MllpConfiguration", "configurationField": "configuration", "description": "Maximum buffer size used when receiving or sending data over the wire." }, "minBufferSize": { "index": 26, "kind": "property", "displayName": "Min Buffer Size", "group": "advanced", "label": "advanced", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 2048, "configurationClass": "org.apache.camel.component.mllp.MllpConfiguration", "configurationField": "configuration", "description": "Minimum buffer size used when receiving or sending data over the wire." }, diff --git a/components/camel-mllp/src/generated/resources/META-INF/org/apache/camel/component/mllp/mllp.json b/components/camel-mllp/src/generated/resources/META-INF/org/apache/camel/component/mllp/mllp.json index d52dd544be9cb..637ac1d3bb852 100644 --- a/components/camel-mllp/src/generated/resources/META-INF/org/apache/camel/component/mllp/mllp.json +++ b/components/camel-mllp/src/generated/resources/META-INF/org/apache/camel/component/mllp/mllp.json @@ -47,7 +47,7 @@ "tcpNoDelay": { "index": 20, "kind": "property", "displayName": "Tcp No Delay", "group": "producer", "label": "advanced,producer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "configurationClass": "org.apache.camel.component.mllp.MllpConfiguration", "configurationField": "configuration", "description": "Enable\/disable the TCP_NODELAY socket option." }, "autowiredEnabled": { "index": 21, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching type, which then gets configured on the component. This can be used for automatic configuring JDBC data sources, JMS connection factories, AWS Clients, etc." }, "defaultCharset": { "index": 22, "kind": "property", "displayName": "Default Charset", "group": "advanced", "label": "advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "ISO-8859-1", "description": "Set the default character set to use for byte to\/from String conversions." }, - "logPhi": { "index": 23, "kind": "property", "displayName": "Log Phi", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether to log PHI" }, + "logPhi": { "index": 23, "kind": "property", "displayName": "Log Phi", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether to log PHI" }, "logPhiMaxBytes": { "index": 24, "kind": "property", "displayName": "Log Phi Max Bytes", "group": "advanced", "label": "advanced", "required": false, "type": "integer", "javaType": "java.lang.Integer", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 5120, "description": "Set the maximum number of bytes of PHI that will be logged in a log entry." }, "maxBufferSize": { "index": 25, "kind": "property", "displayName": "Max Buffer Size", "group": "advanced", "label": "advanced", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 1073741824, "configurationClass": "org.apache.camel.component.mllp.MllpConfiguration", "configurationField": "configuration", "description": "Maximum buffer size used when receiving or sending data over the wire." }, "minBufferSize": { "index": 26, "kind": "property", "displayName": "Min Buffer Size", "group": "advanced", "label": "advanced", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 2048, "configurationClass": "org.apache.camel.component.mllp.MllpConfiguration", "configurationField": "configuration", "description": "Minimum buffer size used when receiving or sending data over the wire." }, diff --git a/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/MllpComponent.java b/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/MllpComponent.java index bffd141c42e46..e0b6eef63d5d6 100644 --- a/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/MllpComponent.java +++ b/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/MllpComponent.java @@ -29,8 +29,8 @@ @Component("mllp") public class MllpComponent extends DefaultComponent implements SSLContextParametersAware { - @Metadata(label = "advanced", defaultValue = "true") - private boolean logPhi = true; + @Metadata(label = "advanced", defaultValue = "false") + private boolean logPhi; @Metadata(label = "advanced", defaultValue = "5120") private int logPhiMaxBytes = 5120; @Metadata(label = "advanced", defaultValue = "ISO-8859-1") diff --git a/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/MllpTcpServerConsumer.java b/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/MllpTcpServerConsumer.java index 638d8537f6d46..4d88b75659598 100644 --- a/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/MllpTcpServerConsumer.java +++ b/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/MllpTcpServerConsumer.java @@ -633,7 +633,7 @@ void sendAcknowledgement(byte[] originalHl7MessageBytes, Exchange exchange, TcpS log.debug("sendAcknowledgement(originalHl7MessageBytes[{}], Exchange[{}], {}) - Sending Acknowledgement: {}", originalHl7MessageBytes == null ? -1 : originalHl7MessageBytes.length, exchange.getExchangeId(), consumerRunnable.getSocket(), - consumerRunnable.getMllpBuffer().toPrintFriendlyHl7String()); + hl7Util.convertToLoggableString(consumerRunnable.getMllpBuffer().toMllpPayload())); } try { @@ -657,7 +657,7 @@ void sendAcknowledgement(byte[] originalHl7MessageBytes, Exchange exchange, TcpS log.debug("sendAcknowledgement(originalHl7MessageBytes[{}], Exchange[{}], {}) - Sending Acknowledgement: {}", originalHl7MessageBytes == null ? -1 : originalHl7MessageBytes.length, exchange.getExchangeId(), consumerRunnable.getSocket(), - hl7Util.convertToPrintFriendlyString(acknowledgementMessageBytes)); + hl7Util.convertToLoggableString(acknowledgementMessageBytes)); } try { diff --git a/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/internal/Hl7Util.java b/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/internal/Hl7Util.java index 4d2c39e8eb026..9a69beab833d3 100644 --- a/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/internal/Hl7Util.java +++ b/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/internal/Hl7Util.java @@ -32,6 +32,11 @@ public final class Hl7Util { public static final String NULL_REPLACEMENT_VALUE = ""; public static final String EMPTY_REPLACEMENT_VALUE = "<>"; + /** + * Substituted for message content when {@code logPhi} is disabled. The conversion methods are reached from log + * statements and from exception messages, so honouring the flag here covers both. + */ + public static final String PHI_SUPPRESSED_REPLACEMENT_VALUE = ""; public static final Map CHARACTER_REPLACEMENTS; @@ -91,6 +96,31 @@ public int getLogPhiMaxBytes() { return logPhiMaxBytes; } + public boolean isLogPhi() { + return logPhi; + } + + /** + * Message content for a log statement: the content itself when {@code logPhi} is enabled, a placeholder otherwise. + *

+ * Deliberately separate from {@link #convertToPrintFriendlyString(byte[])}, which is not a logging helper - it also + * extracts the MSH-9 field when an acknowledgement is generated, so redacting inside it corrupts the + * acknowledgement rather than the log. + */ + public String convertToLoggableString(byte[] phiBytes) { + if (!logPhi) { + return PHI_SUPPRESSED_REPLACEMENT_VALUE; + } + return convertToPrintFriendlyString(phiBytes); + } + + public String convertToLoggableString(byte[] phiBytes, int startPosition, int endPosition) { + if (!logPhi) { + return PHI_SUPPRESSED_REPLACEMENT_VALUE; + } + return convertToPrintFriendlyString(phiBytes, startPosition, endPosition); + } + public String generateInvalidPayloadExceptionMessage(final byte[] hl7Bytes) { if (hl7Bytes == null) { return "HL7 payload is null"; diff --git a/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/internal/MllpSocketBuffer.java b/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/internal/MllpSocketBuffer.java index 73ed4ea0b62c3..047f0d0c57b86 100644 --- a/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/internal/MllpSocketBuffer.java +++ b/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/internal/MllpSocketBuffer.java @@ -209,7 +209,7 @@ public synchronized void readFrom(Socket socket, int receiveTimeout, int readTim if (size() > 0 && !hasCompleteEnvelope()) { if (!hasEndOfData() && hasEndOfBlock() && endOfBlockIndex < size() - 1) { LOG.warn("readFrom({}, {}, {}) - exiting with partial payload {}", socket, receiveTimeout, readTimeout, - hl7Util.convertToPrintFriendlyString(buffer, 0, size() - 1)); + hl7Util.convertToLoggableString(buffer, 0, size() - 1)); } } } @@ -327,6 +327,18 @@ public String toPrintFriendlyStringAndReset() { return answer; } + /** + * The buffer content for a log statement, honouring the component's {@code logPhi} setting, and resetting the + * buffer either way so the caller's behaviour does not depend on whether logging is enabled. + */ + public synchronized String toLoggableStringAndReset() { + String answer = hl7Util.isLogPhi() ? toPrintFriendlyString() : Hl7Util.PHI_SUPPRESSED_REPLACEMENT_VALUE; + + reset(); + + return answer; + } + public synchronized String toHl7String() { return this.toHl7String(charset); } @@ -562,7 +574,7 @@ void readSocketInputStream(InputStream socketInputStream, Socket socket) } else { LOG.warn( "readSocketInputStream(socketInputStream, {}) - ignoring {} bytes received before START_OF_BLOCK: {}", - socket, size(), toPrintFriendlyStringAndReset()); + socket, size(), toLoggableStringAndReset()); } } } catch (SocketTimeoutException timeoutEx) { diff --git a/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/internal/TcpSocketConsumerRunnable.java b/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/internal/TcpSocketConsumerRunnable.java index f8c6da04cd7aa..a35af9e909bb4 100644 --- a/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/internal/TcpSocketConsumerRunnable.java +++ b/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/internal/TcpSocketConsumerRunnable.java @@ -149,16 +149,16 @@ public void run() { hl7MessageBytes = mllpBuffer.toMllpPayload(); if (log.isDebugEnabled()) { log.debug("Received {} byte message {}", hl7MessageBytes.length, - hl7Util.convertToPrintFriendlyString(hl7MessageBytes)); + hl7Util.convertToLoggableString(hl7MessageBytes)); } if (mllpBuffer.hasLeadingOutOfBandData()) { // TODO: Move the conversion utilities to the MllpSocketBuffer to avoid a byte[] copy log.warn("Ignoring leading out-of-band data: {}", - hl7Util.convertToPrintFriendlyString(mllpBuffer.getLeadingOutOfBandData())); + hl7Util.convertToLoggableString(mllpBuffer.getLeadingOutOfBandData())); } if (mllpBuffer.hasTrailingOutOfBandData()) { log.warn("Ignoring trailing out-of-band data: {}", - hl7Util.convertToPrintFriendlyString(mllpBuffer.getTrailingOutOfBandData())); + hl7Util.convertToLoggableString(mllpBuffer.getTrailingOutOfBandData())); } mllpBuffer.reset(); @@ -166,12 +166,12 @@ public void run() { } else if (!mllpBuffer.hasStartOfBlock()) { byte[] payload = mllpBuffer.toByteArray(); log.warn("Ignoring {} byte un-enveloped payload {}", payload.length, - hl7Util.convertToPrintFriendlyString(payload)); + hl7Util.convertToLoggableString(payload)); mllpBuffer.reset(); } else if (!mllpBuffer.isEmpty()) { byte[] payload = mllpBuffer.toByteArray(); log.warn("Partial {} byte payload received {}", payload.length, - hl7Util.convertToPrintFriendlyString(payload)); + hl7Util.convertToLoggableString(payload)); } } catch (SocketTimeoutException timeoutEx) { if (mllpBuffer.isEmpty()) { diff --git a/components/camel-mllp/src/test/java/org/apache/camel/component/mllp/LogPhiTest.java b/components/camel-mllp/src/test/java/org/apache/camel/component/mllp/LogPhiTest.java index dc80cb5bd1daf..39eb4705aab3b 100644 --- a/components/camel-mllp/src/test/java/org/apache/camel/component/mllp/LogPhiTest.java +++ b/components/camel-mllp/src/test/java/org/apache/camel/component/mllp/LogPhiTest.java @@ -107,9 +107,13 @@ public void testLogPhiTrue() throws Exception { testLogPhi(startLogPhi, exceptionMessage -> assertTrue(exceptionMessage.contains("hl7Message"))); } + /** + * logPhi now defaults to false, so the default behaves like testLogPhiFalse: for a protocol whose payload is + * patient data by definition, content stays out of the log unless it is asked for. + */ @Test public void testLogPhiDefault() throws Exception { - testLogPhi(startDefaultPhi, exceptionMessage -> assertTrue(exceptionMessage.contains("hl7Message"))); + testLogPhi(startDefaultPhi, exceptionMessage -> assertFalse(exceptionMessage.contains("hl7Message"))); } public void testLogPhi(Endpoint endpoint, Consumer contains) { diff --git a/components/camel-mllp/src/test/java/org/apache/camel/component/mllp/internal/Hl7UtilTest.java b/components/camel-mllp/src/test/java/org/apache/camel/component/mllp/internal/Hl7UtilTest.java index 0103c2a4bfbc4..82b1cba72ba83 100644 --- a/components/camel-mllp/src/test/java/org/apache/camel/component/mllp/internal/Hl7UtilTest.java +++ b/components/camel-mllp/src/test/java/org/apache/camel/component/mllp/internal/Hl7UtilTest.java @@ -682,6 +682,15 @@ public void testConvertToPrintFriendlyStringWithPhiMaxBytes() { assertEquals("MSH", result); } + @Test + public void testConvertToLoggableStringHonorsLogPhi() { + Hl7Util suppressed = new Hl7Util(5120, false); + + assertEquals(Hl7Util.PHI_SUPPRESSED_REPLACEMENT_VALUE, + suppressed.convertToLoggableString(TEST_MESSAGE_BYTES)); + assertEquals(EXPECTED_MESSAGE, hl7util.convertToLoggableString(TEST_MESSAGE_BYTES)); + } + @Test public void testGenerateAcknowledgementPayloadWithOnlyMsh91() throws Exception { final MllpSocketBuffer mllpSocketBuffer = new MllpSocketBuffer(new MllpEndpointStub()); diff --git a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MllpComponentBuilderFactory.java b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MllpComponentBuilderFactory.java index 024a459c66d81..9f5ef2ae91551 100644 --- a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MllpComponentBuilderFactory.java +++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/MllpComponentBuilderFactory.java @@ -495,7 +495,7 @@ default MllpComponentBuilder defaultCharset(java.lang.String defaultCharset) { * * The option is a: <code>java.lang.Boolean</code> type. * - * Default: true + * Default: false * Group: advanced * * @param logPhi the value to set From 26e2f3f1580839185e6249baff9653f59e9f5b52 Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Mon, 31 Aug 2026 10:36:58 +0200 Subject: [PATCH 2/5] CAMEL-24443: camel-knative - do not trust every certificate when SSL is enabled without a truststore (#25824) KnativeSslClientOptions.configureOptions() installed TrustAllOptions.INSTANCE - a trust manager that accepts every certificate - whenever camel.knative.client.ssl.enabled was true and neither truststore.path nor trust.cert.path was set. No option named trustAll was involved: enabling TLS was itself what turned certificate validation off. Hostname verification in the same method already defaults to true, so the trust decision was the outlier, and KnativeOidcClientOptions extends this class. Leave the trust options unset in that case instead, so the JVM default trust anchors apply - the fallback SSLContextParameters and the rest of Camel use. Accepting any certificate stays available behind the new camel.knative.client.ssl.trust.all property, which defaults to false. KnativeHttpTest.testSecureClientOptionsPropertyConf configures SSL entirely through properties against a self-signed test server, so it relied on the old fallback; it now sets trust.all explicitly, which is the same migration an affected deployment makes. Signed-off-by: Andrea Cosentino (cherry picked from commit 26c899d818b8f33732da3678ae00de6af351b622) Co-authored-by: Claude Opus 5 (1M context) --- .../knative/http/KnativeSslClientOptions.java | 8 ++- .../knative/http/KnativeHttpTest.java | 3 + .../KnativeSslClientOptionsTrustTest.java | 66 +++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 components/camel-knative/camel-knative-http/src/test/java/org/apache/camel/component/knative/http/KnativeSslClientOptionsTrustTest.java diff --git a/components/camel-knative/camel-knative-http/src/main/java/org/apache/camel/component/knative/http/KnativeSslClientOptions.java b/components/camel-knative/camel-knative-http/src/main/java/org/apache/camel/component/knative/http/KnativeSslClientOptions.java index 65ec42fa3da99..67ac05a7ce4e9 100644 --- a/components/camel-knative/camel-knative-http/src/main/java/org/apache/camel/component/knative/http/KnativeSslClientOptions.java +++ b/components/camel-knative/camel-knative-http/src/main/java/org/apache/camel/component/knative/http/KnativeSslClientOptions.java @@ -116,10 +116,16 @@ public void configureOptions(CamelContext camelContext) { } else if (trustCertPath.isPresent()) { String[] trustCertPathItems = trustCertPath.get().split(","); setTrustCertPath(trustCertPathItems); - } else { + } else if (Boolean.parseBoolean( + propertiesComponent.resolveProperty(PROPERTY_PREFIX + "trust.all").orElse("false"))) { + // Explicitly asked for. Useful against a development cluster with a self-signed + // certificate, but it has to be requested rather than being what "no truststore" means. trustOptions = TrustAllOptions.INSTANCE; setTrustOptions(trustOptions); } + // Otherwise leave the trust options unset, so the JVM default trust anchors apply - the same + // fallback SSLContextParameters and the rest of Camel use. Turning TLS on must not be the + // thing that turns certificate validation off. } } diff --git a/components/camel-knative/camel-knative-http/src/test/java/org/apache/camel/component/knative/http/KnativeHttpTest.java b/components/camel-knative/camel-knative-http/src/test/java/org/apache/camel/component/knative/http/KnativeHttpTest.java index 8a7d24be6c510..3b00029394299 100644 --- a/components/camel-knative/camel-knative-http/src/test/java/org/apache/camel/component/knative/http/KnativeHttpTest.java +++ b/components/camel-knative/camel-knative-http/src/test/java/org/apache/camel/component/knative/http/KnativeHttpTest.java @@ -2249,6 +2249,9 @@ void testSecureClientOptionsPropertyConf(CloudEvent ce) throws Exception { context.getPropertiesComponent().addInitialProperty("camel.knative.client.ssl.verify.hostname", "false"); context.getPropertiesComponent().addInitialProperty("camel.knative.client.ssl.key.path", "keystore/client.pem"); context.getPropertiesComponent().addInitialProperty("camel.knative.client.ssl.key.cert.path", "keystore/client.crt"); + // The test server presents a self-signed certificate. Enabling SSL no longer implies trusting every + // certificate, so the trust decision has to be made here. + context.getPropertiesComponent().addInitialProperty("camel.knative.client.ssl.trust.all", "true"); KnativeComponent component = configureKnativeComponent( context, diff --git a/components/camel-knative/camel-knative-http/src/test/java/org/apache/camel/component/knative/http/KnativeSslClientOptionsTrustTest.java b/components/camel-knative/camel-knative-http/src/test/java/org/apache/camel/component/knative/http/KnativeSslClientOptionsTrustTest.java new file mode 100644 index 0000000000000..2abecc3a4da20 --- /dev/null +++ b/components/camel-knative/camel-knative-http/src/test/java/org/apache/camel/component/knative/http/KnativeSslClientOptionsTrustTest.java @@ -0,0 +1,66 @@ +/* + * 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.knative.http; + +import java.util.Properties; + +import org.apache.camel.CamelContext; +import org.apache.camel.impl.DefaultCamelContext; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Enabling SSL is not a request to stop validating certificates. With no truststore and no trust certificates + * configured the client has to fall back to the JVM default trust anchors, the way the rest of Camel does; accepting + * every certificate is available, but only when it is asked for. + */ +class KnativeSslClientOptionsTrustTest { + + @Test + void sslEnabledWithoutATruststoreDoesNotTrustEveryCertificate() throws Exception { + try (CamelContext context = contextWith("camel.knative.client.ssl.enabled", "true")) { + KnativeSslClientOptions options = new KnativeSslClientOptions(context); + + assertThat(options.isSslEnabled()).isTrue(); + // Left unset, so Vert.x falls back to the JVM default trust anchors + assertThat(options.getTrustOptions()).isNull(); + } + } + + @Test + void trustAllIsAvailableButHasToBeRequested() throws Exception { + try (CamelContext context = contextWith( + "camel.knative.client.ssl.enabled", "true", + "camel.knative.client.ssl.trust.all", "true")) { + KnativeSslClientOptions options = new KnativeSslClientOptions(context); + + assertThat(options.getTrustOptions()).isInstanceOf(TrustAllOptions.class); + } + } + + private static CamelContext contextWith(String... keyValues) throws Exception { + Properties properties = new Properties(); + for (int i = 0; i < keyValues.length; i += 2) { + properties.setProperty(keyValues[i], keyValues[i + 1]); + } + CamelContext context = new DefaultCamelContext(); + context.getPropertiesComponent().setInitialProperties(properties); + context.start(); + return context; + } +} From 78754530a16f0b528de62cfa7b635ffd9df555d6 Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Mon, 31 Aug 2026 10:36:58 +0200 Subject: [PATCH 3/5] CAMEL-24437: camel-oauth - bind the authorization code callback to the flow with a state parameter (#25821) buildCodeFlowAuthRequestUrl() sent only a redirect URI and scopes - no state - and OAuthCodeFlowCallback redeemed whatever code arrived and bound the resulting profile to the caller's session. Nothing tied the callback to a flow that session had started, which is the login CSRF that RFC 6749 section 10.12 and OpenID Connect Core require the state binding to prevent. OAuthCodeFlowParams already carried a state field; no processor set it. The hardcoded SameSite=None; Secure session cookie makes it reachable cross-site. OAuthCodeFlowProcessor now generates a 32-byte random state, stores it in the OAuth session, and passes it through both URL builders - VertxOAuth via OAuth2AuthorizationURL.setState, ServletOAuth as a state query parameter. OAuthCodeFlowCallback removes the stored value, so it is single use, and compares it with the callback's state using MessageDigest.isEqual. A callback with no flow in progress, or with a state that does not match, is answered with 400 and stops the route. Scope: state only. nonce needs ID-token validation to be worth sending, PKCE needs a code_verifier carried through AuthCodeCredentials and both authenticate() implementations, and the session cookie's SameSite is a separate change - all three are noted on the issue. Not verified end to end: OAuthCodeFlowVertxTest and OAuthCodeFlowServletTest are gated on an externally running Keycloak at https://oauth.localtest.me/kc, provisioned by the module's Helm chart, and were skipped here. They are what would confirm the provider echoes state back as a state message header. Signed-off-by: Andrea Cosentino (cherry picked from commit 495c5ad39a0f95f823da512b38da5c19609e2a1c) Co-authored-by: Claude Opus 5 (1M context) --- .../camel/oauth/OAuthCodeFlowCallback.java | 21 +++++ .../camel/oauth/OAuthCodeFlowProcessor.java | 19 ++++- .../org/apache/camel/oauth/OAuthSession.java | 6 ++ .../camel/oauth/jakarta/ServletOAuth.java | 3 + .../apache/camel/oauth/vertx/VertxOAuth.java | 8 +- .../oauth/OAuthProcessorFailClosedTest.java | 85 +++++++++++++++++++ 6 files changed, 139 insertions(+), 3 deletions(-) diff --git a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowCallback.java b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowCallback.java index 677305dd5cf2c..c80eda2a7763d 100644 --- a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowCallback.java +++ b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowCallback.java @@ -16,12 +16,16 @@ */ package org.apache.camel.oauth; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + import org.apache.camel.Exchange; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.apache.camel.oauth.OAuth.CAMEL_OAUTH_REDIRECT_URI; import static org.apache.camel.oauth.OAuthProperties.getRequiredProperty; +import static org.apache.camel.oauth.OAuthSession.OAUTH_STATE; public class OAuthCodeFlowCallback extends AbstractOAuthProcessor { @@ -48,6 +52,23 @@ public void process(Exchange exchange) { var oauth = findOAuthOrThrow(context); var session = oauth.getOrCreateSession(exchange); + // The state must match the one this session sent to the identity provider, and it is single use. + // Without that binding any authorization code, obtained in any browser, could be redeemed here and + // bound to whichever session presented it - which is login CSRF. + var expectedState = session. removeValue(OAUTH_STATE).orElse(null); + var actualState = msg.getHeader("state", String.class); + if (expectedState == null) { + log.error("No authorization code flow is in progress for this session"); + reject(exchange, 400, "No authorization code flow in progress"); + return; + } + if (actualState == null || !MessageDigest.isEqual( + expectedState.getBytes(StandardCharsets.UTF_8), actualState.getBytes(StandardCharsets.UTF_8))) { + log.error("Authorization state does not match the flow started by this session"); + reject(exchange, 400, "Authorization state mismatch"); + return; + } + // Exchange the authorization code for access/refresh/id tokens // String redirectUri = getRequiredProperty(exchange.getContext(), CAMEL_OAUTH_REDIRECT_URI); diff --git a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowProcessor.java b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowProcessor.java index 2de2d8969b28a..0ef855c34c04b 100644 --- a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowProcessor.java +++ b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowProcessor.java @@ -16,6 +16,9 @@ */ package org.apache.camel.oauth; +import java.security.SecureRandom; +import java.util.Base64; + import org.apache.camel.Exchange; import org.apache.camel.Message; import org.slf4j.Logger; @@ -23,9 +26,12 @@ import static org.apache.camel.oauth.OAuth.CAMEL_OAUTH_REDIRECT_URI; import static org.apache.camel.oauth.OAuthProperties.getRequiredProperty; +import static org.apache.camel.oauth.OAuthSession.OAUTH_STATE; public class OAuthCodeFlowProcessor extends AbstractOAuthProcessor { + private static final SecureRandom RANDOM = new SecureRandom(); + private final Logger log = LoggerFactory.getLogger(getClass()); @Override @@ -65,8 +71,13 @@ public void process(Exchange exchange) { log.info("Register post login url: {}", postLoginUrl); session.putValue("OAuthPostLoginUrl", postLoginUrl); + // RFC 6749 section 10.12: bind the callback to a flow this session actually started, so an + // authorization code obtained elsewhere cannot be replayed into this session's callback. + var state = newState(); + session.putValue(OAUTH_STATE, state); + var redirectUri = getRequiredProperty(exchange.getContext(), CAMEL_OAUTH_REDIRECT_URI); - var params = new OAuthCodeFlowParams().setRedirectUri(redirectUri); + var params = new OAuthCodeFlowParams().setRedirectUri(redirectUri).setState(state); var authRequestUrl = oauth.buildCodeFlowAuthRequestUrl(params); sendRedirect(msg, authRequestUrl); @@ -75,6 +86,12 @@ public void process(Exchange exchange) { exchange.setRouteStop(true); } + private static String newState() { + var bytes = new byte[32]; + RANDOM.nextBytes(bytes); + return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); + } + private String getPostLoginUrl(Message msg) { String postLoginUrl; var xProto = msg.getHeader("X-Forwarded-Proto", String.class); diff --git a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthSession.java b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthSession.java index 30a986f5e5041..30a7b6ef38169 100644 --- a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthSession.java +++ b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthSession.java @@ -20,6 +20,12 @@ public interface OAuthSession { + /** + * Session key holding the {@code state} value of an in-flight authorization code flow. The callback accepts a code + * only when the request carries the same value, which is what ties the callback to a flow this session started. + */ + String OAUTH_STATE = "OAuthState"; + String getSessionId(); Optional getValue(String key, Class clazz); diff --git a/components/camel-oauth/src/main/java/org/apache/camel/oauth/jakarta/ServletOAuth.java b/components/camel-oauth/src/main/java/org/apache/camel/oauth/jakarta/ServletOAuth.java index 3c3b62ca1f1b6..50cce2027bdb0 100644 --- a/components/camel-oauth/src/main/java/org/apache/camel/oauth/jakarta/ServletOAuth.java +++ b/components/camel-oauth/src/main/java/org/apache/camel/oauth/jakarta/ServletOAuth.java @@ -105,6 +105,9 @@ public String buildCodeFlowAuthRequestUrl(OAuthCodeFlowParams params) { uriBuilder.addParameter("response_type", params.getResponseType().toString().toLowerCase()); uriBuilder.addParameter("client_id", params.getClientId()); uriBuilder.addParameter("redirect_uri", params.getRedirectUri()); + if (params.getState() != null) { + uriBuilder.addParameter("state", params.getState()); + } var requestUrl = uriBuilder.build().toString(); return requestUrl; } catch (URISyntaxException ex) { diff --git a/components/camel-oauth/src/main/java/org/apache/camel/oauth/vertx/VertxOAuth.java b/components/camel-oauth/src/main/java/org/apache/camel/oauth/vertx/VertxOAuth.java index 09c27afda954d..b7a98acc54a03 100644 --- a/components/camel-oauth/src/main/java/org/apache/camel/oauth/vertx/VertxOAuth.java +++ b/components/camel-oauth/src/main/java/org/apache/camel/oauth/vertx/VertxOAuth.java @@ -95,9 +95,13 @@ public String buildCodeFlowAuthRequestUrl(OAuthCodeFlowParams params) { if (params.getScopes() == null) { params.setScope("openid"); } - return oauth2.authorizeURL(new OAuth2AuthorizationURL() + var url = new OAuth2AuthorizationURL() .setRedirectUri(params.getRedirectUri()) - .setScopes(params.getScopes())); + .setScopes(params.getScopes()); + if (params.getState() != null) { + url.setState(params.getState()); + } + return oauth2.authorizeURL(url); } @Override diff --git a/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthProcessorFailClosedTest.java b/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthProcessorFailClosedTest.java index 4da45757e97eb..a619c95c4c508 100644 --- a/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthProcessorFailClosedTest.java +++ b/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthProcessorFailClosedTest.java @@ -16,6 +16,7 @@ */ package org.apache.camel.oauth; +import org.apache.camel.CamelContext; import org.apache.camel.Exchange; import org.apache.camel.impl.DefaultCamelContext; import org.apache.camel.support.DefaultExchange; @@ -68,4 +69,88 @@ void missingAuthorizationCodeStopsTheRoute() throws Exception { assertTrue(exchange.isRouteStop()); } } + + /** + * The state check runs before the authorization code is redeemed, so these paths are reachable without an identity + * provider. Without the check, an authorization code obtained in any browser could be redeemed at this callback and + * bound to whichever session presented it. + */ + @Test + void aCallbackWithNoFlowInProgressStopsTheRoute() throws Exception { + try (DefaultCamelContext context = new DefaultCamelContext()) { + Exchange exchange = new DefaultExchange(context); + exchange.getMessage().setHeader("code", "an-authorization-code"); + exchange.getMessage().setHeader("state", "a-state-we-never-issued"); + + bindTestOAuth(context); + new OAuthCodeFlowCallback().process(exchange); + + assertEquals(400, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)); + assertEquals("No authorization code flow in progress", exchange.getMessage().getBody()); + assertTrue(exchange.isRouteStop()); + } + } + + @Test + void aCallbackWithTheWrongStateStopsTheRoute() throws Exception { + try (DefaultCamelContext context = new DefaultCamelContext()) { + Exchange exchange = new DefaultExchange(context); + exchange.getMessage().setHeader("code", "an-authorization-code"); + exchange.getMessage().setHeader("state", "not-the-issued-state"); + + OAuth oauth = bindTestOAuth(context); + oauth.getOrCreateSession(exchange).putValue(OAuthSession.OAUTH_STATE, "the-issued-state"); + new OAuthCodeFlowCallback().process(exchange); + + assertEquals(400, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)); + assertEquals("Authorization state mismatch", exchange.getMessage().getBody()); + assertTrue(exchange.isRouteStop()); + } + } + + @Test + void aCallbackWithNoStateAtAllStopsTheRoute() throws Exception { + try (DefaultCamelContext context = new DefaultCamelContext()) { + Exchange exchange = new DefaultExchange(context); + exchange.getMessage().setHeader("code", "an-authorization-code"); + + OAuth oauth = bindTestOAuth(context); + oauth.getOrCreateSession(exchange).putValue(OAuthSession.OAUTH_STATE, "the-issued-state"); + new OAuthCodeFlowCallback().process(exchange); + + assertEquals(400, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)); + assertEquals("Authorization state mismatch", exchange.getMessage().getBody()); + assertTrue(exchange.isRouteStop()); + } + } + + /** + * The state check runs before the authorization code is redeemed, so none of the abstract operations are reached - + * which is part of what these tests assert. + */ + private static OAuth bindTestOAuth(DefaultCamelContext context) { + OAuth oauth = new OAuth() { + @Override + public void discoverOAuthConfig(CamelContext ctx) { + throw new UnsupportedOperationException(); + } + + @Override + public UserProfile authenticate(Credentials creds) { + throw new UnsupportedOperationException("the authorization code must not be redeemed"); + } + + @Override + public String buildLogoutRequestUrl(OAuthLogoutParams params) { + throw new UnsupportedOperationException(); + } + + @Override + public String buildCodeFlowAuthRequestUrl(OAuthCodeFlowParams params) { + throw new UnsupportedOperationException(); + } + }; + context.getRegistry().bind(OAuth.class.getName(), oauth); + return oauth; + } } From 2b171f0192afae7bc28b9edb042683956b337c0a Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Mon, 31 Aug 2026 10:36:59 +0200 Subject: [PATCH 4/5] CAMEL-24439: camel-shiro - verify the presented credentials on every exchange (#25822) * CAMEL-24439: camel-shiro - verify the presented credentials on every exchange ShiroSecurityProcessor.authenticateUser() called login() only when the thread-bound subject was not already authenticated for the same username as the incoming ShiroSecurityToken: if (!authenticated || !sameUser) { ... currentUser.login(token); } That conflates "same principal name" with "same credentials". Once a user had authenticated on a worker thread, a later exchange presenting that username with any password was accepted for as long as the subject stayed bound, because the password was never checked. The default alwaysReauthenticate=true masks it, since the processor calls logout() in a finally block after each exchange. With alwaysReauthenticate=false the skip is reachable, and that mode deliberately sets rememberMe(true) to keep subjects long-lived on Camel's shared worker threads. Call login() for every exchange with the credentials that exchange presented. Shiro offers no way to compare presented credentials against a bound subject, so the principal-name comparison could not be made sound and is removed rather than narrowed. The added test sends a valid token for ringo, then the same username with a wrong password on the same thread; without the fix both reach mock:success. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Andrea Cosentino * CAMEL-24439: Simplify test encryption key --------- Signed-off-by: Andrea Cosentino (cherry picked from commit 6f72ae648eb0c6e54a913f7be33cd2c020355b95) Co-authored-by: Claude Opus 5 (1M context) --- .../security/ShiroSecurityProcessor.java | 29 +++--- ...enticationCredentialAlwaysCheckedTest.java | 93 +++++++++++++++++++ 2 files changed, 106 insertions(+), 16 deletions(-) create mode 100644 components/camel-shiro/src/test/java/org/apache/camel/component/shiro/security/ShiroAuthenticationCredentialAlwaysCheckedTest.java diff --git a/components/camel-shiro/src/main/java/org/apache/camel/component/shiro/security/ShiroSecurityProcessor.java b/components/camel-shiro/src/main/java/org/apache/camel/component/shiro/security/ShiroSecurityProcessor.java index 29b1271fa2c58..98f18bec12210 100644 --- a/components/camel-shiro/src/main/java/org/apache/camel/component/shiro/security/ShiroSecurityProcessor.java +++ b/components/camel-shiro/src/main/java/org/apache/camel/component/shiro/security/ShiroSecurityProcessor.java @@ -130,24 +130,21 @@ private void applySecurityPolicy(Exchange exchange) throws Exception { } private void authenticateUser(Subject currentUser, ShiroSecurityToken securityToken) { - boolean authenticated = currentUser.isAuthenticated(); - boolean sameUser = securityToken.getUsername().equals(currentUser.getPrincipal()); - LOG.trace("Authenticated: {}, same Username: {}", authenticated, sameUser); + // The login is not skipped when the thread-bound subject already carries the same principal name. + // A matching username says nothing about the password presented with this exchange, and with + // alwaysReauthenticate=false the subject is deliberately long-lived on a shared worker thread, so + // skipping would let a wrong password through for as long as that subject stays bound. + LOG.trace("Authenticating {} (subject currently authenticated: {})", + securityToken.getUsername(), currentUser.isAuthenticated()); - if (!authenticated || !sameUser) { - UsernamePasswordToken token = new UsernamePasswordToken(securityToken.getUsername(), securityToken.getPassword()); - if (policy.isAlwaysReauthenticate()) { - token.setRememberMe(false); - } else { - token.setRememberMe(true); - } + UsernamePasswordToken token = new UsernamePasswordToken(securityToken.getUsername(), securityToken.getPassword()); + token.setRememberMe(!policy.isAlwaysReauthenticate()); - try { - currentUser.login(token); - LOG.debug("Current user {} successfully authenticated", currentUser.getPrincipal()); - } catch (AuthenticationException ae) { - throw new AuthenticationException("Authentication Failed.", ae.getCause()); - } + try { + currentUser.login(token); + LOG.debug("Current user {} successfully authenticated", currentUser.getPrincipal()); + } catch (AuthenticationException ae) { + throw new AuthenticationException("Authentication Failed.", ae.getCause()); } } diff --git a/components/camel-shiro/src/test/java/org/apache/camel/component/shiro/security/ShiroAuthenticationCredentialAlwaysCheckedTest.java b/components/camel-shiro/src/test/java/org/apache/camel/component/shiro/security/ShiroAuthenticationCredentialAlwaysCheckedTest.java new file mode 100644 index 0000000000000..252d76e0cf2e8 --- /dev/null +++ b/components/camel-shiro/src/test/java/org/apache/camel/component/shiro/security/ShiroAuthenticationCredentialAlwaysCheckedTest.java @@ -0,0 +1,93 @@ +/* + * 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.shiro.security; + +import java.nio.charset.StandardCharsets; + +import org.apache.camel.EndpointInject; +import org.apache.camel.Exchange; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.test.junit5.CamelTestSupport; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.IncorrectCredentialsException; +import org.apache.shiro.authc.LockedAccountException; +import org.apache.shiro.authc.UnknownAccountException; +import org.junit.jupiter.api.Test; + +/** + * With {@code alwaysReauthenticate=false} the Shiro subject stays bound to the thread between exchanges, which is the + * point of the option. The password carried by each exchange still has to be verified: a username that happens to match + * the bound subject's principal is not evidence about the credentials presented this time. + */ +class ShiroAuthenticationCredentialAlwaysCheckedTest extends CamelTestSupport { + + private static final byte[] TEST_KEY = "0123456789abcdef".getBytes(StandardCharsets.US_ASCII); + + @EndpointInject("mock:success") + protected MockEndpoint successEndpoint; + + @EndpointInject("mock:authenticationException") + protected MockEndpoint failureEndpoint; + + @Test + void aWrongPasswordIsRejectedEvenAfterTheSameUserAuthenticated() throws Exception { + successEndpoint.expectedMessageCount(1); + failureEndpoint.expectedMessageCount(1); + + // Authenticates and leaves the subject bound to this thread + template.send("direct:secureEndpoint", injector("ringo", "starr")); + // Same username, wrong password - must not ride the bound subject through + template.send("direct:secureEndpoint", injector("ringo", "not-starr")); + + successEndpoint.assertIsSatisfied(); + failureEndpoint.assertIsSatisfied(); + } + + private TestShiroSecurityTokenInjector injector(String user, String password) { + return new TestShiroSecurityTokenInjector(new ShiroSecurityToken(user, password), TEST_KEY); + } + + @Override + protected RouteBuilder createRouteBuilder() { + final ShiroSecurityPolicy securityPolicy + = new ShiroSecurityPolicy("./src/test/resources/securityconfig.ini", TEST_KEY, false); + + return new RouteBuilder() { + @Override + public void configure() { + onException(UnknownAccountException.class, IncorrectCredentialsException.class, + LockedAccountException.class, AuthenticationException.class).to("mock:authenticationException"); + + from("direct:secureEndpoint").policy(securityPolicy).to("mock:success"); + } + }; + } + + private static class TestShiroSecurityTokenInjector extends ShiroSecurityTokenInjector { + + TestShiroSecurityTokenInjector(ShiroSecurityToken shiroSecurityToken, byte[] bytes) { + super(shiroSecurityToken, bytes); + } + + @Override + public void process(Exchange exchange) { + exchange.getIn().setHeader(ShiroSecurityConstants.SHIRO_SECURITY_TOKEN, encrypt()); + exchange.getIn().setBody("Beatle Mania"); + } + } +} From f5a245e02a85b7138c1fda78b80ae383205760ad Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Mon, 31 Aug 2026 10:46:32 +0200 Subject: [PATCH 5/5] 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 (cherry picked from commit 9d3108def6e8344f65a70ee86832b8f5237bc82d) The generated model, catalog, XSD and YAML DSL files are regenerated from this branch rather than taken from main, since the two lines have drifted. camel-java-io and the canonical/model YAML schemas do not exist here, so those files are dropped. camel-spring.xsd gains the attribute by hand, in the wording the generator produced for camel-xml-io.xsd on this branch. Co-authored-by: Claude Opus 5 (1M context) --- .../apache/camel/catalog/dataformats/pgp.json | 3 +- .../org/apache/camel/catalog/models/pgp.json | 3 +- .../camel/catalog/schemas/camel-spring.xsd | 12 +++ .../camel/catalog/schemas/camel-xml-io.xsd | 12 +++ .../crypto/PGPDataFormatConfigurer.java | 7 ++ .../apache/camel/converter/crypto/pgp.json | 3 +- .../crypto/PGPKeyAccessDataFormat.java | 29 ++++++ .../converter/crypto/PGPDataFormatTest.java | 3 + .../PGPRequireIntegrityProtectionTest.java | 97 +++++++++++++++++++ .../apache/camel/model/dataformat/pgp.json | 3 +- .../camel/model/dataformat/PGPDataFormat.java | 38 ++++++++ .../dataformat/PGPDataFormatReifier.java | 1 + .../org/apache/camel/xml/in/ModelParser.java | 1 + .../org/apache/camel/xml/out/ModelWriter.java | 1 + .../apache/camel/yaml/out/ModelWriter.java | 1 + .../deserializers/ModelDeserializers.java | 6 ++ .../resources/schema/camelYamlDsl.json | 6 ++ 17 files changed, 222 insertions(+), 4 deletions(-) create mode 100644 components/camel-crypto-pgp/src/test/java/org/apache/camel/converter/crypto/PGPRequireIntegrityProtectionTest.java 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 a32dcc611ca40..a9ac2d8c16495 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; for example 2 (= TRIPLE DES), 3 (= CAST5), 4 (= BLOWFISH), 6 (= DES), 7 (= AES_128). 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; for example 0 (= UNCOMPRESSED), 1 (= ZIP), 2 (= ZLIB), 3 (= BZIP2). 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; for example 2 (= SHA1), 8 (= SHA256), 9 (= SHA384), 10 (= SHA512), 11 (=SHA224). 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. There are 4 values possible: optional: The PGP message may or may not contain signatures; if it does contain signatures, then a signature verification is executed. required: The PGP message must contain at least one signature; if this is not the case an exception (PGPException) is thrown. A signature verification is executed. ignore: Contained signatures in the PGP message are ignored; no signature verification is executed. no_signature_allowed: The PGP message must not contain a signature; otherwise an exception (PGPException) is thrown." } + "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. There are 4 values possible: optional: The PGP message may or may not contain signatures; if it does contain signatures, then a signature verification is executed. required: The PGP message must contain at least one signature; if this is not the case an exception (PGPException) is thrown. A signature verification is executed. ignore: Contained signatures in the PGP message are ignored; no signature verification is executed. no_signature_allowed: The PGP message must not contain a signature; otherwise an exception (PGPException) is thrown." }, + "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 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." } } } 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 fda39c0a7b560..2548f5d96bb35 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; for example 2 (= TRIPLE DES), 3 (= CAST5), 4 (= BLOWFISH), 6 (= DES), 7 (= AES_128). 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; for example 0 (= UNCOMPRESSED), 1 (= ZIP), 2 (= ZLIB), 3 (= BZIP2). 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; for example 2 (= SHA1), 8 (= SHA256), 9 (= SHA384), 10 (= SHA512), 11 (=SHA224). 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. There are 4 values possible: optional: The PGP message may or may not contain signatures; if it does contain signatures, then a signature verification is executed. required: The PGP message must contain at least one signature; if this is not the case an exception (PGPException) is thrown. A signature verification is executed. ignore: Contained signatures in the PGP message are ignored; no signature verification is executed. no_signature_allowed: The PGP message must not contain a signature; otherwise an exception (PGPException) is thrown." } + "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. There are 4 values possible: optional: The PGP message may or may not contain signatures; if it does contain signatures, then a signature verification is executed. required: The PGP message must contain at least one signature; if this is not the case an exception (PGPException) is thrown. A signature verification is executed. ignore: Contained signatures in the PGP message are ignored; no signature verification is executed. no_signature_allowed: The PGP message must not contain a signature; otherwise an exception (PGPException) is thrown." }, + "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 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." } } } 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 738e500259cdc..1fc2f2e27cd64 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 @@ -10098,6 +10098,18 @@ required: The PGP message must contain at least one signature; if this is not th thrown. A signature verification is executed. ignore: Contained signatures in the PGP message are ignored; no signature verification is executed. no_signature_allowed: The PGP message must not contain a signature; otherwise an exception (PGPException) is thrown. +]]> + + + + + + + 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 4676541e2b31f..38a70f0c7443c 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 @@ -8771,6 +8771,18 @@ required: The PGP message must contain at least one signature; if this is not th thrown. A signature verification is executed. ignore: Contained signatures in the PGP message are ignored; no signature verification is executed. no_signature_allowed: The PGP message must not contain a signature; otherwise an exception (PGPException) is thrown. +]]> + + + + + + + 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 97f991c429d47..1e0bb26c42aa4 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 boolean configure(CamelContext camelContext, Object obj, String name, Obj 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 getOptionType(String name, boolean ignoreCase) { 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 Object getOptionValue(Object obj, String name, boolean ignoreCase) { 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 a32dcc611ca40..a9ac2d8c16495 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; for example 2 (= TRIPLE DES), 3 (= CAST5), 4 (= BLOWFISH), 6 (= DES), 7 (= AES_128). 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; for example 0 (= UNCOMPRESSED), 1 (= ZIP), 2 (= ZLIB), 3 (= BZIP2). 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; for example 2 (= SHA1), 8 (= SHA256), 9 (= SHA384), 10 (= SHA512), 11 (=SHA224). 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. There are 4 values possible: optional: The PGP message may or may not contain signatures; if it does contain signatures, then a signature verification is executed. required: The PGP message must contain at least one signature; if this is not the case an exception (PGPException) is thrown. A signature verification is executed. ignore: Contained signatures in the PGP message are ignored; no signature verification is executed. no_signature_allowed: The PGP message must not contain a signature; otherwise an exception (PGPException) is thrown." } + "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. There are 4 values possible: optional: The PGP message may or may not contain signatures; if it does contain signatures, then a signature verification is executed. required: The PGP message must contain at least one signature; if this is not the case an exception (PGPException) is thrown. A signature verification is executed. ignore: Contained signatures in the PGP message are ignored; no signature verification is executed. no_signature_allowed: The PGP message must not contain a signature; otherwise an exception (PGPException) is thrown." }, + "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 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." } } } 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 36fca4cee3b22..3f51111a400cb 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 Object unmarshal(Exchange exchange, InputStream encryptedStream) throws E 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 void setSecretKeyAccessor(PGPSecretKeyAccessor secretKeyAccessor) { this.secretKeyAccessor = secretKeyAccessor; } + public boolean isRequireIntegrityProtection() { + return requireIntegrityProtection; + } + + /** + * Whether a message must be integrity protected in order to be decrypted. + *

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

+ * 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 0f03b442477dd..73ea555275b0f 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 void setUpEncryptorAndDecryptor() { 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 0000000000000..fa1f5053f7dd9 --- /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 fda39c0a7b560..2548f5d96bb35 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; for example 2 (= TRIPLE DES), 3 (= CAST5), 4 (= BLOWFISH), 6 (= DES), 7 (= AES_128). 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; for example 0 (= UNCOMPRESSED), 1 (= ZIP), 2 (= ZLIB), 3 (= BZIP2). 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; for example 2 (= SHA1), 8 (= SHA256), 9 (= SHA384), 10 (= SHA512), 11 (=SHA224). 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. There are 4 values possible: optional: The PGP message may or may not contain signatures; if it does contain signatures, then a signature verification is executed. required: The PGP message must contain at least one signature; if this is not the case an exception (PGPException) is thrown. A signature verification is executed. ignore: Contained signatures in the PGP message are ignored; no signature verification is executed. no_signature_allowed: The PGP message must not contain a signature; otherwise an exception (PGPException) is thrown." } + "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. There are 4 values possible: optional: The PGP message may or may not contain signatures; if it does contain signatures, then a signature verification is executed. required: The PGP message must contain at least one signature; if this is not the case an exception (PGPException) is thrown. A signature verification is executed. ignore: Contained signatures in the PGP message are ignored; no signature verification is executed. no_signature_allowed: The PGP message must not contain a signature; otherwise an exception (PGPException) is thrown." }, + "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 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." } } } 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 9923cf7b06b20..a1098cec34ddf 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 @@ -67,6 +67,14 @@ public class PGPDataFormat extends DataFormatDefinition { private String hashAlgorithm; @XmlAttribute 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"); @@ -88,6 +96,7 @@ protected PGPDataFormat(PGPDataFormat source) { this.compressionAlgorithm = source.compressionAlgorithm; this.hashAlgorithm = source.hashAlgorithm; this.signatureVerificationOption = source.signatureVerificationOption; + this.requireIntegrityProtection = source.requireIntegrityProtection; } private PGPDataFormat(Builder builder) { @@ -106,6 +115,7 @@ private PGPDataFormat(Builder builder) { this.compressionAlgorithm = builder.compressionAlgorithm; this.hashAlgorithm = builder.hashAlgorithm; this.signatureVerificationOption = builder.signatureVerificationOption; + this.requireIntegrityProtection = builder.requireIntegrityProtection; } @Override @@ -291,6 +301,17 @@ public void setSignatureVerificationOption(String signatureVerificationOption) { 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}. */ @@ -311,6 +332,7 @@ public static class Builder implements DataFormatBuilder { 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 @@ -495,6 +517,22 @@ public Builder signatureVerificationOption(String signatureVerificationOption) { 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 0b97d0f9ebccd..de05a23a4b5bb 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 @@ protected void prepareDataFormatConfig(Map properties) { 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-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 598c58845a34f..d5df20f561364 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 @@ -2106,6 +2106,7 @@ protected PGPDataFormat doParsePGPDataFormat() throws IOException, XmlPullParser 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 3694b0e391f5c..88484439eaf37 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 @@ -2820,6 +2820,7 @@ protected void doWritePGPDataFormat(String name, PGPDataFormat def) throws IOExc doWriteAttribute("signatureKeyRing", def.getSignatureKeyRing(), null); doWriteAttribute("signatureVerificationOption", def.getSignatureVerificationOption(), null); doWriteAttribute("keyFileName", def.getKeyFileName(), null); + doWriteAttribute("requireIntegrityProtection", def.getRequireIntegrityProtection(), "true"); doWriteAttribute("signatureKeyUserid", def.getSignatureKeyUserid(), null); doWriteAttribute("integrity", def.getIntegrity(), "true"); doWriteAttribute("password", def.getPassword(), null); diff --git a/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/ModelWriter.java b/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/ModelWriter.java index bfe9b4979e8ce..71b9d5772d64e 100644 --- a/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/ModelWriter.java +++ b/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/ModelWriter.java @@ -2820,6 +2820,7 @@ protected void doWritePGPDataFormat(String name, PGPDataFormat def) throws IOExc doWriteAttribute("signatureKeyRing", def.getSignatureKeyRing(), null); doWriteAttribute("signatureVerificationOption", def.getSignatureVerificationOption(), null); doWriteAttribute("keyFileName", def.getKeyFileName(), null); + doWriteAttribute("requireIntegrityProtection", def.getRequireIntegrityProtection(), "true"); doWriteAttribute("signatureKeyUserid", def.getSignatureKeyUserid(), null); doWriteAttribute("integrity", def.getIntegrity(), "true"); doWriteAttribute("password", def.getPassword(), null); 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 b682236ce98c5..1097656e5e558 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 @@ -11783,6 +11783,7 @@ protected boolean setProperty(OutputTypeDefinition target, String propertyKey, @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. For example, if the user ID is Test User then you can use the part Test User or to address the 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; in this case the provider must be registered beforehand and the Bouncy Castle provider must not be registered beforehand. The Sun JCE provider does not work.", 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 the legacy packet.", displayName = "Require Integrity Protection"), @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 can not 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). During the signature verification process the specified User ID restricts the public keys from the public keyring which can be used for the verification. If no User ID is specified for the signature verficiation then any public key in the public keyring can be used for the verification. Can also be only a part of a user ID. For example, if the user ID is Test User then you can use the part Test User or to address the User ID.", displayName = "Signature Key Userid"), @@ -11855,6 +11856,11 @@ protected boolean setProperty(PGPDataFormat target, String propertyKey, String p 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.json b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json index a2d5e3f5b63ff..c112ffe65f732 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 @@ -12495,6 +12495,12 @@ "title" : "Provider", "description" : "Java Cryptography Extension (JCE) provider, default is Bouncy Castle (BC). Alternatively you can use, for example, the IAIK JCE provider; in this case the provider must be registered beforehand and the Bouncy Castle provider must not be registered beforehand. The Sun JCE provider does not work." }, + "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",