This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/main by this push:
new 86947daf176 Adding a new pop switch for the X509TokenValidator in the
STS (#3498)
86947daf176 is described below
commit 86947daf1768c0dd5ca6834b797d30686caa70bb
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Thu Sep 24 14:47:36 2026 +0100
Adding a new pop switch for the X509TokenValidator in the STS (#3498)
---
.../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
*/