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}
+
+
@@ -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();
+ }
+}