From 1df9e555123b9f944ebb2b532f1a72df900d5def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Olivi=C3=A9?= Date: Sun, 7 Dec 2025 20:08:20 +0000 Subject: [PATCH 1/4] Fix MIC calculation for RFC 4130 compliance - Move MIC calculation to after decryption/verification - Calculate MIC on first body part of multipart/signed messages - Ensures MIC is calculated on same data as sender --- .../receiver/net/AS2ReceiverHandler.java | 24 ++++++----- .../com/helger/phase2/util/AS2Helper.java | 40 ++++++++++++++++++- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/phase2-lib/src/main/java/com/helger/phase2/processor/receiver/net/AS2ReceiverHandler.java b/phase2-lib/src/main/java/com/helger/phase2/processor/receiver/net/AS2ReceiverHandler.java index 78ba2fb1..79609cd0 100644 --- a/phase2-lib/src/main/java/com/helger/phase2/processor/receiver/net/AS2ReceiverHandler.java +++ b/phase2-lib/src/main/java/com/helger/phase2/processor/receiver/net/AS2ReceiverHandler.java @@ -565,17 +565,6 @@ public void handleIncomingMessage (@Nonnull final String sClientInfo, () -> AbstractActiveNetModule.DISP_PARTNERSHIP_NOT_FOUND); } - // Calculate MIC before decrypt and decompress (see #140) - try - { - aIncomingMIC = AS2Helper.createMICOnReception (aMsg); - } - catch (final Exception ex) - { - // Ignore error - throw WrappedAS2Exception.wrap (ex); - } - // Per RFC5402 compression is always before encryption but can be before // or after signing of message but only in one place final ICryptoHelper aCryptoHelper = AS2Helper.getCryptoHelper (); @@ -596,6 +585,19 @@ public void handleIncomingMessage (@Nonnull final String sClientInfo, // Verify may fail, if our certificate is expired verify (aMsg, aResHelper); + // Calculate MIC AFTER decryption and signature verification (RFC 4130) + // The MIC must be calculated on the same data that the sender calculated it on, + // which is the decrypted signed content, not the encrypted envelope + try + { + aIncomingMIC = AS2Helper.createMICOnReception (aMsg); + } + catch (final Exception ex) + { + // Ignore error + throw WrappedAS2Exception.wrap (ex); + } + if (aCryptoHelper.isCompressed (aMsg.getContentType ())) { // Per RFC5402 compression is always before encryption but can be diff --git a/phase2-lib/src/main/java/com/helger/phase2/util/AS2Helper.java b/phase2-lib/src/main/java/com/helger/phase2/util/AS2Helper.java index 2ddceb27..b8d36976 100644 --- a/phase2-lib/src/main/java/com/helger/phase2/util/AS2Helper.java +++ b/phase2-lib/src/main/java/com/helger/phase2/util/AS2Helper.java @@ -268,7 +268,45 @@ public static MIC createMICOnReception (@Nonnull final AS2Message aMsg) throws E aPartnership.getEncryptAlgorithm () != null || aPartnership.getCompressionType () != null; - return getCryptoHelper ().calculateMIC (aMsg.getData (), eSigningAlgorithm, bIncludeHeadersInMIC); + // For signed messages (multipart/signed), calculate MIC on the first body part (the actual content), + // not on the multipart wrapper which includes a random boundary parameter + MimeBodyPart aPartToHash = aMsg.getData (); + LOGGER.info ("createMICOnReception: signingAlgorithm=" + aPartnership.getSigningAlgorithm () + ", contentType=" + aPartToHash.getContentType ()); + if (aPartnership.getSigningAlgorithm () != null) + { + try + { + final Object aContent = aPartToHash.getContent (); + LOGGER.info ("createMICOnReception: content class=" + (aContent != null ? aContent.getClass ().getName () : "null")); + if (aContent instanceof com.helger.mail.cte.EContentTransferEncoding) + { + // Already the right part + LOGGER.info ("createMICOnReception: Content is EContentTransferEncoding"); + } + else if (aContent instanceof jakarta.mail.Multipart) + { + // This is multipart/signed - extract the first body part (the signed content) + final jakarta.mail.Multipart aMultipart = (jakarta.mail.Multipart) aContent; + LOGGER.info ("createMICOnReception: Content is Multipart with " + aMultipart.getCount () + " parts"); + if (aMultipart.getCount () > 0) + { + aPartToHash = (MimeBodyPart) aMultipart.getBodyPart (0); + LOGGER.info ("Calculating MIC on first body part of multipart/signed, not on the multipart wrapper"); + } + } + else + { + LOGGER.info ("createMICOnReception: Content is unknown type"); + } + } + catch (final Exception ex) + { + LOGGER.warn ("Failed to extract first body part from multipart/signed: " + ex.getMessage (), ex); + // Fall back to using the whole part + } + } + + return getCryptoHelper ().calculateMIC (aPartToHash, eSigningAlgorithm, bIncludeHeadersInMIC); } /** From 3d67a90cfbf13aa61e037b372e5c91ed0e39dbc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Olivi=C3=A9?= Date: Mon, 8 Dec 2025 17:45:33 +0000 Subject: [PATCH 2/4] Update to callback to mimic send --- .../helger/phase2/crypto/BCCryptoHelper.java | 10 +++- .../helger/phase2/crypto/ICryptoHelper.java | 1 + .../com/helger/phase2/message/AS2Message.java | 2 + .../phase2/message/AbstractMessage.java | 12 +++++ .../com/helger/phase2/message/IMessage.java | 5 ++ .../receiver/net/AS2ReceiverHandler.java | 6 +++ .../com/helger/phase2/util/AS2Helper.java | 49 ++++++------------- .../phase2/crypto/BCCryptoHelperTest.java | 2 +- 8 files changed, 50 insertions(+), 37 deletions(-) diff --git a/phase2-lib/src/main/java/com/helger/phase2/crypto/BCCryptoHelper.java b/phase2-lib/src/main/java/com/helger/phase2/crypto/BCCryptoHelper.java index 83fe2a9f..aaa311d0 100644 --- a/phase2-lib/src/main/java/com/helger/phase2/crypto/BCCryptoHelper.java +++ b/phase2-lib/src/main/java/com/helger/phase2/crypto/BCCryptoHelper.java @@ -694,6 +694,7 @@ public MimeBodyPart verify (@Nonnull final MimeBodyPart aPart, final boolean bUseCertificateInBodyPart, final boolean bForceVerifySigned, @Nullable final Consumer aEffectiveCertificateConsumer, + @Nullable final Consumer aMICSourceConsumer, @Nonnull final AS2ResourceHelper aResHelper) throws GeneralSecurityException, IOException, MessagingException, @@ -753,6 +754,13 @@ public MimeBodyPart verify (@Nonnull final MimeBodyPart aPart, throw new SignatureException ("Verification failed for SignerInfo " + aSignerInfo); } - return aSignedParser.getContent (); + final MimeBodyPart aSignedContent = aSignedParser.getContent (); + + // Invoke callback with the signed content for MIC calculation + // This mirrors the sender's callback pattern where MIC is calculated on pre-signature content + if (aMICSourceConsumer != null) + aMICSourceConsumer.accept (aSignedContent); + + return aSignedContent; } } diff --git a/phase2-lib/src/main/java/com/helger/phase2/crypto/ICryptoHelper.java b/phase2-lib/src/main/java/com/helger/phase2/crypto/ICryptoHelper.java index b01b0779..6545488a 100644 --- a/phase2-lib/src/main/java/com/helger/phase2/crypto/ICryptoHelper.java +++ b/phase2-lib/src/main/java/com/helger/phase2/crypto/ICryptoHelper.java @@ -223,5 +223,6 @@ MimeBodyPart verify (@Nonnull MimeBodyPart aPart, boolean bUseCertificateInBodyPart, boolean bForceVerifySigned, @Nullable Consumer aEffectiveCertificateConsumer, + @Nullable Consumer aMICSourceConsumer, @Nonnull AS2ResourceHelper aResHelper) throws Exception; } diff --git a/phase2-lib/src/main/java/com/helger/phase2/message/AS2Message.java b/phase2-lib/src/main/java/com/helger/phase2/message/AS2Message.java index 7816ea4d..e1fab40e 100644 --- a/phase2-lib/src/main/java/com/helger/phase2/message/AS2Message.java +++ b/phase2-lib/src/main/java/com/helger/phase2/message/AS2Message.java @@ -55,6 +55,8 @@ public class AS2Message extends AbstractMessage public static final String ATTRIBUTE_RECEIVED_COMPRESSED = "as2msg.received.compressed"; /** Optional attribute storing the created MIC (see #74) */ public static final String ATTRIBUTE_MIC = "MIC"; + /** MimeBodyPart to use for MIC calculation - captured during signature verification */ + public static final String ATTRIBUTE_MIC_SOURCE = "as2msg.mic.source"; public static final String PROTOCOL_AS2 = "as2"; public static final String DEFAULT_ID_FORMAT = CPhase2Info.NAME + diff --git a/phase2-lib/src/main/java/com/helger/phase2/message/AbstractMessage.java b/phase2-lib/src/main/java/com/helger/phase2/message/AbstractMessage.java index 99ba4082..b9217de9 100644 --- a/phase2-lib/src/main/java/com/helger/phase2/message/AbstractMessage.java +++ b/phase2-lib/src/main/java/com/helger/phase2/message/AbstractMessage.java @@ -60,6 +60,7 @@ public abstract class AbstractMessage extends AbstractBaseMessage implements IMe private static final Logger LOGGER = LoggerFactory.getLogger (AbstractMessage.class); private MimeBodyPart m_aData; + private MimeBodyPart m_aMICSource; private IMessageMDN m_aMDN; private TempSharedFileInputStream m_aTempSharedFileInputStream; @@ -152,6 +153,17 @@ public final void setData (@Nullable final MimeBodyPart aData) } } + @Nullable + public final MimeBodyPart getMICSource () + { + return m_aMICSource; + } + + public final void setMICSource (@Nullable final MimeBodyPart aMICSource) + { + m_aMICSource = aMICSource; + } + @Nullable public final IMessageMDN getMDN () { diff --git a/phase2-lib/src/main/java/com/helger/phase2/message/IMessage.java b/phase2-lib/src/main/java/com/helger/phase2/message/IMessage.java index bd4d70c2..478dc41d 100644 --- a/phase2-lib/src/main/java/com/helger/phase2/message/IMessage.java +++ b/phase2-lib/src/main/java/com/helger/phase2/message/IMessage.java @@ -106,6 +106,11 @@ default void setSubject (@Nullable final String sSubject) void setData (@Nonnull MimeBodyPart aData); + @Nullable + MimeBodyPart getMICSource (); + + void setMICSource (@Nullable MimeBodyPart aMICSource); + @Nullable IMessageMDN getMDN (); diff --git a/phase2-lib/src/main/java/com/helger/phase2/processor/receiver/net/AS2ReceiverHandler.java b/phase2-lib/src/main/java/com/helger/phase2/processor/receiver/net/AS2ReceiverHandler.java index 79609cd0..56388512 100644 --- a/phase2-lib/src/main/java/com/helger/phase2/processor/receiver/net/AS2ReceiverHandler.java +++ b/phase2-lib/src/main/java/com/helger/phase2/processor/receiver/net/AS2ReceiverHandler.java @@ -302,11 +302,13 @@ protected void verify (@Nonnull final IMessage aMsg, @Nonnull final AS2ResourceH } final Wrapper aCertHolder = new Wrapper <> (); + final Wrapper aMICSourceHolder = new Wrapper <> (); final MimeBodyPart aVerifiedData = aCryptoHelper.verify (aMsg.getData (), aSenderCert, bUseCertificateInBodyPart, bForceVerify, aCertHolder::set, + aMICSourceHolder::set, aResHelper); final Consumer aExternalConsumer = getVerificationCertificateConsumer (); if (aExternalConsumer != null) @@ -314,6 +316,10 @@ protected void verify (@Nonnull final IMessage aMsg, @Nonnull final AS2ResourceH aMsg.setData (aVerifiedData); + // Store the MIC source for later calculation (mirrors sender's callback pattern) + if (aMICSourceHolder.isSet ()) + aMsg.setMICSource (aMICSourceHolder.get ()); + // Remember that message was signed and verified aMsg.attrs ().putIn (AS2Message.ATTRIBUTE_RECEIVED_SIGNED, true); diff --git a/phase2-lib/src/main/java/com/helger/phase2/util/AS2Helper.java b/phase2-lib/src/main/java/com/helger/phase2/util/AS2Helper.java index b8d36976..8e37b117 100644 --- a/phase2-lib/src/main/java/com/helger/phase2/util/AS2Helper.java +++ b/phase2-lib/src/main/java/com/helger/phase2/util/AS2Helper.java @@ -268,43 +268,21 @@ public static MIC createMICOnReception (@Nonnull final AS2Message aMsg) throws E aPartnership.getEncryptAlgorithm () != null || aPartnership.getCompressionType () != null; - // For signed messages (multipart/signed), calculate MIC on the first body part (the actual content), - // not on the multipart wrapper which includes a random boundary parameter - MimeBodyPart aPartToHash = aMsg.getData (); - LOGGER.info ("createMICOnReception: signingAlgorithm=" + aPartnership.getSigningAlgorithm () + ", contentType=" + aPartToHash.getContentType ()); - if (aPartnership.getSigningAlgorithm () != null) + // Use the MIC source captured during signature verification (via callback) + // This mirrors the sender's callback pattern where MIC is calculated on pre-signature content + MimeBodyPart aPartToHash = aMsg.getMICSource (); + if (aPartToHash == null) { - try - { - final Object aContent = aPartToHash.getContent (); - LOGGER.info ("createMICOnReception: content class=" + (aContent != null ? aContent.getClass ().getName () : "null")); - if (aContent instanceof com.helger.mail.cte.EContentTransferEncoding) - { - // Already the right part - LOGGER.info ("createMICOnReception: Content is EContentTransferEncoding"); - } - else if (aContent instanceof jakarta.mail.Multipart) - { - // This is multipart/signed - extract the first body part (the signed content) - final jakarta.mail.Multipart aMultipart = (jakarta.mail.Multipart) aContent; - LOGGER.info ("createMICOnReception: Content is Multipart with " + aMultipart.getCount () + " parts"); - if (aMultipart.getCount () > 0) - { - aPartToHash = (MimeBodyPart) aMultipart.getBodyPart (0); - LOGGER.info ("Calculating MIC on first body part of multipart/signed, not on the multipart wrapper"); - } - } - else - { - LOGGER.info ("createMICOnReception: Content is unknown type"); - } - } - catch (final Exception ex) - { - LOGGER.warn ("Failed to extract first body part from multipart/signed: " + ex.getMessage (), ex); - // Fall back to using the whole part - } + // Fallback for unsigned messages - use the message data directly + aPartToHash = aMsg.getData (); + LOGGER.info ("createMICOnReception: No MIC source captured (unsigned message), using message data directly"); } + else + { + LOGGER.info ("createMICOnReception: Using captured MIC source from signature verification"); + } + + LOGGER.info ("createMICOnReception: signingAlgorithm=" + aPartnership.getSigningAlgorithm () + ", contentType=" + aPartToHash.getContentType ()); return getCryptoHelper ().calculateMIC (aPartToHash, eSigningAlgorithm, bIncludeHeadersInMIC); } @@ -482,6 +460,7 @@ public static void parseMDN (@Nonnull final IMessage aMsg, bUseCertificateInBodyPart, bForceVerify, aCertHolder::set, + null, aResHelper); if (aEffectiveCertificateConsumer != null) aEffectiveCertificateConsumer.accept (aCertHolder.get ()); diff --git a/phase2-lib/src/test/java/com/helger/phase2/crypto/BCCryptoHelperTest.java b/phase2-lib/src/test/java/com/helger/phase2/crypto/BCCryptoHelperTest.java index 3038de80..69a74c5c 100644 --- a/phase2-lib/src/test/java/com/helger/phase2/crypto/BCCryptoHelperTest.java +++ b/phase2-lib/src/test/java/com/helger/phase2/crypto/BCCryptoHelperTest.java @@ -146,7 +146,7 @@ public void testSignWithAllAlgorithms () throws Exception // Verify as well LOGGER.info (" Now verifying result of signing algo " + eAlgo); - aCryptoHelper.verify (aSigned, (X509Certificate) PKE.getCertificate (), bIncludeCert, true, null, aResHelper); + aCryptoHelper.verify (aSigned, (X509Certificate) PKE.getCertificate (), bIncludeCert, true, null, null, aResHelper); } } } From de9a76d8c074cae1327833681f17eab7145dcea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Olivi=C3=A9?= Date: Mon, 8 Dec 2025 17:59:35 +0000 Subject: [PATCH 3/4] Remove unecessary constant --- .../src/main/java/com/helger/phase2/message/AS2Message.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/phase2-lib/src/main/java/com/helger/phase2/message/AS2Message.java b/phase2-lib/src/main/java/com/helger/phase2/message/AS2Message.java index e1fab40e..7816ea4d 100644 --- a/phase2-lib/src/main/java/com/helger/phase2/message/AS2Message.java +++ b/phase2-lib/src/main/java/com/helger/phase2/message/AS2Message.java @@ -55,8 +55,6 @@ public class AS2Message extends AbstractMessage public static final String ATTRIBUTE_RECEIVED_COMPRESSED = "as2msg.received.compressed"; /** Optional attribute storing the created MIC (see #74) */ public static final String ATTRIBUTE_MIC = "MIC"; - /** MimeBodyPart to use for MIC calculation - captured during signature verification */ - public static final String ATTRIBUTE_MIC_SOURCE = "as2msg.mic.source"; public static final String PROTOCOL_AS2 = "as2"; public static final String DEFAULT_ID_FORMAT = CPhase2Info.NAME + From 447d5aeaf9c474c01f83d86db117dd32b3ff1a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Olivi=C3=A9?= Date: Mon, 8 Dec 2025 18:48:01 +0000 Subject: [PATCH 4/4] Handle unsigned messages --- .../receiver/net/AS2ReceiverHandler.java | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/phase2-lib/src/main/java/com/helger/phase2/processor/receiver/net/AS2ReceiverHandler.java b/phase2-lib/src/main/java/com/helger/phase2/processor/receiver/net/AS2ReceiverHandler.java index 56388512..cca75e26 100644 --- a/phase2-lib/src/main/java/com/helger/phase2/processor/receiver/net/AS2ReceiverHandler.java +++ b/phase2-lib/src/main/java/com/helger/phase2/processor/receiver/net/AS2ReceiverHandler.java @@ -33,6 +33,7 @@ package com.helger.phase2.processor.receiver.net; import java.io.IOException; +import java.io.InputStream; import java.net.Socket; import java.security.PrivateKey; import java.security.cert.X509Certificate; @@ -537,11 +538,27 @@ public void handleIncomingMessage (@Nonnull final String sClientInfo, // Put received data in a MIME body part final String sReceivedContentType = AS2HttpHelper.getCleanContentType (aMsg.getHeader (CHttpHeader.CONTENT_TYPE)); + // Read raw bytes from DataSource to preserve original content for MIC calculation + final byte [] aRawBytes; + try (final InputStream aIS = aMsgData.getInputStream ()) + { + aRawBytes = StreamHelper.getAllBytes (aIS); + } + + // Create MimeBodyPart with raw bytes using ByteArrayDataSource + // This ensures MIC calculation uses exact bytes as received, without JavaMail modifications + final ByteArrayDataSource aByteArrayDS = new ByteArrayDataSource (aRawBytes, sReceivedContentType, null); final MimeBodyPart aReceivedPart = new MimeBodyPart (); - aReceivedPart.setDataHandler (new DataHandler (aMsgData)); + aReceivedPart.setDataHandler (new DataHandler (aByteArrayDS)); // Header must be set AFTER the DataHandler! aReceivedPart.setHeader (CHttpHeader.CONTENT_TYPE, sReceivedContentType); + + // Copy Content-Disposition from HTTP headers if present (important for MIC calculation on unsigned messages) + final String sContentDisposition = aMsg.getHeader (CHttpHeader.CONTENT_DISPOSITION); + if (sContentDisposition != null) + aReceivedPart.setHeader (CHttpHeader.CONTENT_DISPOSITION, sContentDisposition); + aMsg.setData (aReceivedPart); } catch (final Exception ex) @@ -591,6 +608,13 @@ public void handleIncomingMessage (@Nonnull final String sClientInfo, // Verify may fail, if our certificate is expired verify (aMsg, aResHelper); + // For unsigned messages, set MIC source to current data (after decrypt/decompress) + // For signed messages, this was already set by the verify() callback + if (aMsg.getMICSource () == null) + { + aMsg.setMICSource (aMsg.getData ()); + } + // Calculate MIC AFTER decryption and signature verification (RFC 4130) // The MIC must be calculated on the same data that the sender calculated it on, // which is the decrypted signed content, not the encrypted envelope