You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by gn...@apache.org on 2026/03/09 10:05:20 UTC

(camel) 02/08: CAMEL-23065: Fix MIC computation for compress-before-sign scenarios

This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch fix/as2-issues
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 356cff07d479232699ff10f561f372588e09f10f
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Mon Mar 9 11:04:21 2026 +0100

    CAMEL-23065: Fix MIC computation for compress-before-sign scenarios
    
    Navigate the entity hierarchy to find the correct entity for MIC
    computation per RFC 5402. For compress-before-sign, the MIC must cover
    the compressed entity (what was signed), not the decompressed EDI
    payload.
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
 .../camel/component/as2/api/util/MicUtils.java     | 96 +++++++++++++++++++++-
 1 file changed, 93 insertions(+), 3 deletions(-)

diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/MicUtils.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/MicUtils.java
index b1699058d8a9..1852f5d530de 100644
--- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/MicUtils.java
+++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/MicUtils.java
@@ -25,9 +25,16 @@ import java.security.cert.Certificate;
 
 import org.apache.camel.component.as2.api.AS2Header;
 import org.apache.camel.component.as2.api.AS2MicAlgorithm;
+import org.apache.camel.component.as2.api.AS2MimeType;
+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.DispositionNotificationOptions;
 import org.apache.camel.component.as2.api.entity.DispositionNotificationOptionsParser;
+import org.apache.camel.component.as2.api.entity.EntityParser;
+import org.apache.camel.component.as2.api.entity.MimeEntity;
+import org.apache.camel.component.as2.api.entity.MultipartSignedEntity;
 import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.HttpException;
 import org.slf4j.Logger;
@@ -98,10 +105,12 @@ public final class MicUtils {
             return null;
         }
 
-        HttpEntity entity = HttpMessageUtils.extractEdiPayload(request,
-                new HttpMessageUtils.DecrpytingAndSigningInfo(validateSigningCertificateChain, decryptingPrivateKey));
+        // Compute MIC over the correct content per RFC 5402:
+        // - For compress-before-sign: MIC covers the compressed entity (what was signed)
+        // - For sign-before-compress or no compression: MIC covers the EDI payload
+        HttpEntity micEntity = findMicEntity(request, validateSigningCertificateChain, decryptingPrivateKey);
 
-        byte[] content = EntityUtils.getContent(entity);
+        byte[] content = EntityUtils.getContent(micEntity);
 
         String micAS2AlgorithmName = AS2MicAlgorithm.getAS2AlgorithmName(micJdkAlgorithmName);
         byte[] mic = createMic(content, micJdkAlgorithmName);
@@ -112,6 +121,87 @@ public final class MicUtils {
         }
     }
 
+    /**
+     * Finds the correct entity to compute the MIC over, per RFC 5402.
+     * <p>
+     * For compress-before-sign, the MIC must cover the compressed entity (what was signed). For sign-before-compress or
+     * no compression, the MIC covers the EDI payload.
+     */
+    private static HttpEntity findMicEntity(
+            ClassicHttpRequest request, Certificate[] validateSigningCertificateChain, PrivateKey decryptingPrivateKey)
+            throws HttpException {
+
+        EntityParser.parseAS2MessageEntity(request);
+
+        String contentTypeString = HttpMessageUtils.getHeaderValue(request, AS2Header.CONTENT_TYPE);
+        if (contentTypeString == null) {
+            throw new HttpException("Failed to create MIC: content type missing from request");
+        }
+        ContentType contentType = ContentType.parse(contentTypeString);
+
+        // Navigate the message structure to find the signed data entity
+        HttpEntity entity = findSignedDataEntity(request, contentType, decryptingPrivateKey);
+        if (entity != null) {
+            return entity;
+        }
+
+        // No multipart/signed found, fall back to extracting the EDI payload
+        return HttpMessageUtils.extractEdiPayload(request,
+                new HttpMessageUtils.DecrpytingAndSigningInfo(validateSigningCertificateChain, decryptingPrivateKey));
+    }
+
+    /**
+     * Navigate the entity hierarchy to find the signed data entity (part 0 of multipart/signed). Returns the signed
+     * data entity which is what the MIC should be computed over, or null if no multipart/signed is found.
+     */
+    private static HttpEntity findSignedDataEntity(
+            ClassicHttpRequest request, ContentType contentType, PrivateKey decryptingPrivateKey)
+            throws HttpException {
+
+        String mimeType = contentType.getMimeType().toLowerCase();
+
+        if (AS2MimeType.MULTIPART_SIGNED.equals(mimeType)) {
+            // Top-level signed: MIC is over the signed data entity (part 0)
+            MultipartSignedEntity multipartSignedEntity
+                    = HttpMessageUtils.getEntity(request, MultipartSignedEntity.class);
+            if (multipartSignedEntity != null) {
+                return multipartSignedEntity.getSignedDataEntity();
+            }
+        } else if (AS2MimeType.APPLICATION_PKCS7_MIME.equals(mimeType)) {
+            String smimeType = contentType.getParameter("smime-type");
+            if ("compressed-data".equals(smimeType)) {
+                // Sign-before-compress: decompress first, then find the signed entity inside
+                ApplicationPkcs7MimeCompressedDataEntity compressedEntity
+                        = HttpMessageUtils.getEntity(request, ApplicationPkcs7MimeCompressedDataEntity.class);
+                if (compressedEntity != null) {
+                    MimeEntity inner = compressedEntity
+                            .getCompressedEntity(new org.bouncycastle.cms.jcajce.ZlibExpanderProvider());
+                    if (inner instanceof MultipartSignedEntity signedEntity) {
+                        return signedEntity.getSignedDataEntity();
+                    }
+                }
+            } else if ("enveloped-data".equals(smimeType) && decryptingPrivateKey != null) {
+                // Encrypted message: decrypt first, then look for signed entity
+                ApplicationPkcs7MimeEnvelopedDataEntity envelopedEntity
+                        = HttpMessageUtils.getEntity(request, ApplicationPkcs7MimeEnvelopedDataEntity.class);
+                if (envelopedEntity != null) {
+                    MimeEntity decryptedEntity = envelopedEntity.getEncryptedEntity(decryptingPrivateKey);
+                    String decryptedContentType = decryptedEntity.getContentType();
+                    if (decryptedContentType != null) {
+                        ContentType decryptedCt = ContentType.parse(decryptedContentType);
+                        String decryptedMime = decryptedCt.getMimeType().toLowerCase();
+                        if (AS2MimeType.MULTIPART_SIGNED.equals(decryptedMime)
+                                && decryptedEntity instanceof MultipartSignedEntity signedEntity) {
+                            return signedEntity.getSignedDataEntity();
+                        }
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+
     public static String getMicJdkAlgorithmName(String[] micAs2AlgorithmNames) {
         if (micAs2AlgorithmNames == null) {
             return AS2MicAlgorithm.SHA_1.getJdkAlgorithmName();