This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch coheigea/WSS-731
in repository https://gitbox.apache.org/repos/asf/ws-wss4j.git

commit b282749470428f150c3cf97214f6f39735f1e6e5
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Thu Oct 8 16:35:14 2026 +0100

    WSS-731 - StAX SignedElements policy rejects a SAML assertion signed 
through STR-Transform
---
 .../test/SamlSTRTransformSignedElementsTest.java   | 162 +++++++++++++++++++++
 .../input/SecurityTokenReferenceInputHandler.java  |  14 ++
 .../WSSSignatureReferenceVerifyInputProcessor.java |  78 +++++++++-
 3 files changed, 253 insertions(+), 1 deletion(-)

diff --git 
a/ws-security-policy-stax/src/test/java/org/apache/wss4j/policy/stax/test/SamlSTRTransformSignedElementsTest.java
 
b/ws-security-policy-stax/src/test/java/org/apache/wss4j/policy/stax/test/SamlSTRTransformSignedElementsTest.java
new file mode 100644
index 000000000..6eca40f5c
--- /dev/null
+++ 
b/ws-security-policy-stax/src/test/java/org/apache/wss4j/policy/stax/test/SamlSTRTransformSignedElementsTest.java
@@ -0,0 +1,162 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.wss4j.policy.stax.test;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Collections;
+
+import javax.xml.stream.XMLStreamException;
+
+import org.apache.wss4j.common.saml.bean.Version;
+import org.apache.wss4j.common.ext.WSSecurityException;
+import org.apache.wss4j.common.saml.builder.SAML1Constants;
+import org.apache.wss4j.policy.stax.enforcer.PolicyEnforcer;
+import org.apache.wss4j.policy.stax.enforcer.PolicyInputProcessor;
+import org.apache.wss4j.stax.ext.WSSConstants;
+import org.apache.wss4j.stax.ext.WSSSecurityProperties;
+import org.apache.wss4j.stax.securityToken.WSSecurityTokenConstants;
+import org.apache.wss4j.stax.test.CallbackHandlerImpl;
+import org.apache.wss4j.stax.test.saml.SAMLCallbackHandlerImpl;
+import org.apache.xml.security.stax.ext.SecurePart;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class SamlSTRTransformSignedElementsTest extends AbstractPolicyTestBase 
{
+
+    private static final String SIGNED_ASSERTION_POLICY =
+            "<sp:SignedElements 
xmlns:sp=\"http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702\";>"
+                    + "<sp:XPath 
xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"; "
+                    + 
"xmlns:wsse=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\";
 "
+                    + "xmlns:saml1=\"urn:oasis:names:tc:SAML:1.0:assertion\">"
+                    + 
"/soap:Envelope/soap:Header/wsse:Security/saml1:Assertion</sp:XPath>"
+                    + "</sp:SignedElements>";
+
+    @Test
+    public void testSaml11STRTransformSatisfiesSignedElements() throws 
Exception {
+        SAMLCallbackHandlerImpl samlCallback = new SAMLCallbackHandlerImpl();
+        samlCallback.setSamlVersion(Version.SAML_11);
+        samlCallback.setStatement(SAMLCallbackHandlerImpl.Statement.ATTR);
+        samlCallback.setConfirmationMethod(SAML1Constants.CONF_SENDER_VOUCHES);
+        samlCallback.setIssuer("www.example.com");
+        samlCallback.setSignAssertion(false);
+
+        WSSSecurityProperties outbound = new WSSSecurityProperties();
+        
outbound.setActions(Collections.singletonList(WSSConstants.SAML_TOKEN_SIGNED));
+        outbound.setSamlCallbackHandler(samlCallback);
+        outbound.setCallbackHandler(new CallbackHandlerImpl());
+        
outbound.loadSignatureKeyStore(getClass().getClassLoader().getResource("transmitter.jks"),
+                "default".toCharArray());
+        outbound.setSignatureUser("transmitter");
+        
outbound.setSignatureKeyIdentifier(WSSecurityTokenConstants.KEYIDENTIFIER_SECURITY_TOKEN_DIRECT_REFERENCE);
+        outbound.addSignaturePart(new SecurePart(WSSConstants.TAG_SOAP11_BODY, 
SecurePart.Modifier.Element));
+
+        byte[] message;
+        try (InputStream input = 
getClass().getClassLoader().getResourceAsStream("testdata/plain-soap-1.1.xml")) 
{
+            message = doOutboundSecurity(outbound, input).toByteArray();
+        }
+
+        // The same message must first pass signature and sender-vouches 
validation without a policy.
+        Document verified = doInboundSecurity(inboundProperties(), new 
ByteArrayInputStream(message));
+        NodeList assertions = 
verified.getElementsByTagNameNS(WSSConstants.NS_SAML, "Assertion");
+        assertEquals(1, assertions.getLength());
+        Element assertion = (Element) assertions.item(0);
+        assertEquals(0, assertion.getElementsByTagNameNS(WSSConstants.NS_DSIG, 
"Signature").getLength());
+        NodeList transforms = 
verified.getElementsByTagNameNS(WSSConstants.NS_DSIG, "Transform");
+        boolean strTransformFound = false;
+        for (int i = 0; i < transforms.getLength(); i++) {
+            Element transform = (Element) transforms.item(i);
+            strTransformFound |= 
WSSConstants.SOAPMESSAGE_NS10_STR_TRANSFORM.equals(transform.getAttribute("Algorithm"));
+        }
+        assertTrue(strTransformFound, "The SOAP signature must cover the 
assertion through an STR Transform");
+
+        PolicyEnforcer policyEnforcer = 
buildAndStartPolicyEngine(SIGNED_ASSERTION_POLICY);
+        WSSSecurityProperties inbound = inboundProperties();
+        inbound.addInputProcessor(new PolicyInputProcessor(policyEnforcer, 
inbound));
+
+        // STR Transform signs the assertion, so enabling this policy must 
also accept the message.
+        Document processed = doInboundSecurity(inbound, new 
ByteArrayInputStream(message), policyEnforcer);
+        assertEquals(1, processed.getElementsByTagNameNS(WSSConstants.NS_SAML, 
"Assertion").getLength());
+    }
+
+    /**
+     * The Body is signed but the Assertion next to it is not, neither 
directly nor through an
+     * STR-Transform, so it must still be rejected.
+     */
+    @Test
+    public void testSaml11UnsignedAssertionViolatesSignedElements() throws 
Exception {
+        SAMLCallbackHandlerImpl samlCallback = new SAMLCallbackHandlerImpl();
+        samlCallback.setSamlVersion(Version.SAML_11);
+        samlCallback.setStatement(SAMLCallbackHandlerImpl.Statement.ATTR);
+        // Not bearer: an unsigned bearer assertion is rejected by the SAML 
token validator before
+        // the policy is checked
+        samlCallback.setConfirmationMethod(SAML1Constants.CONF_SENDER_VOUCHES);
+        samlCallback.setIssuer("www.example.com");
+        samlCallback.setSignAssertion(false);
+
+        WSSSecurityProperties outbound = new WSSSecurityProperties();
+        outbound.setActions(Arrays.asList(WSSConstants.SAML_TOKEN_UNSIGNED, 
WSSConstants.SIGNATURE));
+        outbound.setSamlCallbackHandler(samlCallback);
+        outbound.setCallbackHandler(new CallbackHandlerImpl());
+        
outbound.loadSignatureKeyStore(getClass().getClassLoader().getResource("transmitter.jks"),
+                "default".toCharArray());
+        outbound.setSignatureUser("transmitter");
+        outbound.addSignaturePart(new SecurePart(WSSConstants.TAG_SOAP11_BODY, 
SecurePart.Modifier.Element));
+
+        byte[] message;
+        try (InputStream input = 
getClass().getClassLoader().getResourceAsStream("testdata/plain-soap-1.1.xml")) 
{
+            message = doOutboundSecurity(outbound, input).toByteArray();
+        }
+
+        PolicyEnforcer policyEnforcer = 
buildAndStartPolicyEngine(SIGNED_ASSERTION_POLICY);
+        WSSSecurityProperties inbound = inboundProperties();
+        // Otherwise the sender-vouches check rejects the message before the 
policy does
+        inbound.setValidateSamlSubjectConfirmation(false);
+        inbound.addInputProcessor(new PolicyInputProcessor(policyEnforcer, 
inbound));
+
+        try {
+            doInboundSecurity(inbound, new ByteArrayInputStream(message), 
policyEnforcer);
+            fail("Expected XMLStreamException");
+        } catch (XMLStreamException e) {
+            Throwable throwable = e.getCause();
+            assertNotNull(throwable);
+            assertTrue(throwable instanceof WSSecurityException);
+            assertEquals("Element /{" + WSSConstants.NS_SOAP11 + "}Envelope"
+                            + "/{" + WSSConstants.NS_SOAP11 + "}Header"
+                            + "/{" + WSSConstants.NS_WSSE10 + "}Security"
+                            + "/{" + WSSConstants.NS_SAML + "}Assertion must 
be signed",
+                    throwable.getMessage());
+        }
+    }
+
+    private WSSSecurityProperties inboundProperties() throws Exception {
+        WSSSecurityProperties properties = new WSSSecurityProperties();
+        
properties.loadSignatureVerificationKeystore(getClass().getClassLoader().getResource("receiver.jks"),
+                "default".toCharArray());
+        return properties;
+    }
+}
diff --git 
a/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/processor/input/SecurityTokenReferenceInputHandler.java
 
b/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/processor/input/SecurityTokenReferenceInputHandler.java
index 241900c9e..3170f51ed 100644
--- 
a/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/processor/input/SecurityTokenReferenceInputHandler.java
+++ 
b/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/processor/input/SecurityTokenReferenceInputHandler.java
@@ -18,6 +18,7 @@
  */
 package org.apache.wss4j.stax.impl.processor.input;
 
+import java.util.AbstractMap;
 import java.util.ArrayDeque;
 import java.util.Deque;
 import java.util.List;
@@ -55,6 +56,12 @@ import 
org.apache.xml.security.stax.securityToken.SecurityTokenProvider;
  */
 public class SecurityTokenReferenceInputHandler extends 
AbstractInputSecurityHeaderHandler {
 
+    /**
+     * SecurityContext key of a map from the wsu:Id of a 
SecurityTokenReference to the attribute
+     * name and value that identify the element it dereferences to.
+     */
+    static final String STR_DEREFERENCE_TARGETS = "STRDereferenceTargets";
+
     @Override
     public void handle(final InputProcessorChain inputProcessorChain, final 
XMLSecurityProperties securityProperties,
                        Deque<XMLSecEvent> eventQueue, Integer index) throws 
XMLSecurityException {
@@ -93,6 +100,13 @@ public class SecurityTokenReferenceInputHandler extends 
AbstractInputSecurityHea
                     securityTokenReferenceType.getId(), attributeName,
                     attributeValue, (WSSSecurityProperties) 
securityProperties);
             
inputProcessorChain.addProcessor(internalSecurityTokenReferenceInputHandler);
+            if (securityTokenReferenceType.getId() != null) {
+                //Lets a signature with an STR-Transform reference to this STR 
recognise the
+                //dereferenced element while it is replayed, see 
WSSSignatureReferenceVerifyInputProcessor
+                
inputProcessorChain.getSecurityContext().putAsMap(STR_DEREFERENCE_TARGETS,
+                        securityTokenReferenceType.getId(),
+                        new AbstractMap.SimpleImmutableEntry<>(attributeName, 
attributeValue));
+            }
         } else {
             throw new 
WSSecurityException(WSSecurityException.ErrorCode.UNSUPPORTED_SECURITY_TOKEN);
         }
diff --git 
a/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/processor/input/WSSSignatureReferenceVerifyInputProcessor.java
 
b/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/processor/input/WSSSignatureReferenceVerifyInputProcessor.java
index e85f9b27a..d1f35a6b2 100644
--- 
a/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/processor/input/WSSSignatureReferenceVerifyInputProcessor.java
+++ 
b/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/processor/input/WSSSignatureReferenceVerifyInputProcessor.java
@@ -28,14 +28,18 @@ import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
 import java.util.Base64;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import javax.security.auth.callback.Callback;
 import javax.security.auth.callback.CallbackHandler;
 import javax.xml.namespace.QName;
+import javax.xml.stream.XMLStreamConstants;
 import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.Attribute;
 
 import org.apache.wss4j.binding.wss10.TransformationParametersType;
 import org.apache.wss4j.common.bsp.BSPRule;
@@ -65,6 +69,7 @@ import org.apache.xml.security.stax.ext.Transformer;
 import org.apache.xml.security.stax.ext.XMLSecurityConstants;
 import org.apache.xml.security.stax.ext.XMLSecurityProperties;
 import org.apache.xml.security.stax.ext.XMLSecurityUtils;
+import org.apache.xml.security.stax.ext.stax.XMLSecEndElement;
 import org.apache.xml.security.stax.ext.stax.XMLSecEvent;
 import org.apache.xml.security.stax.ext.stax.XMLSecStartElement;
 import 
org.apache.xml.security.stax.impl.processor.input.AbstractSignatureReferenceVerifyInputProcessor;
@@ -82,6 +87,10 @@ public class WSSSignatureReferenceVerifyInputProcessor 
extends AbstractSignature
     private InternalSignatureReferenceVerifier completedReferenceVerifier;
     private boolean replayChecked = false;
     private ReplayCacheEntry pendingReplayCacheEntry;
+    private final Set<String> markedSTRTransformTargets = new HashSet<>();
+    private Object strTransformTargetMarker;
+    private QName strTransformTargetName;
+    private int strTransformTargetLevel;
 
     public WSSSignatureReferenceVerifyInputProcessor(InputProcessorChain 
inputProcessorChain,
             SignatureType signatureType, InboundSecurityToken 
inboundSecurityToken,
@@ -273,7 +282,74 @@ public class WSSSignatureReferenceVerifyInputProcessor 
extends AbstractSignature
             replayChecked = true;
             pendingReplayCacheEntry = detectReplayAttack(inputProcessorChain);
         }
-        return super.processEvent(inputProcessorChain);
+        XMLSecEvent xmlSecEvent = super.processEvent(inputProcessorChain);
+        markSTRTransformTarget(xmlSecEvent, inputProcessorChain);
+        return xmlSecEvent;
+    }
+
+    /**
+     * An STR-Transform Reference points at a SecurityTokenReference, but what 
it signs is the
+     * element the STR dereferences to (e.g. a SAML Assertion). That element 
precedes the STR, so it
+     * is replayed before the Reference is resolved, and without a marker it 
would be reported as
+     * unsigned to the policy layer. Mark it as signed content while it 
streams, as is done for an
+     * element referenced directly by Id. As there, the digest is still 
verified: the STR is
+     * replayed later, and doFinal() rejects the message if the Reference was 
never processed.
+     */
+    private void markSTRTransformTarget(XMLSecEvent xmlSecEvent, 
InputProcessorChain inputProcessorChain) {
+        if (strTransformTargetMarker != null) {
+            if (XMLStreamConstants.END_ELEMENT == xmlSecEvent.getEventType()) {
+                XMLSecEndElement xmlSecEndElement = xmlSecEvent.asEndElement();
+                if (xmlSecEndElement.getDocumentLevel() == 
strTransformTargetLevel
+                        && 
xmlSecEndElement.getName().equals(strTransformTargetName)) {
+                    
inputProcessorChain.getDocumentContext().unsetIsInSignedContent(strTransformTargetMarker);
+                    strTransformTargetMarker = null;
+                }
+            }
+            return;
+        }
+        if (XMLStreamConstants.START_ELEMENT != xmlSecEvent.getEventType()) {
+            return;
+        }
+        Map<String, Map.Entry<QName, String>> strDereferenceTargets =
+                inputProcessorChain.getSecurityContext().getAsMap(
+                        
SecurityTokenReferenceInputHandler.STR_DEREFERENCE_TARGETS);
+        if (strDereferenceTargets == null || strDereferenceTargets.isEmpty()) {
+            return;
+        }
+        XMLSecStartElement xmlSecStartElement = xmlSecEvent.asStartElement();
+        List<ReferenceType> referenceTypes = 
getSignatureType().getSignedInfo().getReference();
+        for (int i = 0; i < referenceTypes.size(); i++) {
+            ReferenceType referenceType = referenceTypes.get(i);
+            if (getProcessedReferences().contains(referenceType) || 
!isSTRTransformReference(referenceType)) {
+                continue;
+            }
+            String strId = 
XMLSecurityUtils.dropReferenceMarker(referenceType.getURI());
+            Map.Entry<QName, String> target = strDereferenceTargets.get(strId);
+            if (target == null || markedSTRTransformTargets.contains(strId)) {
+                continue;
+            }
+            Attribute attribute = 
xmlSecStartElement.getAttributeByName(target.getKey());
+            if (attribute != null && 
target.getValue().equals(attribute.getValue())) {
+                //Only the first match, as that is the element the STR is 
resolved to. A second one
+                //is rejected as a duplicate Id by 
SecurityTokenReferenceInputHandler.
+                markedSTRTransformTargets.add(strId);
+                strTransformTargetMarker = new Object();
+                strTransformTargetName = xmlSecStartElement.getName();
+                strTransformTargetLevel = 
xmlSecStartElement.getDocumentLevel();
+                inputProcessorChain.getDocumentContext().setIsInSignedContent(
+                        inputProcessorChain.getProcessors().indexOf(this), 
strTransformTargetMarker);
+                return;
+            }
+        }
+    }
+
+    private static boolean isSTRTransformReference(ReferenceType 
referenceType) {
+        //buildTransformerChain() treats a Reference as an STR-Transform one 
when its first
+        //Transform is the STR-Transform
+        return referenceType.getTransforms() != null
+                && !referenceType.getTransforms().getTransform().isEmpty()
+                && WSSConstants.SOAPMESSAGE_NS10_STR_TRANSFORM.equals(
+                        
referenceType.getTransforms().getTransform().get(0).getAlgorithm());
     }
 
     @Override

Reply via email to