This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch coheigea/sts-x509
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit 9c46657d5d136ead46c2fb940a1b471b155bcc31
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Thu Sep 24 07:08:28 2026 +0100

    Adding a new pop switch for the X509TokenValidator in the STS
---
 .../sts/token/validator/X509TokenValidator.java    | 117 ++++++++++++++++++++-
 .../sts/operation/ValidateX509TokenUnitTest.java   |  97 +++++++++++++++++
 .../token/validator/X509TokenValidatorTest.java    |  39 +++++++
 3 files changed, 252 insertions(+), 1 deletion(-)

diff --git 
a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/X509TokenValidator.java
 
b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/X509TokenValidator.java
index 4e6f776cc4c..4423965ae5d 100644
--- 
a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/X509TokenValidator.java
+++ 
b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/X509TokenValidator.java
@@ -19,8 +19,11 @@
 package org.apache.cxf.sts.token.validator;
 
 import java.security.Principal;
+import java.security.cert.Certificate;
 import java.security.cert.X509Certificate;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -31,7 +34,9 @@ import org.w3c.dom.Element;
 import org.w3c.dom.Node;
 
 import org.apache.cxf.common.logging.LogUtils;
+import org.apache.cxf.helpers.CastUtils;
 import org.apache.cxf.helpers.DOMUtils;
+import org.apache.cxf.security.transport.TLSSessionInfo;
 import org.apache.cxf.sts.STSPropertiesMBean;
 import org.apache.cxf.sts.request.ReceivedToken;
 import org.apache.cxf.sts.request.ReceivedToken.STATE;
@@ -44,7 +49,10 @@ import org.apache.wss4j.common.token.BinarySecurity;
 import org.apache.wss4j.common.token.X509Security;
 import org.apache.wss4j.dom.WSConstants;
 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.handler.WSHandlerConstants;
+import org.apache.wss4j.dom.handler.WSHandlerResult;
 import org.apache.wss4j.dom.validate.Credential;
 import org.apache.wss4j.dom.validate.SignatureTrustValidator;
 import org.apache.wss4j.dom.validate.Validator;
@@ -67,6 +75,8 @@ public class X509TokenValidator implements TokenValidator {
 
     private CertConstraintsParser certConstraints = new 
CertConstraintsParser();
 
+    private boolean validateProofOfPossession;
+
     /**
      * Set a list of Strings corresponding to regular expression constraints 
on the subject DN
      * of a certificate
@@ -75,6 +85,33 @@ public class X509TokenValidator implements TokenValidator {
         certConstraints.setSubjectConstraints(subjectConstraints);
     }
 
+    /**
+     * Whether to require the requestor to prove possession of the private key 
that corresponds to
+     * the X.509 certificate being validated. This is disabled by default.
+     *
+     * <p>An X.509 certificate is public data, so trust-chain verification 
alone does not establish
+     * that the requestor is the certificate's subject. When the Validate 
operation is reachable by
+     * untrusted callers, this lets anyone holding a copy of any certificate 
that chains to the STS
+     * truststore have that certificate marked VALID - and, via WS-Trust token 
transformation
+     * (Validate with a requested TokenType), obtain an STS-issued token for 
the certificate's
+     * subject. Enabling this check requires the requestor to prove possession 
of the private key (a
+     * message signature made with, or a TLS client certificate matching, the 
validated certificate)
+     * before the token is considered VALID.
+     *
+     * <p><b>Note:</b> this is off by default because it is incompatible with 
brokered validation, a
+     * common deployment where a trusted intermediary (for example a service 
that already
+     * authenticated the client) forwards the client's bare certificate to the 
STS for
+     * validation/transformation over a separately secured channel. In that 
pattern the intermediary
+     * does not hold the client's private key, so it cannot prove possession 
at the STS. Enable this
+     * only when the Validate operation may be reached by untrusted callers 
and brokered validation
+     * is not in use; otherwise restrict access to the Validate endpoint 
instead.
+     *
+     * @param validateProofOfPossession whether to require proof of possession 
(default false)
+     */
+    public void setValidateProofOfPossession(boolean 
validateProofOfPossession) {
+        this.validateProofOfPossession = validateProofOfPossession;
+    }
+
     /**
      * Set the WSS4J Validator instance to use to validate the token.
      * @param validator the WSS4J Validator instance to use to validate the 
token
@@ -186,9 +223,26 @@ public class X509TokenValidator implements TokenValidator {
             }
 
             Credential returnedCredential = validator.validate(credential, 
requestData);
+            X509Certificate[] validatedCerts = 
returnedCredential.getCertificates();
+
+            // The certificate is trusted, but a certificate is public data. 
Unless the requestor
+            // has proven possession of the corresponding private key, we must 
not confer the
+            // certificate subject's identity - otherwise anyone holding a 
copy of a trusted
+            // certificate could have a token issued in that subject's name 
via token
+            // transformation. See setValidateProofOfPossession().
+            if (validateProofOfPossession
+                && !verifyProofOfPossession(validatedCerts, 
tokenParameters.getMessageContext())) {
+                LOG.log(
+                    Level.WARNING,
+                    "Failed to verify the proof of possession of the private 
key corresponding to "
+                    + "the X.509 certificate being validated"
+                );
+                return response;
+            }
+
             Principal principal = returnedCredential.getPrincipal();
             if (principal == null) {
-                principal = 
returnedCredential.getCertificates()[0].getSubjectX500Principal();
+                principal = validatedCerts[0].getSubjectX500Principal();
             }
             response.setPrincipal(principal);
             validateTarget.setState(STATE.VALID);
@@ -199,4 +253,65 @@ public class X509TokenValidator implements TokenValidator {
         return response;
     }
 
+    /**
+     * Verify that the requestor proved possession of the private key 
corresponding to (one of) the
+     * validated certificate(s), either by signing the request message with it 
or by presenting it
+     * as a TLS client certificate.
+     */
+    protected boolean verifyProofOfPossession(
+        X509Certificate[] validatedCerts,
+        Map<String, Object> messageContext
+    ) {
+        if (validatedCerts == null || validatedCerts.length == 0 || 
messageContext == null) {
+            return false;
+        }
+
+        // Certificate(s) used to sign the request message
+        final List<WSHandlerResult> handlerResults =
+            CastUtils.cast((List<?>) 
messageContext.get(WSHandlerConstants.RECV_RESULTS));
+        if (handlerResults != null && !handlerResults.isEmpty()) {
+            final List<WSSecurityEngineResult> signedResults = new 
ArrayList<>();
+            for (WSHandlerResult handlerResult : handlerResults) {
+                if 
(handlerResult.getActionResults().containsKey(WSConstants.SIGN)) {
+                    
signedResults.addAll(handlerResult.getActionResults().get(WSConstants.SIGN));
+                }
+                if 
(handlerResult.getActionResults().containsKey(WSConstants.UT_SIGN)) {
+                    
signedResults.addAll(handlerResult.getActionResults().get(WSConstants.UT_SIGN));
+                }
+            }
+            for (WSSecurityEngineResult signedResult : signedResults) {
+                X509Certificate signingCert =
+                    
(X509Certificate)signedResult.get(WSSecurityEngineResult.TAG_X509_CERTIFICATE);
+                if (matchesValidatedCert(signingCert, validatedCerts)) {
+                    return true;
+                }
+            }
+        }
+
+        // Certificate presented at the TLS layer
+        TLSSessionInfo tlsInfo = 
(TLSSessionInfo)messageContext.get(TLSSessionInfo.class.getName());
+        if (tlsInfo != null && tlsInfo.getPeerCertificates() != null) {
+            for (Certificate tlsCert : tlsInfo.getPeerCertificates()) {
+                if (tlsCert instanceof X509Certificate
+                    && matchesValidatedCert((X509Certificate)tlsCert, 
validatedCerts)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    private boolean matchesValidatedCert(X509Certificate presentedCert, 
X509Certificate[] validatedCerts) {
+        if (presentedCert == null) {
+            return false;
+        }
+        for (X509Certificate validatedCert : validatedCerts) {
+            if (presentedCert.equals(validatedCert)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
 }
diff --git 
a/services/sts/sts-core/src/test/java/org/apache/cxf/sts/operation/ValidateX509TokenUnitTest.java
 
b/services/sts/sts-core/src/test/java/org/apache/cxf/sts/operation/ValidateX509TokenUnitTest.java
index f3014741011..e4dab8824f2 100644
--- 
a/services/sts/sts-core/src/test/java/org/apache/cxf/sts/operation/ValidateX509TokenUnitTest.java
+++ 
b/services/sts/sts-core/src/test/java/org/apache/cxf/sts/operation/ValidateX509TokenUnitTest.java
@@ -20,8 +20,10 @@ package org.apache.cxf.sts.operation;
 
 import java.security.Principal;
 import java.security.cert.X509Certificate;
+import java.util.ArrayList;
 import java.util.Base64;
 import java.util.Collections;
+import java.util.List;
 import java.util.Properties;
 
 import javax.xml.namespace.QName;
@@ -35,6 +37,8 @@ import org.apache.cxf.sts.STSConstants;
 import org.apache.cxf.sts.STSPropertiesMBean;
 import org.apache.cxf.sts.StaticSTSProperties;
 import org.apache.cxf.sts.common.PasswordCallbackHandler;
+import org.apache.cxf.sts.token.provider.SAMLTokenProvider;
+import org.apache.cxf.sts.token.provider.TokenProvider;
 import org.apache.cxf.sts.token.validator.X509TokenValidator;
 import 
org.apache.cxf.ws.security.sts.provider.model.RequestSecurityTokenResponseType;
 import org.apache.cxf.ws.security.sts.provider.model.RequestSecurityTokenType;
@@ -122,6 +126,81 @@ public class ValidateX509TokenUnitTest {
         assertTrue(validateResponse(response));
     }
 
+    /**
+     * When proof-of-possession checking is enabled, a trusted certificate 
presented as a
+     * ValidateTarget must NOT be transformed into a freshly issued STS token 
unless the requestor
+     * has proven possession of the corresponding private key. A certificate 
is public data, so
+     * trust-chain verification alone must not confer the certificate 
subject's identity.
+     */
+    @org.junit.Test
+    public void 
testValidateX509TokenProofOfPossessionRequiredNoTransformation() throws 
Exception {
+        TokenValidateOperation validateOperation = new 
TokenValidateOperation();
+
+        // Add Token Validator with proof-of-possession checking enabled
+        X509TokenValidator x509TokenValidator = new X509TokenValidator();
+        x509TokenValidator.setValidateProofOfPossession(true);
+        
validateOperation.setTokenValidators(Collections.singletonList(x509TokenValidator));
+
+        // Add a SAMLTokenProvider so that a transformation to a SAML token 
would be possible
+        // if the certificate were (incorrectly) considered validated
+        List<TokenProvider> providerList = new ArrayList<>();
+        providerList.add(new SAMLTokenProvider());
+        validateOperation.setTokenProviders(providerList);
+
+        // Add STSProperties object
+        STSPropertiesMBean stsProperties = new StaticSTSProperties();
+        Crypto crypto = CryptoFactory.getInstance(getEncryptionProperties());
+        stsProperties.setEncryptionCrypto(crypto);
+        stsProperties.setSignatureCrypto(crypto);
+        stsProperties.setEncryptionUsername("myservicekey");
+        stsProperties.setSignatureUsername("mystskey");
+        stsProperties.setCallbackHandler(new PasswordCallbackHandler());
+        stsProperties.setIssuer("STS");
+        validateOperation.setStsProperties(stsProperties);
+
+        // Request a SAML2 token via transformation (TokenType != Status)
+        RequestSecurityTokenType request = new RequestSecurityTokenType();
+        JAXBElement<String> tokenType =
+            new JAXBElement<String>(
+                QNameConstants.TOKEN_TYPE, String.class, 
WSS4JConstants.WSS_SAML2_TOKEN_TYPE
+            );
+        request.getAny().add(tokenType);
+
+        // Present a trusted certificate (public data) that the requestor does 
not possess
+        CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ALIAS);
+        cryptoType.setAlias("myclientkey");
+        X509Certificate[] certs = crypto.getX509Certificates(cryptoType);
+        assertTrue(certs != null && certs.length > 0);
+
+        JAXBElement<BinarySecurityTokenType> binarySecurityTokenType =
+            createBinarySecurityToken(certs[0]);
+        ValidateTargetType validateTarget = new ValidateTargetType();
+        validateTarget.setAny(binarySecurityTokenType);
+
+        JAXBElement<ValidateTargetType> validateTargetType =
+            new JAXBElement<ValidateTargetType>(
+                QNameConstants.VALIDATE_TARGET, ValidateTargetType.class, 
validateTarget
+            );
+        request.getAny().add(validateTargetType);
+
+        // Mock up message context - crucially there is no message signature 
or TLS client
+        // certificate proving possession of the private key
+        MessageImpl msg = new MessageImpl();
+        WrappedMessageContext msgCtx = new WrappedMessageContext(msg);
+        Principal principal = new CustomTokenPrincipal("eve");
+        msgCtx.put(
+            SecurityContext.class.getName(),
+            createSecurityContext(principal)
+        );
+
+        RequestSecurityTokenResponseType response =
+            validateOperation.validate(request, principal, msgCtx);
+
+        // The status must be invalid and no token must have been issued
+        assertFalse(validateResponse(response));
+        assertFalse(hasIssuedToken(response));
+    }
+
     /**
      * Test to validate an invalid X.509 token
      */
@@ -220,6 +299,24 @@ public class ValidateX509TokenUnitTest {
         return false;
     }
 
+    /**
+     * Return true if the response contains a freshly issued token
+     */
+    private boolean hasIssuedToken(RequestSecurityTokenResponseType response) {
+        if (response == null || response.getAny() == null) {
+            return false;
+        }
+        for (Object requestObject : response.getAny()) {
+            if (requestObject instanceof JAXBElement<?>) {
+                JAXBElement<?> jaxbElement = (JAXBElement<?>) requestObject;
+                if (REQUESTED_SECURITY_TOKEN.equals(jaxbElement.getName())) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     private Properties getEncryptionProperties() {
         Properties properties = new Properties();
         properties.put(
diff --git 
a/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/X509TokenValidatorTest.java
 
b/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/X509TokenValidatorTest.java
index 3eb220ccda4..5b64427434c 100644
--- 
a/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/X509TokenValidatorTest.java
+++ 
b/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/X509TokenValidatorTest.java
@@ -99,6 +99,45 @@ public class X509TokenValidatorTest {
         assertTrue(principal != null && principal.getName() != null);
     }
 
+    /**
+     * When proof-of-possession checking is enabled, a trusted certificate 
must NOT be validated
+     * (and therefore must not be usable to obtain a token via transformation) 
unless the requestor
+     * has proven possession of the corresponding private key. The test 
message context carries no
+     * signature or TLS client certificate, so validation must fail.
+     */
+    @org.junit.Test
+    public void testValidCertificateProofOfPossessionRequired() throws 
Exception {
+        X509TokenValidator x509TokenValidator = new X509TokenValidator();
+        x509TokenValidator.setValidateProofOfPossession(true);
+        TokenValidatorParameters validatorParameters = 
createValidatorParameters();
+        TokenRequirements tokenRequirements = 
validatorParameters.getTokenRequirements();
+
+        // Create a ValidateTarget consisting of a trusted X509Certificate
+        BinarySecurityTokenType binarySecurityToken = new 
BinarySecurityTokenType();
+        JAXBElement<BinarySecurityTokenType> tokenType =
+            new JAXBElement<BinarySecurityTokenType>(
+                QNameConstants.BINARY_SECURITY_TOKEN, 
BinarySecurityTokenType.class, binarySecurityToken
+            );
+        CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ALIAS);
+        cryptoType.setAlias("myclientkey");
+        Crypto crypto = 
validatorParameters.getStsProperties().getSignatureCrypto();
+        X509Certificate[] certs = crypto.getX509Certificates(cryptoType);
+        assertTrue(certs != null && certs.length > 0);
+        
binarySecurityToken.setValue(Base64.getMimeEncoder().encodeToString(certs[0].getEncoded()));
+        binarySecurityToken.setValueType(X509TokenValidator.X509_V3_TYPE);
+        binarySecurityToken.setEncodingType(WSS4JConstants.SOAPMESSAGE_NS + 
"#Base64Binary");
+
+        ReceivedToken validateTarget = new ReceivedToken(tokenType);
+        tokenRequirements.setValidateTarget(validateTarget);
+        validatorParameters.setToken(validateTarget);
+
+        // Even though the certificate is trusted, without proof of possession 
it must be INVALID
+        TokenValidatorResponse validatorResponse = 
x509TokenValidator.validateToken(validatorParameters);
+        assertNotNull(validatorResponse);
+        assertNotNull(validatorResponse.getToken());
+        assertTrue(validatorResponse.getToken().getState() == STATE.INVALID);
+    }
+
     /**
      * Test an invalid certificate
      */

Reply via email to