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
