[
https://issues.apache.org/jira/browse/CAMEL-23068?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Reto Peter updated CAMEL-23068:
-------------------------------
Summary: camel-as2 - Signed MDN signature verification fails at receiving
partner — CRLF/LF mismatch in TextPlainEntity.writeTo() (regression from
CAMEL-22037) (was: Signed MDN signature verification fails at receiving
partner — CRLF/LF mismatch in TextPlainEntity.writeTo() (regression from
CAMEL-22037))
> camel-as2 - Signed MDN signature verification fails at receiving partner —
> CRLF/LF mismatch in TextPlainEntity.writeTo() (regression from CAMEL-22037)
> ------------------------------------------------------------------------------------------------------------------------------------------------------
>
> Key: CAMEL-23068
> URL: https://issues.apache.org/jira/browse/CAMEL-23068
> Project: Camel
> Issue Type: Bug
> Components: camel-as2
> Affects Versions: 4.18.0
> Reporter: Reto Peter
> Priority: Major
>
> Signed MDN responses generated by the Camel AS2 server cannot be verified by
> receiving AS2 clients. Clients receive \{{CMSSignerDigestMismatchException:
> message-digest attribute value does not match calculated value}}.
> This is a regression introduced by the fix for CAMEL-22037 (Camel 4.8.8).
> \{panel}
> h3. Root Cause
> CAMEL-22037 changed \{{TextPlainEntity.writeTo()}} to write text content
> directly to the raw output stream, bypassing \{{CanonicalOutputStream}}. This
> correctly preserves original line endings when receiving signed messages, but
> it breaks generating signed MDNs.
> The problem is that \{{writeTo()}} uses two different output streams for
> headers and body:
> \{code:java|title=TextPlainEntity.writeTo() (Camel 4.18.0)}
> @Override
> public void writeTo(OutputStream outstream) throws IOException {
> NoCloseOutputStream ncos = new NoCloseOutputStream(outstream);
> try (CanonicalOutputStream canonicalOutstream = new
> CanonicalOutputStream(ncos, StandardCharsets.US_ASCII.name())) {
> // Write out mime part headers if this is not the main body of message.
> if (!isMainBody()) {
> for (Header header : getAllHeaders()) {
> canonicalOutstream.writeln(header.toString()); // <-- CRLF
> (via CanonicalOutputStream)
> }
> canonicalOutstream.writeln();
> }
> // Write out content
> outstream.write(content.getBytes(StandardCharsets.US_ASCII), 0,
> content.length()); // <-- RAW bytes (LF if source uses LF)
> }
> }
> \{code}
> - Headers are written through \{{CanonicalOutputStream}} → line endings are
> CRLF (\{{\r\n}})
> - Content is written through raw \{{outstream}} → line endings are whatever
> the source string contains
> Combined with \{{ResponseMDN.DEFAULT_MDN_MESSAGE_TEMPLATE}} which uses Java
> text block with LF (\{{\n}}) line endings:
> \{code:java|title=ResponseMDN.DEFAULT_MDN_MESSAGE_TEMPLATE (Camel 4.18.0)}
> private static final String DEFAULT_MDN_MESSAGE_TEMPLATE = """
> MDN for -
> Message ID: $requestHeaders["Message-Id"]
> Subject: $requestHeaders["Subject"]
> Date: $requestHeaders["Date"]
> From: $requestHeaders["AS2-From"]
> To: $requestHeaders["AS2-To"]
> Received on: $responseHeaders["Date"]
> Status: $dispositionType
> """;
> \{code}
> Java text blocks use LF (\{{\n}}) as line endings. This template is
> rendered by Velocity and written as the body of a \{{TextPlainEntity}}.
> h3. The Chain of Events
> \{{ResponseMDN}} renders \{{DEFAULT_MDN_MESSAGE_TEMPLATE}} using Velocity →
> produces string with LF line endings
> \{{TextPlainEntity}} is created with this string as content
> During MDN signing, \{{TextPlainEntity.writeTo()}} is called:
> #* MIME headers → \{{CanonicalOutputStream}} → CRLF
> #* Body text → raw \{{outstream}} → LF (because template has LF)
> The signed data digest is computed over these mixed bytes (CRLF headers +
> LF body)
> MDN is sent to the partner
> Partner receives the MDN and normalizes to CRLF (per MIME canonical form,
> RFC 2045)
> Partner recomputes digest over CRLF-normalized bytes
> Digest mismatch → \{{CMSSignerDigestMismatchException}}
> h3. Steps to Reproduce
> Configure a Camel AS2 server endpoint with signing enabled (signing
> certificate + private key)
> Have a partner send an AS2 message requesting a signed MDN
> (\{{Disposition-Notification-Options}} with \{{signed-receipt-protocol}})
> Camel generates and signs a synchronous MDN
> Partner receives the signed MDN and verifies the signature
> Observe: Partner reports \{{CMSSignerDigestMismatchException}} — signature
> verification fails
> h3. Proposed Fix
> Option A (simple, minimal change): Change \{{DEFAULT_MDN_MESSAGE_TEMPLATE}}
> to use CRLF line endings.
> \{code:diff|title=Fix in ResponseMDN.java}
> - private static final String DEFAULT_MDN_MESSAGE_TEMPLATE = """
> MDN for -
> Message ID: $requestHeaders["Message-Id"]
> Subject: $requestHeaders["Subject"]
> Date: $requestHeaders["Date"]
> From: $requestHeaders["AS2-From"]
> To: $requestHeaders["AS2-To"]
> Received on: $responseHeaders["Date"]
> Status: $dispositionType
> """;
> - private static final String DEFAULT_MDN_MESSAGE_TEMPLATE = "MDN for -\r\n"
> + " Message ID: $requestHeaders[\"Message-Id\"]\r\n"
> + " Subject: $requestHeaders[\"Subject\"]\r\n"
> + " Date: $requestHeaders[\"Date\"]\r\n"
> + " From: $requestHeaders[\"AS2-From\"]\r\n"
> + " To: $requestHeaders[\"AS2-To\"]\r\n"
> + " Received on: $responseHeaders[\"Date\"]\r\n"
> + " Status: $dispositionType \r\n";
> \{code}
> This ensures the body text already has CRLF, so client-side normalization
> does not change the bytes.
> Option B (more robust): Also fix \{{TextPlainEntity.writeTo()}} to write
> content through \{{CanonicalOutputStream}} when generating outbound entities,
> while preserving the CAMEL-22037 fix for inbound entities.
> \{code:diff|title=Fix in TextPlainEntity.java}
> // Write out content
> outstream.write(content.getBytes(StandardCharsets.US_ASCII), 0,
> content.length());
> // Use canonical output for generated content (outbound MDN), raw for
> received content (inbound)
> canonicalOutstream.write(content.getBytes(StandardCharsets.US_ASCII), 0,
> content.length());
> \{code}
> However, this would revert CAMEL-22037's fix for inbound. A better approach
> would be to add a flag (e.g., \{{isGenerated}}) to \{{TextPlainEntity}} to
> distinguish between received entities (preserve original bytes) and generated
> entities (use canonical CRLF).
> Recommendation: Option A is the safest fix — it solves the problem without
> touching \{{TextPlainEntity}} and doesn't risk regressing CAMEL-22037.
> h3. Related Issues
> - [CAMEL-22037|https://issues.apache.org/jira/browse/CAMEL-22037] —
> preserve original line endings on receive (fixed in 4.8.8, introduced this
> regression)
> - [CAMEL-18017|https://issues.apache.org/jira/browse/CAMEL-18017] —
> client-side MDN verification (fixed in 4.6.0)
> - [CAMEL-21296|https://issues.apache.org/jira/browse/CAMEL-21296] —
> client-side MDN verification with CRLF/LF (fixed in 4.8.0)
> h3. Our Workaround
> We register a custom MDN template with CRLF line endings via the
> \{{mdnMessageTemplate}} endpoint parameter:
> \{code:java}
> // Register CRLF-normalized template in Camel registry
> camelContext.getRegistry().bind("mdnMessageTemplate",
> "MDN for -\r\n"
> + " Message ID: $requestHeaders["Message-Id"]\r\n"
> + " Subject: $requestHeaders["Subject"]\r\n"
> + " Date: $requestHeaders["Date"]\r\n"
> + " From: $requestHeaders["AS2-From"]\r\n"
> + " To: $requestHeaders["AS2-To"]\r\n"
> + " Received on: $responseHeaders["Date"]\r\n"
> + " Status: $dispositionType \r\n");
> // Reference in AS2 endpoint URI:
> // as2://server/listen?...&mdnMessageTemplate=#mdnMessageTemplate
> \{code}
> This works but requires every Camel AS2 user to know about the issue and
> apply the same workaround.
> h3. Test Scenario
> Tested with Mendelson AS2 and OpenAS2 as receiving partners.
> Before fix: Both partners reject signed MDNs with
> \{{CMSSignerDigestMismatchException}}.
> After applying CRLF template workaround: Signed MDNs are verified
> successfully by both partners.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)