You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@directory.apache.org by se...@apache.org on 2021/06/20 06:36:50 UTC

[directory-server] 01/02: DIRSERVER-1632: Use improved SASL filter from ldap-api (DIRAPI-373)

This is an automated email from the ASF dual-hosted git repository.

seelmann pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/directory-server.git

commit e71b7260dcdddf8611701384cfe0559c53ec03a5
Author: Stefan Seelmann <ma...@stefan-seelmann.de>
AuthorDate: Sun Jun 20 08:31:28 2021 +0200

    DIRSERVER-1632: Use improved SASL filter from ldap-api (DIRAPI-373)
---
 pom.xml                                            |   2 +-
 .../handlers/sasl/AbstractMechanismHandler.java    |   1 +
 .../server/ldap/handlers/sasl/SaslFilter.java      | 176 ----------
 .../server/operations/bind/SaslBindIT.java         | 355 ++++++++++++++++++---
 4 files changed, 309 insertions(+), 225 deletions(-)

diff --git a/pom.xml b/pom.xml
index 81c6905..71393fe 100644
--- a/pom.xml
+++ b/pom.xml
@@ -46,7 +46,7 @@
     <doclint>none</doclint>
 
     <!-- Set versions for depending projects -->
-    <org.apache.directory.api.version>2.0.2</org.apache.directory.api.version>
+    <org.apache.directory.api.version>2.0.3-SNAPSHOT</org.apache.directory.api.version>
     <org.apache.directory.mavibot.version>1.0.0-M8</org.apache.directory.mavibot.version>
     <org.apache.directory.checkstyle-configuration.version>2.0.1</org.apache.directory.checkstyle-configuration.version>
     <org.apache.directory.jdbm.version>2.0.0-M3</org.apache.directory.jdbm.version>
diff --git a/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/AbstractMechanismHandler.java b/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/AbstractMechanismHandler.java
index 57f50bd..249f858 100644
--- a/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/AbstractMechanismHandler.java
+++ b/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/AbstractMechanismHandler.java
@@ -22,6 +22,7 @@ package org.apache.directory.server.ldap.handlers.sasl;
 
 import javax.security.sasl.SaslServer;
 
+import org.apache.directory.api.ldap.codec.api.SaslFilter;
 import org.apache.directory.server.ldap.LdapSession;
 import org.apache.mina.core.filterchain.IoFilterChain;
 import org.apache.mina.core.session.IoSession;
diff --git a/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/SaslFilter.java b/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/SaslFilter.java
deleted file mode 100644
index ae2f6ba..0000000
--- a/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/SaslFilter.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- *  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;
-
-
-import javax.security.sasl.Sasl;
-import javax.security.sasl.SaslException;
-import javax.security.sasl.SaslServer;
-
-import org.apache.directory.api.ldap.model.constants.SaslQoP;
-import org.apache.mina.core.buffer.IoBuffer;
-import org.apache.mina.core.filterchain.IoFilterAdapter;
-import org.apache.mina.core.session.IoSession;
-import org.apache.mina.core.write.DefaultWriteRequest;
-import org.apache.mina.core.write.WriteRequest;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-
-/**
- * An {@link IoFilterAdapter} that handles integrity and confidentiality protection
- * for a SASL bound session.  The SaslFilter must be constructed with a SASL
- * context that has completed SASL negotiation.  Some SASL mechanisms, such as
- * CRAM-MD5, only support authentication and thus do not need this filter.  DIGEST-MD5
- * and GSSAPI do support message integrity and confidentiality and, therefore,
- * do need this filter.
- * 
- * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
- */
-public class SaslFilter extends IoFilterAdapter
-{
-    private static final Logger LOG = LoggerFactory.getLogger( SaslFilter.class );
-
-    /**
-     * A session attribute key that makes next one write request bypass
-     * this filter (not adding a security layer).  This is a marker attribute,
-     * which means that you can put whatever as its value. ({@link Boolean#TRUE}
-     * is preferred.)  The attribute is automatically removed from the session
-     * attribute map as soon as {@link IoSession#write(Object)} is invoked,
-     * and therefore should be put again if you want to make more messages
-     * bypass this filter.
-     */
-    public static final String DISABLE_SECURITY_LAYER_ONCE = SaslFilter.class.getName() + ".DisableSecurityLayerOnce";
-
-    private SaslServer saslServer;
-
-
-    /**
-     * Creates a new instance of SaslFilter.  The SaslFilter must be constructed
-     * with a SASL context that has completed SASL negotiation.  The SASL context
-     * will be used to provide message integrity and, optionally, message
-     * confidentiality.
-     *
-     * @param saslServer The initialized SASL context.
-     */
-    public SaslFilter( SaslServer saslServer )
-    {
-        if ( saslServer == null )
-        {
-            throw new IllegalStateException();
-        }
-
-        this.saslServer = saslServer;
-    }
-
-
-    @Override
-    public void messageReceived( NextFilter nextFilter, IoSession session, Object message ) throws SaslException
-    {
-        LOG.debug( "Message received:  {}", message );
-
-        /*
-         * Unwrap the data for mechanisms that support QoP (DIGEST-MD5, GSSAPI).
-         */
-        String qop = ( String ) saslServer.getNegotiatedProperty( Sasl.QOP );
-        boolean hasSecurityLayer = ( qop != null && ( qop.equals( SaslQoP.AUTH_INT.getValue() ) || qop
-            .equals( SaslQoP.AUTH_CONF.getValue() ) ) );
-
-        if ( hasSecurityLayer )
-        {
-            /*
-             * Get the buffer as bytes.  First 4 bytes are length as int.
-             */
-            IoBuffer buf = ( IoBuffer ) message;
-            int bufferLength = buf.getInt();
-            byte[] bufferBytes = new byte[bufferLength];
-            buf.get( bufferBytes );
-
-            LOG.debug( "Will use SASL to unwrap received message of length:  {}", bufferLength );
-            byte[] token = saslServer.unwrap( bufferBytes, 0, bufferBytes.length );
-            nextFilter.messageReceived( session, IoBuffer.wrap( token ) );
-        }
-        else
-        {
-            LOG.debug( "Will not use SASL on received message." );
-            nextFilter.messageReceived( session, message );
-        }
-    }
-
-
-    @Override
-    public void filterWrite( NextFilter nextFilter, IoSession session, WriteRequest writeRequest ) throws SaslException
-    {
-        LOG.debug( "Filtering write request:  {}", writeRequest );
-
-        /*
-         * Check if security layer processing should be disabled once.
-         */
-        if ( session.containsAttribute( DISABLE_SECURITY_LAYER_ONCE ) )
-        {
-            // Remove the marker attribute because it is temporary.
-            LOG.debug( "Disabling SaslFilter once; will not use SASL on write request." );
-            session.removeAttribute( DISABLE_SECURITY_LAYER_ONCE );
-            nextFilter.filterWrite( session, writeRequest );
-            return;
-        }
-
-        /*
-         * Wrap the data for mechanisms that support QoP (DIGEST-MD5, GSSAPI).
-         */
-        String qop = ( String ) saslServer.getNegotiatedProperty( Sasl.QOP );
-        boolean hasSecurityLayer = ( qop != null && ( qop.equals( SaslQoP.AUTH_INT.getValue() ) || qop
-            .equals( SaslQoP.AUTH_CONF.getValue() ) ) );
-
-        IoBuffer saslLayerBuffer = null;
-
-        if ( hasSecurityLayer )
-        {
-            /*
-             * Get the buffer as bytes.
-             */
-            IoBuffer buf = ( IoBuffer ) writeRequest.getMessage();
-            int bufferLength = buf.remaining();
-            byte[] bufferBytes = new byte[bufferLength];
-            buf.get( bufferBytes );
-
-            LOG.debug( "Will use SASL to wrap message of length:  {}", bufferLength );
-
-            byte[] saslLayer = saslServer.wrap( bufferBytes, 0, bufferBytes.length );
-
-            /*
-             * Prepend 4 byte length.
-             */
-            saslLayerBuffer = IoBuffer.allocate( 4 + saslLayer.length );
-            saslLayerBuffer.putInt( saslLayer.length );
-            saslLayerBuffer.put( saslLayer );
-            saslLayerBuffer.position( 0 );
-            saslLayerBuffer.limit( 4 + saslLayer.length );
-
-            LOG.debug( "Sending encrypted token of length {}.", saslLayerBuffer.limit() );
-            nextFilter.filterWrite( session, new DefaultWriteRequest( saslLayerBuffer, writeRequest.getFuture() ) );
-        }
-        else
-        {
-            LOG.debug( "Will not use SASL on write request." );
-            nextFilter.filterWrite( session, writeRequest );
-        }
-    }
-}
diff --git a/server-integ/src/test/java/org/apache/directory/server/operations/bind/SaslBindIT.java b/server-integ/src/test/java/org/apache/directory/server/operations/bind/SaslBindIT.java
index 6e27ec9..043e390 100644
--- a/server-integ/src/test/java/org/apache/directory/server/operations/bind/SaslBindIT.java
+++ b/server-integ/src/test/java/org/apache/directory/server/operations/bind/SaslBindIT.java
@@ -27,6 +27,8 @@ import static org.junit.Assert.fail;
 import java.io.File;
 import java.lang.reflect.Field;
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
 
@@ -36,6 +38,7 @@ import javax.naming.directory.Attributes;
 import javax.naming.directory.DirContext;
 import javax.naming.directory.InitialDirContext;
 
+import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.commons.net.SocketClient;
 import org.apache.directory.api.asn1.util.Asn1Buffer;
 import org.apache.directory.api.ldap.codec.api.LdapDecoder;
@@ -45,18 +48,23 @@ import org.apache.directory.api.ldap.model.constants.SaslQoP;
 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.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.ModificationOperation;
 import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
 import org.apache.directory.api.ldap.model.message.BindRequest;
 import org.apache.directory.api.ldap.model.message.BindRequestImpl;
 import org.apache.directory.api.ldap.model.message.BindResponse;
 import org.apache.directory.api.ldap.model.message.Message;
 import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
+import org.apache.directory.api.ldap.model.message.SearchScope;
 import org.apache.directory.api.ldap.model.name.Dn;
 import org.apache.directory.api.util.Network;
 import org.apache.directory.api.util.Strings;
 import org.apache.directory.ldap.client.api.LdapConnection;
 import org.apache.directory.ldap.client.api.LdapNetworkConnection;
+import org.apache.directory.ldap.client.api.NoVerificationTrustManager;
 import org.apache.directory.ldap.client.api.SaslCramMd5Request;
 import org.apache.directory.ldap.client.api.SaslDigestMd5Request;
 import org.apache.directory.ldap.client.api.SaslGssApiRequest;
@@ -68,9 +76,11 @@ import org.apache.directory.server.core.annotations.ContextEntry;
 import org.apache.directory.server.core.annotations.CreateDS;
 import org.apache.directory.server.core.annotations.CreateIndex;
 import org.apache.directory.server.core.annotations.CreatePartition;
+import org.apache.directory.server.core.annotations.LoadSchema;
 import org.apache.directory.server.core.integ.AbstractLdapTestUnit;
 import org.apache.directory.server.core.integ.ApacheDSTestExtension;
 import org.apache.directory.server.core.kerberos.KeyDerivationInterceptor;
+import org.apache.directory.server.ldap.handlers.extended.StartTlsHandler;
 import org.apache.directory.server.ldap.handlers.extended.StoredProcedureExtendedOperationHandler;
 import org.apache.directory.server.ldap.handlers.sasl.cramMD5.CramMd5MechanismHandler;
 import org.apache.directory.server.ldap.handlers.sasl.digestMD5.DigestMd5MechanismHandler;
@@ -80,7 +90,6 @@ import org.apache.directory.server.ldap.handlers.sasl.plain.PlainMechanismHandle
 import org.apache.directory.shared.kerberos.KerberosAttribute;
 import org.apache.kerby.kerberos.kdc.impl.NettyKdcServerImpl;
 import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
-import org.junit.Ignore;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
@@ -88,6 +97,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.nimbusds.jose.util.StandardCharset;
+
 
 /**
  * An {@link AbstractServerTest} testing SASL authentication.
@@ -150,6 +161,18 @@ import org.slf4j.LoggerFactory;
 @CreateDS(
     allowAnonAccess = false,
     name = "SaslBindIT-class",
+    loadedSchemas =
+        {
+            @LoadSchema(name = "apachedns", enabled = true),
+            @LoadSchema(name = "autofs", enabled = true),
+            @LoadSchema(name = "corba", enabled = true),
+            @LoadSchema(name = "dhcp", enabled = true),
+            @LoadSchema(name = "mozilla", enabled = true),
+            @LoadSchema(name = "nis", enabled = true),
+            @LoadSchema(name = "posix", enabled = true),
+            @LoadSchema(name = "rfc2307bis", enabled = true),
+            @LoadSchema(name = "samba", enabled = true)
+        },
     partitions =
         {
             @CreatePartition(
@@ -173,8 +196,9 @@ import org.slf4j.LoggerFactory;
         { KeyDerivationInterceptor.class })
 @CreateLdapServer(transports =
     {
-        @CreateTransport(protocol = "LDAP")
-},
+        @CreateTransport(protocol = "LDAP"),
+        @CreateTransport(protocol = "LDAPS")
+    },
     saslHost = "localhost",
     saslPrincipal = "ldap/localhost@EXAMPLE.COM",
     saslMechanisms =
@@ -186,18 +210,25 @@ import org.slf4j.LoggerFactory;
             @SaslMechanism(name = SupportedSaslMechanisms.NTLM, implClass = NtlmMechanismHandler.class),
             @SaslMechanism(name = SupportedSaslMechanisms.GSS_SPNEGO, implClass = NtlmMechanismHandler.class)
     },
+    maxSizeLimit = 100,
     extendedOpHandlers =
         {
+            StartTlsHandler.class,
             StoredProcedureExtendedOperationHandler.class
     },
     ntlmProvider = BogusNtlmProvider.class)
 public class SaslBindIT extends AbstractLdapTestUnit
 {
+
+    private Dn userDn;
+
     @BeforeEach
     public void init() throws Exception
     {
+        ldapServer.setConfidentialityRequired( false );
         KerberosTestUtils.fixServicePrincipalName( "ldap/" + Network.LOOPBACK_HOSTNAME + "@EXAMPLE.COM",
             new Dn( "uid=ldap,ou=users,dc=example,dc=com" ), getLdapServer() );
+        userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
     }
 
 
@@ -241,7 +272,6 @@ public class SaslBindIT extends AbstractLdapTestUnit
         BindResponse resp = connection.bindSaslPlain( "hnelson", "secret" );
         assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
 
-        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
         Entry entry = connection.lookup( userDn );
         assertEquals( "hnelson", entry.get( "uid" ).getString() );
 
@@ -264,7 +294,6 @@ public class SaslBindIT extends AbstractLdapTestUnit
     @Disabled("Activate and fix when DIRAPI-36 (Provide a SaslBindRequest extending BindRequest that can be used in LdapConnection.bind(...) method) is solved")
     public void testSaslBindNoMech() throws Exception
     {
-        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
         LdapConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, getLdapServer().getPort() );
         BindRequest bindReq = new BindRequestImpl();
         bindReq.setCredentials( "secret" );
@@ -292,7 +321,6 @@ public class SaslBindIT extends AbstractLdapTestUnit
     @Test
     public void testSaslCramMd5Bind() throws Exception
     {
-        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
         LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, getLdapServer().getPort() );
 
         SaslCramMd5Request request = new SaslCramMd5Request();
@@ -315,7 +343,6 @@ public class SaslBindIT extends AbstractLdapTestUnit
     @Test
     public void testSaslCramMd5BindBadPassword() throws Exception
     {
-        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
         LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, getLdapServer().getPort() );
 
         SaslCramMd5Request request = new SaslCramMd5Request();
@@ -329,18 +356,20 @@ public class SaslBindIT extends AbstractLdapTestUnit
 
 
     /**
-     * Tests to make sure DIGEST-MD5 binds below the RootDSE work.
+     * Tests to make sure DIGEST-MD5 binds below the RootDSE work with
+     * SASL Quality of Protection set to 'auth'.
      */
     @Test
-    public void testSaslDigestMd5Bind() throws Exception
+    public void testSaslDigestMd5BindSaslQoPAuth() throws Exception
     {
-        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
-        LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, getLdapServer().getPort() );
+        LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME,
+            getLdapServer().getPort() );
 
         SaslDigestMd5Request request = new SaslDigestMd5Request();
         request.setUsername( userDn.getRdn().getValue() );
         request.setCredentials( "secret" );
         request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
+        request.setQualityOfProtection( SaslQoP.AUTH );
         BindResponse resp = connection.bind( request );
         assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
 
@@ -353,19 +382,19 @@ public class SaslBindIT extends AbstractLdapTestUnit
 
     /**
      * Tests to make sure DIGEST-MD5 binds below the RootDSE work with
-     * SASL Quality of Protection set to 'auth'.
+     * SASL Quality of Protection set to 'auth' over ldaps://.
      */
     @Test
-    public void testSaslDigestMd5BindSaslQoPAuth() throws Exception
+    public void testSaslDigestMd5BindSaslQoPAuthOverLdaps() throws Exception
     {
-        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
-        LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, getLdapServer().getPort() );
+        LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME,
+            getLdapServer().getPortSSL(), true );
+        connection.getConfig().setTrustManagers( new NoVerificationTrustManager() );
 
         SaslDigestMd5Request request = new SaslDigestMd5Request();
         request.setUsername( userDn.getRdn().getValue() );
         request.setCredentials( "secret" );
         request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
-        request.setQualityOfProtection( SaslQoP.AUTH );
         BindResponse resp = connection.bind( request );
         assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
 
@@ -381,24 +410,96 @@ public class SaslBindIT extends AbstractLdapTestUnit
      * SASL Quality of Protection set to 'auth-int'.
      */
     @Test
-    @Disabled
     public void testSaslDigestMd5BindSaslQoPAuthInt() throws Exception
     {
-        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
-        LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, getLdapServer().getPort() );
+        LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME,
+            getLdapServer().getPort() );
 
-        SaslDigestMd5Request request = new SaslDigestMd5Request();
-        request.setUsername( userDn.getRdn().getValue() );
-        request.setCredentials( "secret" );
-        request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
-        request.setQualityOfProtection( SaslQoP.AUTH_INT );
-        BindResponse resp = connection.bind( request );
-        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
+        // Repeat SASL bind to test proper replacement of the SASL filter
+        for ( int i = 0; i < 3; i++ )
+        {
+            SaslDigestMd5Request request = new SaslDigestMd5Request();
+            request.setUsername( userDn.getRdn().getValue() );
+            request.setCredentials( "secret" );
+            request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
+            request.setQualityOfProtection( SaslQoP.AUTH_INT );
+            BindResponse resp = connection.bind( request );
+            assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
 
-        Entry entry = connection.lookup( userDn );
-        assertEquals( "hnelson", entry.get( "uid" ).getString() );
+            Entry entry = connection.lookup( userDn );
+            assertEquals( "hnelson", entry.get( "uid" ).getString() );
 
-        connection.close();
+            testSaslFilter( connection );
+
+            connection.close();
+        }
+    }
+
+
+    /**
+     * Tests to make sure DIGEST-MD5 binds below the RootDSE work with
+     * SASL Quality of Protection set to 'auth-int' over ldaps://.
+     */
+    @Test
+    public void testSaslDigestMd5BindSaslQoPAuthIntOverLdaps() throws Exception
+    {
+        LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME,
+            getLdapServer().getPortSSL(), true );
+        connection.getConfig().setTrustManagers( new NoVerificationTrustManager() );
+
+        // Repeat SASL bind to test proper replacement of the SASL filter
+        for ( int i = 0; i < 3; i++ )
+        {
+            SaslDigestMd5Request request = new SaslDigestMd5Request();
+            request.setUsername( userDn.getRdn().getValue() );
+            request.setCredentials( "secret" );
+            request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
+            request.setQualityOfProtection( SaslQoP.AUTH_INT );
+            BindResponse resp = connection.bind( request );
+            assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
+
+            Entry entry = connection.lookup( userDn );
+            assertEquals( "hnelson", entry.get( "uid" ).getString() );
+
+            testSaslFilter( connection );
+
+            connection.close();
+        }
+    }
+
+
+    /**
+     * Tests to make sure DIGEST-MD5 binds below the RootDSE work with
+     * SASL Quality of Protection set to 'auth-int' over StartTLS
+     */
+    @Test
+    public void testSaslDigestMd5BindSaslQoPAuthIntOverStartTLS() throws Exception
+    {
+        ldapServer.setConfidentialityRequired( true );
+        LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME,
+            getLdapServer().getPort() );
+        connection.getConfig().setTrustManagers( new NoVerificationTrustManager() );
+
+        // Repeat SASL bind to test proper replacement of the SASL filter
+        for ( int i = 0; i < 3; i++ )
+        {
+            connection.startTls();
+
+            SaslDigestMd5Request request = new SaslDigestMd5Request();
+            request.setUsername( userDn.getRdn().getValue() );
+            request.setCredentials( "secret" );
+            request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
+            request.setQualityOfProtection( SaslQoP.AUTH_INT );
+            BindResponse resp = connection.bind( request );
+            assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
+
+            Entry entry = connection.lookup( userDn );
+            assertEquals( "hnelson", entry.get( "uid" ).getString() );
+
+            testSaslFilter( connection );
+
+            connection.close();
+        }
     }
 
 
@@ -407,24 +508,128 @@ public class SaslBindIT extends AbstractLdapTestUnit
      * SASL Quality of Protection set to 'auth-conf'.
      */
     @Test
-    @Disabled
     public void testSaslDigestMd5BindSaslQoPAuthConf() throws Exception
     {
-        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
-        LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, getLdapServer().getPort() );
+        LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME,
+            getLdapServer().getPort() );
 
-        SaslDigestMd5Request request = new SaslDigestMd5Request();
-        request.setUsername( userDn.getRdn().getValue() );
-        request.setCredentials( "secret" );
-        request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
-        request.setQualityOfProtection( SaslQoP.AUTH_CONF );
-        BindResponse resp = connection.bind( request );
-        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
+        // Repeat SASL bind to test proper replacement of the SASL filter
+        for ( int i = 0; i < 3; i++ )
+        {
+            SaslDigestMd5Request request = new SaslDigestMd5Request();
+            request.setUsername( userDn.getRdn().getValue() );
+            request.setCredentials( "secret" );
+            request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
+            request.setQualityOfProtection( SaslQoP.AUTH_CONF );
+            BindResponse resp = connection.bind( request );
+            assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
 
-        Entry entry = connection.lookup( userDn );
-        assertEquals( "hnelson", entry.get( "uid" ).getString() );
+            testSaslFilter( connection );
 
-        connection.close();
+            connection.close();
+        }
+    }
+
+
+    /**
+     * Tests to make sure DIGEST-MD5 binds below the RootDSE work with
+     * SASL Quality of Protection set to 'auth-conf' over ldaps://.
+     */
+    @Test
+    public void testSaslDigestMd5BindSaslQoPAuthConfOverLdaps() throws Exception
+    {
+        ldapServer.setConfidentialityRequired( true );
+        LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME,
+            getLdapServer().getPortSSL(), true );
+        connection.getConfig().setTrustManagers( new NoVerificationTrustManager() );
+
+        // Repeat SASL bind to test proper replacement of the SASL filter
+        for ( int i = 0; i < 3; i++ )
+        {
+            SaslDigestMd5Request request = new SaslDigestMd5Request();
+            request.setUsername( userDn.getRdn().getValue() );
+            request.setCredentials( "secret" );
+            request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
+            request.setQualityOfProtection( SaslQoP.AUTH_CONF );
+            BindResponse resp = connection.bind( request );
+            assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
+
+            testSaslFilter( connection );
+
+            connection.close();
+        }
+    }
+
+
+    /**
+     * Tests to make sure DIGEST-MD5 binds below the RootDSE work with
+     * SASL Quality of Protection set to 'auth-conf' over StartTLS.
+     */
+    @Test
+    public void testSaslDigestMd5BindSaslQoPAuthConfOverStartTLS() throws Exception
+    {
+        ldapServer.setConfidentialityRequired( true );
+        LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME,
+            getLdapServer().getPort() );
+        connection.getConfig().setTrustManagers( new NoVerificationTrustManager() );
+
+        // Repeat SASL bind to test proper replacement of the SASL filter
+        for ( int i = 0; i < 3; i++ )
+        {
+            connection.startTls();
+
+            SaslDigestMd5Request request = new SaslDigestMd5Request();
+            request.setUsername( userDn.getRdn().getValue() );
+            request.setCredentials( "secret" );
+            request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
+            request.setQualityOfProtection( SaslQoP.AUTH_CONF );
+            BindResponse resp = connection.bind( request );
+            assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
+
+            testSaslFilter( connection );
+
+            connection.close();
+        }
+    }
+
+
+    /**
+     * Run various search and modify operations with small and large payloads
+     * to test proper SASL message splitting and re-assembling.
+     */
+    private void testSaslFilter( LdapNetworkConnection connection )
+        throws LdapException, LdapInvalidAttributeValueException
+    {
+        // lookup Root DSE
+        assertTrue( connection.lookup( "", "+", "*" ).containsAttribute( "namingContexts" ) );
+
+        // lookup cn=schema with all schemas enabled which is larger than the SASL max buffer size
+        Entry lookup = connection.lookup( "cn=schema", "+", "*" );
+        assertTrue( lookup.containsAttribute( "objectClasses", "attributeTypes" ) );
+
+        // subtree search which returns 100 entries
+        List<Entry> entries = new ArrayList<>();
+        for ( Entry e : connection.search( "ou=schema", "(objectClass=*)", SearchScope.SUBTREE, "+", "*" ) )
+        {
+            entries.add( e );
+        }
+        assertEquals( 100, entries.size() );
+
+        // do a small modification
+        connection.modify( userDn,
+            new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE, "description", "test" ) );
+
+        // do a medium size modification
+        String largeString = RandomStringUtils.randomAscii( 10000 );
+        connection.modify( userDn,
+            new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE, "description", largeString ) );
+        assertEquals( largeString, connection.lookup( userDn ).get( "description" ).getString() );
+
+        // do a large modification
+        byte[] largeBytes = RandomStringUtils.random( 500_000 ).getBytes( StandardCharset.UTF_8 );
+        connection.modify( userDn,
+            new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE, "userCertificate", largeBytes ) );
+        assertEquals( largeBytes.length, connection.lookup( userDn ).get( "userCertificate" ).getBytes().length );
     }
 
 
@@ -434,7 +639,6 @@ public class SaslBindIT extends AbstractLdapTestUnit
     @Test
     public void testSaslDigestMd5BindBadRealm() throws Exception
     {
-        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
         LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, getLdapServer().getPort() );
 
         SaslDigestMd5Request request = new SaslDigestMd5Request();
@@ -454,7 +658,6 @@ public class SaslBindIT extends AbstractLdapTestUnit
     @Test
     public void testSaslDigestMd5BindBadPassword() throws Exception
     {
-        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
         LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, getLdapServer().getPort() );
 
         SaslDigestMd5Request request = new SaslDigestMd5Request();
@@ -495,7 +698,6 @@ public class SaslBindIT extends AbstractLdapTestUnit
         kerbyServer.createPrincipal( ldap, "randall" );
         kerbyServer.start();
 
-        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
         LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, getLdapServer().getPort() );
 
         //kdcServer.getConfig().setPaEncTimestampRequired( false );
@@ -517,13 +719,72 @@ public class SaslBindIT extends AbstractLdapTestUnit
         kerbyServer.stop();
     }
 
+
+    @Test
+    public void testSaslGssApiBindSaslQoPAuthConfOverStartTLS() throws Exception
+    {
+        SimpleKdcServer kerbyServer = new SimpleKdcServer();
+
+        String basedir = System.getProperty( "basedir" );
+        if ( basedir == null )
+        {
+            basedir = new File( "." ).getCanonicalPath();
+        }
+
+        kerbyServer.setKdcRealm( "EXAMPLE.COM" );
+        kerbyServer.setAllowUdp( true );
+        kerbyServer.setWorkDir( new File( basedir + "/target" ) );
+
+        kerbyServer.setInnerKdcImpl( new NettyKdcServerImpl( kerbyServer.getKdcSetting() ) );
+        kerbyServer.init();
+
+        // Create principals
+        String hnelson = "hnelson@EXAMPLE.COM";
+        String ldap = "ldap/" + Network.LOOPBACK_HOSTNAME + "@EXAMPLE.COM";
+        kerbyServer.createPrincipal( hnelson, "secret" );
+        kerbyServer.createPrincipal( ldap, "randall" );
+        kerbyServer.start();
+
+        //kdcServer.getConfig().setPaEncTimestampRequired( false );
+
+        ldapServer.setConfidentialityRequired( true );
+        LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME,
+            getLdapServer().getPort() );
+        connection.getConfig().setTrustManagers( new NoVerificationTrustManager() );
+
+        // Repeat SASL bind to test proper replacement of the SASL filter
+        for ( int i = 0; i < 3; i++ )
+        {
+            connection.startTls();
+
+            SaslGssApiRequest request = new SaslGssApiRequest();
+            request.setUsername( userDn.getRdn().getValue() );
+            request.setCredentials( "secret" );
+            request.setRealmName( ldapServer.getSaslRealms().get( 0 ).toUpperCase( Locale.ROOT ) );
+            request.setKdcHost( Network.LOOPBACK_HOSTNAME );
+            request.setKdcPort( kerbyServer.getKdcPort() );
+            request.setQualityOfProtection( SaslQoP.AUTH_CONF );
+            BindResponse resp = connection.bind( request );
+            assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
+
+            Entry entry = connection.lookup( userDn );
+            assertEquals( "hnelson", entry.get( "uid" ).getString() );
+
+            testSaslFilter( connection );
+
+            connection.close();
+        }
+
+        kerbyServer.stop();
+    }
+
+
     /**
      * Tests to make sure GSS-API binds below the RootDSE fail if the realm is bad.
      */
     @Test
     public void testSaslGssApiBindBadRealm() throws Exception
     {
-        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
         LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() );
 
         SaslGssApiRequest request = new SaslGssApiRequest();
@@ -553,7 +814,6 @@ public class SaslBindIT extends AbstractLdapTestUnit
     @Test
     public void testSaslGssApiBindBadPassword() throws Exception
     {
-        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
         LdapNetworkConnection connection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() );
 
         SaslGssApiRequest request = new SaslGssApiRequest();
@@ -642,7 +902,6 @@ public class SaslBindIT extends AbstractLdapTestUnit
         LdapNetworkConnection connection;
         BindResponse resp;
         Entry entry;
-        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
 
         for ( int i = 0; i < 1000; i++ )
         {