This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/stax-sender-vouches in repository https://gitbox.apache.org/repos/asf/ws-wss4j.git
commit 1e9a64bf20b780f43f670884f21180209ba078f9 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Mon Sep 21 10:59:37 2026 +0100 Align StaX sender vouches with DOM layer --- .../processor/input/SAMLTokenInputHandler.java | 46 ++++++- .../processor/input/SenderVouchesIdentityTest.java | 137 +++++++++++++++++++++ 2 files changed, 182 insertions(+), 1 deletion(-) diff --git a/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/processor/input/SAMLTokenInputHandler.java b/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/processor/input/SAMLTokenInputHandler.java index c47c247f3..f98b04476 100644 --- a/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/processor/input/SAMLTokenInputHandler.java +++ b/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/processor/input/SAMLTokenInputHandler.java @@ -525,6 +525,40 @@ public class SAMLTokenInputHandler extends AbstractInputSecurityHeaderHandler { * which can not be done until the whole soap-header is processed and we know that the whole soap-body * is signed. */ + /** + * Whether a security token identifies the sender that signed with it, as opposed to merely + * proving possession of a key. + * + * A certificate or a public key was checked against the receiver's own truststore when the + * token was verified - see X509SecurityTokenImpl#verify and + * RsaKeyValueSecurityTokenImpl#verify, both of which call Crypto#verifyTrust. A bare symmetric + * key was not, and could not be: an EncryptedKey that the sender minted for itself under the + * receiver's public certificate proves possession of a key the sender chose, and nothing + * whatsoever about who sent it. + * + * Two symmetric cases do carry an identity. A key derived from a UsernameToken required the + * password to derive, and a Kerberos session key came out of a ticket the KDC issued to a + * named client. These are the streaming counterparts of the results the DOM engine stamps + * with TAG_VALIDATED_TOKEN, plus its UT_SIGN case - see DOMSAMLUtil#establishesSenderIdentity. + * + * The token is resolved to the root of its key wrapping chain first, so that a + * DerivedKeyToken is judged on whatever it was derived from. + */ + static boolean establishesSenderIdentity(SecurityToken securityToken) throws XMLSecurityException { + SecurityToken rootToken = WSSUtils.getRootToken(securityToken); + + X509Certificate[] x509Certificates = rootToken.getX509Certificates(); + if (x509Certificates != null && x509Certificates.length > 0) { + return true; + } + if (rootToken.getPublicKey() != null) { + return true; + } + + return WSSecurityTokenConstants.USERNAME_TOKEN.equals(rootToken.getTokenType()) + || WSSecurityTokenConstants.KERBEROS_TOKEN.equals(rootToken.getTokenType()); + } + static class SAMLTokenVerifierInputProcessor extends AbstractInputProcessor implements SecurityEventListener { private SamlAssertionWrapper samlAssertionWrapper; @@ -708,10 +742,20 @@ public class SAMLTokenInputHandler extends AbstractInputSecurityHeaderHandler { samlTokenSignedElementSecurityEvent = signedElementSecurityEvent; } } + // 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, 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. if (bodySignedPartSecurityEvent != null && samlTokenSignedElementSecurityEvent != null && bodySignedPartSecurityEvent.getSecurityToken() - == samlTokenSignedElementSecurityEvent.getSecurityToken()) { + == samlTokenSignedElementSecurityEvent.getSecurityToken() + && (samlAssertionWrapper.isSigned() + || establishesSenderIdentity( + bodySignedPartSecurityEvent.getSecurityToken()))) { return; } methodNotSatisfied = true; diff --git a/ws-security-stax/src/test/java/org/apache/wss4j/stax/impl/processor/input/SenderVouchesIdentityTest.java b/ws-security-stax/src/test/java/org/apache/wss4j/stax/impl/processor/input/SenderVouchesIdentityTest.java new file mode 100644 index 000000000..30b4d7b97 --- /dev/null +++ b/ws-security-stax/src/test/java/org/apache/wss4j/stax/impl/processor/input/SenderVouchesIdentityTest.java @@ -0,0 +1,137 @@ +/** + * 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.stax.impl.processor.input; + +import java.security.KeyStore; +import java.security.cert.X509Certificate; + +import org.apache.wss4j.stax.impl.securityToken.EncryptedKeySha1SecurityTokenImpl; +import org.apache.wss4j.stax.securityToken.WSSecurityTokenConstants; +import org.apache.xml.security.stax.securityToken.SecurityTokenConstants; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * An unsigned sender-vouches assertion is only as good as the identity of whoever signed the + * message that carries it. These are the cases SAMLTokenInputHandler#establishesSenderIdentity + * has to separate: a credential the receiver made a trust decision about, against a bare + * symmetric key that says nothing about who sent the message. + */ +public class SenderVouchesIdentityTest { + + /** + * The bypass: an EncryptedKey the sender minted for itself under the receiver's public + * certificate. The HMAC signature it keys verifies perfectly and identifies nobody. + */ + @Test + public void testEncryptedKeyDoesNotVouch() throws Exception { + assertFalse(SAMLTokenInputHandler.establishesSenderIdentity( + token(WSSecurityTokenConstants.EncryptedKeyToken))); + } + + /** + * The same key reached through a DerivedKeyToken. The derived token is judged on what it was + * derived from, so this must not become a way round the previous case. + */ + @Test + public void testKeyDerivedFromAnEncryptedKeyDoesNotVouch() throws Exception { + EncryptedKeySha1SecurityTokenImpl derivedKey = token(WSSecurityTokenConstants.DerivedKeyToken); + derivedKey.setKeyWrappingToken(token(WSSecurityTokenConstants.EncryptedKeyToken)); + + assertFalse(SAMLTokenInputHandler.establishesSenderIdentity(derivedKey)); + } + + /** + * A certificate was checked against the receiver's truststore when the token was verified. + */ + @Test + public void testCertificateVouches() throws Exception { + EncryptedKeySha1SecurityTokenImpl securityToken = token(WSSecurityTokenConstants.X509V3Token); + securityToken.setX509Certificates(new X509Certificate[] {transmitterCertificate()}); + + assertTrue(SAMLTokenInputHandler.establishesSenderIdentity(securityToken)); + } + + /** + * So was a bare public key - see RsaKeyValueSecurityTokenImpl#verify. + */ + @Test + public void testPublicKeyVouches() throws Exception { + EncryptedKeySha1SecurityTokenImpl securityToken = token(WSSecurityTokenConstants.KeyValueToken); + securityToken.setPublicKey(transmitterCertificate().getPublicKey()); + + assertTrue(SAMLTokenInputHandler.establishesSenderIdentity(securityToken)); + } + + /** + * A UsernameToken derived key carries no credential for a trust decision, but deriving it + * required the password, so the sender is authenticated all the same. This is the streaming + * counterpart of the DOM engine's UT_SIGN case. + */ + @Test + public void testUsernameTokenVouches() throws Exception { + assertTrue(SAMLTokenInputHandler.establishesSenderIdentity( + token(WSSecurityTokenConstants.USERNAME_TOKEN))); + } + + /** + * A Kerberos session key came out of a ticket the KDC issued to a named client. + */ + @Test + public void testKerberosTokenVouches() throws Exception { + assertTrue(SAMLTokenInputHandler.establishesSenderIdentity( + token(WSSecurityTokenConstants.KERBEROS_TOKEN))); + } + + /** + * A token carrying only a symmetric key of some other provenance - a SecurityContextToken, + * say - is no better placed to vouch than an EncryptedKey is. + */ + @Test + public void testOtherSymmetricTokenDoesNotVouch() throws Exception { + assertFalse(SAMLTokenInputHandler.establishesSenderIdentity( + token(WSSecurityTokenConstants.SECURITY_CONTEXT_TOKEN))); + } + + /** + * A token of the given type carrying nothing else. EncryptedKeySha1SecurityTokenImpl is used + * only because it is a concrete inbound token whose credentials can be set from a test; what + * is under test is the rule, not the class. + */ + private EncryptedKeySha1SecurityTokenImpl token(SecurityTokenConstants.TokenType tokenType) { + return new EncryptedKeySha1SecurityTokenImpl(null, null, "sha1-identifier", "token-id") { + @Override + public SecurityTokenConstants.TokenType getTokenType() { + return tokenType; + } + }; + } + + private X509Certificate transmitterCertificate() throws Exception { + KeyStore keyStore = KeyStore.getInstance("jks"); + keyStore.load(this.getClass().getClassLoader().getResourceAsStream("transmitter.jks"), + "default".toCharArray()); + X509Certificate certificate = (X509Certificate)keyStore.getCertificate("transmitter"); + assertNotNull(certificate, "precondition: the test keystore holds the transmitter certificate"); + return certificate; + } +}
