From 5f9feee3f86e3d856063c8b6d355e30c962258f7 Mon Sep 17 00:00:00 2001 From: mposolda Date: Thu, 21 Mar 2019 11:55:49 +0100 Subject: [PATCH] KEYCLOAK-9846 Verifying signatures on CRL during X509 authentication --- .../org/keycloak/common/util/CRLUtils.java | 92 -------- .../truststore/TruststoreProvider.java | 14 ++ ...actX509ClientCertificateAuthenticator.java | 7 +- .../x509/CertificateValidator.java | 34 +-- .../x509/ValidateX509CertificateUsername.java | 2 +- .../X509ClientCertificateAuthenticator.java | 2 +- .../NginxProxySslClientCertificateLookup.java | 75 +------ .../truststore/FileTruststoreProvider.java | 20 +- .../FileTruststoreProviderFactory.java | 96 +++++++- .../java/org/keycloak/utils/CRLUtils.java | 206 ++++++++++++++++++ .../auth-server/jboss/common/keystore/ca.crt | 91 ++++++-- .../common/keystore/intermediate-ca-3.crl | 13 ++ .../intermediate-ca-invalid-signature.crl | 17 ++ .../jboss/common/keystore/keycloak.truststore | Bin 211943 -> 214778 bytes .../root/ca/newcerts/intermediate-ca-3.crt | 28 +++ .../root/ca/newcerts/intermediate-ca-3.key | 27 +++ .../jboss/common/pki/root/ca/openssl.cnf | 4 +- .../servers/auth-server/jboss/pom.xml | 3 +- .../x509/AbstractX509AuthenticationTest.java | 2 + .../org/keycloak/testsuite/x509/CRLRule.java | 2 + .../testsuite/x509/X509BrowserCRLTest.java | 44 +++- .../resources/keystore/keycloak.truststore | Bin 209371 -> 212206 bytes 22 files changed, 562 insertions(+), 217 deletions(-) delete mode 100644 common/src/main/java/org/keycloak/common/util/CRLUtils.java create mode 100644 services/src/main/java/org/keycloak/utils/CRLUtils.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/intermediate-ca-3.crl create mode 100644 testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/intermediate-ca-invalid-signature.crl create mode 100644 testsuite/integration-arquillian/servers/auth-server/jboss/common/pki/root/ca/newcerts/intermediate-ca-3.crt create mode 100644 testsuite/integration-arquillian/servers/auth-server/jboss/common/pki/root/ca/newcerts/intermediate-ca-3.key diff --git a/common/src/main/java/org/keycloak/common/util/CRLUtils.java b/common/src/main/java/org/keycloak/common/util/CRLUtils.java deleted file mode 100644 index 4e7e0ae0ba..0000000000 --- a/common/src/main/java/org/keycloak/common/util/CRLUtils.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2016 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.common.util; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.security.cert.X509Certificate; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -import org.bouncycastle.asn1.ASN1InputStream; -import org.bouncycastle.asn1.DERIA5String; -import org.bouncycastle.asn1.DEROctetString; -import org.bouncycastle.asn1.x509.CRLDistPoint; -import org.bouncycastle.asn1.x509.DistributionPoint; -import org.bouncycastle.asn1.x509.DistributionPointName; -import org.bouncycastle.asn1.x509.GeneralName; -import org.bouncycastle.asn1.x509.GeneralNames; - -/** - * @author Peter Nalyvayko - * @version $Revision: 1 $ - * @since 10/31/2016 - */ - -public final class CRLUtils { - - static { - BouncyIntegration.init(); - } - - private static final String CRL_DISTRIBUTION_POINTS_OID = "2.5.29.31"; - - /** - * Retrieves a list of CRL distribution points from CRLDP v3 certificate extension - * See CRL validation - * @param cert - * @return - * @throws IOException - */ - public static List getCRLDistributionPoints(X509Certificate cert) throws IOException { - byte[] data = cert.getExtensionValue(CRL_DISTRIBUTION_POINTS_OID); - if (data == null) { - return Collections.emptyList(); - } - - List distributionPointUrls = new LinkedList<>(); - DEROctetString octetString; - try (ASN1InputStream crldpExtensionInputStream = new ASN1InputStream(new ByteArrayInputStream(data))) { - octetString = (DEROctetString)crldpExtensionInputStream.readObject(); - } - byte[] octets = octetString.getOctets(); - - CRLDistPoint crlDP; - try (ASN1InputStream crldpInputStream = new ASN1InputStream(new ByteArrayInputStream(octets))) { - crlDP = CRLDistPoint.getInstance(crldpInputStream.readObject()); - } - - for (DistributionPoint dp : crlDP.getDistributionPoints()) { - DistributionPointName dpn = dp.getDistributionPoint(); - if (dpn != null && dpn.getType() == DistributionPointName.FULL_NAME) { - GeneralName[] names = GeneralNames.getInstance(dpn.getName()).getNames(); - for (GeneralName gn : names) { - if (gn.getTagNo() == GeneralName.uniformResourceIdentifier) { - String url = DERIA5String.getInstance(gn.getName()).getString(); - distributionPointUrls.add(url); - } - } - } - } - - return distributionPointUrls; - } - -} diff --git a/server-spi-private/src/main/java/org/keycloak/truststore/TruststoreProvider.java b/server-spi-private/src/main/java/org/keycloak/truststore/TruststoreProvider.java index 00b868a2e1..e5238f109e 100755 --- a/server-spi-private/src/main/java/org/keycloak/truststore/TruststoreProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/truststore/TruststoreProvider.java @@ -20,6 +20,10 @@ package org.keycloak.truststore; import org.keycloak.provider.Provider; import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Map; + +import javax.security.auth.x500.X500Principal; /** * @author Marko Strukelj @@ -29,4 +33,14 @@ public interface TruststoreProvider extends Provider { HostnameVerificationPolicy getPolicy(); KeyStore getTruststore(); + + /** + * @return root certificates from the configured truststore as a map where the key is the X500Principal of the corresponding X509Certificate + */ + Map getRootCertificates(); + + /** + * @return intermediate certificates from the configured truststore as a map where the key is the X500Principal of the corresponding X509Certificate + */ + Map getIntermediateCertificates(); } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java index 66b03b26c2..e8571d34e4 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java @@ -86,10 +86,11 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth protected static class CertificateValidatorConfigBuilder { - static CertificateValidator.CertificateValidatorBuilder fromConfig(X509AuthenticatorConfigModel config) throws Exception { + static CertificateValidator.CertificateValidatorBuilder fromConfig(KeycloakSession session, X509AuthenticatorConfigModel config) throws Exception { CertificateValidator.CertificateValidatorBuilder builder = new CertificateValidator.CertificateValidatorBuilder(); return builder + .session(session) .keyUsage() .parse(config.getKeyUsage()) .extendedKeyUsage() @@ -105,8 +106,8 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth } // The method is purely for purposes of facilitating the unit testing - public CertificateValidator.CertificateValidatorBuilder certificateValidationParameters(X509AuthenticatorConfigModel config) throws Exception { - return CertificateValidatorConfigBuilder.fromConfig(config); + public CertificateValidator.CertificateValidatorBuilder certificateValidationParameters(KeycloakSession session, X509AuthenticatorConfigModel config) throws Exception { + return CertificateValidatorConfigBuilder.fromConfig(session, config); } protected static class UserIdentityExtractorBuilder { diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java index 1c16322503..900e4217df 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java @@ -18,7 +18,8 @@ package org.keycloak.authentication.authenticators.x509; -import org.keycloak.common.util.CRLUtils; +import org.keycloak.models.KeycloakSession; +import org.keycloak.utils.CRLUtils; import org.keycloak.common.util.OCSPUtils; import org.keycloak.models.Constants; import org.keycloak.services.ServicesLogger; @@ -54,7 +55,6 @@ import java.util.Set; import java.util.LinkedList; import java.util.ArrayList; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.processing.core.util.XMLSignatureUtil; @@ -354,7 +354,7 @@ public class CertificateValidator { } } - + KeycloakSession session; X509Certificate[] _certChain; int _keyUsageBits; List _extendedKeyUsage; @@ -373,7 +373,8 @@ public class CertificateValidator { boolean cRLDPCheckingEnabled, CRLLoaderImpl crlLoader, boolean oCSPCheckingEnabled, - OCSPChecker ocspChecker) { + OCSPChecker ocspChecker, + KeycloakSession session) { _certChain = certChain; _keyUsageBits = keyUsageBits; _extendedKeyUsage = extendedKeyUsage; @@ -382,6 +383,7 @@ public class CertificateValidator { _crlLoader = crlLoader; _ocspEnabled = oCSPCheckingEnabled; this.ocspChecker = ocspChecker; + this.session = session; if (ocspChecker == null) throw new IllegalArgumentException("ocspChecker"); @@ -497,15 +499,11 @@ public class CertificateValidator { } } - private static void checkRevocationStatusUsingCRL(X509Certificate[] certs, CRLLoaderImpl crLoader) throws GeneralSecurityException { + private static void checkRevocationStatusUsingCRL(X509Certificate[] certs, CRLLoaderImpl crLoader, KeycloakSession session) throws GeneralSecurityException { Collection crlColl = crLoader.getX509CRLs(); if (crlColl != null && crlColl.size() > 0) { for (X509CRL it : crlColl) { - if (it.isRevoked(certs[0])) { - String message = String.format("Certificate has been revoked, certificate's subject: %s", certs[0].getSubjectDN().getName()); - logger.debug(message); - throw new GeneralSecurityException(message); - } + CRLUtils.check(certs, it, session); } } } @@ -519,7 +517,7 @@ public class CertificateValidator { return new ArrayList<>(); } - private static void checkRevocationStatusUsingCRLDistributionPoints(X509Certificate[] certs) throws GeneralSecurityException { + private static void checkRevocationStatusUsingCRLDistributionPoints(X509Certificate[] certs, KeycloakSession session) throws GeneralSecurityException { List distributionPoints = getCRLDistributionPoints(certs[0]); if (distributionPoints == null || distributionPoints.size() == 0) { @@ -527,7 +525,7 @@ public class CertificateValidator { } for (String dp : distributionPoints) { logger.tracef("CRL Distribution point: \"%s\"", dp); - checkRevocationStatusUsingCRL(certs, new CRLFileLoader(dp)); + checkRevocationStatusUsingCRL(certs, new CRLFileLoader(dp), session); } } @@ -537,9 +535,9 @@ public class CertificateValidator { } if (_crlCheckingEnabled) { if (!_crldpEnabled) { - checkRevocationStatusUsingCRL(_certChain, _crlLoader /*"crl.pem"*/); + checkRevocationStatusUsingCRL(_certChain, _crlLoader, session); } else { - checkRevocationStatusUsingCRLDistributionPoints(_certChain); + checkRevocationStatusUsingCRLDistributionPoints(_certChain, session); } } if (_ocspEnabled) { @@ -556,6 +554,7 @@ public class CertificateValidator { // instances of CertificateValidator type. The design is an adaption of // the approach described in http://programmers.stackexchange.com/questions/252067/learning-to-write-dsls-utilities-for-unit-tests-and-am-worried-about-extensablit + KeycloakSession session; int _keyUsageBits; List _extendedKeyUsage; boolean _crlCheckingEnabled; @@ -732,6 +731,11 @@ public class CertificateValidator { } } + public CertificateValidatorBuilder session(KeycloakSession session) { + this.session = session; + return this; + } + public KeyUsageValidationBuilder keyUsage() { return new KeyUsageValidationBuilder(this); } @@ -750,7 +754,7 @@ public class CertificateValidator { } return new CertificateValidator(certs, _keyUsageBits, _extendedKeyUsage, _crlCheckingEnabled, _crldpEnabled, _crlLoader, _ocspEnabled, - new BouncyCastleOCSPChecker(_responderUri, _responderCert)); + new BouncyCastleOCSPChecker(_responderUri, _responderCert), session); } } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsername.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsername.java index ba0491730a..c86eafb63f 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsername.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsername.java @@ -69,7 +69,7 @@ public class ValidateX509CertificateUsername extends AbstractX509ClientCertifica } // Validate X509 client certificate try { - CertificateValidator.CertificateValidatorBuilder builder = certificateValidationParameters(config); + CertificateValidator.CertificateValidatorBuilder builder = certificateValidationParameters(context.getSession(), config); CertificateValidator validator = builder.build(certs); validator.checkRevocationStatus() .validateKeyUsage() diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticator.java index c55252fa28..1aaaaf3dcc 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticator.java @@ -82,7 +82,7 @@ public class X509ClientCertificateAuthenticator extends AbstractX509ClientCertif // Validate X509 client certificate try { - CertificateValidator.CertificateValidatorBuilder builder = certificateValidationParameters(config); + CertificateValidator.CertificateValidatorBuilder builder = certificateValidationParameters(context.getSession(), config); CertificateValidator validator = builder.build(certs); validator.checkRevocationStatus() .validateKeyUsage() diff --git a/services/src/main/java/org/keycloak/services/x509/NginxProxySslClientCertificateLookup.java b/services/src/main/java/org/keycloak/services/x509/NginxProxySslClientCertificateLookup.java index 287d2131b8..a27a6b3e4b 100644 --- a/services/src/main/java/org/keycloak/services/x509/NginxProxySslClientCertificateLookup.java +++ b/services/src/main/java/org/keycloak/services/x509/NginxProxySslClientCertificateLookup.java @@ -3,26 +3,20 @@ 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; @@ -253,7 +247,8 @@ public class NginxProxySslClientCertificateLookup extends AbstractClientCertific if ( provider != null && provider.getTruststore() != null ) { truststore = provider.getTruststore(); - readTruststore(); + trustedRootCerts = new HashSet<>(provider.getRootCertificates().values()); + intermediateCerts = new HashSet<>(provider.getIntermediateCertificates().values()); log.debug("Keycloak truststore loaded for NGINX x509cert-lookup provider."); isTruststoreLoaded = true; @@ -263,70 +258,4 @@ public class NginxProxySslClientCertificateLookup extends AbstractClientCertific return isTruststoreLoaded; } - /** - * 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(); - intermediateCerts = new HashSet(); - - try { - - enumeration = truststore.aliases(); - log.trace("Checking " + truststore.size() + " entries from the truststore."); - 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("Trusted root CA found in trustore : alias : "+alias + " | Subject DN : " + ((X509Certificate) certificate).getSubjectDN() ); - } else { - intermediateCerts.add(cax509cert); - log.debug("Intermediate CA found in trustore : alias : "+alias + " | Subject DN : " + ((X509Certificate) certificate).getSubjectDN() ); - } - } else - log.info("Skipping certificate with alias ["+ alias + "] from truststore, 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 " + cert.getSubjectDN() + " detected as intermediate CA"); - } catch (InvalidKeyException keyEx) { - // Invalid key --> not self-signed - log.trace("certificate " + cert.getSubjectDN() + " detected as intermediate CA"); - } - return false; - } - } diff --git a/services/src/main/java/org/keycloak/truststore/FileTruststoreProvider.java b/services/src/main/java/org/keycloak/truststore/FileTruststoreProvider.java index e8d6f02944..4fa13cb4cb 100755 --- a/services/src/main/java/org/keycloak/truststore/FileTruststoreProvider.java +++ b/services/src/main/java/org/keycloak/truststore/FileTruststoreProvider.java @@ -18,6 +18,10 @@ package org.keycloak.truststore; import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Map; + +import javax.security.auth.x500.X500Principal; /** * @author Marko Strukelj @@ -26,10 +30,14 @@ public class FileTruststoreProvider implements TruststoreProvider { private final HostnameVerificationPolicy policy; private final KeyStore truststore; + private final Map rootCertificates; + private final Map intermediateCertificates; - FileTruststoreProvider(KeyStore truststore, HostnameVerificationPolicy policy) { + FileTruststoreProvider(KeyStore truststore, HostnameVerificationPolicy policy, Map rootCertificates, Map intermediateCertificates) { this.policy = policy; this.truststore = truststore; + this.rootCertificates = rootCertificates; + this.intermediateCertificates = intermediateCertificates; } @Override @@ -42,6 +50,16 @@ public class FileTruststoreProvider implements TruststoreProvider { return truststore; } + @Override + public Map getRootCertificates() { + return rootCertificates; + } + + @Override + public Map getIntermediateCertificates() { + return intermediateCertificates; + } + @Override public void close() { } diff --git a/services/src/main/java/org/keycloak/truststore/FileTruststoreProviderFactory.java b/services/src/main/java/org/keycloak/truststore/FileTruststoreProviderFactory.java index 9a99f113bd..c3af8473e1 100755 --- a/services/src/main/java/org/keycloak/truststore/FileTruststoreProviderFactory.java +++ b/services/src/main/java/org/keycloak/truststore/FileTruststoreProviderFactory.java @@ -26,7 +26,22 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +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.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import javax.security.auth.x500.X500Principal; /** * @author Marko Strukelj @@ -85,7 +100,8 @@ public class FileTruststoreProviderFactory implements TruststoreProviderFactory } } - provider = new FileTruststoreProvider(truststore, verificationPolicy); + TruststoreCertificatesLoader certsLoader = new TruststoreCertificatesLoader(truststore); + provider = new FileTruststoreProvider(truststore, verificationPolicy, certsLoader.trustedRootCerts, certsLoader.intermediateCerts); TruststoreProviderSingleton.set(provider); log.debug("File trustore provider initialized: " + new File(storepath).getAbsolutePath()); } @@ -116,4 +132,82 @@ public class FileTruststoreProviderFactory implements TruststoreProviderFactory public String getId() { return "file"; } + + + + private class TruststoreCertificatesLoader { + + private Map trustedRootCerts = new HashMap<>(); + private Map intermediateCerts = new HashMap<>(); + + + public TruststoreCertificatesLoader(KeyStore truststore) { + readTruststore(truststore); + } + + /** + * Get all certificates from Keycloak Truststore, and classify them in two lists : root CAs and intermediates CAs + */ + private void readTruststore(KeyStore truststore) { + + //Reading truststore aliases & certificates + Enumeration enumeration; + + try { + + enumeration = truststore.aliases(); + log.trace("Checking " + truststore.size() + " entries from the truststore."); + while(enumeration.hasMoreElements()) { + + String alias = (String)enumeration.nextElement(); + Certificate certificate = truststore.getCertificate(alias); + + if (certificate instanceof X509Certificate) { + X509Certificate cax509cert = (X509Certificate) certificate; + if (isSelfSigned(cax509cert)) { + X500Principal principal = cax509cert.getSubjectX500Principal(); + trustedRootCerts.put(principal, cax509cert); + log.debug("Trusted root CA found in trustore : alias : "+alias + " | Subject DN : " + principal); + } else { + X500Principal principal = cax509cert.getSubjectX500Principal(); + intermediateCerts.put(principal, cax509cert); + log.debug("Intermediate CA found in trustore : alias : "+alias + " | Subject DN : " + principal); + } + } else + log.info("Skipping certificate with alias ["+ alias + "] from truststore, 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. + */ + private 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 " + cert.getSubjectDN() + " detected as intermediate CA"); + } catch (InvalidKeyException keyEx) { + // Invalid key --> not self-signed + log.trace("certificate " + cert.getSubjectDN() + " detected as intermediate CA"); + } + return false; + } + } } diff --git a/services/src/main/java/org/keycloak/utils/CRLUtils.java b/services/src/main/java/org/keycloak/utils/CRLUtils.java new file mode 100644 index 0000000000..bca202e949 --- /dev/null +++ b/services/src/main/java/org/keycloak/utils/CRLUtils.java @@ -0,0 +1,206 @@ +/* + * Copyright 2017 Red Hat, 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.utils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import javax.security.auth.x500.X500Principal; + +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.DERIA5String; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.x509.CRLDistPoint; +import org.bouncycastle.asn1.x509.DistributionPoint; +import org.bouncycastle.asn1.x509.DistributionPointName; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.jboss.logging.Logger; +import org.keycloak.common.util.BouncyIntegration; +import org.keycloak.models.KeycloakSession; +import org.keycloak.truststore.TruststoreProvider; +import org.wildfly.security.x500.X500; + +/** + * @author Peter Nalyvayko + * @version $Revision: 1 $ + * @since 10/31/2016 + */ + +public final class CRLUtils { + + private static final Logger log = Logger.getLogger(CRLUtils.class); + + + static { + BouncyIntegration.init(); + } + + private static final String CRL_DISTRIBUTION_POINTS_OID = "2.5.29.31"; + + /** + * Retrieves a list of CRL distribution points from CRLDP v3 certificate extension + * See CRL validation + * @param cert + * @return + * @throws IOException + */ + public static List getCRLDistributionPoints(X509Certificate cert) throws IOException { + byte[] data = cert.getExtensionValue(CRL_DISTRIBUTION_POINTS_OID); + if (data == null) { + return Collections.emptyList(); + } + + List distributionPointUrls = new LinkedList<>(); + DEROctetString octetString; + try (ASN1InputStream crldpExtensionInputStream = new ASN1InputStream(new ByteArrayInputStream(data))) { + octetString = (DEROctetString)crldpExtensionInputStream.readObject(); + } + byte[] octets = octetString.getOctets(); + + CRLDistPoint crlDP; + try (ASN1InputStream crldpInputStream = new ASN1InputStream(new ByteArrayInputStream(octets))) { + crlDP = CRLDistPoint.getInstance(crldpInputStream.readObject()); + } + + for (DistributionPoint dp : crlDP.getDistributionPoints()) { + DistributionPointName dpn = dp.getDistributionPoint(); + if (dpn != null && dpn.getType() == DistributionPointName.FULL_NAME) { + GeneralName[] names = GeneralNames.getInstance(dpn.getName()).getNames(); + for (GeneralName gn : names) { + if (gn.getTagNo() == GeneralName.uniformResourceIdentifier) { + String url = DERIA5String.getInstance(gn.getName()).getString(); + distributionPointUrls.add(url); + } + } + } + } + + return distributionPointUrls; + } + + + /** + * Check the signature on CRL and check if 1st certificate from the chain ((The actual certificate from the client)) is valid and not available on CRL. + * + * @param certs The 1st certificate is the actual certificate of the user. The other certificates represents the certificate chain + * @param crl Given CRL + * @throws GeneralSecurityException if some error in validation happens. Typically certificate not valid, or CRL signature not valid + */ + public static void check(X509Certificate[] certs, X509CRL crl, KeycloakSession session) throws GeneralSecurityException { + if (certs.length < 2) { + throw new GeneralSecurityException("Not possible to verify signature on CRL. X509 certificate doesn't have CA chain available on it"); + } + + X500Principal crlIssuerPrincipal = crl.getIssuerX500Principal(); + X509Certificate crlSignatureCertificate = null; + + // Try to find the certificate in the CA chain, which was used to sign the CRL + for (int i=1 ; i rootCerts = truststoreProvider.getRootCertificates(); + Map intermediateCerts = truststoreProvider.getIntermediateCertificates(); + + X509Certificate crlSignatureCertificate = intermediateCerts.get(crlIssuerPrincipal); + if (crlSignatureCertificate == null) { + crlSignatureCertificate = rootCerts.get(crlIssuerPrincipal); + } + + if (crlSignatureCertificate == null) { + throw new GeneralSecurityException("Not available certificate for CRL issuer '" + crlIssuerPrincipal + "' in the truststore, nor in the CA chain"); + } else { + log.tracef("Found CRL issuer certificate with subject '%s' in the truststore. Verifying trust anchor", crlIssuerPrincipal); + } + + // Check if CRL issuer has trust anchor with the checked certificate (See https://tools.ietf.org/html/rfc5280#section-6.3.3 , paragraph (f)) + Set certificateCAPrincipals = Arrays.asList(certs).stream() + .map(X509Certificate::getSubjectX500Principal) + .collect(Collectors.toSet()); + + // Remove the checked certificate itself + certificateCAPrincipals.remove(certs[0].getSubjectX500Principal()); + + X509Certificate currentCRLAnchorCertificate = crlSignatureCertificate; + X500Principal currentCRLAnchorPrincipal = crlIssuerPrincipal; + while (true) { + if (certificateCAPrincipals.contains(currentCRLAnchorPrincipal)) { + log.tracef("Found trust anchor of the CRL issuer '%s' in the CA chain. Anchor is '%s'", crlIssuerPrincipal, currentCRLAnchorPrincipal); + break; + } + + // Try to see the anchor + currentCRLAnchorPrincipal = currentCRLAnchorCertificate.getIssuerX500Principal(); + + currentCRLAnchorCertificate = intermediateCerts.get(currentCRLAnchorPrincipal); + if (currentCRLAnchorCertificate == null) { + currentCRLAnchorCertificate = rootCerts.get(currentCRLAnchorPrincipal); + } + if (currentCRLAnchorCertificate == null) { + throw new GeneralSecurityException("Certificate for CRL issuer '" + crlIssuerPrincipal + "' available in the truststore, but doesn't have trust anchors with the CA chain."); + } + } + + return crlSignatureCertificate; + } + +} diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ca.crt b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ca.crt index d37a76ed8c..bcfaf61a04 100644 --- a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ca.crt +++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ca.crt @@ -1,26 +1,69 @@ -Bag Attributes - friendlyName: localhost - localKeyID: 54 69 6D 65 20 31 34 37 37 32 37 36 33 32 32 32 32 35 -subject=/C=US/ST=MA/L=Westword/O=Red Hat/OU=Keycloak/CN=localhost -issuer=/C=US/ST=MA/L=Westword/O=Red Hat/OU=Keycloak/CN=localhost -----BEGIN CERTIFICATE----- -MIIDazCCAlOgAwIBAgIERfv3izANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJV -UzELMAkGA1UECBMCTUExETAPBgNVBAcTCFdlc3R3b3JkMRAwDgYDVQQKEwdSZWQg -SGF0MREwDwYDVQQLEwhLZXljbG9hazESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTE1 -MTIwNDA2NTExOFoXDTQ1MTEyNjA2NTExOFowZjELMAkGA1UEBhMCVVMxCzAJBgNV -BAgTAk1BMREwDwYDVQQHEwhXZXN0d29yZDEQMA4GA1UEChMHUmVkIEhhdDERMA8G -A1UECxMIS2V5Y2xvYWsxEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAIb7QEw18tpTIVoLUS8kpZaU84btm4nkbVrVNOxC -zsOVfhFGsc6kUamhHokvvOSWqHS+5FOTVWHPYrNTIwm1vodkqiy7xLCC8MWTrtU5 -RwcrCZ8Mwkm0EUCLCTY113j9egIg+Uj4nkQyTPGNliygf+ef3finzUfarc1lBAHD -+Z7cjrx4odtvQu88oGdhEXv5GoIno4bwkLRJKWWw9MRZGBxdTJlRGJ2hr0FVtNTw -sMvgR6ZeDosH8zNNLikLuwMAl7qxCgzppfmZCGKF2H/JLaXUo1oCIwdtCSSJufGJ -sa9cjdehroVIaiVaASQDKVUStoFz4kYrqUzOves4waJsRvcCAwEAAaMhMB8wHQYD -VR0OBBYEFFCfEXmWKTtaiZG7tCvBrmQiujrLMA0GCSqGSIb3DQEBCwUAA4IBAQAD -j/o+snjk/pydFLd3T6gr7k+ZWBi0gQKOOZ+xO9opblYMtG4bRm7wqsTyheUyeTQT -DZNXIFN4fgCcvHpEi+3M9XL8gySVsu7XzN49UT+KXavwISlbWyryZDH42L/MNCjG -Z8CD4IsyPAawgrC2Pc8NH8De5YqsGn2DId6R6xjFEumYtAEXXe3Wcp9T4G6yWSXO -s0rARNfE534Rvne7Gx18g/Lj0BBP7qh3bNeReRmHKpnRK/V90SJNOkpaFF4oAMQr -0pcZTJa4zoNcAoLHnwNBZmq43cPrffEOOMaCadiSSQ6bsJ0adZ+MSeJ1j4C9SrUn -M9ES3g9Wj9OcCsHzrTAm +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 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +SCc= -----END CERTIFICATE----- diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/intermediate-ca-3.crl b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/intermediate-ca-3.crl new file mode 100644 index 0000000000..5fadae53f0 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/intermediate-ca-3.crl @@ -0,0 +1,13 @@ +-----BEGIN X509 CRL----- +MIIB3zCByAIBATANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJVUzELMAkGA1UE +CAwCTUExEDAOBgNVBAoMB1JlZCBIYXQxETAPBgNVBAsMCEtleWNsb2FrMSMwIQYD +VQQDDBpLZXljbG9hayBJbnRlcm1lZGlhdGUgQ0EgMxcNMTkwMzIxMTgzNTQ3WhcN +MTkwNDIwMTgzNTQ3WqAwMC4wHwYDVR0jBBgwFoAUGFyPCYy3yxTPqAoIcfLf0fto +r9MwCwYDVR0UBAQCAhACMA0GCSqGSIb3DQEBCwUAA4IBAQDlowilp+jZ2OtWpwSa +bE2Cglm/6K8pNAzPROSv+yEc0aFUT+tm6yRWaITy+1hl5cLh2QAJk3E7gtXRhhXc +mf39U0k/WOSmcZCCUwgJuhudpeweFk7XOL3+Jr0npRTaWP583nZNVZQzFxHBZ5T/ +7H3rzjcuvYo9IzRroTNEaol19EgavrnAmG2jMM4Q/9YdYIBjxFJAigH/uGV51O0G +EV4PNDm+uCvfYQfa1GsGCszaUH2Ixpl9Jr2MP6LN5/w4dGzdq+yDz1srsfgFzVNj +cBz3lf1ogFKeRmLF2tTNJ1HLTsOImjvnZqRiD/3EkzLBMsptVFns+/Aq5MRo1pKJ +iNEi +-----END X509 CRL----- diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/intermediate-ca-invalid-signature.crl b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/intermediate-ca-invalid-signature.crl new file mode 100644 index 0000000000..7b03c91124 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/intermediate-ca-invalid-signature.crl @@ -0,0 +1,17 @@ +-----BEGIN X509 CRL----- +MIICsjCCAZoCAQEwDQYJKoZIhvcNAQELBQAwgYcxCzAJBgNVBAYTAlVTMQswCQYD +VQQIDAJNQTEQMA4GA1UECgwHUmVkIEhhdDERMA8GA1UECwwIS2V5Y2xvYWsxITAf +BgNVBAMMGEtleWNsb2FrIEludGVybWVkaWF0ZSBDQTEjMCEGCSqGSIb3DQEJARYU +Y29udGFjdEBrZXljbG9hay5vcmcXDTE5MDMyMDE5Mjk1MVoXDTE5MDQxOTE5Mjk1 +MVowFTATAgIQCRcNMTkwMzIwMTkyOTUxWqCBxjCBwzCBtAYDVR0jBIGsMIGpgBRw +AVgUGev7EtO+WNLtEcYGQYoadaGBjaSBijCBhzELMAkGA1UEBhMCVVMxCzAJBgNV +BAgMAk1BMRAwDgYDVQQKDAdSZWQgSGF0MREwDwYDVQQLDAhLZXljbG9hazEhMB8G +A1UEAwwYS2V5Y2xvYWsgSW50ZXJtZWRpYXRlIENBMSMwIQYJKoZIhvcNAQkBFhRj +b250YWN0QGtleWNsb2FrLm9yZ4IBATAKBgNVHRQEAwIBATANBgkqhkiG9w0BAQsF +AAOCAQEAmurquuW//sYdX0WrAeBDXY81Y0riz9mYgmrYKXMANE2fpybzReYnI0WC +4JRzfKfdLYWbcJCxdKpsRyzjCh6YVuelKG4nRUHb32/j9XFwdTW1Kv+20PYsZ5co +doPRty9F7Bg06yV0TJ6bOeYiUx9sR0rXSNvMRp7/fT8PPOZodnU1rkFN9/7CI4tf +BxaxjTezklBJGRJ3Vj12kGu9D1NFP5k5qrAT2jZfkRx8LOZIkhgfOaYp+0OPN4Q6 +weWWX5ityl5iJWoovNd3cQzY/GtpouX8AzmiKfOiY/oQIyx6U8V0V2peNA1gZFtd +aHRBPWv6jH8p4KrqxKl+LtyBVl4JLg== +-----END X509 CRL----- diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/keycloak.truststore b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/keycloak.truststore index d4618681cb2ecc64950b24f75d0c9d583756c7c5..d81fa2ac369fd76fb7a80fcb2bf301bcdb04ebc8 100644 GIT binary patch delta 2266 zcmYk;X*iS%8wc?57_t|Jv9C$?r^Z;yWXqBvS(_q^LB^I)5*k~!v5($4)>mW68lh|< zr^z;!iflz1qJ=RbWIdfu*SXGz`@{d!{pJ4uu6uElXFHXbXMcHr83KU-Kd12#&ncAN z5ns0Ap33r6>v>$umm z4wg;tO@zb`NQ|lL!s+_>ZFixacAnT2*R2>Ss+*~dV4ITSQ1h-{mf`2V#jLAu-8)CA z$m$R8D%R;miM-PvAT*Vf!%~e(R`08y*WIxFEI%fDWvYCml4lm4gm zoHSfla;RC|Zg;cblA)TlL^yBHZkmM(s=Eg7FGdp5l$vvQuk`nzNUgmRKI;Mb3 zIak7}wWwoUS@=D`6>;iM?x|O<7r0l7L_cUpdhjGgm}G?=Y5A}{W6QZKPKX{K7*uD( zzgKq9&KY5vt{JV7G13F05mjk!bw%wA@6(Qn+pFlC_}qQLV7D>ug>?N$n9$WX^D?qt2HoE;9WXXr0f&(gU0kUd(Q(%qSibj zNt5qhlRH&Cq5b*Ud5v^f7Xdx`$%|!Wci;d;bxOOGDPo4HP4LR~;bC82z;K4qvwPt7 zXpQow7QfHbO31O}k(lB8Hp7QW&OMA&k>G9wDHP^Aa8jxnm28xt;K+MIx+Y6tIi+|# zu~B*+-|_ekec8!0O10$A&SP##g;IXX?tV77HgdX@!v1Wz1C0x#T+R`3=!G@B-DB zmi7`Ac|{jWCN~)jJ9z>@h_FoL+4P0dAh>MkDg0GF{N1>kyFea|q^)=E z4L`A_Z)M{U-Ffv9fC~vG=sV)j3p>_n7YhzOeVW~WKeJ?Y!_;Gc` z91SQ4hle@yOYp5g)mz`T`ZR6{R?xbzdsv;;GU)RCkX(C#71?BhgOmo0RgMKVMFdq! zC^dZSb_E&Bh}%8KO3-+wcQ%F=y&_$bS@I~CR?NS;nIlvkZ;?B^VWw9)r8UxJ`$p zJ=qnpW(gSKykXo(Jr_UXc)W7yh&XxT#81tT;NP@qfjXep?+AMQmW_>Ux(O)A$%C>#4;jkJ zBZmGhsm!_mES51gB)yE0Cw#~6RBPar!2!3oh9lw%{tFS#>$J~<)Px@u#Y@wV z=pGs=$=P02w84_X;N7`V+D>$Kc-)$~-4k{Mak%W$v(Yc5xKt0Iv4K^r*{bQ$Q-kHu z{%&`*g^YPaH{ok|gnCSzi|N8mn>(ks^AWurZ_d9jF(98u~6sg%&j4Ad{4Gm-(HK`HE^xD_(BSsZQ8Rz#DkHL#HP9T z!51av`id|3m-12U`qSUER{Z~xn`K_*9c=C7>_i1=XtDcl>xeouRbZ!6BC=C~Vq$K6 z;B!_mzg>-2eOSVPu55JppXQr@#{X%)h;uq;N)rt>T+YRDef9mT9q-x^vcxu^LpYRO zWv$S#nOPa-k9hbRJjykIi-tAF#beCjOdp5dgX@-q*zlbpm-~1YmgRnq%EjYB4;wgJ z8H7B8-1#xf(Xo^-pZjWC67`Idp$EJ3PKEmrmpxvg9*3)W!q|pW8P)v7`D1xr@c5_1n!_Zjy;gX9}_t{{av za$k}aiyK# z8dJB$Z!s=icl7-eNs{{W1#|o4TR8sdm4w=A-_M(3ca?4%x#!E*K&20tD3@saiM1Il zxeuT-Ua34VQ;D-#QjDYSHMDWm6!J{&QZSL-j{fZ{wjp~sKbg(YwSe!qAw?o4I&3{eb4E*DBc^4P@CiMOm81WTH&q zaq%1X;6D6?N%S@Or6CvDiQO~4pSLH8V`gvXb|jOI5>w@7JRuNj@UM hD%$jT4v@mY{DRJTg=1_r3o(vf6!g}|iKLydzW^uPE%X2Y delta 81 zcmV-X0IvV~jSc7b3=ID5{_Ow&00IC20J5lv_5Yus3eBoMW)mYe diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/pki/root/ca/newcerts/intermediate-ca-3.crt b/testsuite/integration-arquillian/servers/auth-server/jboss/common/pki/root/ca/newcerts/intermediate-ca-3.crt new file mode 100644 index 0000000000..a555defcfd --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/pki/root/ca/newcerts/intermediate-ca-3.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEsTCCApmgAwIBAgICC7kwDQYJKoZIhvcNAQELBQAwgYsxCzAJBgNVBAYTAlVT +MQswCQYDVQQIDAJNQTEPMA0GA1UEBwwGQm9zdG9uMRAwDgYDVQQKDAdSZWQgSGF0 +MREwDwYDVQQLDAhLZXljbG9hazEUMBIGA1UEAwwLS2V5Y2xvYWsgQ0ExIzAhBgkq +hkiG9w0BCQEWFGNvbnRhY3RAa2V5Y2xvYWsub3JnMB4XDTE5MDMyMTE4MTk1MFoX +DTQ2MDgwNjE4MTk1MFowZDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1BMRAwDgYD +VQQKDAdSZWQgSGF0MREwDwYDVQQLDAhLZXljbG9hazEjMCEGA1UEAwwaS2V5Y2xv +YWsgSW50ZXJtZWRpYXRlIENBIDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQD1afusL2MqMMxg20HTGyVt5nxc9MvpzU9yWhKyper9hh/iEDkH06SZ+vIn +UWGTeRfGmoA3W+IGF4TzSFO2BjOLz6bowNTqpWONZAK0swMauE23sbxA7XfKxmIt +5pDkQWAWb2kzLoKEXkbmdlF+O/qcM8i+1U7fpg1NjQI1DHvBVMvul/aGyzP6q/Hj +NEg+7o3y21T/3gq/Yma9L+awX1wLLxtDlSGjP1Q2C19w87ijUIwFUo7AzTOn03SI +t+Pfc1cdIsmjUG2lDqJTyo/VuqmfWWBMm6p+Ya/Z4Nipk87nxoW60y4EjRL7vxx9 +vyWiRUhP/2pEE5y9LQ0uzxxA4kIPAgMBAAGjRTBDMB0GA1UdDgQWBBQYXI8JjLfL +FM+oCghx8t/R+2iv0zASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIB +hjANBgkqhkiG9w0BAQsFAAOCAgEAB5SrTyWz95GqgG0zrtIwJArMY4EUuISGWEgX +8GpVyP9xTM1he/vOXqFpAQHtygiq5yhyrrMJuu5/m0Ca5NZM1NaM6unJr7mIPEKK +BP/85Fue/gBe/Q0Vx1VnmxZhszqQT4hLIfR8anLXgK9w8E6lB4HAXgK+VUzzxXLE +43oc8/L9swEnvCKsLHKynexNxKRIZD3GPmLCciOz+101Fb6a0Nc8+A+5soiQi8gC +/K9ygXvxqmHtnrTndZmrtJFOaeXwMDAw7q3j25MkRt4fBPcKX0soBuI6/cTE2veH +GPYXW4QbVswrhJVD/vVuU0VFR1fLnuw9NgDkGz0qoi20x/Ew3HiVi++9EU9YsPgR +StcfsCTfth2TLsPmSFSKeGMR3e+Hr6xr6fcahSl6QmKcI60EHw+kKqQ3bBq7QOxi +zEuQ0SunRyuEUewnhMT9s7sIrZ4M2fyiWH3GVm2971J0/ey3NEYSq/Y9fAO6+9xG +Wlvaps8X5GLs/XYt9HwZNtdb9F3YMNSFi10WBIKqdpppLT9uuIDSK5S+s5chuych +y2TZiCBwKBt1lRdqEZ6J4vMj7M5eIdj8Pmvp+tmL1kewuTfKaU+2Bpy6l42sYkyM +jy7Z0XyMITIBnAf9SmElQPUjq+IIYL6HK/i8mI15AHHIiwm1eaUdbvBKJnQS0++b +0iwvQA4= +-----END CERTIFICATE----- diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/pki/root/ca/newcerts/intermediate-ca-3.key b/testsuite/integration-arquillian/servers/auth-server/jboss/common/pki/root/ca/newcerts/intermediate-ca-3.key new file mode 100644 index 0000000000..30de8863c7 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/pki/root/ca/newcerts/intermediate-ca-3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA9Wn7rC9jKjDMYNtB0xslbeZ8XPTL6c1PcloSsqXq/YYf4hA5 +B9OkmfryJ1Fhk3kXxpqAN1viBheE80hTtgYzi8+m6MDU6qVjjWQCtLMDGrhNt7G8 +QO13ysZiLeaQ5EFgFm9pMy6ChF5G5nZRfjv6nDPIvtVO36YNTY0CNQx7wVTL7pf2 +hssz+qvx4zRIPu6N8ttU/94Kv2JmvS/msF9cCy8bQ5Uhoz9UNgtfcPO4o1CMBVKO +wM0zp9N0iLfj33NXHSLJo1BtpQ6iU8qP1bqpn1lgTJuqfmGv2eDYqZPO58aFutMu +BI0S+78cfb8lokVIT/9qRBOcvS0NLs8cQOJCDwIDAQABAoIBAQDMV7AH4fk3AyTa +LRa1GbBjvvukRuyXQ624MInLGN3+tTRM/mcOPkqbL9l7pYaSzcxfQPwrnCUqH2FD +VODm+mjnLEL1IMLokke/Thv2q+uUzwtfPe3bPh91xxOu1oGknU7Nv3yf8kUYxItS +kAgxDO4SLAgl5eTj0hbXkObalwdgpIIpobuiZUtJYRT1kJmmWJS8TDQirEyh298D +RdLHFB2wFgYkg9EjATM4tnv66YqPlW9YCkt1IlhqNyn+Ui8vo5shNXq1Q8x3p7he +ZdCqGL3Ddg9e9tZnOcFPCNQhFVIUV3T5e9SsPVimsJBiOzEzL0eYZ+8KyJogYeOq +yEPjqu0BAoGBAP4rGb1PE5xOg6WJ/PCXlFnCDpEqkXoZbDOPZn7cUl2OvzlIciTl +VgWt1HXq50zVfu0FHZx8PdmCj5av4wMmmcWvu7HZGTm4TErJ777M6c9IS/fKyL25 +XBzw+HOcvCwq+pMTxKNZxAlNlTW0k9GwJeajXlzFyTDO5pTMiLrlJEOPAoGBAPcu +u283/CJMKE5YDgecYgwfaznX+Jk7jkiJpxfWWGYJUE3xueFROeqcwSI3WqRKm48N +hb9DC6yFtgO3uC7zUZFxOltkkkWSSB1PNmjLQl+y7E/UqrDEehIT/MhKXxUW9z4S +ADMZTZT0qeTlbBuwx6CAdnCAATRsW9peOziiKNmBAoGBAOLKtqbzHn6EmHdnjyln +N9p3i+QAZdrbQG8pb72W/m+45exJNoCxmnZqy3+EYWtvvVflDq0JN28UTueYfinb +ka6RxhtFqnqUdo7tbV2FHsP0sMSkT0brVMQGSMtweX+3werm4rkXahMbBR7syFF8 +qfUIpTSGz6UbmSgA8ahCun8FAoGARLJiOUjP9CBCW3Oxgn/95+ybeloBp2Sb6KEJ +JWDW9JTGEsOJq4tNk1y5eG717A8oKJvTfhJ+HhaTPXlD4RiSpN9ZHqlW1asQC8VG +E93ZtosdjhpGzhXs7zVK3cd9oXjegguyroDrxOgyh4ETiKaa9Ip/YEjTDOTIqmnh +/51hyQECgYBJ3KwFfHlm77betQz9lvfU5uRBHjoIdor59tTXmvQ9ffdQtIuMN84w +2CbBuXrAD1hT2HRazfUncjI2Fj+pXD5+Hu42I31WxwNTdo/POznYu58MLog9RNGv +Avn456Fw8UmSkxm+zGnVJND9/0761/gOcabnIIm6KjI6WeACD1wJFA== +-----END RSA PRIVATE KEY----- diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/pki/root/ca/openssl.cnf b/testsuite/integration-arquillian/servers/auth-server/jboss/common/pki/root/ca/openssl.cnf index b596754b01..91de563cdc 100644 --- a/testsuite/integration-arquillian/servers/auth-server/jboss/common/pki/root/ca/openssl.cnf +++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/pki/root/ca/openssl.cnf @@ -94,7 +94,7 @@ keyUsage = critical, digitalSignature, cRLSign, keyCertSign [ v3_intermediate_ca ] # Extensions for a typical intermediate CA (`man x509v3_config`). subjectKeyIdentifier = hash -authorityKeyIdentifier = keyid:always,issuer +#authorityKeyIdentifier = keyid:always,issuer basicConstraints = critical, CA:true, pathlen:0 keyUsage = critical, digitalSignature, cRLSign, keyCertSign @@ -104,7 +104,7 @@ basicConstraints = CA:FALSE nsCertType = client, email nsComment = "OpenSSL Generated Client Certificate" subjectKeyIdentifier = hash -authorityKeyIdentifier = keyid,issuer +#authorityKeyIdentifier = keyid,issuer keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment extendedKeyUsage = clientAuth, emailProtection diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml index 36830e97c5..2b9b5e62dc 100644 --- a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml @@ -190,8 +190,7 @@ ca.crt client.crt client.key - intermediate-ca.crl - empty.crl + *.crl other_client.jks diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/AbstractX509AuthenticationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/AbstractX509AuthenticationTest.java index 1f7f9f5516..5ff3ff454f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/AbstractX509AuthenticationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/AbstractX509AuthenticationTest.java @@ -92,6 +92,8 @@ public abstract class AbstractX509AuthenticationTest extends AbstractTestRealmKe public static final String EMPTY_CRL_PATH = "empty.crl"; public static final String INTERMEDIATE_CA_CRL_PATH = "intermediate-ca.crl"; + public static final String INTERMEDIATE_CA_INVALID_SIGNATURE_CRL_PATH = "intermediate-ca-invalid-signature.crl"; + public static final String INTERMEDIATE_CA_3_CRL_PATH = "intermediate-ca-3.crl"; protected final Logger log = Logger.getLogger(this.getClass()); static final String REQUIRED = "REQUIRED"; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/CRLRule.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/CRLRule.java index 05af98fc4d..a6352dbf48 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/CRLRule.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/CRLRule.java @@ -58,6 +58,8 @@ public class CRLRule extends ExternalResource { PathHandler pathHandler = new PathHandler(); pathHandler.addExactPath(AbstractX509AuthenticationTest.EMPTY_CRL_PATH, new CRLHandler(AbstractX509AuthenticationTest.EMPTY_CRL_PATH)); pathHandler.addExactPath(AbstractX509AuthenticationTest.INTERMEDIATE_CA_CRL_PATH, new CRLHandler(AbstractX509AuthenticationTest.INTERMEDIATE_CA_CRL_PATH)); + pathHandler.addExactPath(AbstractX509AuthenticationTest.INTERMEDIATE_CA_INVALID_SIGNATURE_CRL_PATH, new CRLHandler(AbstractX509AuthenticationTest.INTERMEDIATE_CA_INVALID_SIGNATURE_CRL_PATH)); + pathHandler.addExactPath(AbstractX509AuthenticationTest.INTERMEDIATE_CA_3_CRL_PATH, new CRLHandler(AbstractX509AuthenticationTest.INTERMEDIATE_CA_3_CRL_PATH)); crlResponder = Undertow.builder().addHttpListener(CRL_RESPONDER_PORT, CRL_RESPONDER_HOST) .setHandler( diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509BrowserCRLTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509BrowserCRLTest.java index 50a0b3ddec..ab35aaa6d5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509BrowserCRLTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509BrowserCRLTest.java @@ -30,6 +30,7 @@ import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.util.ContainerAssume; import org.keycloak.testsuite.util.PhantomJSBrowser; +import org.keycloak.testsuite.util.WaitUtils; import org.openqa.selenium.WebDriver; import static org.hamcrest.Matchers.containsString; @@ -121,6 +122,42 @@ public class X509BrowserCRLTest extends AbstractX509AuthenticationTest { } + @Test + public void loginFailedWithInvalidSignatureCRL() { + X509AuthenticatorConfigModel config = + new X509AuthenticatorConfigModel() + .setCRLEnabled(true) + .setCRLRelativePath(CRLRule.CRL_RESPONDER_ORIGIN + "/" + INTERMEDIATE_CA_INVALID_SIGNATURE_CRL_PATH) + .setConfirmationPageAllowed(true) + .setMappingSourceType(SUBJECTDN_EMAIL) + .setUserIdentityMapperType(USERNAME_EMAIL); + AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", config.getConfig()); + String cfgId = createConfig(browserExecution.getId(), cfg); + Assert.assertNotNull(cfgId); + + // Verify there is an error message because of invalid CRL signature + assertLoginFailedWithExpectedX509Error("Certificate validation's failed.\nSignature length not correct"); + } + + + @Test + public void loginSuccessWithCRLSignedWithIntermediateCA3FromTruststore() { + X509AuthenticatorConfigModel config = + new X509AuthenticatorConfigModel() + .setCRLEnabled(true) + .setCRLRelativePath(CRLRule.CRL_RESPONDER_ORIGIN + "/" + INTERMEDIATE_CA_3_CRL_PATH) + .setConfirmationPageAllowed(true) + .setMappingSourceType(SUBJECTDN_EMAIL) + .setUserIdentityMapperType(USERNAME_EMAIL); + AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", config.getConfig()); + String cfgId = createConfig(browserExecution.getId(), cfg); + Assert.assertNotNull(cfgId); + + // Verify there is an error message because of invalid CRL signature + x509BrowserLogin(config, userId, "test-user@localhost", "test-user@localhost"); + } + + @Test public void loginWithMultipleRevocationLists() { X509AuthenticatorConfigModel config = @@ -157,13 +194,17 @@ public class X509BrowserCRLTest extends AbstractX509AuthenticationTest { private void assertLoginFailedDueRevokedCertificate() { + assertLoginFailedWithExpectedX509Error("Certificate validation's failed.\nCertificate has been revoked, certificate's subject:"); + } + + private void assertLoginFailedWithExpectedX509Error(String expectedError) { loginConfirmationPage.open(); loginPage.assertCurrent(); // Verify there is an error message Assert.assertNotNull(loginPage.getError()); - Assert.assertThat(loginPage.getError(), containsString("Certificate validation's failed.\nCertificate has been revoked, certificate's subject:")); + Assert.assertThat(loginPage.getError(), containsString(expectedError)); // Continue with form based login loginPage.login("test-user@localhost", "password"); @@ -177,5 +218,4 @@ public class X509BrowserCRLTest extends AbstractX509AuthenticationTest { .removeDetail(Details.REDIRECT_URI) .assertEvent(); } - } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/keystore/keycloak.truststore b/testsuite/integration-arquillian/tests/base/src/test/resources/keystore/keycloak.truststore index da0f709f5a7e85c53c4d205b8ed72443d9a78172..67b00f86300c39f5957a3fc9a930872a5fdbbd56 100644 GIT binary patch delta 2723 zcmd6oc{J2*8^>p}&L9j!WSb=0_-PtTnQYNyNtUD(#!#lRG^S*0G=z*LCeJ)1uWV(D zsBBT5XR-~c$eN`gS{M_e$NIFN=RN2BCAJd>7y?E=%K!0rbcovb zeX-yK2#S_roh)XI0t$!M`l+!UioF@Bm>fQU}?{7hG3ds(YU*K2A#tywim zGk@1Mm3DH-`%w=YSNi%@(E>LoUTpX^h@+h-p>$8vezf$lBXKN8C;f<7*un?pw788l z|4O-%_0ajZrf(NAl1WVoA^R#;n`@*8b>7ry?4j)z7GKg^!3-9)$&aZy=03U~8fDPw ze)~5rZqtVax>f2>Y0Hsf7E%4;{x*jvdvv6q8bxi`-XET)COk+bSU&MiZKOX}yOqnl z67PTKsJPekHg#O${(Pvxl#yj|_NM?(?5;oHgU>w=!e?{jUmHgHAg)AMrG;*)eZ4&5 zB(kbNyVTdwiQ&bLY8^7n=;m9`Tgh~{wy0w&4Rq`D+^ulLKU< z{^JGKSt+mzi-ClSu~L@70yGF1x}o#T&PB24dd|W8fh8W#8BL?fFt*D%w;Igs z?YP&^vVNl33Cx)L%6>qHzv|QVmb%Vi8k+2XDaj~nn_U#*9-F%hp^>P;rmiI0$rXp> z!#9K;KD^d`GoeMju=->^7k4p-a0Xx}uRgKIon_~dK=O|bhJ#0@U47yYo~s;d#hUaW zwVn$O$|uEK3r9!dhvlNfgIFVUOwsXiNm50ga1|aT>_gzc555qjbK&FXHoXhdMVuz$ z8quh(5H@p@n&~1%n^jFXq@;(0s)3P%3_v?Sw@h5n`Nq{5Y*@>-d|NW#@0uIu(yVfR zYX03!PM+l4Qie=vtW9Ru;y#mtL4)olrx%DMk#0be)+9W@R*fp`+G5lkS#V{5;v4+z z7As%Mqt~=%x|Q$%ey$1X*WRcmP`Y=RFR&{5VKvrYbxny@7MvGcwM^YB`h_SM9_#f$H$rD!PVw?2szhr^G{mpk^`u|6n|eIGLh>0h4{MIZ_Hop zSoy_#*nNNwy9>}E8Q+-?f!F^3!QL3a?$DRh|H5}1@D=t3fDvHuqXc}usmSh|ivBC} ze09t>^KAZQp8fyW(-#dfpV85@Pvn(SJ(3pC;WcF5t)T8d8F6fZ^RCl@a#X6a;Qd;x zGIx`)PPp9H}n!kcI=93E>imV;d` z`8d9lu!om4H{`N0BKpYtM1$UBh37H2o`Pk%YM!Z!0o*faz9#QrqJUHK<4)|YZfbn> z_-OFqEoyCf2mK4gbDi5$Ue*=;>xh@#_2P@f@`17bBIeVn=F4^67+8|j$|J?dN9ws& z1c!}Nd!@4)bj2`Tie_9b3yJU4vH~psK`pssDI$s09AwvRQQ@<5qt93T%DY)?0|@%x zp(J#z#6i7kDYZDtAA9EouvN5!bO~7#6YEAm@lPFU-8gI4Neo|k>T#0{2G6t$7f)@c z+^G_&ZBlU6xq2>#9S%t?EL= zDN0{dc@Y0VfzYML9+1XEcJogt>~UI{;{4oN6xmQ-qg8_l6$EK$Zz-n0PxDjhnQQfN0ebp zqMo+f1-ZM{*zCcjIJrfXdyIl>hjvW5<@%=pXDh3HhdJ5PcmvRc0$wXumv*n^Bk<}% zI%Bp$(!s6bv!qR*a>drs2V0n!KPL$;2^UC`!?SVarKjI5ZN97-X5pQME`upcki^ZMu4=wN%L>J_1^| z^2++C`(R-gYQlSZ?Y!TR^*v28ANZZC9Z++OI+l(K01-d9X=LBi}Q WlQ#2Rw4M~FxVr0%?)AzChw1^s{T%oJ