Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

/**
Expand Down Expand Up @@ -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 <code>true</code> if incoming UserMessages must be signed and/or encrypted as defined
* by the security settings of the effective PMode leg, <code>false</code> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -155,7 +156,9 @@ public interface IAS4ParsedMessageCallback
void handle (@NonNull HttpHeaderMap aHttpHeaders,
@NonNull Document aSoapDocument,
@NonNull ESoapVersion eSoapVersion,
@NonNull ICommonsList <WSS4JAttachment> aIncomingAttachments) throws WSSecurityException, MessagingException, Phase4Exception;
@NonNull ICommonsList <WSS4JAttachment> aIncomingAttachments) throws WSSecurityException,
MessagingException,
Phase4Exception;
}

private static final Logger LOGGER = Phase4LoggerFactory.getLogger (AS4IncomingHandler.class);
Expand All @@ -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");
Expand Down Expand Up @@ -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 <WSS4JAttachment> aIncomingAttachments = new CommonsArrayList <> ();
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -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 <code>null</code>.
* @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 <String> 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.
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <Object []> data ()
{
return new CommonsArrayList <> (ESoapVersion.values (), x -> new Object [] { x });
}

private final ESoapVersion m_eSoapVersion;

public UserMessageEnforcePModeSecurityTest (@NonNull final ESoapVersion eSOAPVersion)
{
m_eSoapVersion = eSOAPVersion;
}

@Before
public void enableEnforcement ()
{
SystemProperties.setPropertyValue (AS4Configuration.PROPERTY_PHASE4_INCOMING_SECURITY_ENFORCE_PMODE, "true");
}

@After
public void resetEnforcement ()
{
SystemProperties.removePropertyValue (AS4Configuration.PROPERTY_PHASE4_INCOMING_SECURITY_ENFORCE_PMODE);
}

@NonNull
private static Node _readPayload ()
{
return DOMReader.readXMLDOM (new ClassPathResource (AS4TestConstants.TEST_SOAP_BODY_PAYLOAD_XML));
}

@Test
public void testNotSignedNotEncryptedShouldFail () throws Exception
{
final Node aPayload = _readPayload ();
final Document aDoc = MockMessages.createUserMessageNotSigned (m_eSoapVersion, aPayload, null)
.getAsSoapDocument (aPayload);

sendPlainMessageExpectError (new HttpXMLEntity (aDoc, m_eSoapVersion.getMimeType ()),
EEbmsError.EBMS_POLICY_NONCOMPLIANCE.getErrorCode ());
}

@Test
public void testSignedNotEncryptedShouldFail () throws Exception
{
final Document aDoc = MockMessages.createUserMessageSigned (m_eSoapVersion, _readPayload (), null, s_aResMgr);

sendPlainMessageExpectError (new HttpXMLEntity (aDoc, m_eSoapVersion.getMimeType ()),
EEbmsError.EBMS_POLICY_NONCOMPLIANCE.getErrorCode ());
}

@Test
public void testEncryptedNotSignedShouldFail () throws Exception
{
final Node aPayload = _readPayload ();
Document aDoc = MockMessages.createUserMessageNotSigned (m_eSoapVersion, aPayload, null)
.getAsSoapDocument (aPayload);
aDoc = AS4Encryptor.encryptSoapBodyPayload (m_aCryptoFactory, m_eSoapVersion, aDoc, false, m_aCryptParams);

sendPlainMessageExpectError (new HttpXMLEntity (aDoc, m_eSoapVersion.getMimeType ()),
EEbmsError.EBMS_POLICY_NONCOMPLIANCE.getErrorCode ());
}

@Test
public void testSignedAndEncryptedSuccess () throws Exception
{
Document aDoc = MockMessages.createUserMessageSigned (m_eSoapVersion, _readPayload (), null, s_aResMgr);
aDoc = AS4Encryptor.encryptSoapBodyPayload (m_aCryptoFactory, m_eSoapVersion, aDoc, false, m_aCryptParams);

final String sResponse = sendPlainMessageExpectSuccess (new HttpXMLEntity (aDoc, m_eSoapVersion.getMimeType ()));
assertTrue (sResponse.contains (AS4TestConstants.RECEIPT_ASSERTCHECK));
}

@Test
public void testNotSignedNotEncryptedSuccessIfDisabled () throws Exception
{
SystemProperties.setPropertyValue (AS4Configuration.PROPERTY_PHASE4_INCOMING_SECURITY_ENFORCE_PMODE, "false");

final Node aPayload = _readPayload ();
final Document aDoc = MockMessages.createUserMessageNotSigned (m_eSoapVersion, aPayload, null)
.getAsSoapDocument (aPayload);

final String sResponse = sendPlainMessageExpectSuccess (new HttpXMLEntity (aDoc, m_eSoapVersion.getMimeType ()));
assertTrue (sResponse.contains (AS4TestConstants.RECEIPT_ASSERTCHECK));
}
}
Loading