Reto Peter created CAMEL-23064:
----------------------------------

             Summary: 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


{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