Reto Peter created CAMEL-23068:
----------------------------------
Summary: 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
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)