diff --git a/phase4-lib/src/main/java/com/helger/phase4/config/AS4Configuration.java b/phase4-lib/src/main/java/com/helger/phase4/config/AS4Configuration.java
index 2f8da5f5c..02ab49867 100644
--- a/phase4-lib/src/main/java/com/helger/phase4/config/AS4Configuration.java
+++ b/phase4-lib/src/main/java/com/helger/phase4/config/AS4Configuration.java
@@ -26,6 +26,7 @@
import com.helger.annotation.CheckForSigned;
import com.helger.annotation.concurrent.GuardedBy;
+import com.helger.annotation.misc.ChangeNextMajorRelease;
import com.helger.base.CGlobal;
import com.helger.base.concurrent.SimpleReadWriteLock;
import com.helger.base.debug.GlobalDebug;
@@ -156,6 +157,18 @@ public final class AS4Configuration
public static final String PROPERTY_PHASE4_INCOMING_SIGNATURE_REQUIRE_FULL_COVERAGE = "phase4.incoming.signature.requirefullcoverage";
public static final boolean DEFAULT_PHASE4_INCOMING_SIGNATURE_REQUIRE_FULL_COVERAGE = true;
+ /**
+ * The boolean property that defines, if the security settings of the effective PMode leg are
+ * enforced on incoming UserMessages. If enabled, an incoming UserMessage must be signed, if the
+ * PMode leg defines a signature algorithm, and it must be encrypted, if the PMode leg defines an
+ * encryption algorithm. Signal messages are not affected.
+ *
+ * @since 4.8.0
+ */
+ public static final String PROPERTY_PHASE4_INCOMING_SECURITY_ENFORCE_PMODE = "phase4.incoming.security.enforcepmode";
+ @ChangeNextMajorRelease ("Swap to true")
+ public static final boolean DEFAULT_PHASE4_INCOMING_SECURITY_ENFORCE_PMODE = false;
+
private static final Logger LOGGER = Phase4LoggerFactory.getLogger (AS4Configuration.class);
/**
@@ -565,4 +578,16 @@ public static boolean isIncomingSignatureRequireFullCoverage ()
return getConfig ().getAsBoolean (PROPERTY_PHASE4_INCOMING_SIGNATURE_REQUIRE_FULL_COVERAGE,
DEFAULT_PHASE4_INCOMING_SIGNATURE_REQUIRE_FULL_COVERAGE);
}
+
+ /**
+ * @return true if incoming UserMessages must be signed and/or encrypted as defined
+ * by the security settings of the effective PMode leg, false if not.
+ * Defaults to {@value #DEFAULT_PHASE4_INCOMING_SECURITY_ENFORCE_PMODE}.
+ * @since 4.8.0
+ */
+ public static boolean isIncomingSecurityEnforcePMode ()
+ {
+ return getConfig ().getAsBoolean (PROPERTY_PHASE4_INCOMING_SECURITY_ENFORCE_PMODE,
+ DEFAULT_PHASE4_INCOMING_SECURITY_ENFORCE_PMODE);
+ }
}
diff --git a/phase4-lib/src/main/java/com/helger/phase4/incoming/AS4IncomingHandler.java b/phase4-lib/src/main/java/com/helger/phase4/incoming/AS4IncomingHandler.java
index 3efe405b5..5f889c5b8 100644
--- a/phase4-lib/src/main/java/com/helger/phase4/incoming/AS4IncomingHandler.java
+++ b/phase4-lib/src/main/java/com/helger/phase4/incoming/AS4IncomingHandler.java
@@ -98,6 +98,7 @@
import com.helger.phase4.model.message.MessageHelperMethods;
import com.helger.phase4.model.pmode.IPMode;
import com.helger.phase4.model.pmode.leg.PModeLeg;
+import com.helger.phase4.model.pmode.leg.PModeLegSecurity;
import com.helger.phase4.model.pmode.resolve.IAS4PModeResolver;
import com.helger.phase4.profile.IAS4Profile;
import com.helger.phase4.profile.IAS4ProfileValidator;
@@ -155,7 +156,9 @@ public interface IAS4ParsedMessageCallback
void handle (@NonNull HttpHeaderMap aHttpHeaders,
@NonNull Document aSoapDocument,
@NonNull ESoapVersion eSoapVersion,
- @NonNull ICommonsList aIncomingAttachments) throws WSSecurityException, MessagingException, Phase4Exception;
+ @NonNull ICommonsList aIncomingAttachments) throws WSSecurityException,
+ MessagingException,
+ Phase4Exception;
}
private static final Logger LOGGER = Phase4LoggerFactory.getLogger (AS4IncomingHandler.class);
@@ -179,7 +182,10 @@ public static void parseAS4Message (@NonNull final IAS4IncomingAttachmentFactory
@NonNull @WillClose final InputStream aPayloadIS,
@NonNull final HttpHeaderMap aHttpHeaders,
@NonNull final IAS4ParsedMessageCallback aParsedMessageCallback,
- @Nullable final IAS4IncomingDumper aIncomingDumper) throws Phase4Exception, IOException, MessagingException, WSSecurityException
+ @Nullable final IAS4IncomingDumper aIncomingDumper) throws Phase4Exception,
+ IOException,
+ MessagingException,
+ WSSecurityException
{
ValueEnforcer.notNull (aIAF, "IncomingAttachmentFactory");
ValueEnforcer.notNull (aResHelper, "ResHelper");
@@ -207,8 +213,8 @@ public static void parseAS4Message (@NonNull final IAS4IncomingAttachmentFactory
}
// Fallback to global dumper if none is provided
- final IAS4IncomingDumper aRealIncomingDumper = aIncomingDumper != null ? aIncomingDumper
- : AS4DumpManager.getIncomingDumper ();
+ final IAS4IncomingDumper aRealIncomingDumper = aIncomingDumper != null ? aIncomingDumper : AS4DumpManager
+ .getIncomingDumper ();
Document aSoapDocument = null;
ESoapVersion eSoapVersion = null;
final ICommonsList aIncomingAttachments = new CommonsArrayList <> ();
@@ -293,11 +299,10 @@ public static void parseAS4Message (@NonNull final IAS4IncomingAttachmentFactory
try (final MultipartItemInputStream aBodyPartIS = aMulti.createInputStream ())
{
// Limit the size of a single attachment (see issue #318)
- final InputStream aPartIS = nIndex == 0 ? aBodyPartIS
- : new AS4SizeLimitedInputStream (aBodyPartIS,
- "The incoming attachment #" +
- nIndex,
- nMaxAttachmentSizeBytes);
+ final InputStream aPartIS = nIndex == 0 ? aBodyPartIS : new AS4SizeLimitedInputStream (aBodyPartIS,
+ "The incoming attachment #" +
+ nIndex,
+ nMaxAttachmentSizeBytes);
// Read the headers only - the content stays on the stream and is
// consumed in a streaming way (see issue #382)
@@ -751,10 +756,9 @@ private static void _checkSignatureCoverage (@NonNull final Document aSoapDocume
final Element aSoapHeader = XMLHelper.getFirstChildElementOfName (aSoapEnvelope,
eSoapVersion.getNamespaceURI (),
eSoapVersion.getHeaderElementName ());
- final Element aMessagingElement = aSoapHeader == null ? null
- : XMLHelper.getFirstChildElementOfName (aSoapHeader,
- CAS4.EBMS_NS,
- "Messaging");
+ final Element aMessagingElement = aSoapHeader == null ? null : XMLHelper.getFirstChildElementOfName (aSoapHeader,
+ CAS4.EBMS_NS,
+ "Messaging");
if (!aIncomingState.isElementSigned (aMessagingElement))
aUncoveredParts.add ("the ebMS Messaging header element");
}
@@ -801,6 +805,44 @@ private static void _checkSignatureCoverage (@NonNull final Document aSoapDocume
}
}
+ /**
+ * Check that an incoming UserMessage was signed and/or encrypted, if the effective PMode leg
+ * requires it. See issue #404.
+ *
+ * @param aIncomingState
+ * The current incoming message state. Must contain the effective PMode leg.
+ * @param aEffectiveLeg
+ * The effective PMode leg of the incoming UserMessage. May not be null.
+ * @param aEbmsErrorMessagesTarget
+ * The error list to fill in case the security requirements are not met.
+ */
+ private static void _checkSecurityAgainstPModeLeg (@NonNull final IAS4IncomingMessageState aIncomingState,
+ @NonNull final PModeLeg aEffectiveLeg,
+ @NonNull final AS4ErrorList aEbmsErrorMessagesTarget)
+ {
+ final PModeLegSecurity aSecurity = aEffectiveLeg.getSecurity ();
+ if (aSecurity != null)
+ {
+ final ICommonsList aMissing = new CommonsArrayList <> ();
+ if (aSecurity.hasX509SignatureAlgorithm () && !aIncomingState.isSoapSignatureChecked ())
+ aMissing.add ("signed");
+ if (aSecurity.hasX509EncryptionAlgorithm () && !aIncomingState.isSoapDecrypted ())
+ aMissing.add ("encrypted");
+
+ if (aMissing.isNotEmpty ())
+ {
+ final String sDetails = "The incoming UserMessage is not " +
+ StringImplode.imploder ().source (aMissing).separator (" and ").build () +
+ ", but the effective PMode leg requires it";
+ LOGGER.error (sDetails + ". Rejecting the message.");
+ aEbmsErrorMessagesTarget.add (EEbmsError.EBMS_POLICY_NONCOMPLIANCE.errorBuilder (aIncomingState.getLocale ())
+ .refToMessageInError (aIncomingState.getMessageID ())
+ .errorDetail (sDetails)
+ .build ());
+ }
+ }
+ }
+
/**
* Create an input stream provider that can be read multiple times, by lazily copying the data of
* the provided single-read input stream provider to a temporary file on first access.
@@ -1061,6 +1103,14 @@ public static IAS4IncomingMessageState processEbmsMessage (@NonNull @WillNotClos
if (aEffectiveLeg == null)
throw new Phase4IncomingException ("No AS4 P-Mode leg could be determined!").setRetryFeasible (false);
+ // Make sure the message was secured as the PMode leg requires it (see issue #404)
+ if (AS4Configuration.isIncomingSecurityEnforcePMode ())
+ {
+ _checkSecurityAgainstPModeLeg (aIncomingState, aEffectiveLeg, aEbmsErrorMessagesTarget);
+ if (aEbmsErrorMessagesTarget.isNotEmpty ())
+ return aIncomingState;
+ }
+
// Only do profile checks if a profile is set
// Profile Checks gets set when started with Server
if (aValidator != null)
diff --git a/phase4-lib/src/test/java/com/helger/phase4/config/AS4ConfigurationTest.java b/phase4-lib/src/test/java/com/helger/phase4/config/AS4ConfigurationTest.java
index 29881ba53..a80fcd320 100644
--- a/phase4-lib/src/test/java/com/helger/phase4/config/AS4ConfigurationTest.java
+++ b/phase4-lib/src/test/java/com/helger/phase4/config/AS4ConfigurationTest.java
@@ -140,4 +140,22 @@ public void testIncomingSignatureRequireFullCoverage ()
SystemProperties.removePropertyValue (sKey);
}
}
+
+ @Test
+ public void testIncomingSecurityEnforcePMode ()
+ {
+ // Disabled by default for backwards compatibility
+ assertFalse (AS4Configuration.isIncomingSecurityEnforcePMode ());
+
+ final String sKey = AS4Configuration.PROPERTY_PHASE4_INCOMING_SECURITY_ENFORCE_PMODE;
+ try
+ {
+ SystemProperties.setPropertyValue (sKey, "true");
+ assertTrue (AS4Configuration.isIncomingSecurityEnforcePMode ());
+ }
+ finally
+ {
+ SystemProperties.removePropertyValue (sKey);
+ }
+ }
}
diff --git a/phase4-test/src/test/java/com/helger/phase4/server/message/UserMessageEnforcePModeSecurityTest.java b/phase4-test/src/test/java/com/helger/phase4/server/message/UserMessageEnforcePModeSecurityTest.java
new file mode 100644
index 000000000..c3a2a520f
--- /dev/null
+++ b/phase4-test/src/test/java/com/helger/phase4/server/message/UserMessageEnforcePModeSecurityTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2015-2026 Philip Helger (www.helger.com)
+ * philip[at]helger[dot]com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.helger.phase4.server.message;
+
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collection;
+
+import org.jspecify.annotations.NonNull;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import com.helger.base.system.SystemProperties;
+import com.helger.collection.commons.CommonsArrayList;
+import com.helger.io.resource.ClassPathResource;
+import com.helger.phase4.AS4TestConstants;
+import com.helger.phase4.config.AS4Configuration;
+import com.helger.phase4.messaging.crypto.AS4Encryptor;
+import com.helger.phase4.messaging.http.HttpXMLEntity;
+import com.helger.phase4.model.ESoapVersion;
+import com.helger.phase4.model.error.EEbmsError;
+import com.helger.xml.serialize.read.DOMReader;
+
+/**
+ * Test the enforcement of the PMode leg security settings on incoming UserMessages. The PMode used
+ * by the mock messages defines both a signature and an encryption algorithm. See issue #404.
+ *
+ * @author Philip Helger
+ */
+@RunWith (Parameterized.class)
+public final class UserMessageEnforcePModeSecurityTest extends AbstractUserMessageTestSetUp
+{
+ @Parameters (name = "{index}: {0}")
+ public static Collection