Fix (keycloak#15493): make nginx certificate-lookup thread safe (#15480)

Closes #15493
This commit is contained in:
Karim Boukari 2023-01-10 11:56:40 +01:00 committed by GitHub
parent 4d55c6a647
commit bcc23b6330
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 97 additions and 79 deletions

View file

@ -3,7 +3,6 @@ package org.keycloak.services.x509;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertPath;
@ -19,6 +18,7 @@ import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.jboss.logging.Logger;
@ -27,10 +27,6 @@ import org.keycloak.http.HttpRequest;
import org.keycloak.common.crypto.CryptoIntegration;
import org.keycloak.common.util.PemException;
import org.keycloak.common.util.PemUtils;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.truststore.TruststoreProvider;
import org.keycloak.truststore.TruststoreProviderFactory;
/**
* The NGINX Provider extract end user X.509 certificate send during TLS mutual authentication,
@ -62,20 +58,27 @@ public class NginxProxySslClientCertificateLookup extends AbstractClientCertific
private static final Logger log = Logger.getLogger(NginxProxySslClientCertificateLookup.class);
private static boolean isTruststoreLoaded = false;
private static KeyStore truststore = null;
private static Set<X509Certificate> trustedRootCerts = null;
private static Set<X509Certificate> intermediateCerts = null;
private final boolean isTruststoreLoaded;
private final Set<X509Certificate> trustedRootCerts;
private final Set<X509Certificate> intermediateCerts;
public NginxProxySslClientCertificateLookup(String sslCientCertHttpHeader,
public NginxProxySslClientCertificateLookup(String sslClientCertHttpHeader,
String sslCertChainHttpHeaderPrefix,
int certificateChainLength,
KeycloakSession kcsession) {
super(sslCientCertHttpHeader, sslCertChainHttpHeaderPrefix, certificateChainLength);
Set<X509Certificate> intermediateCerts,
Set<X509Certificate> trustedRootCerts,
boolean isTruststoreLoaded
) {
super(sslClientCertHttpHeader, sslCertChainHttpHeaderPrefix, certificateChainLength);
if (!loadKeycloakTrustStore(kcsession)) {
Objects.requireNonNull(intermediateCerts,"requireNonNull intermediateCerts");
Objects.requireNonNull(trustedRootCerts,"requireNonNull trustedRootCerts");
this.intermediateCerts = intermediateCerts;
this.trustedRootCerts = trustedRootCerts;
this.isTruststoreLoaded = isTruststoreLoaded;
if (!this.isTruststoreLoaded) {
log.warn("Keycloak Truststore is null or empty, but it's required for NGINX x509cert-lookup provider");
log.warn(" see Keycloak documentation here : https://www.keycloak.org/docs/latest/server_installation/index.html#_truststore");
}
@ -126,7 +129,7 @@ public class NginxProxySslClientCertificateLookup extends AbstractClientCertific
X509Certificate clientCert = getCertificateFromHttpHeader(httpRequest, sslClientCertHttpHeader);
if (clientCert != null) {
log.debugf("End user certificate found : Subject DN=[%s] SerialNumber=[%s]", clientCert.getSubjectDN(), clientCert.getSerialNumber());
log.debugf("End user certificate found : Subject DN=[%s] SerialNumber=[%s]", clientCert.getSubjectX500Principal(), clientCert.getSerialNumber());
// Rebuilding the end user certificate chain using Keycloak Truststore
X509Certificate[] certChain = buildChain(clientCert);
@ -134,9 +137,9 @@ public class NginxProxySslClientCertificateLookup extends AbstractClientCertific
log.info("Impossible to rebuild end user cert chain : client certificate authentication will fail." );
chain.add(clientCert);
} else {
for (X509Certificate cacert : certChain) {
chain.add(cacert);
log.debugf("Rebuilded user cert chain DN : %s", cacert.getSubjectDN().toString() );
for (X509Certificate caCert : certChain) {
chain.add(caCert);
log.debugf("Rebuilded user cert chain DN : %s", caCert.getSubjectX500Principal());
}
}
}
@ -148,25 +151,25 @@ public class NginxProxySslClientCertificateLookup extends AbstractClientCertific
* we are rebuilding here the end user certificate chain with Keycloak truststore.
* <br>
* Please note that Keycloak truststore must contain root and intermediate CA's certificates.
* @param end_user_auth_cert
* @param endUserAuthCert
* @return
*/
public X509Certificate[] buildChain(X509Certificate end_user_auth_cert) {
private X509Certificate[] buildChain(X509Certificate endUserAuthCert) {
X509Certificate[] user_cert_chain = null;
X509Certificate[] userCertChain = new X509Certificate[0];
try {
// No truststore : no way!
if (isTruststoreLoaded == false) {
if (!isTruststoreLoaded) {
log.warn("Keycloak Truststore is null, but it is required !");
log.warn(" see https://www.keycloak.org/docs/latest/server_installation/index.html#_truststore");
return null;
return userCertChain;
}
// Create the selector that specifies the starting certificate
X509CertSelector selector = new X509CertSelector();
selector.setCertificate(end_user_auth_cert);
selector.setCertificate(endUserAuthCert);
// Create the trust anchors (set of root CA certificates)
Set<TrustAnchor> trustAnchors = new HashSet<TrustAnchor>();
@ -184,9 +187,9 @@ public class NginxProxySslClientCertificateLookup extends AbstractClientCertific
pkixParams.setMaxPathLength(certificateChainLength);
// Adding the list of intermediate certificates + end user certificate
intermediateCerts.add(end_user_auth_cert);
CollectionCertStoreParameters intermediateCA_userCert = new CollectionCertStoreParameters(intermediateCerts);
CertStore intermediateCertStore = CryptoIntegration.getProvider().getCertStore(intermediateCA_userCert);
intermediateCerts.add(endUserAuthCert);
CollectionCertStoreParameters intermediateCAUserCert = new CollectionCertStoreParameters(intermediateCerts);
CertStore intermediateCertStore = CryptoIntegration.getProvider().getCertStore(intermediateCAUserCert);
pkixParams.addCertStore(intermediateCertStore);
// Build and verify the certification chain (revocation status excluded)
@ -194,9 +197,9 @@ public class NginxProxySslClientCertificateLookup extends AbstractClientCertific
CertPath certPath = certPathBuilder.build(pkixParams).getCertPath();
log.debug("Certification path building OK, and contains " + certPath.getCertificates().size() + " X509 Certificates");
user_cert_chain = convertCertPathtoX509CertArray( certPath );
userCertChain = convertCertPathToX509CertArray(certPath);
} catch (NoSuchAlgorithmException e) {
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchProviderException e) {
log.error(e.getLocalizedMessage(),e);
} catch (CertPathBuilderException e) {
if (log.isEnabled(Level.TRACE)) {
@ -204,64 +207,32 @@ public class NginxProxySslClientCertificateLookup extends AbstractClientCertific
} else {
log.warn(e.getLocalizedMessage());
}
} catch (InvalidAlgorithmParameterException e) {
log.error(e.getLocalizedMessage(),e);
} catch (NoSuchProviderException e) {
log.error(e.getLocalizedMessage(),e);
} finally {
if (isTruststoreLoaded) {
//Remove end user certificate
intermediateCerts.remove(end_user_auth_cert);
intermediateCerts.remove(endUserAuthCert);
}
}
return user_cert_chain;
return userCertChain;
}
public X509Certificate[] convertCertPathtoX509CertArray( CertPath certPath ) {
private X509Certificate[] convertCertPathToX509CertArray(CertPath certPath ) {
X509Certificate[] x509certchain = null;
if (certPath != null) {
List<X509Certificate> trustedX509Chain = new ArrayList<X509Certificate>();
for (Certificate certificate : certPath.getCertificates()) {
if (certificate instanceof X509Certificate) {
trustedX509Chain.add((X509Certificate) certificate);
}
}
x509certchain = trustedX509Chain.toArray(new X509Certificate[0]);
X509Certificate[] x509certChain = new X509Certificate[0];
if (certPath == null){
return x509certChain;
}
return x509certchain;
}
/** Loading truststore @ first login
*
* @param kcsession
* @return
*/
public boolean loadKeycloakTrustStore(KeycloakSession kcsession) {
if (!isTruststoreLoaded) {
log.debug(" Loading Keycloak truststore ...");
KeycloakSessionFactory factory = kcsession.getKeycloakSessionFactory();
TruststoreProviderFactory truststoreFactory = (TruststoreProviderFactory) factory.getProviderFactory(TruststoreProvider.class, "file");
TruststoreProvider provider = truststoreFactory.create(kcsession);
if (provider != null && provider.getTruststore() != null) {
truststore = provider.getTruststore();
trustedRootCerts = new HashSet<>(provider.getRootCertificates().values());
intermediateCerts = new HashSet<>(provider.getIntermediateCertificates().values());
log.debug("Keycloak truststore loaded for NGINX x509cert-lookup provider.");
isTruststoreLoaded = true;
List<X509Certificate> trustedX509Chain = new ArrayList<X509Certificate>();
for (Certificate certificate : certPath.getCertificates()) {
if (certificate instanceof X509Certificate) {
trustedX509Chain.add((X509Certificate) certificate);
}
}
return isTruststoreLoaded;
}
return trustedX509Chain.toArray(x509certChain);
}
}

View file

@ -3,6 +3,13 @@ package org.keycloak.services.x509;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.truststore.TruststoreProvider;
import org.keycloak.truststore.TruststoreProviderFactory;
import java.security.cert.X509Certificate;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* The factory and the corresponding providers extract a client certificate
@ -15,29 +22,40 @@ import org.keycloak.models.KeycloakSession;
public class NginxProxySslClientCertificateLookupFactory extends AbstractClientCertificateFromHttpHeadersLookupFactory {
private final static Logger logger = Logger.getLogger(NginxProxySslClientCertificateLookupFactory.class);
private static final Logger logger = Logger.getLogger(NginxProxySslClientCertificateLookupFactory.class);
private final static String PROVIDER = "nginx";
private static final String PROVIDER = "nginx";
protected final static String TRUST_PROXY_VERIFICATION = "trust-proxy-verification";
protected static final String TRUST_PROXY_VERIFICATION = "trust-proxy-verification";
protected boolean trustProxyVerification = false;
protected boolean trustProxyVerification;
private volatile boolean isTruststoreLoaded;
private Set<X509Certificate> trustedRootCerts;
private Set<X509Certificate> intermediateCerts;
@Override
public void init(Config.Scope config) {
super.init(config);
trustProxyVerification = config.getBoolean(TRUST_PROXY_VERIFICATION, false);
this.trustProxyVerification = config.getBoolean(TRUST_PROXY_VERIFICATION, false);
logger.tracev("{0}: ''{1}''", TRUST_PROXY_VERIFICATION, trustProxyVerification);
this.isTruststoreLoaded = false;
this.trustedRootCerts = ConcurrentHashMap.newKeySet();
this.intermediateCerts = ConcurrentHashMap.newKeySet();
}
@Override
public X509ClientCertificateLookup create(KeycloakSession session) {
loadKeycloakTrustStore(session);
if (trustProxyVerification) {
return new NginxProxyTrustedClientCertificateLookup(sslClientCertHttpHeader,
sslChainHttpHeaderPrefix, certificateChainLength);
} else {
return new NginxProxySslClientCertificateLookup(sslClientCertHttpHeader,
sslChainHttpHeaderPrefix, certificateChainLength, session);
sslChainHttpHeaderPrefix, certificateChainLength, intermediateCerts, trustedRootCerts, isTruststoreLoaded);
}
}
@ -45,4 +63,33 @@ public class NginxProxySslClientCertificateLookupFactory extends AbstractClientC
public String getId() {
return PROVIDER;
}
/** Loading truststore @ first login
*
* @param kcSession keycloak session
*/
private void loadKeycloakTrustStore(KeycloakSession kcSession) {
if (isTruststoreLoaded){
return;
}
synchronized (this) {
if (isTruststoreLoaded) {
return;
}
logger.debug(" Loading Keycloak truststore ...");
KeycloakSessionFactory factory = kcSession.getKeycloakSessionFactory();
TruststoreProviderFactory truststoreFactory = (TruststoreProviderFactory) factory.getProviderFactory(TruststoreProvider.class, "file");
TruststoreProvider provider = truststoreFactory.create(kcSession);
if (provider != null && provider.getTruststore() != null) {
trustedRootCerts.addAll(provider.getRootCertificates().values());
intermediateCerts.addAll(provider.getIntermediateCertificates().values());
logger.debug("Keycloak truststore loaded for NGINX x509cert-lookup provider.");
isTruststoreLoaded = true;
}
}
}
}