This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch 3_0_x-fixes
in repository https://gitbox.apache.org/repos/asf/ws-wss4j.git
The following commit(s) were added to refs/heads/3_0_x-fixes by this push:
new fb49fef3c WSS-731 - StAX SignedElements policy rejects a SAML
assertion signed through STR-Transform (#745)
fb49fef3c is described below
commit fb49fef3c1ae394f005fe0783a913e85dc8b687e
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Fri Oct 9 08:11:22 2026 +0100
WSS-731 - StAX SignedElements policy rejects a SAML assertion signed
through STR-Transform (#745)
---
.../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