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 <[email protected]> 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 <[email protected]> --- .../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();
