You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@camel.apache.org by "Guillaume Nodet (Jira)" <ji...@apache.org> on 2026/03/09 10:43:00 UTC

[jira] [Assigned] (CAMEL-23065) camel-as2 - MicUtils.createReceivedContentMic() computes wrong MIC for compressed messages (RFC 5402 violation)

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

Guillaume Nodet reassigned CAMEL-23065:
---------------------------------------

    Assignee: Guillaume Nodet

> camel-as2 - MicUtils.createReceivedContentMic() computes wrong MIC for compressed messages (RFC 5402 violation)
> ---------------------------------------------------------------------------------------------------------------
>
>                 Key: CAMEL-23065
>                 URL: https://issues.apache.org/jira/browse/CAMEL-23065
>             Project: Camel
>          Issue Type: Bug
>          Components: camel-as2
>    Affects Versions: 4.18.0
>            Reporter: Reto Peter
>            Assignee: Guillaume Nodet
>            Priority: Minor
>         Attachments: MicUtils.java
>
>
> {panel:title=Problem}
>   \{{MicUtils.createReceivedContentMic()}} in \{{camel-as2-api}} always computes the MIC (Message Integrity Check) over the fully decompressed content. Per RFC 5402 (AS2 Compression), the MIC must be computed over "what was signed" — which depends on the
>   compression ordering. When compression is used, the MDN returned to the sender contains a wrong MIC, causing the sender to flag the transfer as compromised (MIC mismatch).
>   \{panel}
>   h3. RFC 5402 Background
>   AS2 with compression supports two orderings:
>   Compress-before-sign (compress → sign → encrypt):
>   \{code}
>   Enveloped (encrypted)
>     └─ MultipartSigned
>          └─ part 0: CompressedDataEntity   ← MIC must be computed over THIS (compressed bytes)
>   \{code}
>   Sign-before-compress (sign → compress → encrypt):
>   \{code}
>   Enveloped (encrypted)
>     └─ CompressedDataEntity
>          └─ (decompress) → MultipartSigned
>                               └─ part 0: ApplicationEntity   ← MIC must be computed over THIS (uncompressed bytes)
>   \{code}
>   RFC 5402 Section 4 states: "The MIC MUST be calculated over the same content that was signed."
>   h3. Root Cause
>   The current \{{createReceivedContentMic()}} method calls \{{HttpMessageUtils.extractEdiPayload()}} which fully decompresses the content, then hashes whatever comes out via \{{EntityUtils.getContent(entity)}}:
>   \{code:java|title=Current code in MicUtils.java (Camel 4.18.0)}
>   public static ReceivedContentMic createReceivedContentMic(
>           ClassicHttpRequest request, Certificate[] validateSigningCertificateChain,
>           PrivateKey decryptingPrivateKey) throws HttpException {
>   // ... disposition notification options parsing ...
>   HttpEntity entity = HttpMessageUtils.extractEdiPayload(request,
>           new HttpMessageUtils.DecrpytingAndSigningInfo(
>               validateSigningCertificateChain, decryptingPrivateKey));
>   byte[] content = EntityUtils.getContent(entity);  // <-- BUG: always uses decompressed content
>   String micAS2AlgorithmName = AS2MicAlgorithm.getAS2AlgorithmName(micJdkAlgorithmName);
>   byte[] mic = createMic(content, micJdkAlgorithmName);
>   // ...
>   }
>   \{code}
>   \{{extractEdiPayload()}} processes the full entity hierarchy (decrypt → decompress → verify signature → extract payload). The returned entity is always the final decompressed payload. This means:
>   - Compress-before-sign: The sender signed the compressed entity, so the MIC should be over the compressed bytes. But Camel hashes the decompressed bytes → MIC mismatch.
>   - Sign-before-compress: The sender signed the original uncompressed entity, and Camel hashes the decompressed (= original) bytes → MIC matches (only by coincidence).
>   h3. Steps to Reproduce
>   Configure a Camel AS2 server endpoint with encryption, signing, and compression enabled
>   Have a partner send an AS2 message using compress-before-sign ordering (this is the default ordering in most AS2 implementations, including Mendelson)
>   Observe: MDN is returned, but the \{{Received-Content-MIC}} value in the MDN does not match the sender's expected MIC
>   The sender reports a MIC verification failure / integrity error
>   h3. Proposed Fix
>   After extracting the EDI payload (which triggers entity parsing), navigate the parsed entity hierarchy on the request to find the entity at the "signed" level, then compute the MIC over the correct entity.
>   h4. New helper record and method to add
>   \{code:java|title=New helper: findSignedDataEntity()}
>   private record SignedDataResult(MimeEntity entity, boolean compressionDetected) {}
>   /**
>   - Navigate the parsed entity hierarchy to find the entity at the "signed" level.
>   - Handles both compression orderings defined by RFC 5402.
>    */
>   private static SignedDataResult findSignedDataEntity(ClassicHttpRequest request, PrivateKey decryptKey) {
>    try {
>    if (!(request instanceof HttpEntityContainer)) {
>        return null;
>    }
>    HttpEntity outerEntity = ((HttpEntityContainer) request).getEntity();
>    // If encrypted, decrypt to get the inner entity
>    MimeEntity innerEntity;
>    if (outerEntity instanceof ApplicationPkcs7MimeEnvelopedDataEntity envelopedEntity) {
>        if (decryptKey == null) {
>            return null;
>        }
>        innerEntity = envelopedEntity.getEncryptedEntity(decryptKey);
>    } else if (outerEntity instanceof MimeEntity) {
>        innerEntity = (MimeEntity) outerEntity;
>    } else {
>        return null;
>    }
>    // Case 1: Compress-before-sign (compress → sign → encrypt)
>    // After decryption: MultipartSigned → part 0 is CompressedDataEntity
>    if (innerEntity instanceof MultipartSignedEntity signedEntity) {
>        MimeEntity signedData = signedEntity.getSignedDataEntity();
>        boolean compressed = signedData instanceof ApplicationPkcs7MimeCompressedDataEntity;
>        return new SignedDataResult(signedData, compressed);
>    }
>    // Case 2: Sign-before-compress (sign → compress → encrypt)
>    // After decryption: CompressedDataEntity → decompress → MultipartSigned → part 0
>    if (innerEntity instanceof ApplicationPkcs7MimeCompressedDataEntity compressedEntity) {
>        MimeEntity decompressed = compressedEntity.getCompressedEntity(new ZlibExpanderProvider());
>        if (decompressed instanceof MultipartSignedEntity signedEntity) {
>            return new SignedDataResult(signedEntity.getSignedDataEntity(), true);
>        }
>        return new SignedDataResult(innerEntity, true);
>    }
>    // Plain or other structure — no compression
>    return new SignedDataResult(innerEntity, false);
>    } catch (Exception e) {
>    LOG.warn("Failed to navigate entity hierarchy for MIC computation: {}", e.getMessage());
>    return null;
>    }
>   }
>   \{code}
>   h4. Modified \{{createReceivedContentMic()}}
>   Replace the single \{{EntityUtils.getContent(entity)}} call with compression-aware logic:
>   \{code:diff}
>        HttpEntity entity = HttpMessageUtils.extractEdiPayload(request,
>                new HttpMessageUtils.DecrpytingAndSigningInfo(
>                    validateSigningCertificateChain, decryptingPrivateKey));
>   - byte[] content = EntityUtils.getContent(entity);
>   - byte[] content;
>   - // RFC 5402: MIC is computed over "what was signed":
>   - // - Compress-before-sign: signed data IS the compressed entity → hash compressed entity
>   - // - Sign-before-compress: signed data is the original uncompressed entity → hash content
>   - SignedDataResult result = findSignedDataEntity(request, decryptingPrivateKey);
>   - MimeEntity signedDataEntity = result != null ? result.entity : null;
>   - if (signedDataEntity instanceof ApplicationPkcs7MimeCompressedDataEntity) {
>      // Compress-before-sign: hash the compressed MIME entity (what was signed)
>      ByteArrayOutputStream compressedBaos = new ByteArrayOutputStream();
>      try {
>          signedDataEntity.writeTo(compressedBaos);
>          content = compressedBaos.toByteArray();
>      } catch (Exception e) {
>          LOG.warn("Failed to get compressed entity bytes, falling back to decompressed content: {}",
>              e.getMessage());
>          content = EntityUtils.getContent(entity);
>      }
>   - } else {
>      // Sign-before-compress OR no compression — decompressed content is correct
>      content = EntityUtils.getContent(entity);
>   - }
>   - String micAS2AlgorithmName = AS2MicAlgorithm.getAS2AlgorithmName(micJdkAlgorithmName);
>   \{code}
>   h4. Additional imports needed
>   \{code:java}
>   import java.io.ByteArrayOutputStream;
>   import org.apache.camel.component.as2.api.entity.ApplicationPkcs7MimeCompressedDataEntity;
>   import org.apache.camel.component.as2.api.entity.ApplicationPkcs7MimeEnvelopedDataEntity;
>   import org.apache.camel.component.as2.api.entity.MimeEntity;
>   import org.apache.camel.component.as2.api.entity.MultipartSignedEntity;
>   import org.apache.hc.core5.http.HttpEntityContainer;
>   import org.bouncycastle.cms.jcajce.ZlibExpanderProvider;
>   \{code}
>   h3. Why This Fix is Correct
>   The key insight is that \{{extractEdiPayload()}} already parses the full entity hierarchy and attaches the parsed entities to the request object. After that call, we can navigate the already-parsed hierarchy (no re-parsing needed) to find which entity was
>   signed:
>   - Compress-before-sign: After decryption we get \{{MultipartSigned}} → its \{{getSignedDataEntity()}} returns the \{{CompressedDataEntity}} → we hash its \{{writeTo()}} output (the compressed MIME bytes, including headers)
>   - Sign-before-compress: After decryption we get \{{CompressedDataEntity}} → decompress → \{{MultipartSigned}} → its \{{getSignedDataEntity()}} returns the original \{{ApplicationEntity}} → we hash via \{{EntityUtils.getContent()}} (same as before)
>   - No compression: Falls through to the existing \{{EntityUtils.getContent()}} path (unchanged behavior)
>   h3. Test Scenario
>   Tested with:
>   - Mendelson AS2 (SYNC MDN) — uses compress-before-sign by default
>   - OpenAS2 (ASYNC MDN) — tested both compression orderings
>   Before fix: MIC mismatch reported by both partners when compression was enabled.
>   After fix: MIC matches correctly for both compression orderings



--
This message was sent by Atlassian Jira
(v8.20.10#820010)