Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -31,7 +34,9 @@
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;
Expand All @@ -44,7 +49,10 @@
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;
Expand All @@ -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
Expand All @@ -75,6 +85,33 @@ public void setSubjectConstraints(List<String> subjectConstraints) {
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
Expand Down Expand Up @@ -186,9 +223,26 @@ public TokenValidatorResponse validateToken(TokenValidatorParameters tokenParame
}

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);
Expand All @@ -199,4 +253,65 @@ public TokenValidatorResponse validateToken(TokenValidatorParameters tokenParame
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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@

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;
Expand All @@ -35,6 +37,8 @@
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;
Expand Down Expand Up @@ -122,6 +126,81 @@ public void testValidateX509Token() throws Exception {
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
*/
Expand Down Expand Up @@ -220,6 +299,24 @@ private boolean validateResponse(RequestSecurityTokenResponseType response) {
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,45 @@ public void testValidCertificate() throws Exception {
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
*/
Expand Down
Loading