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)

Reply via email to