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 0fa3438ff Fix sender-vouches signature referencing (#685)
0fa3438ff is described below
commit 0fa3438ffb3a757a3854e0f5bcac43c037fca08f
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Thu Sep 10 14:12:19 2026 +0100
Fix sender-vouches signature referencing (#685)
---
.../apache/wss4j/dom/str/SignatureSTRParser.java | 25 +++-
.../saml/SamlSenderVouchesSecretKeyBypassTest.java | 136 +++++++++++++++++++++
2 files changed, 155 insertions(+), 6 deletions(-)
diff --git
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/str/SignatureSTRParser.java
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/str/SignatureSTRParser.java
index 478556fac..aae89bfbe 100644
---
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/str/SignatureSTRParser.java
+++
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/str/SignatureSTRParser.java
@@ -101,19 +101,32 @@ public class SignatureSTRParser implements STRParser {
/**
* A method to create a Principal from a SAML Assertion
* @param samlAssertion An SamlAssertionWrapper object
+ * @param secretKey the raw secret key (if any) taken from the assertion's
Subject KeyInfo,
+ * which is only a meaningful proof-of-possession
credential for the
+ * holder-of-key confirmation method
* @return A principal
*/
private Principal createPrincipalFromSAML(
- SamlAssertionWrapper samlAssertion, STRParserResult parserResult
- ) {
+ SamlAssertionWrapper samlAssertion, STRParserResult parserResult,
byte[] secretKey
+ ) throws WSSecurityException {
SAMLTokenPrincipalImpl samlPrincipal = new
SAMLTokenPrincipalImpl(samlAssertion);
String confirmMethod = null;
List<String> methods = samlAssertion.getConfirmationMethods();
if (methods != null && !methods.isEmpty()) {
confirmMethod = methods.get(0);
}
- if (OpenSAMLUtil.isMethodHolderOfKey(confirmMethod) &&
samlAssertion.isSigned()) {
+ boolean holderOfKey = OpenSAMLUtil.isMethodHolderOfKey(confirmMethod);
+ if (holderOfKey && samlAssertion.isSigned()) {
parserResult.setTrustedCredential(true);
+ } else if (!holderOfKey && secretKey != null && secretKey.length > 0) {
+ // A raw secret key from the Subject KeyInfo of a
non-holder-of-key assertion (e.g.
+ // sender-vouches) has no defined proof-of-possession semantics,
and no trust anchor is
+ // ever checked for it (SignatureTrustValidator only validates
certs / public keys). Using
+ // such a key to "verify" a signature is a self-referential no-op
that an attacker can
+ // trivially satisfy by embedding their own key in an unsigned
assertion, so reject it.
+ throw new WSSecurityException(
+ WSSecurityException.ErrorCode.FAILED_CHECK,
"invalidSAMLsecurity"
+ );
}
return samlPrincipal;
}
@@ -145,7 +158,7 @@ public class SignatureSTRParser implements STRParser {
}
secretKey = samlKi.getSecret();
parserResult.setPublicKey(samlKi.getPublicKey());
- parserResult.setPrincipal(createPrincipalFromSAML(samlAssertion,
parserResult));
+ parserResult.setPrincipal(createPrincipalFromSAML(samlAssertion,
parserResult, secretKey));
}
parserResult.setSecretKey(secretKey);
}
@@ -313,7 +326,7 @@ public class SignatureSTRParser implements STRParser {
}
parserResult.setSecretKey(keyInfo.getSecret());
parserResult.setPublicKey(keyInfo.getPublicKey());
- parserResult.setPrincipal(createPrincipalFromSAML(samlAssertion,
parserResult));
+ parserResult.setPrincipal(createPrincipalFromSAML(samlAssertion,
parserResult, keyInfo.getSecret()));
}
REFERENCE_TYPE referenceType = getReferenceType(secRef);
@@ -391,7 +404,7 @@ public class SignatureSTRParser implements STRParser {
parserResult.setCerts(new
X509Certificate[]{foundCerts[0]});
}
secretKey = keyInfo.getSecret();
- principal = createPrincipalFromSAML(samlAssertion,
parserResult);
+ principal = createPrincipalFromSAML(samlAssertion,
parserResult, secretKey);
} else if (el.equals(WSConstants.ENCRYPTED_KEY)) {
STRParserUtil.checkEncryptedKeyBSPCompliance(secRef,
data.getBSPEnforcer());
Processor proc =
data.getWssConfig().getProcessor(WSConstants.ENCRYPTED_KEY);
diff --git
a/ws-security-dom/src/test/java/org/apache/wss4j/dom/saml/SamlSenderVouchesSecretKeyBypassTest.java
b/ws-security-dom/src/test/java/org/apache/wss4j/dom/saml/SamlSenderVouchesSecretKeyBypassTest.java
new file mode 100644
index 000000000..a9f4f211c
--- /dev/null
+++
b/ws-security-dom/src/test/java/org/apache/wss4j/dom/saml/SamlSenderVouchesSecretKeyBypassTest.java
@@ -0,0 +1,136 @@
+/**
+ * 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.Base64;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.apache.wss4j.common.ext.WSSecurityException;
+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.bean.KeyInfoBean;
+import org.apache.wss4j.common.saml.builder.SAML1Constants;
+import org.apache.wss4j.common.token.SecurityTokenReference;
+import org.apache.wss4j.common.util.SOAPUtil;
+import org.apache.wss4j.dom.WSConstants;
+import org.apache.wss4j.dom.WSDocInfo;
+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.handler.RequestData;
+import org.apache.wss4j.dom.str.STRParser;
+import org.apache.wss4j.dom.str.STRParserParameters;
+import org.apache.wss4j.dom.str.STRParserResult;
+import org.apache.wss4j.dom.str.SignatureSTRParser;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Regression test for a sender-vouches trust bypass: an unsigned SAML
assertion carries a raw,
+ * attacker-chosen secret key in its Subject KeyInfo
(SubjectConfirmationData/ds:KeyInfo/wst:BinarySecret).
+ * That confirmation method is not holder-of-key, so the secret has no defined
proof-of-possession
+ * semantics and no trust anchor is ever consulted for it -
SignatureTrustValidator only validates
+ * certs/public keys, so a raw secret credential used to "verify" a signature
is a self-referential
+ * no-op that any attacker can satisfy. SignatureSTRParser must reject such a
credential instead of
+ * silently handing back the attacker's own key.
+ */
+public class SamlSenderVouchesSecretKeyBypassTest {
+
+ private static final String WST_NS =
"http://schemas.xmlsoap.org/ws/2005/02/trust";
+
+ public SamlSenderVouchesSecretKeyBypassTest() throws Exception {
+ WSSConfig.init();
+ }
+
+ @Test
+ public void testUnsignedSenderVouchesSubjectSecretKeyIsRejected() throws
Exception {
+ // An unsigned, sender-vouches SAML assertion (the default for
SAML1CallbackHandler) whose
+ // Subject KeyInfo embeds a raw secret of the attacker's own choosing.
+ byte[] attackerSecret =
"attacker-controlled-shared-secret".getBytes("UTF-8");
+
+ SAML1CallbackHandler callbackHandler = new SAML1CallbackHandler();
+ callbackHandler.setStatement(SAML1CallbackHandler.Statement.AUTHN);
+
callbackHandler.setConfirmationMethod(SAML1Constants.CONF_SENDER_VOUCHES);
+ callbackHandler.setIssuer("attacker.example.com");
+
+ SAMLCallback samlCallback = new SAMLCallback();
+ SAMLUtil.doSAMLCallback(callbackHandler, samlCallback);
+
samlCallback.getSubject().setKeyInfo(createBinarySecretKeyInfo(attackerSecret));
+
+ SamlAssertionWrapper samlAssertion = new
SamlAssertionWrapper(samlCallback);
+ Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG);
+ samlAssertion.toDOM(doc);
+ samlAssertion.parseSubject(new WSSSAMLKeyInfoProcessor(new
RequestData()), null);
+
+ assertThrows(WSSecurityException.class, () ->
parseSubjectKeyIdentifier(doc, samlAssertion));
+ }
+
+ // Directly exercises SignatureSTRParser, i.e. what a ds:Signature's
KeyInfo/SecurityTokenReference
+ // resolves to when it points at the SAML assertion above - this is the
exact sink that a forged
+ // "self-vouching" HMAC signature over the SOAP Body would rely on.
+ private STRParserResult parseSubjectKeyIdentifier(
+ Document doc, SamlAssertionWrapper samlAssertion
+ ) throws WSSecurityException {
+ WSDocInfo wsDocInfo = new WSDocInfo(doc);
+ WSSecurityEngineResult samlResult =
+ new WSSecurityEngineResult(WSConstants.ST_UNSIGNED, samlAssertion);
+ samlResult.put(WSSecurityEngineResult.TAG_ID, samlAssertion.getId());
+ wsDocInfo.addResult(samlResult);
+
+ RequestData requestData = new RequestData();
+ requestData.setWsDocInfo(wsDocInfo);
+
+ SecurityTokenReference secRef = new SecurityTokenReference(doc);
+ secRef.addTokenType(WSConstants.WSS_SAML_TOKEN_TYPE);
+ secRef.setKeyIdentifier(WSConstants.WSS_SAML_KI_VALUE_TYPE,
samlAssertion.getId());
+
+ STRParserParameters parameters = new STRParserParameters();
+ parameters.setData(requestData);
+ parameters.setStrElement(secRef.getElement());
+
+ STRParser strParser = new SignatureSTRParser();
+ return strParser.parseSecurityTokenReference(parameters);
+ }
+
+ private KeyInfoBean createBinarySecretKeyInfo(byte[] secret) throws
Exception {
+ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+ dbf.setNamespaceAware(true);
+ DocumentBuilder docBuilder = dbf.newDocumentBuilder();
+ Document keyInfoDoc = docBuilder.newDocument();
+
+ Element keyInfoElement =
keyInfoDoc.createElementNS(WSConstants.SIG_NS, "ds:KeyInfo");
+ keyInfoElement.setAttributeNS("http://www.w3.org/2000/xmlns/",
"xmlns:ds", WSConstants.SIG_NS);
+ Element binarySecretElement = keyInfoDoc.createElementNS(WST_NS,
"wst:BinarySecret");
+ binarySecretElement.setAttributeNS("http://www.w3.org/2000/xmlns/",
"xmlns:wst", WST_NS);
+
binarySecretElement.setTextContent(Base64.getEncoder().encodeToString(secret));
+ keyInfoElement.appendChild(binarySecretElement);
+ keyInfoDoc.appendChild(keyInfoElement);
+
+ KeyInfoBean keyInfo = new KeyInfoBean();
+ keyInfo.setElement(keyInfoElement);
+ return keyInfo;
+ }
+}