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

Guillaume Nodet closed CAMEL-23064.
-----------------------------------
    Fix Version/s: 4.19.0
       Resolution: Fixed

> 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
>            Assignee: Guillaume Nodet
>            Priority: Minor
>             Fix For: 4.19.0
>
>         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