[ 
https://issues.apache.org/jira/browse/CAMEL-23068?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Claus Ibsen updated CAMEL-23068:
--------------------------------
    Priority: Minor  (was: Major)

> 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: Minor
>
> 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)

Reply via email to