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

Guillaume Nodet updated CAMEL-23064:
------------------------------------
    Description: 
{panel:title=Problem}
{{HttpMessageUtils.extractEdiPayload()}} in {{camel-as2-api}} strictly 
validates inner content types and rejects anything other than 
{{application/edifact}}, {{application/edi-x12}}, or 
{{application/edi-consent}}. Real-world AS2 partners frequently send EDI 
payloads wrapped in {{text/plain}}, {{application/octet-stream}}, 
{{application/xml}}, etc.

This causes a 500 error with no MDN returned, which violates the AS2 protocol 
(RFC 4130) requirement to always return an MDN when one is requested.
{panel}

h3. Steps to Reproduce

# Configure a Camel AS2 server endpoint with encryption and signing enabled
# Have a partner send an AS2 message where the inner payload (after decryption 
and signature verification) has content type {{text/plain; charset=US-ASCII}} 
instead of {{application/edifact}}
# Observe: server returns HTTP 500 with no MDN

This is common with partners using AS2Gateway, SAP, Mendelson, and other AS2 
implementations that do not set strict EDI content types on the inner payload.

h3. Error Message

{code}
org.apache.hc.core5.http.HttpException: Failed to extract EDI payload:
  invalid content type 'text/plain; charset=US-ASCII' for AS2 compressed and 
signed entity
    at 
o.a.c.component.as2.api.util.HttpMessageUtils.extractEdiPayloadFromEnvelopedEntity(HttpMessageUtils.java:261)
    at 
o.a.c.component.as2.api.util.HttpMessageUtils.extractEnvelopedData(HttpMessageUtils.java:178)
    at 
o.a.c.component.as2.api.util.HttpMessageUtils.extractEdiPayload(HttpMessageUtils.java:145)
{code}

h3. Root Cause

{{HttpMessageUtils}} has 6 locations across 3 methods that throw 
{{HttpException}} when the inner entity is not an {{ApplicationEntity}}. The 
AS2 protocol (RFC 4130) does not mandate specific inner content types — the 
content type of the EDI payload is between the trading partners and is not part 
of the AS2 transport specification.

h4. Affected code locations

1. {{extractEdiPayload()}} — default case (top-level):
{code:java}
default:
    throw new HttpException("Failed to extract EDI message: invalid content 
type '"
        + contentType.getMimeType() + "' for AS2 request message");
{code}

2. {{extractMultipartSigned()}} — else branch:
{code:java}
MimeEntity mimeEntity = multipartSignedEntity.getSignedDataEntity();
if (mimeEntity instanceof ApplicationEntity) {
    ediEntity = (ApplicationEntity) mimeEntity;
} else if (mimeEntity instanceof ApplicationPkcs7MimeCompressedDataEntity 
compressedDataEntity) {
    ediEntity = extractEdiPayloadFromCompressedEntity(...);
} else {
    throw new HttpException("Failed to extract EDI payload: invalid content 
type '"
        + mimeEntity.getContentType() + "' for AS2 compressed and signed 
message");
}
{code}

3. {{extractEdiPayloadFromEnvelopedEntity()}} — MULTIPART_SIGNED else branch:
{code:java}
} else {
    throw new HttpException("Failed to extract EDI payload: invalid content 
type '"
        + mimeEntity.getContentType() + "' for AS2 compressed and signed 
entity");
}
{code}

4. {{extractEdiPayloadFromEnvelopedEntity()}} — default case:
{code:java}
default:
    throw new HttpException("Failed to extract EDI payload: invalid content 
type '"
        + contentType.getMimeType() + "' for AS2 enveloped entity");
{code}

5. {{extractEdiPayloadFromCompressedEntity()}} — else branch:
{code:java}
} else {
    throw new HttpException("Failed to extract EDI payload: invalid content 
type '"
        + mimeEntity.getContentType() + "' for AS2 compressed and signed 
entity");
}
{code}

6. {{extractEdiPayloadFromCompressedEntity()}} — default case:
{code:java}
default:
    throw new HttpException("Failed to extract EDI payload: invalid content 
type '"
        + contentType.getMimeType() + "' for AS2 compressed entity");
{code}

h3. Secondary Impact: MDN MIC Computation Also Fails

Even if the payload extraction were caught at a higher level, the MDN 
generation also fails. {{ResponseMDN}} (the httpProcessor interceptor) calls 
{{extractEdiPayloadFromEnvelopedEntity()}} during MIC computation via 
{{DispositionNotificationMultipartReportEntity}}. This hits the same content 
type rejection, causing the MDN generation to throw, which {{HttpService}} 
catches and converts to a 500 error response. The partner receives HTTP 500 
instead of a proper MDN.

This means the fix must be in {{HttpMessageUtils}} itself — wrapping at higher 
levels cannot fully resolve the issue.

h3. Proposed Fix

Instead of throwing when the inner entity is not an {{ApplicationEntity}}, wrap 
it in a {{GenericApplicationEntity}} that:
* Stores the content bytes (for {{getEdiMessage()}})
* Delegates {{writeTo()}} to the original {{MimeEntity}} to preserve the exact 
byte representation for correct MIC computation

h4. New helper method to add

{code:java}
private static ApplicationEntity wrapMimeEntityAsApplication(MimeEntity 
mimeEntity) throws HttpException {
    try {
        String contentTypeString = mimeEntity.getContentType();
        ContentType contentType = contentTypeString != null
            ? ContentType.parse(contentTypeString)
            : ContentType.DEFAULT_TEXT;

        byte[] content;
        try (InputStream is = mimeEntity.getContent()) {
            content = is.readAllBytes();
        }

        return new GenericApplicationEntity(content, contentType, null, false, 
mimeEntity);
    } catch (Exception e) {
        throw new HttpException(
            "Failed to read EDI payload from non-standard content type '" + 
mimeEntity.getContentType() + "'", e);
    }
}
{code}

h4. New inner class to add

{code:java}
/**
 * Concrete ApplicationEntity subclass for wrapping non-standard content types.
 * CRITICAL: writeTo() delegates to the original MimeEntity to preserve the 
exact byte
 * representation (including headers like Content-Transfer-Encoding). The MIC 
is computed
 * by hashing the writeTo() output, so it MUST match the bytes the sender 
signed.
 * Using ApplicationEntity's default writeTo() would produce different bytes 
(different
 * headers, canonical encoding), causing MIC mismatch errors at the partner.
 */
private static class GenericApplicationEntity extends ApplicationEntity {
    private final MimeEntity originalEntity;

    GenericApplicationEntity(byte[] content, ContentType contentType, String 
transferEncoding,
                             boolean isMainBody, MimeEntity originalEntity) {
        super(content, contentType, transferEncoding, isMainBody, null);
        this.originalEntity = originalEntity;
    }

    @Override
    public void writeTo(OutputStream outstream) throws IOException {
        if (originalEntity != null) {
            originalEntity.writeTo(outstream);
        } else {
            super.writeTo(outstream);
        }
    }

    @Override
    public void close() {
    }
}
{code}

h4. Changes to existing code

Replace each of the 6 throwing branches with a call to 
{{wrapMimeEntityAsApplication()}}. Example for {{extractMultipartSigned()}}:

{code:diff}
    MimeEntity mimeEntity = multipartSignedEntity.getSignedDataEntity();
    if (mimeEntity instanceof ApplicationEntity) {
        ediEntity = (ApplicationEntity) mimeEntity;
    } else if (mimeEntity instanceof ApplicationPkcs7MimeCompressedDataEntity 
compressedDataEntity) {
        ediEntity = extractEdiPayloadFromCompressedEntity(compressedDataEntity, 
decrpytingAndSigningInfo, true);
    } else {
-       throw new HttpException(
-           "Failed to extract EDI payload: invalid content type '" + 
mimeEntity.getContentType()
-                                + "' for AS2 compressed and signed message");
+       ediEntity = wrapMimeEntityAsApplication(mimeEntity);
    }
{code}

The same pattern applies to all other throwing branches. The top-level 
{{extractEdiPayload()}} default case additionally needs security checks before 
wrapping (if signing/encryption was expected but not found, that should still 
be an error).

h3. Why {{writeTo()}} Delegation is Critical

Without delegating {{writeTo()}} to the original entity, the MIC (Message 
Integrity Check) returned in the MDN will not match the partner's expected 
value:
* The MIC is a SHA-256 hash of the {{writeTo()}} output of the signed data 
entity
* {{ApplicationEntity.writeTo()}} produces different bytes than the original 
{{MimeEntity.writeTo()}} (different MIME headers, e.g., missing 
{{Content-Transfer-Encoding: 7bit}})
* The partner compares the returned MIC with the MIC they computed before 
sending
* A mismatch causes the partner to flag the transfer as compromised

h3. Additional Note: Existing Bug in {{getEntity()}}

While investigating this issue, we noticed a bug in 
{{HttpMessageUtils.getEntity()}}:

{code:java}
} else if (message instanceof BasicClassicHttpResponse httpResponse) {
    HttpEntity entity = httpResponse.getEntity();
    if (entity != null && type.isInstance(entity)) {
        type.cast(entity);  // BUG: should be "return type.cast(entity);"
    }
}
{code}

The response branch casts but does not return the entity. This means 
{{getEntity()}} always returns {{null}} for response messages.

h3. Test Scenario

Partner configuration: AS2 partner sending encrypted (enveloped-data) + signed 
messages where the inner payload content type is {{text/plain; 
charset=US-ASCII}}.

Message structure:
{code}
application/pkcs7-mime; smime-type=enveloped-data    (outer: encrypted)
  └─ multipart/signed                                (after decryption: signed)
       └─ text/plain; charset=US-ASCII               (inner payload — rejected 
by Camel)
{code}

*Expected behavior:* Message accepted, EDI payload extracted, synchronous MDN 
returned with correct MIC.

*Actual behavior (Camel 4.18.0):* HTTP 500, no MDN, partner retries.


  was:
{panel:title=Problem}
  \{{HttpMessageUtils.extractEdiPayload()}} in \{{camel-as2-api}} strictly 
validates inner content types and rejects anything other than 
\{{application/edifact}}, \{{application/edi-x12}}, or 
\{{application/edi-consent}}. Real-world AS2 partners frequently send EDI
  payloads wrapped in \{{text/plain}}, \{{application/octet-stream}}, 
\{{application/xml}}, etc.

  This causes a 500 error with no MDN returned, which violates the AS2 protocol 
(RFC 4130) requirement to always return an MDN when one is requested.
  \{panel}

  h3. Steps to Reproduce

  Configure a Camel AS2 server endpoint with encryption and signing enabled

  Have a partner send an AS2 message where the inner payload (after decryption 
and signature verification) has content type \{{text/plain; charset=US-ASCII}} 
instead of \{{application/edifact}}

  Observe: server returns HTTP 500 with no MDN

  This is common with partners using AS2Gateway, SAP, Mendelson, and other AS2 
implementations that do not set strict EDI content types on the inner payload.

  h3. Error Message

  \{code}
  org.apache.hc.core5.http.HttpException: Failed to extract EDI payload:
    invalid content type 'text/plain; charset=US-ASCII' for AS2 compressed and 
signed entity
      at 
o.a.c.component.as2.api.util.HttpMessageUtils.extractEdiPayloadFromEnvelopedEntity(HttpMessageUtils.java:261)
      at 
o.a.c.component.as2.api.util.HttpMessageUtils.extractEnvelopedData(HttpMessageUtils.java:178)
      at 
o.a.c.component.as2.api.util.HttpMessageUtils.extractEdiPayload(HttpMessageUtils.java:145)
  \{code}

  h3. Root Cause

  \{{HttpMessageUtils}} has 6 locations across 3 methods that throw 
\{{HttpException}} when the inner entity is not an \{{ApplicationEntity}}. The 
AS2 protocol (RFC 4130) does not mandate specific inner content types — the 
content type of the EDI payload is between
   the trading partners and is not part of the AS2 transport specification.

  h4. Affected code locations

  1. \{{extractEdiPayload()}} — default case (top-level):
  \{code:java}
  default:
      throw new HttpException("Failed to extract EDI message: invalid content 
type '"
          + contentType.getMimeType() + "' for AS2 request message");
  \{code}

  2. \{{extractMultipartSigned()}} — else branch:
  \{code:java}
  MimeEntity mimeEntity = multipartSignedEntity.getSignedDataEntity();
  if (mimeEntity instanceof ApplicationEntity) {
      ediEntity = (ApplicationEntity) mimeEntity;
  } else if (mimeEntity instanceof ApplicationPkcs7MimeCompressedDataEntity 
compressedDataEntity) {
      ediEntity = extractEdiPayloadFromCompressedEntity(...);
  } else {
      throw new HttpException("Failed to extract EDI payload: invalid content 
type '"
          + mimeEntity.getContentType() + "' for AS2 compressed and signed 
message");
  }
  \{code}

  3. \{{extractEdiPayloadFromEnvelopedEntity()}} — MULTIPART_SIGNED else branch:
  \{code:java}
  } else {
      throw new HttpException("Failed to extract EDI payload: invalid content 
type '"
          + mimeEntity.getContentType() + "' for AS2 compressed and signed 
entity");
  }
  \{code}

  4. \{{extractEdiPayloadFromEnvelopedEntity()}} — default case:
  \{code:java}
  default:
      throw new HttpException("Failed to extract EDI payload: invalid content 
type '"
          + contentType.getMimeType() + "' for AS2 enveloped entity");
  \{code}

  5. \{{extractEdiPayloadFromCompressedEntity()}} — else branch:
  \{code:java}
  } else {
      throw new HttpException("Failed to extract EDI payload: invalid content 
type '"
          + mimeEntity.getContentType() + "' for AS2 compressed and signed 
entity");
  }
  \{code}

  6. \{{extractEdiPayloadFromCompressedEntity()}} — default case:
  \{code:java}
  default:
      throw new HttpException("Failed to extract EDI payload: invalid content 
type '"
          + contentType.getMimeType() + "' for AS2 compressed entity");
  \{code}

  h3. Secondary Impact: MDN MIC Computation Also Fails

  Even if the payload extraction were caught at a higher level, the MDN 
generation also fails. \{{ResponseMDN}} (the httpProcessor interceptor) calls 
\{{extractEdiPayloadFromEnvelopedEntity()}} during MIC computation via
  \{{DispositionNotificationMultipartReportEntity}}. This hits the same content 
type rejection, causing the MDN generation to throw, which \{{HttpService}} 
catches and converts to a 500 error response. The partner receives HTTP 500 
instead of a proper MDN.

  This means the fix must be in \{{HttpMessageUtils}} itself — wrapping at 
higher levels cannot fully resolve the issue.

  h3. Proposed Fix

  Instead of throwing when the inner entity is not an \{{ApplicationEntity}}, 
wrap it in a \{{GenericApplicationEntity}} that:
  Stores the content bytes (for \{{getEdiMessage()}})

  Delegates \{{writeTo()}} to the original \{{MimeEntity}} to preserve the 
exact byte representation for correct MIC computation

  h4. New helper method to add

  \{code:java}
  private static ApplicationEntity wrapMimeEntityAsApplication(MimeEntity 
mimeEntity) throws HttpException {
      try {
          String contentTypeString = mimeEntity.getContentType();
          ContentType contentType = contentTypeString != null
              ? ContentType.parse(contentTypeString)
              : ContentType.DEFAULT_TEXT;

      byte[] content;
      try (InputStream is = mimeEntity.getContent()) {
          content = is.readAllBytes();
      }

      return new GenericApplicationEntity(content, contentType, null, false, 
mimeEntity);
  } catch (Exception e) {
      throw new HttpException(
          "Failed to read EDI payload from non-standard content type '" + 
mimeEntity.getContentType() + "'", e);
  }
  }
  \{code}

  h4. New inner class to add

  \{code:java}
  /**
  - Concrete ApplicationEntity subclass for wrapping non-standard content types.
  - CRITICAL: writeTo() delegates to the original MimeEntity to preserve the 
exact byte
  - representation (including headers like Content-Transfer-Encoding). The MIC 
is computed
  - by hashing the writeTo() output, so it MUST match the bytes the sender 
signed.
  - Using ApplicationEntity's default writeTo() would produce different bytes 
(different
  - headers, canonical encoding), causing MIC mismatch errors at the partner.
   */
  private static class GenericApplicationEntity extends ApplicationEntity {
   private final MimeEntity originalEntity;

  -  GenericApplicationEntity(byte[] content, ContentType contentType, String 
transferEncoding,
                        boolean isMainBody, MimeEntity originalEntity) {
   super(content, contentType, transferEncoding, isMainBody, null);
   this.originalEntity = originalEntity;
   }

  -  @Override
   public void writeTo(OutputStream outstream) throws IOException {
   if (originalEntity != null) {
       originalEntity.writeTo(outstream);
   } else {
       super.writeTo(outstream);
   }
   }

  -  @Override
   public void close() {
   }
  }
  \{code}

  h4. Changes to existing code

  Replace each of the 6 throwing branches with a call to 
\{{wrapMimeEntityAsApplication()}}. Example for \{{extractMultipartSigned()}}:

  \{code:diff}
       MimeEntity mimeEntity = multipartSignedEntity.getSignedDataEntity();
       if (mimeEntity instanceof ApplicationEntity) {
           ediEntity = (ApplicationEntity) mimeEntity;
       } else if (mimeEntity instanceof 
ApplicationPkcs7MimeCompressedDataEntity compressedDataEntity) {
           ediEntity = 
extractEdiPayloadFromCompressedEntity(compressedDataEntity, 
decrpytingAndSigningInfo, true);
       } else {
     throw new HttpException(
             "Failed to extract EDI payload: invalid content type '" + 
mimeEntity.getContentType()
                             + "' for AS2 compressed and signed message");
     ediEntity = wrapMimeEntityAsApplication(mimeEntity);
  -    }
  \{code}

  The same pattern applies to all other throwing branches. The top-level 
\{{extractEdiPayload()}} default case additionally needs security checks before 
wrapping (if signing/encryption was expected but not found, that should still 
be an error).

  h3. Why \{{writeTo()}} Delegation is Critical

  Without delegating \{{writeTo()}} to the original entity, the MIC (Message 
Integrity Check) returned in the MDN will not match the partner's expected 
value:
  - The MIC is a SHA-256 hash of the \{{writeTo()}} output of the signed data 
entity
  - \{{ApplicationEntity.writeTo()}} produces different bytes than the original 
\{{MimeEntity.writeTo()}} (different MIME headers, e.g., missing 
\{{Content-Transfer-Encoding: 7bit}})
  - The partner compares the returned MIC with the MIC they computed before 
sending
  - A mismatch causes the partner to flag the transfer as compromised

  h3. Additional Note: Existing Bug in \{{getEntity()}}

  While investigating this issue, we noticed a bug in 
\{{HttpMessageUtils.getEntity()}}:

  \{code:java}
  } else if (message instanceof BasicClassicHttpResponse httpResponse) {
      HttpEntity entity = httpResponse.getEntity();
      if (entity != null && type.isInstance(entity)) {
          type.cast(entity);  // BUG: should be "return type.cast(entity);"
      }
  }
  \{code}

  The response branch casts but does not return the entity. This means 
\{{getEntity()}} always returns \{{null}} for response messages.

  h3. Test Scenario

  Partner configuration: AS2 partner sending encrypted (enveloped-data) + 
signed messages where the inner payload content type is \{{text/plain; 
charset=US-ASCII}}.

  Message structure:
  \{code}
  application/pkcs7-mime; smime-type=enveloped-data    (outer: encrypted)
    └─ multipart/signed                                (after decryption: 
signed)
         └─ text/plain; charset=US-ASCII               (inner payload — 
rejected by Camel)
  \{code}

  Expected behavior: Message accepted, EDI payload extracted, synchronous MDN 
returned with correct MIC.

  Actual behavior (Camel 4.18.0): HTTP 500, no MDN, partner retries.


> camel-as2 - HttpMessageUtils.extractEdiPayload() rejects valid AS2 messages 
> with non-standard content types (text/plain, application/octet-stream, etc.)
> --------------------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: CAMEL-23064
>                 URL: https://issues.apache.org/jira/browse/CAMEL-23064
>             Project: Camel
>          Issue Type: Bug
>          Components: camel-as2
>    Affects Versions: 4.18.0
>            Reporter: Reto Peter
>            Priority: Minor
>         Attachments: HttpMessageUtils.java
>
>
> {panel:title=Problem}
> {{HttpMessageUtils.extractEdiPayload()}} in {{camel-as2-api}} strictly 
> validates inner content types and rejects anything other than 
> {{application/edifact}}, {{application/edi-x12}}, or 
> {{application/edi-consent}}. Real-world AS2 partners frequently send EDI 
> payloads wrapped in {{text/plain}}, {{application/octet-stream}}, 
> {{application/xml}}, etc.
> This causes a 500 error with no MDN returned, which violates the AS2 protocol 
> (RFC 4130) requirement to always return an MDN when one is requested.
> {panel}
> h3. Steps to Reproduce
> # Configure a Camel AS2 server endpoint with encryption and signing enabled
> # Have a partner send an AS2 message where the inner payload (after 
> decryption and signature verification) has content type {{text/plain; 
> charset=US-ASCII}} instead of {{application/edifact}}
> # Observe: server returns HTTP 500 with no MDN
> This is common with partners using AS2Gateway, SAP, Mendelson, and other AS2 
> implementations that do not set strict EDI content types on the inner payload.
> h3. Error Message
> {code}
> org.apache.hc.core5.http.HttpException: Failed to extract EDI payload:
>   invalid content type 'text/plain; charset=US-ASCII' for AS2 compressed and 
> signed entity
>     at 
> o.a.c.component.as2.api.util.HttpMessageUtils.extractEdiPayloadFromEnvelopedEntity(HttpMessageUtils.java:261)
>     at 
> o.a.c.component.as2.api.util.HttpMessageUtils.extractEnvelopedData(HttpMessageUtils.java:178)
>     at 
> o.a.c.component.as2.api.util.HttpMessageUtils.extractEdiPayload(HttpMessageUtils.java:145)
> {code}
> h3. Root Cause
> {{HttpMessageUtils}} has 6 locations across 3 methods that throw 
> {{HttpException}} when the inner entity is not an {{ApplicationEntity}}. The 
> AS2 protocol (RFC 4130) does not mandate specific inner content types — the 
> content type of the EDI payload is between the trading partners and is not 
> part of the AS2 transport specification.
> h4. Affected code locations
> 1. {{extractEdiPayload()}} — default case (top-level):
> {code:java}
> default:
>     throw new HttpException("Failed to extract EDI message: invalid content 
> type '"
>         + contentType.getMimeType() + "' for AS2 request message");
> {code}
> 2. {{extractMultipartSigned()}} — else branch:
> {code:java}
> MimeEntity mimeEntity = multipartSignedEntity.getSignedDataEntity();
> if (mimeEntity instanceof ApplicationEntity) {
>     ediEntity = (ApplicationEntity) mimeEntity;
> } else if (mimeEntity instanceof ApplicationPkcs7MimeCompressedDataEntity 
> compressedDataEntity) {
>     ediEntity = extractEdiPayloadFromCompressedEntity(...);
> } else {
>     throw new HttpException("Failed to extract EDI payload: invalid content 
> type '"
>         + mimeEntity.getContentType() + "' for AS2 compressed and signed 
> message");
> }
> {code}
> 3. {{extractEdiPayloadFromEnvelopedEntity()}} — MULTIPART_SIGNED else branch:
> {code:java}
> } else {
>     throw new HttpException("Failed to extract EDI payload: invalid content 
> type '"
>         + mimeEntity.getContentType() + "' for AS2 compressed and signed 
> entity");
> }
> {code}
> 4. {{extractEdiPayloadFromEnvelopedEntity()}} — default case:
> {code:java}
> default:
>     throw new HttpException("Failed to extract EDI payload: invalid content 
> type '"
>         + contentType.getMimeType() + "' for AS2 enveloped entity");
> {code}
> 5. {{extractEdiPayloadFromCompressedEntity()}} — else branch:
> {code:java}
> } else {
>     throw new HttpException("Failed to extract EDI payload: invalid content 
> type '"
>         + mimeEntity.getContentType() + "' for AS2 compressed and signed 
> entity");
> }
> {code}
> 6. {{extractEdiPayloadFromCompressedEntity()}} — default case:
> {code:java}
> default:
>     throw new HttpException("Failed to extract EDI payload: invalid content 
> type '"
>         + contentType.getMimeType() + "' for AS2 compressed entity");
> {code}
> h3. Secondary Impact: MDN MIC Computation Also Fails
> Even if the payload extraction were caught at a higher level, the MDN 
> generation also fails. {{ResponseMDN}} (the httpProcessor interceptor) calls 
> {{extractEdiPayloadFromEnvelopedEntity()}} during MIC computation via 
> {{DispositionNotificationMultipartReportEntity}}. This hits the same content 
> type rejection, causing the MDN generation to throw, which {{HttpService}} 
> catches and converts to a 500 error response. The partner receives HTTP 500 
> instead of a proper MDN.
> This means the fix must be in {{HttpMessageUtils}} itself — wrapping at 
> higher levels cannot fully resolve the issue.
> h3. Proposed Fix
> Instead of throwing when the inner entity is not an {{ApplicationEntity}}, 
> wrap it in a {{GenericApplicationEntity}} that:
> * Stores the content bytes (for {{getEdiMessage()}})
> * Delegates {{writeTo()}} to the original {{MimeEntity}} to preserve the 
> exact byte representation for correct MIC computation
> h4. New helper method to add
> {code:java}
> private static ApplicationEntity wrapMimeEntityAsApplication(MimeEntity 
> mimeEntity) throws HttpException {
>     try {
>         String contentTypeString = mimeEntity.getContentType();
>         ContentType contentType = contentTypeString != null
>             ? ContentType.parse(contentTypeString)
>             : ContentType.DEFAULT_TEXT;
>         byte[] content;
>         try (InputStream is = mimeEntity.getContent()) {
>             content = is.readAllBytes();
>         }
>         return new GenericApplicationEntity(content, contentType, null, 
> false, mimeEntity);
>     } catch (Exception e) {
>         throw new HttpException(
>             "Failed to read EDI payload from non-standard content type '" + 
> mimeEntity.getContentType() + "'", e);
>     }
> }
> {code}
> h4. New inner class to add
> {code:java}
> /**
>  * Concrete ApplicationEntity subclass for wrapping non-standard content 
> types.
>  * CRITICAL: writeTo() delegates to the original MimeEntity to preserve the 
> exact byte
>  * representation (including headers like Content-Transfer-Encoding). The MIC 
> is computed
>  * by hashing the writeTo() output, so it MUST match the bytes the sender 
> signed.
>  * Using ApplicationEntity's default writeTo() would produce different bytes 
> (different
>  * headers, canonical encoding), causing MIC mismatch errors at the partner.
>  */
> private static class GenericApplicationEntity extends ApplicationEntity {
>     private final MimeEntity originalEntity;
>     GenericApplicationEntity(byte[] content, ContentType contentType, String 
> transferEncoding,
>                              boolean isMainBody, MimeEntity originalEntity) {
>         super(content, contentType, transferEncoding, isMainBody, null);
>         this.originalEntity = originalEntity;
>     }
>     @Override
>     public void writeTo(OutputStream outstream) throws IOException {
>         if (originalEntity != null) {
>             originalEntity.writeTo(outstream);
>         } else {
>             super.writeTo(outstream);
>         }
>     }
>     @Override
>     public void close() {
>     }
> }
> {code}
> h4. Changes to existing code
> Replace each of the 6 throwing branches with a call to 
> {{wrapMimeEntityAsApplication()}}. Example for {{extractMultipartSigned()}}:
> {code:diff}
>     MimeEntity mimeEntity = multipartSignedEntity.getSignedDataEntity();
>     if (mimeEntity instanceof ApplicationEntity) {
>         ediEntity = (ApplicationEntity) mimeEntity;
>     } else if (mimeEntity instanceof ApplicationPkcs7MimeCompressedDataEntity 
> compressedDataEntity) {
>         ediEntity = 
> extractEdiPayloadFromCompressedEntity(compressedDataEntity, 
> decrpytingAndSigningInfo, true);
>     } else {
> -       throw new HttpException(
> -           "Failed to extract EDI payload: invalid content type '" + 
> mimeEntity.getContentType()
> -                                + "' for AS2 compressed and signed message");
> +       ediEntity = wrapMimeEntityAsApplication(mimeEntity);
>     }
> {code}
> The same pattern applies to all other throwing branches. The top-level 
> {{extractEdiPayload()}} default case additionally needs security checks 
> before wrapping (if signing/encryption was expected but not found, that 
> should still be an error).
> h3. Why {{writeTo()}} Delegation is Critical
> Without delegating {{writeTo()}} to the original entity, the MIC (Message 
> Integrity Check) returned in the MDN will not match the partner's expected 
> value:
> * The MIC is a SHA-256 hash of the {{writeTo()}} output of the signed data 
> entity
> * {{ApplicationEntity.writeTo()}} produces different bytes than the original 
> {{MimeEntity.writeTo()}} (different MIME headers, e.g., missing 
> {{Content-Transfer-Encoding: 7bit}})
> * The partner compares the returned MIC with the MIC they computed before 
> sending
> * A mismatch causes the partner to flag the transfer as compromised
> h3. Additional Note: Existing Bug in {{getEntity()}}
> While investigating this issue, we noticed a bug in 
> {{HttpMessageUtils.getEntity()}}:
> {code:java}
> } else if (message instanceof BasicClassicHttpResponse httpResponse) {
>     HttpEntity entity = httpResponse.getEntity();
>     if (entity != null && type.isInstance(entity)) {
>         type.cast(entity);  // BUG: should be "return type.cast(entity);"
>     }
> }
> {code}
> The response branch casts but does not return the entity. This means 
> {{getEntity()}} always returns {{null}} for response messages.
> h3. Test Scenario
> Partner configuration: AS2 partner sending encrypted (enveloped-data) + 
> signed messages where the inner payload content type is {{text/plain; 
> charset=US-ASCII}}.
> Message structure:
> {code}
> application/pkcs7-mime; smime-type=enveloped-data    (outer: encrypted)
>   └─ multipart/signed                                (after decryption: 
> signed)
>        └─ text/plain; charset=US-ASCII               (inner payload — 
> rejected by Camel)
> {code}
> *Expected behavior:* Message accepted, EDI payload extracted, synchronous MDN 
> returned with correct MIC.
> *Actual behavior (Camel 4.18.0):* HTTP 500, no MDN, partner retries.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to