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

Claus Ibsen updated CAMEL-23065:
--------------------------------
    Priority: Minor  (was: Major)

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

Reply via email to