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)