Fix (keycloak#15493): make nginx certificate-lookup thread safe (#15480)
Closes #15493
This commit is contained in:
parent
4d55c6a647
commit
bcc23b6330
2 changed files with 97 additions and 79 deletions
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue