[
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)