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 */
