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 bdfa9560..244eda45 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 @@ -697,6 +697,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, @@ -754,6 +755,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 4069cb73..2c1807a5 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 @@ -224,5 +224,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/AbstractMessage.java b/phase2-lib/src/main/java/com/helger/phase2/message/AbstractMessage.java index 47c9d608..0d442bb9 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 95727d0c..dc5e0634 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 @@ -107,6 +107,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 db5d318c..ea2781f2 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; @@ -302,11 +303,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 +317,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); @@ -531,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) @@ -565,17 +588,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 +608,26 @@ 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 + 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 d63e9915..6c71a0e8 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,23 @@ public static MIC createMICOnReception (@NonNull final AS2Message aMsg) throws E aPartnership.getEncryptAlgorithm () != null || aPartnership.getCompressionType () != null; - return getCryptoHelper ().calculateMIC (aMsg.getData (), eSigningAlgorithm, bIncludeHeadersInMIC); + // 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) + { + // 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); } /** @@ -444,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); } } }