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

Reto Peter updated CAMEL-23064:
-------------------------------
    Attachment: HttpMessageUtils.java

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