Fixing UserFederationLdapConnectionTest,LDAPUserLoginTest to work with FIPS (#15299)

closes #14965
This commit is contained in:
Marek Posolda 2022-11-03 16:35:57 +01:00 committed by GitHub
parent 2ba5ca3c5f
commit f616495b05
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 164 additions and 29 deletions

View file

@ -325,7 +325,7 @@ jobs:
declare -A PARAMS TESTGROUP
PARAMS["bcfips-nonapproved-pkcs12"]="-Pauth-server-quarkus,auth-server-fips140-2"
# Tests in the package "forms" and some keystore related tests
TESTGROUP["group1"]="-Dtest=org.keycloak.testsuite.forms.**,ClientAuthSignedJWTTest,CredentialsTest,JavaKeystoreKeyProviderTest,ServerInfoTest"
TESTGROUP["group1"]="-Dtest=org.keycloak.testsuite.forms.**,ClientAuthSignedJWTTest,CredentialsTest,JavaKeystoreKeyProviderTest,ServerInfoTest,UserFederationLdapConnectionTest,LDAPUserLoginTest"
./mvnw clean install -nsu -B ${PARAMS["${{ matrix.server }}"]} ${TESTGROUP["${{ matrix.tests }}"]} -f testsuite/integration-arquillian/tests/base/pom.xml | misc/log/trimmer.sh

View file

@ -15,11 +15,13 @@ import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CollectionCertStoreParameters;
import java.security.spec.ECParameterSpec;
import java.util.function.Supplier;
import java.util.stream.Stream;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKeyFactory;
import javax.net.ssl.SSLSocketFactory;
import org.keycloak.common.util.KeystoreUtil.KeystoreFormat;
@ -113,4 +115,13 @@ public interface CryptoProvider {
Signature getSignature(String sigAlgName) throws NoSuchAlgorithmException, NoSuchProviderException;
/**
* Wrap given SSLSocketFactory and decorate it with some additional functionality.
*
* This method is used in the context of truststore (where Keycloak is SSL client)
*
* @param delegate The original factory to wrap. Usually default java SSLSocketFactory
* @return decorated factory
*/
SSLSocketFactory wrapFactoryForTruststore(SSLSocketFactory delegate);
}

View file

@ -20,6 +20,7 @@ package org.keycloak.common.util;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.regex.Pattern;
@ -47,6 +48,16 @@ public class UriUtils {
return originPattern.matcher(url).matches();
}
public static String getHost(String uri) {
try {
if (uri == null) return null;
URI url = new URI(uri);
return url.getHost();
} catch (URISyntaxException uriSyntaxException) {
throw new IllegalArgumentException("URI '" + uri + "' is not valid.");
}
}
public static MultivaluedHashMap<String, String> decodeQueryString(String queryString) {
MultivaluedHashMap<String, String> map = new MultivaluedHashMap<String, String>();
if (queryString == null || queryString.equals("")) return map;

View file

@ -18,10 +18,12 @@ import java.security.cert.CollectionCertStoreParameters;
import java.security.spec.ECParameterSpec;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKeyFactory;
import javax.net.ssl.SSLSocketFactory;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
@ -176,4 +178,8 @@ public class DefaultCryptoProvider implements CryptoProvider {
}
@Override
public SSLSocketFactory wrapFactoryForTruststore(SSLSocketFactory delegate) {
return delegate;
}
}

View file

@ -34,10 +34,12 @@ import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKeyFactory;
import javax.net.ssl.SSLSocketFactory;
import org.keycloak.common.crypto.CertificateUtilsProvider;
import org.keycloak.common.crypto.CryptoConstants;
@ -167,4 +169,9 @@ public class WildFlyElytronProvider implements CryptoProvider {
return Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(sigAlgName));
}
@Override
public SSLSocketFactory wrapFactoryForTruststore(SSLSocketFactory delegate) {
return delegate;
}
}

View file

@ -49,6 +49,10 @@
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi-private</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>

View file

@ -1,5 +1,6 @@
package org.keycloak.crypto.fips;
import java.net.Socket;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyFactory;
import java.security.KeyPairGenerator;
@ -22,12 +23,17 @@ import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CollectionCertStoreParameters;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKeyFactory;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import org.bouncycastle.asn1.x9.ECNamedCurveTable;
import org.bouncycastle.asn1.x9.X9ECParameters;
@ -35,7 +41,9 @@ import org.bouncycastle.crypto.fips.FipsRSA;
import org.bouncycastle.crypto.fips.FipsSHS;
import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.bouncycastle.jsse.util.CustomSSLSocketFactory;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.util.IPAddress;
import org.jboss.logging.Logger;
import org.keycloak.common.crypto.CryptoProvider;
import org.keycloak.common.crypto.ECDSACryptoProvider;
@ -45,7 +53,10 @@ import org.keycloak.common.crypto.PemUtilsProvider;
import org.keycloak.common.crypto.UserIdentityExtractorProvider;
import org.keycloak.common.util.BouncyIntegration;
import org.keycloak.common.util.KeystoreUtil.KeystoreFormat;
import org.keycloak.common.util.Resteasy;
import org.keycloak.crypto.JavaAlgorithm;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
/**
@ -209,4 +220,46 @@ public class FIPS1402Provider implements CryptoProvider {
return Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(sigAlgName), BouncyIntegration.PROVIDER);
}
@Override
public SSLSocketFactory wrapFactoryForTruststore(SSLSocketFactory delegate) {
KeycloakSession session = Resteasy.getProvider().getContextData(KeycloakSession.class);
if (session == null) {
log.tracef("Not found keycloakSession in the resteasy context when trying to retrieve hostname attribute from it");
return delegate;
}
String hostname = session.getAttribute(Constants.SSL_SERVER_HOST_ATTR, String.class);
log.tracef("Found hostname '%s' to be used by SSLSocketFactory", hostname);
if (hostname == null) return delegate;
// See https://downloads.bouncycastle.org/fips-java/BC-FJA-(D)TLSUserGuide-1.0.9.pdf - Section 3.5.2 (Endpoint identification)
return new CustomSSLSocketFactory(delegate) {
@Override
protected Socket configureSocket(Socket s) {
if (s instanceof SSLSocket) {
SSLSocket ssl = (SSLSocket)s;
SNIHostName sniHostName = getSNIHostName(hostname);
if (sniHostName != null) {
SSLParameters sslParameters = new SSLParameters();
sslParameters.setServerNames(Collections.singletonList(sniHostName));
ssl.setSSLParameters(sslParameters);
}
}
return s;
}
private SNIHostName getSNIHostName(String host) {
if (!IPAddress.isValid(host)) {
try {
return new SNIHostName(host);
} catch (RuntimeException e) {
log.warnf(e, "Not possible to create SNIHostName from the host '%s'", host);
}
}
return null;
}
};
}
}

View file

@ -31,8 +31,12 @@ import java.util.stream.Collectors;
import javax.naming.directory.SearchControls;
import org.jboss.logging.Logger;
import org.keycloak.common.util.UriUtils;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
@ -57,6 +61,8 @@ import org.keycloak.storage.ldap.mappers.membership.MembershipType;
*/
public class LDAPUtils {
private static final Logger log = Logger.getLogger(LDAPUtils.class);
/**
* Method to crate a user in the LDAP. The user will be created when all
* mandatory attributes specified by the mappers are set. The method
@ -374,4 +380,10 @@ public class LDAPUtils {
return userModelProperties;
}
public static void setLDAPHostnameToKeycloakSession(KeycloakSession session,LDAPConfig ldapConfig) {
String hostname = UriUtils.getHost(ldapConfig.getConnectionUrl());
session.setAttribute(Constants.SSL_SERVER_HOST_ATTR, hostname);
log.tracef("Setting LDAP server hostname '%s' as KeycloakSession attribute", hostname);
}
}

View file

@ -4,6 +4,7 @@ import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.truststore.TruststoreProvider;
import org.keycloak.vault.VaultCharSecret;
@ -66,6 +67,8 @@ public final class LDAPContextManager implements AutoCloseable {
}
private void createLdapContext() throws NamingException {
LDAPUtils.setLDAPHostnameToKeycloakSession(session, ldapConfig);
Hashtable<Object, Object> connProp = getConnectionProperties(ldapConfig);
if (!LDAPConstants.AUTH_TYPE_NONE.equals(ldapConfig.getAuthType())) {

View file

@ -23,6 +23,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.store.ldap.extended.PasswordModifyRequest;
@ -496,6 +497,7 @@ public class LDAPOperationManager {
StartTlsResponse tlsResponse = null;
try {
LDAPUtils.setLDAPHostnameToKeycloakSession(session, config);
Hashtable<Object, Object> env = LDAPContextManager.getNonAuthConnectionProperties(config);

View file

@ -82,6 +82,8 @@ public final class LdapMapContextManager implements AutoCloseable {
}
private void createLdapContext() throws NamingException {
LdapMapUtil.setLDAPHostnameToKeycloakSession(session, ldapMapConfig);
Hashtable<Object, Object> connProp = getConnectionProperties(ldapMapConfig);
if (!LDAPConstants.AUTH_TYPE_NONE.equals(ldapMapConfig.getAuthType())) {

View file

@ -390,6 +390,7 @@ public class LdapMapOperationManager implements AutoCloseable {
StartTlsResponse tlsResponse = null;
try {
LdapMapUtil.setLDAPHostnameToKeycloakSession(session, config);
Hashtable<Object, Object> env = LdapMapContextManager.getNonAuthConnectionProperties(config);

View file

@ -17,7 +17,12 @@
package org.keycloak.models.map.storage.ldap.store;
import org.jboss.logging.Logger;
import org.keycloak.common.util.UriUtils;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.map.storage.ldap.config.LdapMapConfig;
import java.text.SimpleDateFormat;
import java.util.Date;
@ -30,6 +35,8 @@ import java.util.TimeZone;
*/
public class LdapMapUtil {
private static final Logger logger = Logger.getLogger(LdapMapUtil.class);
/**
* <p>Formats the given date.</p>
*
@ -250,5 +257,11 @@ public class LdapMapUtil {
}
}
public static void setLDAPHostnameToKeycloakSession(KeycloakSession session, LdapMapConfig ldapConfig) {
String hostname = UriUtils.getHost(ldapConfig.getConnectionUrl());
session.setAttribute(Constants.SSL_SERVER_HOST_ATTR, hostname);
logger.tracef("Setting LDAP server hostname '%s' as KeycloakSession attribute", hostname);
}
}

View file

@ -146,4 +146,9 @@ public final class Constants {
public static final Boolean REALM_ATTR_USERNAME_CASE_SENSITIVE_DEFAULT = Boolean.FALSE;
public static final String REALM_ATTR_USERNAME_CASE_SENSITIVE = "keycloak.username-search.case-sensitive";
/**
* Attribute of KeycloakSession where the hostname of the SSL server can be saved. This can be used by org.keycloak.truststore.SSLSocketFactory
*/
public static final String SSL_SERVER_HOST_ATTR = "sslServerHost";
}

View file

@ -181,8 +181,16 @@ public class FileTruststoreProviderFactory implements TruststoreProviderFactory
enumeration = truststore.aliases();
log.trace("Checking " + truststore.size() + " entries from the truststore.");
while(enumeration.hasMoreElements()) {
String alias = (String)enumeration.nextElement();
readTruststoreEntry(truststore, alias);
}
} catch (KeyStoreException e) {
log.error("Error while reading Keycloak truststore "+e.getMessage(),e);
}
}
private void readTruststoreEntry(KeyStore truststore, String alias) {
try {
Certificate certificate = truststore.getCertificate(alias);
if (certificate instanceof X509Certificate) {
@ -190,24 +198,16 @@ public class FileTruststoreProviderFactory implements TruststoreProviderFactory
if (isSelfSigned(cax509cert)) {
X500Principal principal = cax509cert.getSubjectX500Principal();
trustedRootCerts.put(principal, cax509cert);
log.debug("Trusted root CA found in trustore : alias : "+alias + " | Subject DN : " + principal);
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);
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);
log.info("Skipping certificate with alias [" + alias + "] from truststore, because it's not an X509Certificate");
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | NoSuchProviderException e) {
log.warnf("Error while reading Keycloak truststore entry [%s]. Exception message: %s", alias, e.getMessage(), e);
}
}

View file

@ -18,6 +18,7 @@
package org.keycloak.truststore;
import org.jboss.logging.Logger;
import org.keycloak.common.crypto.CryptoIntegration;
import java.io.IOException;
import java.net.InetAddress;
@ -30,9 +31,11 @@ import java.util.Comparator;
* <p>
* This SSLSocketFactory can only use truststore configured by TruststoreProvider after the ProviderFactory was
* initialized using standard Spi load / init mechanism. That will only happen if "truststore" provider is configured
* in standalone.xml or domain.xml.
* by the Keycloak Provider SPI configuration mechanism
* <p>
* If TruststoreProvider is not available this SSLSocketFactory will delegate all operations to javax.net.ssl.SSLSocketFactory.getDefault().
* If TruststoreProvider is not available this SSLSocketFactory will delegate all operations to the SSLSocketFactory
* returned by {@link org.keycloak.common.crypto.CryptoProvider#wrapFactoryForTruststore(javax.net.ssl.SSLSocketFactory)},
* which will delegate further to the factory returned by javax.net.ssl.SSLSocketFactory.getDefault().
*
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@ -58,7 +61,7 @@ public class SSLSocketFactory extends javax.net.ssl.SSLSocketFactory implements
sf = (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault();
}
sslsf = sf;
sslsf = CryptoIntegration.getProvider().wrapFactoryForTruststore(sf);
}
public static synchronized SSLSocketFactory getDefault() {

View file

@ -302,6 +302,7 @@
"file": "${keycloak.truststore.file:target/dependency/keystore/keycloak.truststore}",
"password": "${keycloak.truststore.password:secret}",
"hostname-verification-policy": "${keycloak.truststore.policy:WILDCARD}",
"type": "${keycloak.truststore.type:}",
"disabled": "${keycloak.truststore.disabled:false}"
}
},

View file

@ -29,7 +29,6 @@ import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.server.core.api.DirectoryService;
import org.apache.directory.server.core.api.interceptor.Interceptor;
import org.apache.directory.server.core.api.partition.Partition;
import org.apache.directory.server.core.factory.AvlPartitionFactory;
import org.apache.directory.server.core.factory.DefaultDirectoryServiceFactory;
import org.apache.directory.server.core.factory.JdbmPartitionFactory;
import org.apache.directory.server.core.normalization.NormalizationInterceptor;
@ -45,6 +44,7 @@ import org.keycloak.common.util.StreamUtil;
import java.io.File;
import java.io.InputStream;
import java.security.KeyStore;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -175,7 +175,8 @@ public class LDAPEmbeddedServer {
public void init() throws Exception {
log.info("Creating LDAP Directory Service. Config: baseDN=" + baseDN + ", bindHost=" + bindHost + ", bindPort=" + bindPort +
", ldapSaslPrincipal=" + ldapSaslPrincipal + ", directoryServiceFactory=" + directoryServiceFactory + ", ldif=" + ldifFile);
", ldapSaslPrincipal=" + ldapSaslPrincipal + ", directoryServiceFactory=" + directoryServiceFactory + ", ldif=" + ldifFile +
", enableSSL=" + enableSSL + ", enableStartTLS: " + enableStartTLS + ", keystoreFile: " + keystoreFile + ", default java keystore type: " + KeyStore.getDefaultType());
this.directoryService = createDirectoryService();