diff --git a/config/alfresco/subsystems/Authentication/common-ldap-context.xml b/config/alfresco/subsystems/Authentication/common-ldap-context.xml index 9098cb1603..744dc2b9fc 100644 --- a/config/alfresco/subsystems/Authentication/common-ldap-context.xml +++ b/config/alfresco/subsystems/Authentication/common-ldap-context.xml @@ -102,6 +102,17 @@ --> + + + ${ldap.authentication.truststore.path} + + + ${ldap.authentication.truststore.type} + + + ${ldap.authentication.truststore.passphrase} + + @@ -133,6 +144,11 @@ follow + + + + ${ldap.authentication.java.naming.security.protocol} + @@ -187,6 +203,11 @@ follow + + + + ${ldap.authentication.java.naming.security.protocol} + diff --git a/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties b/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties index fcb3539aaa..0a93ee02e9 100644 --- a/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties +++ b/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties @@ -123,4 +123,11 @@ ldap.synchronization.groupMemberAttributeName=member ldap.synchronization.enableProgressEstimation=true # Requests timeout, in miliseconds, use 0 for none (default) -ldap.authentication.java.naming.read.timeout=0 \ No newline at end of file +ldap.authentication.java.naming.read.timeout=0 + +# LDAPS truststore configuration properties +#ldap.authentication.truststore.path= +#ldap.authentication.truststore.passphrase= +#ldap.authentication.truststore.type= +# Set to 'ssl' to enable truststore configuration via subsystem's properties +#ldap.authentication.java.naming.security.protocol=ssl \ No newline at end of file diff --git a/config/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties b/config/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties index b453d2f134..0fa288ce9a 100644 --- a/config/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties +++ b/config/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties @@ -129,4 +129,11 @@ ldap.synchronization.groupMemberAttributeName=member ldap.synchronization.enableProgressEstimation=true # Requests timeout, in miliseconds, use 0 for none (default) -ldap.authentication.java.naming.read.timeout=0 \ No newline at end of file +ldap.authentication.java.naming.read.timeout=0 + +# LDAPS truststore configuration properties +#ldap.authentication.truststore.path= +#ldap.authentication.truststore.passphrase= +#ldap.authentication.truststore.type= +# Set to 'ssl' to enable truststore configuration via subsystem's properties +#ldap.authentication.java.naming.security.protocol=ssl \ No newline at end of file diff --git a/source/java/org/alfresco/repo/security/authentication/AlfrescoSSLSocketFactory.java b/source/java/org/alfresco/repo/security/authentication/AlfrescoSSLSocketFactory.java new file mode 100644 index 0000000000..72d2bb590b --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/AlfrescoSSLSocketFactory.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2005-2014 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.security.authentication; + +import org.alfresco.error.AlfrescoRuntimeException; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +/** + * SSL socket factory that uses custom trustStore + *
The factory should be first initialized + * + * @author alex.mukha + * @since 5.0 + */ +public class AlfrescoSSLSocketFactory extends SSLSocketFactory +{ + private static SSLContext context; + + public AlfrescoSSLSocketFactory() + { + } + + /** + * Initialize the factory with custom trustStore + * @param trustStore + */ + public static synchronized void initTrustedSSLSocketFactory(final KeyStore trustStore) + { + try + { + final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509"); + trustManagerFactory.init(trustStore); + context = SSLContext.getInstance("SSL"); + context.init(null, trustManagerFactory.getTrustManagers(), SecureRandom.getInstance("SHA1PRNG")); + } + catch (NoSuchAlgorithmException nsae) + { + throw new AlfrescoRuntimeException("The SSL socket factory cannot be initialized.", nsae); + } + catch (KeyStoreException kse) + { + throw new AlfrescoRuntimeException("The SSL socket factory cannot be initialized.", kse); + } + catch (KeyManagementException kme) + { + throw new AlfrescoRuntimeException("The SSL socket factory cannot be initialized.", kme); + } + } + + public static synchronized SocketFactory getDefault() + { + if (context == null) + { + throw new AlfrescoRuntimeException("The factory was not initialized."); + } + return new AlfrescoSSLSocketFactory(); + } + + @Override + public String[] getDefaultCipherSuites() + { + return context.getSocketFactory().getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() + { + return context.getSocketFactory().getSupportedCipherSuites(); + } + + @Override + public Socket createSocket(Socket socket, String s, int i, boolean b) throws IOException + { + return context.getSocketFactory().createSocket(socket, s, i, b); + } + + @Override + public Socket createSocket(String s, int i) throws IOException, UnknownHostException + { + return context.getSocketFactory().createSocket(s, i); + } + + @Override + public Socket createSocket(String s, int i, InetAddress inetAddress, int i2) throws IOException, UnknownHostException + { + return context.getSocketFactory().createSocket(s, i, inetAddress, i2); + } + + @Override + public Socket createSocket(InetAddress inetAddress, int i) throws IOException + { + return context.getSocketFactory().createSocket(inetAddress, i); + } + + @Override + public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress2, int i2) throws IOException + { + return context.getSocketFactory().createSocket(inetAddress, i, inetAddress2, i2); + } +} diff --git a/source/java/org/alfresco/repo/security/authentication/ldap/LDAPInitialDirContextFactoryImpl.java b/source/java/org/alfresco/repo/security/authentication/ldap/LDAPInitialDirContextFactoryImpl.java index 8302b135b9..9b730b5aa0 100644 --- a/source/java/org/alfresco/repo/security/authentication/ldap/LDAPInitialDirContextFactoryImpl.java +++ b/source/java/org/alfresco/repo/security/authentication/ldap/LDAPInitialDirContextFactoryImpl.java @@ -18,7 +18,13 @@ */ package org.alfresco.repo.security.authentication.ldap; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; import java.util.Collections; import java.util.HashSet; import java.util.Hashtable; @@ -41,9 +47,12 @@ import javax.naming.ldap.LdapContext; import javax.naming.ldap.PagedResultsControl; import javax.naming.ldap.PagedResultsResponseControl; +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.security.authentication.AlfrescoSSLSocketFactory; import org.alfresco.repo.security.authentication.AuthenticationDiagnostic; import org.alfresco.repo.security.authentication.AuthenticationException; import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.PropertyCheck; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; @@ -58,6 +67,48 @@ public class LDAPInitialDirContextFactoryImpl implements LDAPInitialDirContextFa private Map defaultEnvironment = Collections. emptyMap(); private Map authenticatedEnvironment = Collections. emptyMap(); + private String trustStorePath; + private String trustStoreType; + private String trustStorePassPhrase; + + public String getTrustStorePath() + { + return trustStorePath; + } + + public void setTrustStorePath(String trustStorePath) + { + if (PropertyCheck.isValidPropertyString(trustStorePath)) + { + this.trustStorePath = trustStorePath; + } + } + + public String getTrustStoreType() + { + return trustStoreType; + } + + public void setTrustStoreType(String trustStoreType) + { + if (PropertyCheck.isValidPropertyString(trustStoreType)) + { + this.trustStoreType = trustStoreType; + } + } + + public String getTrustStorePassPhrase() + { + return trustStorePassPhrase; + } + + public void setTrustStorePassPhrase(String trustStorePassPhrase) + { + if (PropertyCheck.isValidPropertyString(trustStorePassPhrase)) + { + this.trustStorePassPhrase = trustStorePassPhrase; + } + } static { @@ -114,6 +165,13 @@ public class LDAPInitialDirContextFactoryImpl implements LDAPInitialDirContextFa String securityPrincipal = env.get(Context.SECURITY_PRINCIPAL); String providerURL = env.get(Context.PROVIDER_URL); + if (isSSLSocketFactoryRequired()) + { + KeyStore trustStore = initTrustStore(); + AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(trustStore); + env.put("java.naming.ldap.factory.socket", AlfrescoSSLSocketFactory.class.getName()); + } + if(diagnostic == null) { diagnostic = new AuthenticationDiagnostic(); @@ -392,6 +450,12 @@ public class LDAPInitialDirContextFactoryImpl implements LDAPInitialDirContextFa env.putAll(authenticatedEnvironment); env.remove(Context.SECURITY_PRINCIPAL); env.remove(Context.SECURITY_CREDENTIALS); + if (isSSLSocketFactoryRequired()) + { + KeyStore trustStore = initTrustStore(); + AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(trustStore); + env.put("java.naming.ldap.factory.socket", AlfrescoSSLSocketFactory.class.getName()); + } try { new InitialDirContext(env); @@ -418,6 +482,12 @@ public class LDAPInitialDirContextFactoryImpl implements LDAPInitialDirContextFa env.putAll(authenticatedEnvironment); env.put(Context.SECURITY_PRINCIPAL, "daftAsABrush"); env.put(Context.SECURITY_CREDENTIALS, "daftAsABrush"); + if (isSSLSocketFactoryRequired()) + { + KeyStore trustStore = initTrustStore(); + AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(trustStore); + env.put("java.naming.ldap.factory.socket", AlfrescoSSLSocketFactory.class.getName()); + } try { @@ -447,6 +517,12 @@ public class LDAPInitialDirContextFactoryImpl implements LDAPInitialDirContextFa env.putAll(authenticatedEnvironment); env.put(Context.SECURITY_PRINCIPAL, "cn=daftAsABrush,dc=woof"); env.put(Context.SECURITY_CREDENTIALS, "daftAsABrush"); + if (isSSLSocketFactoryRequired()) + { + KeyStore trustStore = initTrustStore(); + AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(trustStore); + env.put("java.naming.ldap.factory.socket", AlfrescoSSLSocketFactory.class.getName()); + } try { @@ -481,6 +557,12 @@ public class LDAPInitialDirContextFactoryImpl implements LDAPInitialDirContextFa env.putAll(authenticatedEnvironment); env.put(Context.SECURITY_PRINCIPAL, principal); env.put(Context.SECURITY_CREDENTIALS, "sdasdasdasdasd123123123"); + if (isSSLSocketFactoryRequired()) + { + KeyStore trustStore = initTrustStore(); + AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(trustStore); + env.put("java.naming.ldap.factory.socket", AlfrescoSSLSocketFactory.class.getName()); + } if (!checkedEnvs.contains(env)) { @@ -513,7 +595,80 @@ public class LDAPInitialDirContextFactoryImpl implements LDAPInitialDirContextFa } } + /** + * Check if it required to use custom SSL socket factory with custom trustStore. + *
Required for LDAPS configuration. The ldap.authentication.java.naming.security.protocol should be set to "ssl" for LDAPS. + *
The following properties should be set: + *
    + *
  • ldap.authentication.truststore.path + *
  • ldap.authentication.truststore.type + *
  • ldap.authentication.truststore.passphrase + *
  • ldap.authentication.java.naming.security.protocol + *
+ * + * @return true if all the required properties are set + */ + private boolean isSSLSocketFactoryRequired() + { + boolean result = false; + // Check for LDAPS config + String protocol = authenticatedEnvironment.get(Context.SECURITY_PROTOCOL); + if (protocol != null && protocol.equals("ssl")) + { + if (getTrustStoreType() != null && getTrustStorePath() != null && getTrustStoreType() != null) + { + result = true; + } + else + { + logger.warn("The SSL configuration for LDAPS is not full, the default configuration will be used."); + } + } + return result; + } - - + /** + * Initialize trustStore with Spring set properties: + *
    + *
  • ldap.authentication.truststore.path + *
  • ldap.authentication.truststore.type + *
  • ldap.authentication.truststore.passphrase + *
+ * + * @return {@link KeyStore} with loaded trustStore file + */ + private KeyStore initTrustStore() + { + KeyStore ks; + String trustStoreType = getTrustStoreType(); + try + { + ks = KeyStore.getInstance(trustStoreType); + } + catch (KeyStoreException kse) + { + throw new AlfrescoRuntimeException("No provider supports " + trustStoreType, kse); + } + try + { + ks.load(new FileInputStream(getTrustStorePath()), getTrustStorePassPhrase().toCharArray()); + } + catch (FileNotFoundException fnfe) + { + throw new AlfrescoRuntimeException("The truststore file is not found.", fnfe); + } + catch (IOException ioe) + { + throw new AlfrescoRuntimeException("The truststore file cannot be read.", ioe); + } + catch (NoSuchAlgorithmException nsae) + { + throw new AlfrescoRuntimeException("Algorithm used to check the integrity of the truststore cannot be found.", nsae); + } + catch (CertificateException ce) + { + throw new AlfrescoRuntimeException("The certificates cannot be loaded from truststore.", ce); + } + return ks; + } } diff --git a/source/test-java/org/alfresco/repo/security/SecurityTestSuite.java b/source/test-java/org/alfresco/repo/security/SecurityTestSuite.java index fb060f0c20..00a6825fba 100644 --- a/source/test-java/org/alfresco/repo/security/SecurityTestSuite.java +++ b/source/test-java/org/alfresco/repo/security/SecurityTestSuite.java @@ -23,6 +23,7 @@ import junit.framework.Test; import junit.framework.TestSuite; import org.alfresco.repo.ownable.impl.OwnableServiceTest; +import org.alfresco.repo.security.authentication.AlfrescoSSLSocketFactoryTest; import org.alfresco.repo.security.authentication.AuthenticationBootstrapTest; import org.alfresco.repo.security.authentication.AuthenticationTest; import org.alfresco.repo.security.authentication.AuthorizationTest; @@ -81,7 +82,7 @@ public class SecurityTestSuite extends TestSuite suite.addTestSuite(AuthorityBridgeTableAsynchronouslyRefreshedCacheTest.class); suite.addTest(new JUnit4TestAdapter(HomeFolderProviderSynchronizerTest.class)); - + suite.addTest(new JUnit4TestAdapter(AlfrescoSSLSocketFactoryTest.class)); return suite; } } diff --git a/source/test-java/org/alfresco/repo/security/authentication/AlfrescoSSLSocketFactoryTest.java b/source/test-java/org/alfresco/repo/security/authentication/AlfrescoSSLSocketFactoryTest.java new file mode 100644 index 0000000000..67a6ba3f10 --- /dev/null +++ b/source/test-java/org/alfresco/repo/security/authentication/AlfrescoSSLSocketFactoryTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2005-2014 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.security.authentication; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.junit.Test; + +import java.security.KeyStore; + +import static org.junit.Assert.fail; + +/** + * SSL socket factory test + * + * @author alex.mukha + * @since 5.0 + */ +public class AlfrescoSSLSocketFactoryTest +{ + private static final String KEYSTORE_TYPE = "JCEKS"; + + @Test + public void testConfiguration() throws Exception + { + KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE); + // try to use the factory without initialization + try + { + AlfrescoSSLSocketFactory.getDefault(); + fail("An AlfrescoRuntimeException should be thrown as the factory is not initialized."); + } + catch (AlfrescoRuntimeException are) + { + // Expected + } + + // initialize and get an instance of AlfrescoSSLSocketFactory + AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(ks); + AlfrescoSSLSocketFactory.getDefault(); + } +}