[KEYCLOAK-8580] Add Nginx certificate lookup provider
This commit is contained in:
parent
60a8267576
commit
ab8789739f
5 changed files with 410 additions and 1 deletions
|
@ -40,7 +40,7 @@ public abstract class AbstractClientCertificateFromHttpHeadersLookup implements
|
||||||
|
|
||||||
protected final String sslClientCertHttpHeader;
|
protected final String sslClientCertHttpHeader;
|
||||||
protected final String sslCertChainHttpHeaderPrefix;
|
protected final String sslCertChainHttpHeaderPrefix;
|
||||||
private final int certificateChainLength;
|
protected final int certificateChainLength;
|
||||||
|
|
||||||
public AbstractClientCertificateFromHttpHeadersLookup(String sslCientCertHttpHeader,
|
public AbstractClientCertificateFromHttpHeadersLookup(String sslCientCertHttpHeader,
|
||||||
String sslCertChainHttpHeaderPrefix,
|
String sslCertChainHttpHeaderPrefix,
|
||||||
|
|
|
@ -0,0 +1,360 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2017 Analytical Graphics, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.x509;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.KeyStoreException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertPath;
|
||||||
|
import java.security.cert.CertPathBuilder;
|
||||||
|
import java.security.cert.CertPathBuilderException;
|
||||||
|
import java.security.cert.CertStore;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CollectionCertStoreParameters;
|
||||||
|
import java.security.cert.PKIXBuilderParameters;
|
||||||
|
import java.security.cert.TrustAnchor;
|
||||||
|
import java.security.cert.X509CertSelector;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.jboss.logging.Logger.Level;
|
||||||
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
|
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,
|
||||||
|
* and forwarded in an http header.
|
||||||
|
*
|
||||||
|
* NGINX configuration must have :
|
||||||
|
* <code>
|
||||||
|
* server {
|
||||||
|
* ...
|
||||||
|
* ssl_client_certificate path-to-my-trustyed-cas-for client auth.pem;
|
||||||
|
* ssl_verify_client on|optional_no_ca;
|
||||||
|
* ssl_verify_depth 2;
|
||||||
|
* ...
|
||||||
|
* location / {
|
||||||
|
* ...
|
||||||
|
* sproxy_set_header ssl-client-cert $ssl_client_escaped_cert;
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
* Note that $ssl_client_cert is deprecated, use only $ssl_client_escaped_cert with this implementation
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:arnault.michel@toad-consulting.com">Arnault MICHEL</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
* @since 10/09/2018
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class NginxProxySslClientCertificateLookup extends AbstractClientCertificateFromHttpHeadersLookup {
|
||||||
|
|
||||||
|
private static final Logger log = Logger.getLogger(NginxProxySslClientCertificateLookup.class);
|
||||||
|
|
||||||
|
private static KeyStore truststore = null;
|
||||||
|
private static Set<X509Certificate> trustedRootCerts = null;
|
||||||
|
private static Set<X509Certificate> intermediateCerts = null;
|
||||||
|
|
||||||
|
|
||||||
|
public NginxProxySslClientCertificateLookup(String sslCientCertHttpHeader,
|
||||||
|
String sslCertChainHttpHeaderPrefix,
|
||||||
|
int certificateChainLength,
|
||||||
|
KeycloakSession kcsession) {
|
||||||
|
super(sslCientCertHttpHeader, sslCertChainHttpHeaderPrefix, certificateChainLength);
|
||||||
|
|
||||||
|
if (!loadKeycloakTrustStore(kcsession)) {
|
||||||
|
log.warn("Keycloak Truststore is null or empty, but it's needed to rebuild client certificate chain with nginx Keycloak Provider");
|
||||||
|
log.warn(" see Keycloak documentation here : https://www.keycloak.org/docs/latest/server_installation/index.html#_truststore");
|
||||||
|
}
|
||||||
|
log.debug(" Keycloak truststore loaded for NGINX client certificate provider.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removing PEM Headers and end of lines
|
||||||
|
* @param pem
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private static String removeBeginEnd(String pem) {
|
||||||
|
pem = pem.replace("-----BEGIN CERTIFICATE-----", "");
|
||||||
|
pem = pem.replace("-----END CERTIFICATE-----", "");
|
||||||
|
pem = pem.replace("\r\n", "");
|
||||||
|
pem = pem.replace("\n", "");
|
||||||
|
return pem.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decoding end user certificate, including URL decodeding due to ssl_client_escaped_cert nginx variable.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected X509Certificate decodeCertificateFromPem(String pem) throws PemException {
|
||||||
|
|
||||||
|
if (pem == null) {
|
||||||
|
log.info("End user TLS Certificate is NULL! ");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
pem = java.net.URLDecoder.decode(pem, "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
log.error("Cannot URL decode the end user TLS Certificate : " + pem,e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pem.startsWith("-----BEGIN CERTIFICATE-----")) {
|
||||||
|
pem = removeBeginEnd(pem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PemUtils.decodeCertificate(pem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public X509Certificate[] getCertificateChain(HttpRequest httpRequest) throws GeneralSecurityException {
|
||||||
|
List<X509Certificate> chain = new ArrayList<>();
|
||||||
|
|
||||||
|
// Get the client certificate
|
||||||
|
X509Certificate clientCert = getCertificateFromHttpHeader(httpRequest, sslClientCertHttpHeader);
|
||||||
|
log.debugf("End user certificate found : DN=[%s] SerialNumber=[%s]", clientCert.getSubjectDN().toString(), clientCert.getSerialNumber().toString() );
|
||||||
|
|
||||||
|
if (clientCert != null) {
|
||||||
|
|
||||||
|
// Rebuilding the end user certificate chain using Keycloak Truststore
|
||||||
|
X509Certificate[] certChain = buildChain(clientCert);
|
||||||
|
for (X509Certificate cacert : certChain) {
|
||||||
|
chain.add(cacert);
|
||||||
|
log.debugf("Rebuilded user cert chain DN : %s", cacert.getSubjectDN().toString() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chain.toArray(new X509Certificate[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* As NGINX cannot actually send the CA Chain in http header,
|
||||||
|
* @param end_user_auth_cert
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public X509Certificate[] buildChain(X509Certificate end_user_auth_cert) {
|
||||||
|
|
||||||
|
String javasecuritydebugoriginalsettings = setJVMDebuggingForCertPathBuilder();
|
||||||
|
|
||||||
|
X509Certificate[] user_cert_chain = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// No truststore : no way!
|
||||||
|
if (truststore == null) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the selector that specifies the starting certificate
|
||||||
|
X509CertSelector selector = new X509CertSelector();
|
||||||
|
selector.setCertificate(end_user_auth_cert);
|
||||||
|
|
||||||
|
// 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, as it's possibly done after depending on Keycloak settings
|
||||||
|
pkixParams.setRevocationEnabled(false);
|
||||||
|
pkixParams.setExplicitPolicyRequired(false);
|
||||||
|
pkixParams.setAnyPolicyInhibited(false);
|
||||||
|
pkixParams.setPolicyQualifiersRejected(false);
|
||||||
|
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 = CertStore.getInstance("Collection", intermediateCA_userCert, "BC");
|
||||||
|
pkixParams.addCertStore(intermediateCertStore);
|
||||||
|
|
||||||
|
// Build and verify the certification chain (revocation status excluded)
|
||||||
|
CertPathBuilder certPathBuilder = CertPathBuilder.getInstance("PKIX","BC");
|
||||||
|
CertPath certPath = certPathBuilder.build(pkixParams).getCertPath();
|
||||||
|
log.debug("Certification path building OK, and contains " + certPath.getCertificates().size() + " X509 Certificates");
|
||||||
|
|
||||||
|
//Remove end user certificate
|
||||||
|
intermediateCerts.remove(end_user_auth_cert);
|
||||||
|
|
||||||
|
user_cert_chain = convertCertPathtoX509CertArray( certPath );
|
||||||
|
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
log.error(e.getLocalizedMessage(),e);
|
||||||
|
} catch (CertPathBuilderException e) {
|
||||||
|
if ( log.isEnabled(Level.TRACE) )
|
||||||
|
log.debug(e.getLocalizedMessage(),e);
|
||||||
|
else
|
||||||
|
log.warn(e.getLocalizedMessage());
|
||||||
|
} catch (InvalidAlgorithmParameterException e) {
|
||||||
|
log.error(e.getLocalizedMessage(),e);
|
||||||
|
} catch (NoSuchProviderException e) {
|
||||||
|
log.error(e.getLocalizedMessage(),e);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Reset java security debug property to original value
|
||||||
|
if (javasecuritydebugoriginalsettings!=null)
|
||||||
|
System.setProperty("java.security.debug",javasecuritydebugoriginalsettings);
|
||||||
|
|
||||||
|
//Remove end user certificate
|
||||||
|
intermediateCerts.remove(end_user_auth_cert);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add setting JVM system properties for helping debugging CertPathBuilder
|
||||||
|
* only if the trace log level is enabled.
|
||||||
|
*
|
||||||
|
* @return the original value of system property java.security.debug
|
||||||
|
*/
|
||||||
|
private String setJVMDebuggingForCertPathBuilder() {
|
||||||
|
|
||||||
|
String origjvmsecdebprop = null;
|
||||||
|
if ( log.isEnabled(Level.TRACE) ) {
|
||||||
|
origjvmsecdebprop = System.getProperty("java.security.debug");
|
||||||
|
if (origjvmsecdebprop.indexOf("certpath") == -1) {
|
||||||
|
if (origjvmsecdebprop.length() == 0)
|
||||||
|
System.setProperty("java.security.debug","certpath");
|
||||||
|
else
|
||||||
|
System.setProperty("java.security.debug",origjvmsecdebprop + ",certpath");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return origjvmsecdebprop;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return x509certchain;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean loadKeycloakTrustStore(KeycloakSession kcsession) {
|
||||||
|
|
||||||
|
boolean isTSLoaded = false;
|
||||||
|
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();
|
||||||
|
readTruststore();
|
||||||
|
|
||||||
|
isTSLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isTSLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all certificates from Keycloak Truststore, and classify them in two lists : root CAs and intermediates CAs
|
||||||
|
*/
|
||||||
|
private void readTruststore() {
|
||||||
|
|
||||||
|
//Reading truststore aliases & certificates
|
||||||
|
Enumeration enumeration;
|
||||||
|
trustedRootCerts = new HashSet<X509Certificate>();
|
||||||
|
intermediateCerts = new HashSet<X509Certificate>();
|
||||||
|
try {
|
||||||
|
|
||||||
|
enumeration = truststore.aliases();
|
||||||
|
|
||||||
|
while(enumeration.hasMoreElements()) {
|
||||||
|
|
||||||
|
String alias = (String)enumeration.nextElement();
|
||||||
|
Certificate certificate = truststore.getCertificate(alias);
|
||||||
|
|
||||||
|
if (certificate instanceof X509Certificate) {
|
||||||
|
X509Certificate cax509cert = (X509Certificate) certificate;
|
||||||
|
if (isSelfSigned(cax509cert)) {
|
||||||
|
trustedRootCerts.add(cax509cert);
|
||||||
|
log.debug("Adding certificate from trustore as trsusted root CA (alias : "+alias + " | Subject DN : " + ((X509Certificate) certificate).getSubjectDN() +")");
|
||||||
|
} else {
|
||||||
|
intermediateCerts.add(cax509cert);
|
||||||
|
log.debug("Adding certificate from trustore as intermediate CA (alias : "+alias + " | Subject DN : " + ((X509Certificate) certificate).getSubjectDN() +")");
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
log.warn("Skipping certificate in "+ alias + " because it's not an X509Certificate");
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (KeyStoreException e) {
|
||||||
|
log.error("Error while reading Keycloak truststore "+e.getMessage(),e);
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
log.error("Error while reading Keycloak truststore "+e.getMessage(),e);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
log.error("Error while reading Keycloak truststore "+e.getMessage(),e);
|
||||||
|
} catch (NoSuchProviderException e) {
|
||||||
|
log.error("Error while reading Keycloak truststore "+e.getMessage(),e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether given X.509 certificate is self-signed.
|
||||||
|
*/
|
||||||
|
public boolean isSelfSigned(X509Certificate cert)
|
||||||
|
throws CertificateException, NoSuchAlgorithmException,
|
||||||
|
NoSuchProviderException {
|
||||||
|
try {
|
||||||
|
// Try to verify certificate signature with its own public key
|
||||||
|
PublicKey key = cert.getPublicKey();
|
||||||
|
cert.verify(key);
|
||||||
|
log.trace("certificate " + cert.getSubjectDN() + " detected as root CA");
|
||||||
|
return true;
|
||||||
|
} catch (SignatureException sigEx) {
|
||||||
|
// Invalid signature --> not self-signed
|
||||||
|
log.trace("certificate have a bad signature : " + sigEx.getMessage(),sigEx);
|
||||||
|
} catch (InvalidKeyException keyEx) {
|
||||||
|
// Invalid key --> not self-signed
|
||||||
|
log.trace("certificate " + cert.getSubjectDN() + " detected as intermediate CA");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package org.keycloak.services.x509;
|
||||||
|
/*
|
||||||
|
* Copyright 2017 Analytical Graphics, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:arnault.michel@toad-consulting.com">Arnault MICHEL</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
* @since 10/09/2018
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class NginxProxySslClientCertificateLookupFactory extends AbstractClientCertificateFromHttpHeadersLookupFactory {
|
||||||
|
|
||||||
|
private final static String PROVIDER = "nginx";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public X509ClientCertificateLookup create(KeycloakSession session) {
|
||||||
|
return new NginxProxySslClientCertificateLookup(sslClientCertHttpHeader,
|
||||||
|
sslChainHttpHeaderPrefix, certificateChainLength, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return PROVIDER;
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,3 +19,4 @@
|
||||||
org.keycloak.services.x509.DefaultClientCertificateLookupFactory
|
org.keycloak.services.x509.DefaultClientCertificateLookupFactory
|
||||||
org.keycloak.services.x509.HaProxySslClientCertificateLookupFactory
|
org.keycloak.services.x509.HaProxySslClientCertificateLookupFactory
|
||||||
org.keycloak.services.x509.ApacheProxySslClientCertificateLookupFactory
|
org.keycloak.services.x509.ApacheProxySslClientCertificateLookupFactory
|
||||||
|
org.keycloak.services.x509.NginxProxySslClientCertificateLookupFactory
|
||||||
|
|
|
@ -174,6 +174,12 @@
|
||||||
"sslClientCert": "x-ssl-client-cert",
|
"sslClientCert": "x-ssl-client-cert",
|
||||||
"sslCertChainPrefix": "x-ssl-client-cert-chain",
|
"sslCertChainPrefix": "x-ssl-client-cert-chain",
|
||||||
"certificateChainLength": 1
|
"certificateChainLength": 1
|
||||||
|
},
|
||||||
|
"nginx": {
|
||||||
|
"enabled": true,
|
||||||
|
"sslClientCert": "x-ssl-client-cert",
|
||||||
|
"sslCertChainPrefix": "x-ssl-client-cert-chain",
|
||||||
|
"certificateChainLength": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue