You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@directory.apache.org by el...@apache.org on 2019/05/09 22:44:01 UTC
[directory-server] branch master updated: Applied patch for PR # 15
("Provide SASL EXTERNAL authentication using client certificates")
This is an automated email from the ASF dual-hosted git repository.
elecharny pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/directory-server.git
The following commit(s) were added to refs/heads/master by this push:
new 030638e Applied patch for PR # 15 ("Provide SASL EXTERNAL authentication using client certificates")
030638e is described below
commit 030638e8dc7304a3cf5295b10ff1e554a478c4f9
Author: emmanuel lecharny <el...@apache.org>
AuthorDate: Fri May 10 00:43:57 2019 +0200
Applied patch for PR # 15 ("Provide SASL EXTERNAL authentication using
client certificates")
---
core/pom.xml | 2 +
.../server/core/security/TlsKeyGenerator.java | 12 +-
.../certificate/CertificateMechanismHandler.java | 79 ++++++++
.../external/certificate/ExternalSaslServer.java | 184 ++++++++++++++++++
.../server/annotations/CreateTransport.java | 3 +
.../apache/directory/server/annotations/Sasl.java | 4 +-
.../server/factory/ServerAnnotationProcessor.java | 2 +
server-config/src/test/resources/ldapServer.ldif | 7 +
.../ClientCertificateAuthenticationIT.java | 206 +++++++++++++++++++++
.../ssl/ClientCertificateSslSocketFactory.java | 112 +++++++++++
10 files changed, 608 insertions(+), 3 deletions(-)
diff --git a/core/pom.xml b/core/pom.xml
index 57a03bb..1fb881b 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -258,6 +258,8 @@
org.apache.directory.server.i18n;version=${project.version},
org.bouncycastle.jce.provider;version=${bcprov.version},
org.bouncycastle.x509;version=${bcprov.version},
+ org.bouncycastle.asn1;version=${bcprov.version},
+ org.bouncycastle.asn1.x509;version=${bcprov.version},
org.slf4j;version=${slf4j.api.bundleversion}
</Import-Package>
</instructions>
diff --git a/core/src/main/java/org/apache/directory/server/core/security/TlsKeyGenerator.java b/core/src/main/java/org/apache/directory/server/core/security/TlsKeyGenerator.java
index 9404cf6..ec5d23b 100644
--- a/core/src/main/java/org/apache/directory/server/core/security/TlsKeyGenerator.java
+++ b/core/src/main/java/org/apache/directory/server/core/security/TlsKeyGenerator.java
@@ -47,8 +47,12 @@ import org.apache.directory.api.ldap.model.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.server.i18n.I18n;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.bouncycastle.x509.X509V1CertificateGenerator;
+import org.bouncycastle.x509.X509V3CertificateGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -297,7 +301,7 @@ public final class TlsKeyGenerator
// Generate the self-signed certificate
BigInteger serialNumber = BigInteger.valueOf( System.currentTimeMillis() );
- X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
+ X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();
X500Principal issuerName = new X500Principal( issuerDN );
X500Principal subjectName = new X500Principal( subjectDN );
@@ -308,6 +312,10 @@ public final class TlsKeyGenerator
certGen.setSubjectDN( subjectName );
certGen.setPublicKey( publicKey );
certGen.setSignatureAlgorithm( "SHA256With" + keyAlgo );
+ certGen.addExtension( Extension.basicConstraints, false, new BasicConstraints( false ) );
+ certGen.addExtension( Extension.extendedKeyUsage, true, new ExtendedKeyUsage(
+ new KeyPurposeId[] { KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth } ) );
+
try
diff --git a/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/external/certificate/CertificateMechanismHandler.java b/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/external/certificate/CertificateMechanismHandler.java
new file mode 100644
index 0000000..9d73921
--- /dev/null
+++ b/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/external/certificate/CertificateMechanismHandler.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.directory.server.ldap.handlers.sasl.external.certificate;
+
+import org.apache.directory.api.ldap.model.message.BindRequest;
+import org.apache.directory.server.core.api.CoreSession;
+import org.apache.directory.server.ldap.LdapSession;
+import org.apache.directory.server.ldap.handlers.sasl.AbstractMechanismHandler;
+import org.apache.directory.server.ldap.handlers.sasl.SaslConstants;
+
+import javax.security.sasl.SaslServer;
+
+/**
+ * The External Sasl mechanism handler which to authenticate user by client certificate (ssl).
+ *
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+public class CertificateMechanismHandler extends AbstractMechanismHandler
+{
+ public SaslServer handleMechanism( LdapSession ldapSession, BindRequest bindRequest ) throws Exception
+ {
+ SaslServer ss = ( SaslServer ) ldapSession.getSaslProperty( SaslConstants.SASL_SERVER );
+
+ if ( ss == null )
+ {
+ String saslHost = ldapSession.getLdapServer().getSaslHost();
+ String userBaseDn = ldapSession.getLdapServer().getSearchBaseDn();
+ ldapSession.putSaslProperty( SaslConstants.SASL_HOST, saslHost );
+ ldapSession.putSaslProperty( SaslConstants.SASL_USER_BASE_DN, userBaseDn );
+
+ CoreSession adminSession = ldapSession.getLdapServer().getDirectoryService().getAdminSession();
+
+ ss = new ExternalSaslServer( ldapSession, adminSession, bindRequest );
+
+ ldapSession.putSaslProperty( SaslConstants.SASL_SERVER, ss );
+ }
+
+ return ss;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ public void init( LdapSession ldapSession )
+ {
+ // Store the host in the ldap session
+ String saslHost = ldapSession.getLdapServer().getSaslHost();
+ ldapSession.putSaslProperty( SaslConstants.SASL_HOST, saslHost );
+ }
+
+
+ /**
+ * Remove the SaslServer and Mechanism property.
+ *
+ * @param ldapSession the Ldapsession instance
+ */
+ public void cleanup( LdapSession ldapSession )
+ {
+ ldapSession.clearSaslProperties();
+ }
+}
\ No newline at end of file
diff --git a/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/external/certificate/ExternalSaslServer.java b/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/external/certificate/ExternalSaslServer.java
new file mode 100644
index 0000000..48a5e0d
--- /dev/null
+++ b/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/external/certificate/ExternalSaslServer.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.directory.server.ldap.handlers.sasl.external.certificate;
+
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.directory.api.ldap.model.constants.AuthenticationLevel;
+import org.apache.directory.api.ldap.model.constants.SchemaConstants;
+import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.entry.Value;
+import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException;
+import org.apache.directory.api.ldap.model.filter.EqualityNode;
+import org.apache.directory.api.ldap.model.message.BindRequest;
+import org.apache.directory.api.ldap.model.message.SearchScope;
+import org.apache.directory.api.util.Strings;
+import org.apache.directory.server.core.api.CoreSession;
+import org.apache.directory.server.core.api.DirectoryService;
+import org.apache.directory.server.core.api.LdapPrincipal;
+import org.apache.directory.server.core.api.OperationEnum;
+import org.apache.directory.server.core.api.OperationManager;
+import org.apache.directory.server.core.api.filtering.EntryFilteringCursor;
+import org.apache.directory.server.core.api.interceptor.context.BindOperationContext;
+import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
+import org.apache.directory.server.ldap.LdapServer;
+import org.apache.directory.server.ldap.LdapSession;
+import org.apache.directory.server.ldap.handlers.sasl.AbstractSaslServer;
+import org.apache.directory.server.ldap.handlers.sasl.SaslConstants;
+import org.apache.mina.filter.ssl.SslFilter;
+
+import javax.naming.Context;
+import javax.net.ssl.SSLSession;
+import javax.security.sasl.SaslException;
+import java.security.cert.Certificate;
+
+
+/**
+ * A SaslServer implementation for certificate based SASL EXTERNAL mechanism.
+ *
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+public final class ExternalSaslServer extends AbstractSaslServer
+{
+ /**
+ * The possible states for the negotiation of a EXTERNAL mechanism.
+ */
+ private enum NegotiationState
+ {
+ INITIALIZED, // Negotiation has just started
+ COMPLETED // The user/password have been received
+ }
+
+ /** The current negotiation state */
+ private NegotiationState state;
+
+ /**
+ *
+ * Creates a new instance of ExternalSaslServer.
+ *
+ * @param ldapSession The associated LdapSession instance
+ * @param adminSession The Administrator session
+ * @param bindRequest The associated BindRequest object
+ */
+ ExternalSaslServer( LdapSession ldapSession, CoreSession adminSession, BindRequest bindRequest )
+ {
+ super( ldapSession, adminSession, bindRequest );
+ state = NegotiationState.INITIALIZED;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getMechanismName()
+ {
+ return SupportedSaslMechanisms.EXTERNAL;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ public byte[] evaluateResponse( byte[] initialResponse ) throws SaslException
+ {
+ try
+ {
+ SSLSession sslSession = ( SSLSession ) getLdapSession().getIoSession().getAttribute( SslFilter.SSL_SESSION );
+ Certificate[] peerCertificates = sslSession.getPeerCertificates();
+
+ if ( null == peerCertificates || 1 > peerCertificates.length )
+ {
+ throw new SaslException( "No peer certificate provided - cancel bind." );
+ }
+
+ getLdapSession().setCoreSession( authenticate( peerCertificates[0] ) );
+ state = NegotiationState.COMPLETED;
+ }
+ catch ( Exception e )
+ {
+ throw new SaslException( "Error authentication using client certificate: " + ExceptionUtils.getStackTrace( e ), e );
+ }
+
+ return Strings.EMPTY_BYTES;
+ }
+
+
+ /**
+ * Provides {@code true} if negationstate is {@link NegotiationState#COMPLETED}
+ *
+ * @return {@code true} if completed, otherwise {@code false}
+ */
+ public boolean isComplete()
+ {
+ return state == NegotiationState.COMPLETED;
+ }
+
+
+ /**
+ * Try to authenticate the user against the underlying LDAP server.
+ * We identify the user using the provided peercertificate.
+ */
+ private CoreSession authenticate( Certificate peerCertificate ) throws Exception
+ {
+ LdapSession ldapSession = getLdapSession();
+ CoreSession adminSession = getAdminSession();
+ DirectoryService directoryService = adminSession.getDirectoryService();
+ LdapServer ldapServer = ldapSession.getLdapServer();
+ OperationManager operationManager = directoryService.getOperationManager();
+
+ // find user by userCertificate
+ EqualityNode<String> filter = new EqualityNode<>(
+ directoryService.getSchemaManager().getAttributeType( SchemaConstants.USER_CERTIFICATE_AT ),
+ new Value( peerCertificate.getEncoded() ) );
+
+ SearchOperationContext searchContext = new SearchOperationContext( directoryService.getAdminSession() );
+ searchContext.setDn( directoryService.getDnFactory().create( ldapServer.getSearchBaseDn() ) );
+ searchContext.setScope( SearchScope.SUBTREE );
+ searchContext.setFilter( filter );
+ searchContext.setSizeLimit( 1 );
+ searchContext.setNoAttributes( true );
+
+ try ( EntryFilteringCursor cursor = operationManager.search( searchContext ) )
+ {
+ if ( cursor.next() )
+ {
+ Entry entry = cursor.get();
+
+ BindOperationContext bindContext = new BindOperationContext( ldapSession.getCoreSession() );
+ bindContext.setDn( entry.getDn() );
+ bindContext.setSaslMechanism( getMechanismName() );
+ bindContext.setSaslAuthId( getBindRequest().getName() );
+ bindContext.setIoSession( ldapSession.getIoSession() );
+ bindContext.setInterceptors( directoryService.getInterceptors( OperationEnum.BIND ) );
+
+ operationManager.bind( bindContext );
+
+ ldapSession.putSaslProperty( SaslConstants.SASL_AUTHENT_USER, new LdapPrincipal( directoryService.getSchemaManager(),
+ entry.getDn(), AuthenticationLevel.STRONG ) );
+ getLdapSession().putSaslProperty( Context.SECURITY_PRINCIPAL, getBindRequest().getName() );
+
+ return bindContext.getSession();
+ }
+
+ throw new LdapAuthenticationException( "Cannot authenticate user cert=" + peerCertificate );
+ }
+ }
+}
\ No newline at end of file
diff --git a/server-annotations/src/main/java/org/apache/directory/server/annotations/CreateTransport.java b/server-annotations/src/main/java/org/apache/directory/server/annotations/CreateTransport.java
index b3fe0cb..05c7f44 100644
--- a/server-annotations/src/main/java/org/apache/directory/server/annotations/CreateTransport.java
+++ b/server-annotations/src/main/java/org/apache/directory/server/annotations/CreateTransport.java
@@ -68,4 +68,7 @@ public @interface CreateTransport
/** @return The number of threads to use. Default to 3*/
int nbThreads() default 3;
+
+ /** @return A flag to tell if the transport should ask for client certificate. Default to false */
+ boolean clientAuth() default false;
}
\ No newline at end of file
diff --git a/server-annotations/src/main/java/org/apache/directory/server/annotations/Sasl.java b/server-annotations/src/main/java/org/apache/directory/server/annotations/Sasl.java
index bd17688..9e99493 100644
--- a/server-annotations/src/main/java/org/apache/directory/server/annotations/Sasl.java
+++ b/server-annotations/src/main/java/org/apache/directory/server/annotations/Sasl.java
@@ -30,6 +30,7 @@ import java.lang.annotation.Target;
import org.apache.directory.server.ldap.handlers.sasl.SimpleMechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.cramMD5.CramMd5MechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.digestMD5.DigestMd5MechanismHandler;
+import org.apache.directory.server.ldap.handlers.sasl.external.certificate.CertificateMechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.gssapi.GssapiMechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.ntlm.NtlmMechanismHandler;
@@ -77,6 +78,7 @@ public @interface Sasl
CramMd5MechanismHandler.class,
DigestMd5MechanismHandler.class,
GssapiMechanismHandler.class,
- NtlmMechanismHandler.class
+ NtlmMechanismHandler.class,
+ CertificateMechanismHandler.class
};
}
\ No newline at end of file
diff --git a/server-annotations/src/main/java/org/apache/directory/server/factory/ServerAnnotationProcessor.java b/server-annotations/src/main/java/org/apache/directory/server/factory/ServerAnnotationProcessor.java
index 7b17dd3..8e813b0 100644
--- a/server-annotations/src/main/java/org/apache/directory/server/factory/ServerAnnotationProcessor.java
+++ b/server-annotations/src/main/java/org/apache/directory/server/factory/ServerAnnotationProcessor.java
@@ -490,6 +490,7 @@ public final class ServerAnnotationProcessor
int nbThreads = transportBuilder.nbThreads();
int backlog = transportBuilder.backlog();
String address = transportBuilder.address();
+ boolean clientAuth = transportBuilder.clientAuth();
if ( Strings.isEmpty( address ) )
{
@@ -517,6 +518,7 @@ public final class ServerAnnotationProcessor
{
Transport tcp = new TcpTransport( address, port, nbThreads, backlog );
tcp.setEnableSSL( true );
+ ( ( TcpTransport ) tcp ).setWantClientAuth( clientAuth );
return Collections.singletonList( tcp );
}
else if ( protocol.equalsIgnoreCase( "UDP" ) )
diff --git a/server-config/src/test/resources/ldapServer.ldif b/server-config/src/test/resources/ldapServer.ldif
index 69a161d..4d3c625 100644
--- a/server-config/src/test/resources/ldapServer.ldif
+++ b/server-config/src/test/resources/ldapServer.ldif
@@ -153,6 +153,13 @@ objectclass: top
ads-saslMechName: SIMPLE
ads-enabled: true
+dn: ads-saslMechName=external,ou=saslmechhandlers,ads-serverId=ldapServer,ou=servers,ads-directoryServiceId=default,ou=config
+ads-saslMechClassName: org.apache.directory.server.ldap.handlers.sasl.external.certificate.CertificateMechanismHandler
+objectclass: ads-saslMechHandler
+objectclass: top
+ads-saslMechName: EXTERNAL
+ads-enabled: true
+
dn: ou=replConsumers,ads-serverId=ldapServer,ou=servers,ads-directoryServiceId=default,ou=config
objectClass: organizationalUnit
objectClass: top
diff --git a/server-integ/src/test/java/org/apache/directory/server/ldap/handlers/sasl/external/ClientCertificateAuthenticationIT.java b/server-integ/src/test/java/org/apache/directory/server/ldap/handlers/sasl/external/ClientCertificateAuthenticationIT.java
new file mode 100644
index 0000000..50d175f
--- /dev/null
+++ b/server-integ/src/test/java/org/apache/directory/server/ldap/handlers/sasl/external/ClientCertificateAuthenticationIT.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.directory.server.ldap.handlers.sasl.external;
+
+import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms;
+import org.apache.directory.api.ldap.model.entry.DefaultEntry;
+import org.apache.directory.api.ldap.model.entry.DefaultModification;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.entry.Modification;
+import org.apache.directory.api.ldap.model.entry.ModificationOperation;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.api.util.Network;
+import org.apache.directory.server.annotations.CreateLdapServer;
+import org.apache.directory.server.annotations.CreateTransport;
+import org.apache.directory.server.annotations.SaslMechanism;
+import org.apache.directory.server.core.annotations.*;
+import org.apache.directory.server.core.integ.AbstractLdapTestUnit;
+import org.apache.directory.server.core.integ.FrameworkRunner;
+import org.apache.directory.server.core.security.TlsKeyGenerator;
+import org.apache.directory.server.ldap.handlers.sasl.external.certificate.CertificateMechanismHandler;
+import org.apache.directory.server.ssl.ClientCertificateSslSocketFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import java.io.ByteArrayInputStream;
+import java.io.FileOutputStream;
+import java.net.InetAddress;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.util.Date;
+import java.util.Hashtable;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test the authentication using EXTERNAL SASL client certificate authentication.
+ * Stores the client certificate on a testuser which is also used for ldap connection.
+ *
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+@RunWith(FrameworkRunner.class)
+@CreateDS(allowAnonAccess = true, name = "ClientCertificateAuthenticationIT-class",
+ partitions =
+ {
+ @CreatePartition(
+ name = "example",
+ suffix = "dc=example,dc=com",
+ contextEntry = @ContextEntry(
+ entryLdif =
+ "dn: dc=example,dc=com\n" +
+ "dc: example\n" +
+ "objectClass: top\n" +
+ "objectClass: domain\n\n"),
+ indexes =
+ {
+ @CreateIndex(attribute = "objectClass"),
+ @CreateIndex(attribute = "dc"),
+ @CreateIndex(attribute = "ou")
+ })
+ })
+@CreateLdapServer(
+ transports =
+ {
+ @CreateTransport(protocol = "LDAPS", clientAuth = true)
+ },
+ saslMechanisms =
+ {
+ @SaslMechanism(name = SupportedSaslMechanisms.EXTERNAL, implClass = CertificateMechanismHandler.class)
+ })
+@ApplyLdifs(
+ {
+ // Entry # 1
+ "dn: ou=users,dc=example,dc=com",
+ "objectClass: organizationalUnit",
+ "objectClass: top",
+ "ou: users\n",
+
+ // Entry # 2
+ "dn: uid=testsubject,ou=users,dc=example,dc=com",
+ "objectClass: inetOrgPerson",
+ "objectClass: organizationalPerson",
+ "objectClass: person",
+ "objectClass: top",
+ "uid: testsubject",
+ "userPassword: not_set",
+ "cn: Test Subject",
+ "sn: Subject"
+ }
+)
+public class ClientCertificateAuthenticationIT extends AbstractLdapTestUnit
+{
+
+ private Dn authenticationUserDn;
+
+ /**
+ * Setup the test, prepare certificate and testuser
+ * @throws Exception on any error
+ */
+ @Before
+ public void installKeyStoreWithCertificate() throws Exception
+ {
+ authenticationUserDn = new Dn("uid=testsubject,ou=users,dc=example,dc=com");
+
+ String hostName = InetAddress.getLocalHost().getHostName();
+ String issuerDn = TlsKeyGenerator.CERTIFICATE_PRINCIPAL_DN;
+ String subjectDn = "CN=" + hostName;
+ Date startDate = new Date();
+ Date expiryDate = new Date( System.currentTimeMillis() + TlsKeyGenerator.YEAR_MILLIS );
+ String keyAlgo = "RSA";
+ int keySize = 1024;
+
+ Entry entry = new DefaultEntry();
+ TlsKeyGenerator.addKeyPair( entry, issuerDn, subjectDn, startDate, expiryDate, keyAlgo, keySize, null );
+
+ // prepare socket factory to provide client certificate
+ try ( ByteArrayInputStream in = new ByteArrayInputStream( TlsKeyGenerator.getCertificate( entry ).getEncoded() ) )
+ {
+ CertificateFactory factory = CertificateFactory.getInstance( "X.509" );
+ Certificate cert = factory.generateCertificate( in );
+ KeyStore ks = KeyStore.getInstance( KeyStore.getDefaultType() );
+ ks.load( null, null );
+ ks.setKeyEntry("apacheds", TlsKeyGenerator.getKeyPair( entry ).getPrivate(), ClientCertificateSslSocketFactory.ksPassword, new Certificate[] { cert } );
+ ks.store( new FileOutputStream( ClientCertificateSslSocketFactory.ksFile ), ClientCertificateSslSocketFactory.ksPassword );
+ }
+
+ // set certificte to testuser
+ Modification mod = new DefaultModification( ModificationOperation.ADD_ATTRIBUTE,
+ TlsKeyGenerator.USER_CERTIFICATE_AT, entry.get( TlsKeyGenerator.USER_CERTIFICATE_AT ).getBytes() );
+ getLdapServer().getDirectoryService().getAdminSession().modify(new Dn("uid=testsubject,ou=users,dc=example,dc=com"), mod );
+ }
+
+ /**
+ * Cleanup test, remove keystore
+ * @throws Exception on any error
+ */
+ @After
+ public void teardown() throws Exception {
+ if ( ClientCertificateSslSocketFactory.ksFile != null && ClientCertificateSslSocketFactory.ksFile.exists() )
+ {
+ ClientCertificateSslSocketFactory.ksFile.delete();
+ }
+ }
+
+ /**
+ * Do just a connect and a simple search to verify if authentication works.
+ * The test checks the authentication user in the current ldap session.
+ *
+ * @throws Exception on any error
+ */
+ @Test
+ public void testExternalClientCertificateAuthentication() throws Exception
+ {
+ // create a new secure connection
+ Hashtable<Object, Object> env = new Hashtable<>();
+ env.put( "java.naming.factory.initial", "com.sun.jndi.ldap.LdapCtxFactory" );
+ env.put( "java.naming.provider.url", Network.ldapLoopbackUrl( getLdapServer().getPortSSL() ) );
+ env.put( "java.naming.security.protocol", "ssl");
+ env.put( "java.naming.ldap.factory.socket", ClientCertificateSslSocketFactory.class.getName () );
+ env.put( "java.naming.security.authentication", "EXTERNAL" );
+
+ DirContext ctx = new InitialDirContext( env );
+ try
+ {
+ String searchFilter = "(objectClass=*)";
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope( SearchControls.OBJECT_SCOPE );
+
+ NamingEnumeration<SearchResult> results = ctx.search("dc=example,dc=com", searchFilter, searchControls );
+ assertTrue( results.hasMore() );
+
+ assertEquals(authenticationUserDn.getName(),
+ getLdapServer().getLdapSessionManager().getSessions()[0].getCoreSession().getAuthenticatedPrincipal().getDn().getName());
+ }
+ finally
+ {
+ ctx.close();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/server-integ/src/test/java/org/apache/directory/server/ssl/ClientCertificateSslSocketFactory.java b/server-integ/src/test/java/org/apache/directory/server/ssl/ClientCertificateSslSocketFactory.java
new file mode 100644
index 0000000..880f2c8
--- /dev/null
+++ b/server-integ/src/test/java/org/apache/directory/server/ssl/ClientCertificateSslSocketFactory.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.directory.server.ssl;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.*;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+
+
+/**
+ * Factory to create a SSLContext providing a client certificate.
+ *
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+public class ClientCertificateSslSocketFactory extends SocketFactory
+{
+ private static SocketFactory customSSLSocketFactory;
+
+ public static File ksFile = new File("target/clientkeystore.jks");;
+ public static char[] ksPassword = "changeit".toCharArray();
+
+ {
+ try {
+ KeyStore keyStore = KeyStore.getInstance( KeyStore.getDefaultType() );
+
+ try( InputStream keyInput = new FileInputStream( ksFile.getAbsoluteFile() ) )
+ {
+ keyStore.load( keyInput, ksPassword );
+ }
+
+ KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance( "SunX509" );
+ keyManagerFactory.init( keyStore, ksPassword );
+
+ SSLContext context = SSLContext.getInstance( "TLS" );
+ context.init( keyManagerFactory.getKeyManagers(), BogusTrustManagerFactory.X509_MANAGERS, new SecureRandom() );
+
+ customSSLSocketFactory = context.getSocketFactory();
+ }
+ catch ( Exception e )
+ {
+ throw new RuntimeException( "Error initializing ClientCertificateSslSocketFactory ", e );
+ }
+ }
+
+
+ /**
+ * This method is needed. It is called by the LDAP Context to create the connection
+ *
+ * @see SocketFactory#getDefault()
+ */
+ @SuppressWarnings("unused")
+ public static SocketFactory getDefault()
+ {
+ return new ClientCertificateSslSocketFactory();
+ }
+
+ /**
+ * @see SocketFactory#createSocket(String, int)
+ */
+ public Socket createSocket(String arg0, int arg1) throws IOException
+ {
+ return customSSLSocketFactory.createSocket(arg0, arg1);
+ }
+
+ /**
+ * @see SocketFactory#createSocket(InetAddress, int)
+ */
+ public Socket createSocket(InetAddress arg0, int arg1) throws IOException
+ {
+ return customSSLSocketFactory.createSocket(arg0, arg1);
+ }
+
+ /**
+ * @see SocketFactory#createSocket(String, int, InetAddress, int)
+ */
+ public Socket createSocket(String arg0, int arg1, InetAddress arg2, int arg3) throws IOException
+ {
+ return customSSLSocketFactory.createSocket(arg0, arg1, arg2, arg3);
+ }
+
+ /**
+ * @see SocketFactory#createSocket(InetAddress, int, InetAddress, int)
+ */
+ public Socket createSocket(InetAddress arg0, int arg1, InetAddress arg2, int arg3) throws IOException
+ {
+ return customSSLSocketFactory.createSocket(arg0, arg1, arg2, arg3);
+ }
+}
\ No newline at end of file