KEYCLOAK-16462 X509 Auth: add option to revalidate certificate trust

This commit is contained in:
Luca Leonardo Scorcia 2021-09-13 12:12:38 +02:00 committed by GitHub
parent a6cd80c933
commit af8354267b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 141 additions and 5 deletions

View file

@ -85,6 +85,7 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
public static final String CERTIFICATE_EXTENDED_KEY_USAGE = "x509-cert-auth.extendedkeyusage";
static final String DEFAULT_MATCH_ALL_EXPRESSION = "(.*?)(?:$)";
public static final String CONFIRMATION_PAGE_DISALLOWED = "x509-cert-auth.confirmation-page-disallowed";
public static final String REVALIDATE_CERTIFICATE = "x509-cert-auth.revalidate-certificate-enabled";
protected Response createInfoResponse(AuthenticationFlowContext context, String infoMessage, Object ... parameters) {
@ -110,6 +111,8 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
.oCSPEnabled(config.getOCSPEnabled())
.oCSPResponseCertificate(config.getOCSPResponderCertificate())
.oCSPResponderURI(config.getOCSPResponder())
.trustValidation()
.enabled(config.getRevalidateCertificateEnabled())
.timestampValidation()
.enabled(config.isCertValidationEnabled());
}

View file

@ -207,6 +207,12 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen
identityConfirmationPageDisallowed.setLabel("Bypass identity confirmation");
identityConfirmationPageDisallowed.setHelpText("By default, the users are prompted to confirm their identity extracted from X509 client certificate. The identity confirmation prompt is skipped if the option is switched on.");
ProviderConfigProperty revalidateCertificateEnabled = new ProviderConfigProperty();
revalidateCertificateEnabled.setType(BOOLEAN_TYPE);
revalidateCertificateEnabled.setName(REVALIDATE_CERTIFICATE);
revalidateCertificateEnabled.setLabel("Revalidate Client Certificate");
revalidateCertificateEnabled.setHelpText("Forces revalidation of the client certificate according to the certificates defined in the truststore. This is useful when behind a non-validating proxy or when the number of allowed certificate chains would be too large for mutual SSL negotiation.");
configProperties = asList(mappingMethodList,
canonicalDn,
serialnumberHex,
@ -222,7 +228,8 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen
ocspResponderCert,
keyUsage,
extendedKeyUsage,
identityConfirmationPageDisallowed);
identityConfirmationPageDisallowed,
revalidateCertificateEnabled);
}
@Override

View file

@ -47,16 +47,25 @@ import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.security.cert.CRLException;
import java.security.cert.CertPathBuilder;
import java.security.cert.CertPathBuilderException;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertStore;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.CRLException;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.PKIXCertPathBuilderResult;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.security.cert.X509CertSelector;
import java.security.cert.X509CRL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
@ -388,6 +397,7 @@ public class CertificateValidator {
boolean _ocspEnabled;
OCSPChecker ocspChecker;
boolean _timestampValidationEnabled;
boolean _trustValidationEnabled;
public CertificateValidator() {
@ -400,7 +410,8 @@ public class CertificateValidator {
boolean oCSPCheckingEnabled,
OCSPChecker ocspChecker,
KeycloakSession session,
boolean timestampValidationEnabled) {
boolean timestampValidationEnabled,
boolean trustValidationEnabled) {
_certChain = certChain;
_keyUsageBits = keyUsageBits;
_extendedKeyUsage = extendedKeyUsage;
@ -411,6 +422,7 @@ public class CertificateValidator {
this.ocspChecker = ocspChecker;
this.session = session;
_timestampValidationEnabled = timestampValidationEnabled;
_trustValidationEnabled = trustValidationEnabled;
if (ocspChecker == null)
throw new IllegalArgumentException("ocspChecker");
@ -487,6 +499,7 @@ public class CertificateValidator {
validateKeyUsage(_certChain, _keyUsageBits);
return this;
}
public CertificateValidator validateExtendedKeyUsage() throws GeneralSecurityException {
validateExtendedKeyUsage(_certChain, _extendedKeyUsage);
return this;
@ -519,6 +532,79 @@ public class CertificateValidator {
return this;
}
public CertificateValidator validateTrust() throws GeneralSecurityException {
if (!_trustValidationEnabled)
return this;
TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class);
if (truststoreProvider == null || truststoreProvider.getTruststore() == null) {
logger.error("Cannot validate client certificate trust: Truststore not available");
}
else
{
Set<X509Certificate> trustedRootCerts = truststoreProvider.getRootCertificates().entrySet().stream().map(t -> t.getValue()).collect(Collectors.toSet());
Set<X509Certificate> trustedIntermediateCerts = truststoreProvider.getIntermediateCertificates().entrySet().stream().map(t -> t.getValue()).collect(Collectors.toSet());
logger.debugf("Found %d trusted root certs, %d trusted intermediate certs", trustedRootCerts.size(), trustedIntermediateCerts.size());
verifyCertificateTrust(_certChain, trustedRootCerts, trustedIntermediateCerts);
}
return this;
}
/**
* Attempts to build a certification chain for given certificate and to verify
* it. Relies on a set of root CA certificates (trust anchors) and a set of
* intermediate certificates (to be used as part of the chain).
* @param certChain - client chain presented for validation. cert to validate is assumed to be the first in the chain
* @param trustedRootCerts - set of trusted root CA certificates
* @param trustedIntermediateCerts - set of intermediate certificates
* @return the certification chain (if verification is successful)
* @throws GeneralSecurityException - if the verification is not successful
* (e.g. certification path cannot be built or some certificate in the
* chain is expired)
*/
private static PKIXCertPathBuilderResult verifyCertificateTrust(X509Certificate[] certChain, Set<X509Certificate> trustedRootCerts,
Set<X509Certificate> trustedIntermediateCerts) throws GeneralSecurityException {
// Create the selector that specifies the starting certificate
X509CertSelector selector = new X509CertSelector();
selector.setCertificate(certChain[0]);
// Create the trust anchors (set of root CA certificates)
Set<TrustAnchor> trustAnchors = new HashSet<TrustAnchor>();
for (X509Certificate trustedRootCert : trustedRootCerts) {
trustAnchors.add(new TrustAnchor(trustedRootCert, null));
}
// Configure the PKIX certificate builder algorithm parameters
PKIXBuilderParameters pkixParams =
new PKIXBuilderParameters(trustAnchors, selector);
// Disable CRL checks (this is done manually as additional step)
pkixParams.setRevocationEnabled(false);
// Specify a list of intermediate certificates
Set<X509Certificate> intermediateCerts = new HashSet<X509Certificate>();
for (X509Certificate intermediateCert : trustedIntermediateCerts) {
intermediateCerts.add(intermediateCert);
}
// Client certificates have to be added to the list of intermediate certs
for (X509Certificate clientCert : certChain) {
intermediateCerts.add(clientCert);
}
CertStore intermediateCertStore = CertStore.getInstance("Collection",
new CollectionCertStoreParameters(intermediateCerts), "BC");
pkixParams.addCertStore(intermediateCertStore);
// Build and verify the certification chain
CertPathBuilder builder = CertPathBuilder.getInstance("PKIX", "BC");
PKIXCertPathBuilderResult result =
(PKIXCertPathBuilderResult) builder.build(pkixParams);
return result;
}
private X509Certificate findCAInTruststore(X500Principal issuer) throws GeneralSecurityException {
TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class);
if (truststoreProvider == null || truststoreProvider.getTruststore() == null) {
@ -593,6 +679,7 @@ public class CertificateValidator {
}
}
}
private static List<String> getCRLDistributionPoints(X509Certificate cert) {
try {
return CRLUtils.getCRLDistributionPoints(cert);
@ -650,6 +737,7 @@ public class CertificateValidator {
String _responderUri;
X509Certificate _responderCert;
boolean _timestampValidationEnabled;
boolean _trustValidationEnabled;
public CertificateValidatorBuilder() {
_extendedKeyUsage = new LinkedList<>();
@ -831,6 +919,19 @@ public class CertificateValidator {
}
}
public class TrustValidationBuilder {
CertificateValidatorBuilder _parent;
protected TrustValidationBuilder(CertificateValidatorBuilder parent) {
_parent = parent;
}
public CertificateValidatorBuilder enabled(boolean value) {
_trustValidationEnabled = value;
return _parent;
}
}
public CertificateValidatorBuilder session(KeycloakSession session) {
this.session = session;
return this;
@ -852,13 +953,17 @@ public class CertificateValidator {
return new TimestampValidationBuilder(this);
}
public TrustValidationBuilder trustValidation() {
return new TrustValidationBuilder(this);
}
public CertificateValidator build(X509Certificate[] certs) {
if (_crlLoader == null) {
_crlLoader = new CRLFileLoader(session, "");
}
return new CertificateValidator(certs, _keyUsageBits, _extendedKeyUsage,
_crlCheckingEnabled, _crldpEnabled, _crlLoader, _ocspEnabled,
new BouncyCastleOCSPChecker(session, _responderUri, _responderCert), session, _timestampValidationEnabled);
new BouncyCastleOCSPChecker(session, _responderUri, _responderCert), session, _timestampValidationEnabled, _trustValidationEnabled);
}
}

View file

@ -74,6 +74,7 @@ public class ValidateX509CertificateUsername extends AbstractX509ClientCertifica
CertificateValidator.CertificateValidatorBuilder builder = certificateValidationParameters(context.getSession(), config);
CertificateValidator validator = builder.build(certs);
validator.checkRevocationStatus()
.validateTrust()
.validateKeyUsage()
.validateExtendedKeyUsage()
.validateTimestamps();

View file

@ -271,4 +271,13 @@ public class X509AuthenticatorConfigModel extends AuthenticatorConfigModel {
getConfig().put(SERIALNUMBER_HEX, Boolean.toString(value));
return this;
}
public boolean getRevalidateCertificateEnabled() {
return Boolean.parseBoolean(getConfig().get(REVALIDATE_CERTIFICATE));
}
public X509AuthenticatorConfigModel setRevalidateCertificateEnabled(boolean value) {
getConfig().put(REVALIDATE_CERTIFICATE, Boolean.toString(value));
return this;
}
}

View file

@ -85,6 +85,7 @@ public class X509ClientCertificateAuthenticator extends AbstractX509ClientCertif
CertificateValidator.CertificateValidatorBuilder builder = certificateValidationParameters(context.getSession(), config);
CertificateValidator validator = builder.build(certs);
validator.checkRevocationStatus()
.validateTrust()
.validateKeyUsage()
.validateExtendedKeyUsage()
.validateTimestamps();

View file

@ -434,6 +434,11 @@ public abstract class AbstractX509AuthenticationTest extends AbstractTestRealmKe
.setExtendedKeyUsage(extendedKeyUsage);
}
protected static X509AuthenticatorConfigModel createLoginSubjectEmailWithRevalidateCert(boolean revalidateCertEnabled) {
return createLoginSubjectEmail2UsernameOrEmailConfig()
.setRevalidateCertificateEnabled(revalidateCertEnabled);
}
protected static X509AuthenticatorConfigModel createLoginSubjectCN2UsernameOrEmailConfig() {
return new X509AuthenticatorConfigModel()
.setConfirmationPageAllowed(true)

View file

@ -114,6 +114,11 @@ public class X509BrowserLoginTest extends AbstractX509AuthenticationTest {
x509BrowserLogin(createLoginSubjectEmailWithExtendedKeyUsage("serverAuth"), userId, "test-user@localhost", "test-user@localhost");
}
@Test
public void loginWithRevalidateCertEnabledCertIsTrusted() throws Exception {
x509BrowserLogin(createLoginSubjectEmailWithRevalidateCert(true), userId, "test-user@localhost", "test-user@localhost");
}
@Test
public void loginIgnoreX509IdentityContinueToFormLogin() throws Exception {
// Set the X509 authenticator configuration