This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/sender-vouches in repository https://gitbox.apache.org/repos/asf/ws-wss4j.git
commit 156afb578342571628ff0c01c9c405bb17ecd205 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Mon Sep 21 09:50:09 2026 +0100 Fix SenderVouches EncryptedKey case --- .../org/apache/wss4j/dom/saml/DOMSAMLUtil.java | 35 +++++ .../SamlSenderVouchesVouchingIdentityTest.java | 170 +++++++++++++++++++++ 2 files changed, 205 insertions(+) diff --git a/ws-security-dom/src/main/java/org/apache/wss4j/dom/saml/DOMSAMLUtil.java b/ws-security-dom/src/main/java/org/apache/wss4j/dom/saml/DOMSAMLUtil.java index 9ebaadad8..36dc4544f 100644 --- a/ws-security-dom/src/main/java/org/apache/wss4j/dom/saml/DOMSAMLUtil.java +++ b/ws-security-dom/src/main/java/org/apache/wss4j/dom/saml/DOMSAMLUtil.java @@ -242,6 +242,26 @@ public final class DOMSAMLUtil { return true; } + + /** + * Whether a signature result identifies the sender that produced it, as opposed to merely + * proving possession of a key. + * + * @param signedResult a SIGN or UT_SIGN result + * @return true if a trust decision was taken on the signing credential, or the signature was + * made with a key derived from a UsernameToken whose password was verified + */ + private static boolean establishesSenderIdentity(WSSecurityEngineResult signedResult) { + if (Boolean.TRUE.equals(signedResult.get(WSSecurityEngineResult.TAG_VALIDATED_TOKEN))) { + return true; + } + + // A UsernameToken derived key is not stamped as a validated token - no Validator runs on + // the signing credential itself - but the token's password was verified before the key was + // derived from it, so the sender is authenticated all the same. + Integer action = (Integer)signedResult.get(WSSecurityEngineResult.TAG_ACTION); + return action != null && WSConstants.UT_SIGN == action.intValue(); + } /** * Return true if there is a signature which references the Assertion and the SOAP Body. * @param assertionWrapper the SamlAssertionWrapper object @@ -254,7 +274,22 @@ public final class DOMSAMLUtil { Element body, List<WSSecurityEngineResult> signed ) { + // A sender-vouches assertion is only worth as much as the identity of whoever vouched for + // it. An assertion that is itself signed carries that backing already - the signature was + // trust-verified against its issuer (see SamlAssertionValidator.verifySignedAssertion) - + // and the message signature merely binds it to this message, so any signature will do. + // + // An unsigned assertion has no such backing. The only party asserting it is whoever signed + // the message, so that signature has to have been made with a credential whose identity was + // actually established. A signature verified with a bare symmetric key - one taken from an + // EncryptedKey that the sender minted for itself, say - proves possession of that key and + // nothing whatsoever about who sent it, and so cannot vouch for anybody. + boolean vouchingIdentityRequired = !assertionWrapper.isSigned(); + for (WSSecurityEngineResult signedResult : signed) { + if (vouchingIdentityRequired && !establishesSenderIdentity(signedResult)) { + continue; + } @SuppressWarnings("unchecked") List<WSDataRef> sl = (List<WSDataRef>)signedResult.get( diff --git a/ws-security-dom/src/test/java/org/apache/wss4j/dom/saml/SamlSenderVouchesVouchingIdentityTest.java b/ws-security-dom/src/test/java/org/apache/wss4j/dom/saml/SamlSenderVouchesVouchingIdentityTest.java new file mode 100644 index 000000000..b5c22567d --- /dev/null +++ b/ws-security-dom/src/test/java/org/apache/wss4j/dom/saml/SamlSenderVouchesVouchingIdentityTest.java @@ -0,0 +1,170 @@ +/** + * 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.dom.saml; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.wss4j.common.crypto.Crypto; +import org.apache.wss4j.common.crypto.CryptoFactory; +import org.apache.wss4j.common.saml.SAMLCallback; +import org.apache.wss4j.common.saml.SAMLUtil; +import org.apache.wss4j.common.saml.SamlAssertionWrapper; +import org.apache.wss4j.common.saml.builder.SAML1Constants; +import org.apache.wss4j.common.util.SOAPUtil; +import org.apache.wss4j.dom.WSConstants; +import org.apache.wss4j.dom.WSDataRef; +import org.apache.wss4j.dom.common.SAML1CallbackHandler; +import org.apache.wss4j.dom.engine.WSSConfig; +import org.apache.wss4j.dom.engine.WSSecurityEngineResult; +import org.apache.wss4j.dom.util.WSSecurityUtil; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * A sender-vouches assertion is a claim made by whoever sent the message, so it is worth exactly as + * much as that sender's identity. An assertion that is itself signed carries its issuer's signature, + * which is trust-verified separately; an unsigned one does not, and the only thing asserting it is + * the signature over the message. That signature therefore has to have been made with a credential + * whose identity was established - not merely with a key the sender happens to hold, such as one + * taken from an EncryptedKey the sender minted for itself. + */ +public class SamlSenderVouchesVouchingIdentityTest { + + private final Crypto crypto; + + public SamlSenderVouchesVouchingIdentityTest() throws Exception { + WSSConfig.init(); + crypto = CryptoFactory.getInstance(); + } + + /** + * The shape of the bypass: an unsigned sender-vouches assertion, and a signature that covers it + * and the Body but was verified with a bare symmetric key, so it says nothing about who sent it. + */ + @Test + public void testUnsignedAssertionRejectsUnidentifiedSigner() throws Exception { + Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG); + SamlAssertionWrapper assertion = senderVouchesAssertion(doc, false); + Element body = WSSecurityUtil.findBodyElement(doc); + + List<WSSecurityEngineResult> signed = Collections.singletonList( + coveringSignature(WSConstants.SIGN, false, assertion.getElement(), body)); + + assertFalse(DOMSAMLUtil.checkSenderVouches(assertion, null, body, signed), + "An unsigned sender-vouches assertion must not be accepted on the word of a signer " + + "whose identity was never established"); + } + + /** + * The same coverage from a credential a Validator made a trust decision about is what + * sender-vouches is supposed to look like. + */ + @Test + public void testUnsignedAssertionAcceptsTrustValidatedSigner() throws Exception { + Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG); + SamlAssertionWrapper assertion = senderVouchesAssertion(doc, false); + Element body = WSSecurityUtil.findBodyElement(doc); + + List<WSSecurityEngineResult> signed = Collections.singletonList( + coveringSignature(WSConstants.SIGN, true, assertion.getElement(), body)); + + assertTrue(DOMSAMLUtil.checkSenderVouches(assertion, null, body, signed)); + } + + /** + * A UsernameToken derived key is never stamped as a validated token, but deriving it required + * the password, so the sender is authenticated and may vouch. + */ + @Test + public void testUnsignedAssertionAcceptsUsernameTokenSigner() throws Exception { + Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG); + SamlAssertionWrapper assertion = senderVouchesAssertion(doc, false); + Element body = WSSecurityUtil.findBodyElement(doc); + + List<WSSecurityEngineResult> signed = Collections.singletonList( + coveringSignature(WSConstants.UT_SIGN, false, assertion.getElement(), body)); + + assertTrue(DOMSAMLUtil.checkSenderVouches(assertion, null, body, signed)); + } + + /** + * An assertion signed by its issuer already carries a vouching identity of its own - the + * message signature only binds it to this message - so the symmetric-binding deployments that + * rely on that pattern must keep working. + */ + @Test + public void testSignedAssertionAcceptsAnySigner() throws Exception { + Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG); + SamlAssertionWrapper assertion = senderVouchesAssertion(doc, true); + assertTrue(assertion.isSigned(), "precondition: the assertion is signed"); + Element body = WSSecurityUtil.findBodyElement(doc); + + List<WSSecurityEngineResult> signed = Collections.singletonList( + coveringSignature(WSConstants.SIGN, false, assertion.getElement(), body)); + + assertTrue(DOMSAMLUtil.checkSenderVouches(assertion, null, body, signed)); + } + + private SamlAssertionWrapper senderVouchesAssertion(Document doc, boolean sign) throws Exception { + SAML1CallbackHandler callbackHandler = new SAML1CallbackHandler(); + callbackHandler.setStatement(SAML1CallbackHandler.Statement.AUTHN); + callbackHandler.setConfirmationMethod(SAML1Constants.CONF_SENDER_VOUCHES); + callbackHandler.setIssuer("www.example.com"); + + SAMLCallback samlCallback = new SAMLCallback(); + SAMLUtil.doSAMLCallback(callbackHandler, samlCallback); + + SamlAssertionWrapper samlAssertion = new SamlAssertionWrapper(samlCallback); + if (sign) { + samlAssertion.signAssertion("16c73ab6-b892-458f-abf5-2f875f74882e", "security", + crypto, false); + } + samlAssertion.toDOM(doc); + return samlAssertion; + } + + /** + * A signature result covering the given elements. {@code validated} is what + * SignatureProcessor stamps when a trust decision was actually taken on the signing credential. + */ + private WSSecurityEngineResult coveringSignature( + int action, boolean validated, Element... protectedElements + ) { + List<WSDataRef> dataRefs = new ArrayList<>(); + for (Element protectedElement : protectedElements) { + WSDataRef dataRef = new WSDataRef(); + dataRef.setProtectedElement(protectedElement); + dataRefs.add(dataRef); + } + + WSSecurityEngineResult result = new WSSecurityEngineResult(action); + result.put(WSSecurityEngineResult.TAG_DATA_REF_URIS, dataRefs); + if (validated) { + result.put(WSSecurityEngineResult.TAG_VALIDATED_TOKEN, Boolean.TRUE); + } + return result; + } +}
