DIRAPI-373: Implement SASL integrity and confidentiality layer
diff --git a/integ-osgi/src/test/java/org/apache/directory/api/osgi/ApiLdapClientApiOsgiTest.java b/integ-osgi/src/test/java/org/apache/directory/api/osgi/ApiLdapClientApiOsgiTest.java
index 797bd5d..71e7262 100644
--- a/integ-osgi/src/test/java/org/apache/directory/api/osgi/ApiLdapClientApiOsgiTest.java
+++ b/integ-osgi/src/test/java/org/apache/directory/api/osgi/ApiLdapClientApiOsgiTest.java
@@ -22,6 +22,8 @@
import org.apache.commons.pool2.PooledObjectFactory;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
+import org.apache.directory.api.ldap.codec.api.SaslFilter;
+import org.apache.directory.api.ldap.model.constants.SaslQoP;
import org.apache.directory.ldap.client.api.DefaultPoolableLdapConnectionFactory;
import org.apache.directory.ldap.client.api.Krb5LoginConfiguration;
import org.apache.directory.ldap.client.api.LdapConnection;
@@ -63,6 +65,11 @@
ldapConnectionPool.getLdapApiService();
ldapConnectionPool.getTestOnBorrow();
ldapConnectionPool.close();
+
+ // Test SaslFilter
+ SaslFilter.OFFSET.toString();
+ SaslQoP.AUTH_CONF.name();
+ org.apache.mina.core.buffer.IoBuffer.isUseDirectBuffer();
}
}
diff --git a/integ-osgi/src/test/java/org/apache/directory/api/osgi/ApiLdapCodecCoreOsgiTest.java b/integ-osgi/src/test/java/org/apache/directory/api/osgi/ApiLdapCodecCoreOsgiTest.java
index c92be35..b878b9f 100644
--- a/integ-osgi/src/test/java/org/apache/directory/api/osgi/ApiLdapCodecCoreOsgiTest.java
+++ b/integ-osgi/src/test/java/org/apache/directory/api/osgi/ApiLdapCodecCoreOsgiTest.java
@@ -35,9 +35,10 @@
import org.apache.directory.api.ldap.codec.actions.response.search.done.InitSearchResultDone;
import org.apache.directory.api.ldap.codec.api.LdapApiService;
import org.apache.directory.api.ldap.codec.api.LdapApiServiceFactory;
+import org.apache.directory.api.ldap.codec.api.SaslFilter;
import org.apache.directory.api.ldap.codec.search.AndFilter;
import org.apache.directory.api.ldap.codec.search.SubstringFilter;
-import org.apache.directory.api.ldap.model.message.SearchRequest;
+import org.apache.directory.api.ldap.model.constants.SaslQoP;
import org.apache.directory.api.ldap.model.message.SearchRequestImpl;
import org.apache.directory.api.ldap.model.message.controls.SortRequest;
import org.junit.Test;
@@ -74,6 +75,11 @@
new SubstringFilter();
new SearchRequestImpl();
+
+ // Test SaslFilter
+ SaslFilter.OFFSET.toString();
+ SaslQoP.AUTH_CONF.name();
+ org.apache.mina.core.buffer.IoBuffer.isUseDirectBuffer();
}
diff --git a/ldap/client/api/pom.xml b/ldap/client/api/pom.xml
index 227a204..88a892c 100644
--- a/ldap/client/api/pom.xml
+++ b/ldap/client/api/pom.xml
@@ -179,10 +179,12 @@
org.apache.directory.api.ldap.model.schema.syntaxCheckers;version=${project.version},
org.apache.directory.api.ldap.schema.manager.impl;version=${project.version},
org.apache.directory.api.util;version=${project.version},
+ org.apache.mina.core.buffer;version=${mina.core.version},
org.apache.mina.core.filterchain;version=${mina.core.version},
org.apache.mina.core.future;version=${mina.core.version},
org.apache.mina.core.service;version=${mina.core.version},
org.apache.mina.core.session;version=${mina.core.version},
+ org.apache.mina.core.write;version=${mina.core.version},
org.apache.mina.filter;version=${mina.core.version},
org.apache.mina.filter.codec;version=${mina.core.version},
org.apache.mina.filter.ssl;version=${mina.core.version},
diff --git a/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/LdapNetworkConnection.java b/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/LdapNetworkConnection.java
index 26c4db2..08b9395 100644
--- a/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/LdapNetworkConnection.java
+++ b/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/LdapNetworkConnection.java
@@ -66,6 +66,7 @@
import org.apache.directory.api.ldap.codec.api.LdapDecoder;
import org.apache.directory.api.ldap.codec.api.LdapMessageContainer;
import org.apache.directory.api.ldap.codec.api.MessageEncoderException;
+import org.apache.directory.api.ldap.codec.api.SaslFilter;
import org.apache.directory.api.ldap.codec.api.SchemaBinaryAttributeDetector;
import org.apache.directory.api.ldap.extras.controls.ad.TreeDelete;
import org.apache.directory.api.ldap.extras.controls.ad.TreeDeleteImpl;
@@ -167,6 +168,7 @@
import org.apache.directory.ldap.client.api.future.ResponseFuture;
import org.apache.directory.ldap.client.api.future.SearchFuture;
import org.apache.mina.core.filterchain.IoFilter;
+import org.apache.mina.core.filterchain.IoFilterChain;
import org.apache.mina.core.future.CloseFuture;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.future.WriteFuture;
@@ -195,6 +197,7 @@
*/
public class LdapNetworkConnection extends AbstractLdapConnection implements LdapAsyncConnection
{
+
/** logger for reporting errors that might not be handled properly upstream */
private static final Logger LOG = LoggerFactory.getLogger( LdapNetworkConnection.class );
@@ -236,12 +239,18 @@
*/
private List<ConnectionClosedEventListener> conCloseListeners;
- /** The Ldap codec protocol filter */
+ /** The LDAP codec protocol filter */
private IoFilter ldapProtocolFilter = new ProtocolCodecFilter( codec.getProtocolCodecFactory() );
- /** the SslFilter key */
+ /** The LDAP coded protocol filter key */
+ private static final String LDAP_CODEC_FILTER_KEY = "ldapCodec";
+
+ /** The SslFilter key */
private static final String SSL_FILTER_KEY = "sslFilter";
+ /** The SaslFilter key */
+ private static final String SASL_FILTER_KEY = "saslFilter";
+
/** The exception stored in the session if we've got one */
private static final String EXCEPTION_KEY = "sessionException";
@@ -517,7 +526,7 @@
}
// Add the codec to the chain
- connector.getFilterChain().addLast( "ldapCodec", ldapProtocolFilter );
+ connector.getFilterChain().addLast( LDAP_CODEC_FILTER_KEY, ldapProtocolFilter );
// If we use SSL, we have to add the SslFilter to the chain
if ( config.isUseSsl() )
@@ -4879,6 +4888,26 @@
/**
+ * Adds a {@link SaslFilter} to the session's filter chain.
+ *
+ * @param saslClient The initialized SASL client
+ *
+ * @throws LdapException
+ */
+ private void addSaslFilter( SaslClient saslClient ) throws LdapException
+ {
+ IoFilterChain filterChain = ioSession.getFilterChain();
+ if ( filterChain.contains( SASL_FILTER_KEY ) )
+ {
+ filterChain.remove( SASL_FILTER_KEY );
+ }
+
+ SaslFilter saslFilter = new SaslFilter( saslClient );
+ filterChain.addBefore( LDAP_CODEC_FILTER_KEY, SASL_FILTER_KEY, saslFilter );
+ }
+
+
+ /**
* Adds {@link SslFilter} to the IOConnector or IOSession's filter chain
*
* @throws LdapException If the SSL filter addition failed
@@ -5130,6 +5159,15 @@
}
}
+ /*
+ * Install the SASL filter when the SASL auth is complete.
+ * This adds the security layer if it was negotiated.
+ */
+ if ( sc.isComplete() )
+ {
+ addSaslFilter( sc );
+ }
+
bindFuture.set( bindResponse );
return bindFuture;
diff --git a/ldap/codec/core/pom.xml b/ldap/codec/core/pom.xml
index ab0be9d..8b1ee29 100644
--- a/ldap/codec/core/pom.xml
+++ b/ldap/codec/core/pom.xml
@@ -140,6 +140,7 @@
org.apache.directory.api.asn1.ber.tlv;version=${project.version},
org.apache.directory.api.asn1.util;version=${project.version},
org.apache.directory.api.i18n;version=${project.version},
+ org.apache.directory.api.ldap.model.constants;version=${project.version},
org.apache.directory.api.ldap.model.entry;version=${project.version},
org.apache.directory.api.ldap.model.exception;version=${project.version},
org.apache.directory.api.ldap.model.filter;version=${project.version},
@@ -150,6 +151,12 @@
org.apache.directory.api.ldap.model.url;version=${project.version},
org.apache.directory.api.util;version=${project.version},
org.apache.directory.api.util.exception;version=${project.version},
+ org.apache.mina.core.buffer;version=${mina.core.version},
+ org.apache.mina.core.filterchain;version=${mina.core.version},
+ org.apache.mina.core.future;version=${mina.core.version},
+ org.apache.mina.core.service;version=${mina.core.version},
+ org.apache.mina.core.session;version=${mina.core.version},
+ org.apache.mina.core.write;version=${mina.core.version},
org.apache.mina.filter.codec;version=${mina.core.version},
org.apache.mina.util;version=${mina.core.version},
org.osgi.framework;version="[1.0.0,2.0.0)",
diff --git a/ldap/codec/core/src/main/java/org/apache/directory/api/ldap/codec/api/SaslFilter.java b/ldap/codec/core/src/main/java/org/apache/directory/api/ldap/codec/api/SaslFilter.java
new file mode 100644
index 0000000..63091ac
--- /dev/null
+++ b/ldap/codec/core/src/main/java/org/apache/directory/api/ldap/codec/api/SaslFilter.java
@@ -0,0 +1,305 @@
+/*
+ * 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.api.ldap.codec.api;
+
+
+import javax.security.sasl.Sasl;
+import javax.security.sasl.SaslClient;
+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";
+
+ /**
+ * A session attribute key that holds the received bytes of partially received
+ * SASL message.
+ */
+ public static final String BYTES = SaslFilter.class.getName() + ".Buffer";
+
+ /**
+ * A session attribute key that holds the offset of partially received
+ * SASL message.
+ */
+ public static final String OFFSET = SaslFilter.class.getName() + ".Offset";
+
+ /** The SASL client, only set if the filter is used at the client side. */
+ private final SaslClient saslClient;
+
+ /** The SASL server, only set if the filter is used at the server side. */
+ private final SaslServer saslServer;
+
+ /** True if a security layer has been negotiated */
+ private boolean hasSecurityLayer;
+
+ /** The negotiated max buffer size */
+ private int maxBufferSize;
+
+ /**
+ * Creates a new instance of SaslFilter. The SaslFilter must be constructed
+ * with a SASL client that has completed SASL negotiation. The SASL client
+ * will be used to provide message integrity and, optionally, message
+ * confidentiality.
+ *
+ * @param saslClient The initialized SASL client.
+ */
+ public SaslFilter( SaslClient saslClient )
+ {
+ if ( saslClient == null )
+ {
+ throw new IllegalArgumentException();
+ }
+
+ this.saslServer = null;
+ this.saslClient = saslClient;
+ initHasSecurityLayer( ( String ) saslClient.getNegotiatedProperty( Sasl.QOP ) );
+ initMaxBuffer( ( String ) saslClient.getNegotiatedProperty( Sasl.MAX_BUFFER ) );
+ }
+
+
+ /**
+ * Creates a new instance of SaslFilter. The SaslFilter must be constructed
+ * with a SASL server that has completed SASL negotiation. The SASL server
+ * will be used to provide message integrity and, optionally, message
+ * confidentiality.
+ *
+ * @param saslClient The initialized SASL server.
+ */
+ public SaslFilter( SaslServer saslServer )
+ {
+ if ( saslServer == null )
+ {
+ throw new IllegalArgumentException();
+ }
+
+ this.saslClient = null;
+ this.saslServer = saslServer;
+ initHasSecurityLayer( ( String ) saslServer.getNegotiatedProperty( Sasl.QOP ) );
+ initMaxBuffer( ( String ) saslServer.getNegotiatedProperty( Sasl.MAX_BUFFER ) );
+ }
+
+
+ private void initHasSecurityLayer( String qop )
+ {
+ this.hasSecurityLayer = ( qop != null && ( qop.equals( SaslQoP.AUTH_INT.getValue() ) || qop
+ .equals( SaslQoP.AUTH_CONF.getValue() ) ) );
+ }
+
+
+ private void initMaxBuffer( String maxBuffer )
+ {
+ this.maxBufferSize = maxBuffer != null ? Integer.parseInt( maxBuffer ) : 65536;
+ }
+
+
+ @Override
+ public synchronized void messageReceived( NextFilter nextFilter, IoSession session, Object message )
+ throws SaslException
+ {
+ LOG.debug( "Message received: {}", message );
+
+ if ( !hasSecurityLayer )
+ {
+ LOG.debug( "Will not use SASL on received message." );
+ nextFilter.messageReceived( session, message );
+ return;
+ }
+
+ /*
+ * Unwrap the data for mechanisms that support QoP (DIGEST-MD5, GSSAPI).
+ */
+ IoBuffer buf = ( IoBuffer ) message;
+ while ( buf.hasRemaining() )
+ {
+ /*
+ * Check for a previously received partial SASL message which is stored in the session.
+ * Otherwise read the first 4 bytes which is the length and allocate the bytes.
+ * Ensure the buffer size doesn't exceed the negotiated max buffer size.
+ */
+ byte[] bytes = ( byte[] ) session.getAttribute( BYTES, null );
+ int offset = ( int ) session.getAttribute( OFFSET, -1 );
+ if ( bytes == null )
+ {
+ int bufferSize = buf.getInt();
+ if ( bufferSize > maxBufferSize )
+ {
+ throw new IllegalStateException(
+ bufferSize + " exceeds the negotiated receive buffer size limit: " + maxBufferSize );
+ }
+ bytes = new byte[bufferSize];
+ offset = 0;
+ }
+
+ /*
+ * Get the buffer as bytes. Handle the case that only a part of the SASL message was received.
+ */
+ int length = Math.min( bytes.length - offset, buf.remaining() );
+ buf.get( bytes, offset, length );
+
+ /*
+ * Check if the full SASL message was received. If not store the partially received data in
+ * the session so it can be resumed when the next message is received.
+ */
+ offset += length;
+ if ( offset < bytes.length )
+ {
+ LOG.debug( "Partial SASL message received: {}/{}", offset, bytes.length );
+ session.setAttribute( BYTES, bytes );
+ session.setAttribute( OFFSET, offset );
+ break;
+ }
+
+ /*
+ * Unwrap the SASL message and forward it to the next filter.
+ */
+ LOG.debug( "Will use SASL to unwrap received message of length: {}", bytes.length );
+ byte[] token = unwrap( bytes, 0, bytes.length );
+ nextFilter.messageReceived( session, IoBuffer.wrap( token ) );
+
+ /*
+ * Finally clear the session attributes.
+ */
+ session.removeAttribute( BYTES );
+ session.removeAttribute( OFFSET );
+ }
+ }
+
+
+ @Override
+ public synchronized 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;
+ }
+
+ if ( !hasSecurityLayer )
+ {
+ LOG.debug( "Will not use SASL on write request." );
+ nextFilter.filterWrite( session, writeRequest );
+ return;
+ }
+
+ /*
+ * Wrap the data for mechanisms that support QoP (DIGEST-MD5, GSSAPI).
+ */
+
+ /*
+ * Get the buffer as bytes.
+ */
+ IoBuffer buf = ( IoBuffer ) writeRequest.getMessage();
+ int bufferLength = buf.remaining();
+ byte[] bufferBytes = new byte[bufferLength];
+ buf.get( bufferBytes );
+
+ LOG.info( "Will use SASL to wrap message of length: {}", bufferLength );
+
+ /*
+ * Ensure to not send larger SASL message than negotiated.
+ */
+ int max = maxBufferSize - 200;
+ for ( int offset = 0; offset < bufferLength; offset += max )
+ {
+ int length = Math.min( bufferLength - offset, max );
+ byte[] saslLayer = wrap( bufferBytes, offset, length );
+
+ /*
+ * Prepend 4 byte length.
+ */
+ IoBuffer 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() ) );
+ }
+ }
+
+
+ private byte[] wrap( byte[] buffer, int offset, int length ) throws SaslException
+ {
+ if ( saslClient != null )
+ {
+ return saslClient.wrap( buffer, offset, length );
+ }
+ else
+ {
+ return saslServer.wrap( buffer, offset, length );
+ }
+ }
+
+
+ private byte[] unwrap( byte[] buffer, int offset, int length ) throws SaslException
+ {
+ if ( saslClient != null )
+ {
+ return saslClient.unwrap( buffer, offset, length );
+ }
+ else
+ {
+ return saslServer.unwrap( buffer, offset, length );
+ }
+ }
+
+}