You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@directory.apache.org by er...@apache.org on 2007/05/13 06:59:17 UTC
svn commit: r537553 - in
/directory/sandbox/erodriguez/sasl-gssapi/src/main/java/org/apache/directory/server/saslgssapi:
HandleGssapi.java SaslGssapiFilter.java
Author: erodriguez
Date: Sat May 12 21:59:16 2007
New Revision: 537553
URL: http://svn.apache.org/viewvc?view=rev&rev=537553
Log:
Added an implementation of a GSSAPI-only SASL Bind handler. This was written using JGSS prior to the current SASL Bind handler, which uses the JDK 1.5 SaslServer to also support DIGEST-MD5 and CRAM-MD5, in addition to GSSAPI.
Added:
directory/sandbox/erodriguez/sasl-gssapi/src/main/java/org/apache/directory/server/saslgssapi/HandleGssapi.java (with props)
directory/sandbox/erodriguez/sasl-gssapi/src/main/java/org/apache/directory/server/saslgssapi/SaslGssapiFilter.java (with props)
Added: directory/sandbox/erodriguez/sasl-gssapi/src/main/java/org/apache/directory/server/saslgssapi/HandleGssapi.java
URL: http://svn.apache.org/viewvc/directory/sandbox/erodriguez/sasl-gssapi/src/main/java/org/apache/directory/server/saslgssapi/HandleGssapi.java?view=auto&rev=537553
==============================================================================
--- directory/sandbox/erodriguez/sasl-gssapi/src/main/java/org/apache/directory/server/saslgssapi/HandleGssapi.java (added)
+++ directory/sandbox/erodriguez/sasl-gssapi/src/main/java/org/apache/directory/server/saslgssapi/HandleGssapi.java Sat May 12 21:59:16 2007
@@ -0,0 +1,386 @@
+/*
+ * 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.saslgssapi;
+
+
+import java.security.Principal;
+import java.security.PrivilegedAction;
+
+import javax.naming.Context;
+import javax.security.auth.Subject;
+import javax.security.auth.kerberos.KerberosKey;
+import javax.security.auth.kerberos.KerberosPrincipal;
+
+import org.apache.directory.shared.ldap.message.BindRequest;
+import org.apache.directory.shared.ldap.message.BindResponse;
+import org.apache.directory.shared.ldap.message.LdapResult;
+import org.apache.directory.shared.ldap.message.ResultCodeEnum;
+import org.apache.mina.common.ByteBuffer;
+import org.apache.mina.common.IoSession;
+import org.apache.mina.handler.chain.IoHandlerCommand;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.MessageProp;
+import org.ietf.jgss.Oid;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ * @version $Rev$, $Date$
+ */
+public class HandleGssapi implements IoHandlerCommand
+{
+ private static final Logger log = LoggerFactory.getLogger( HandleGssapi.class );
+
+ private static final String SASL_GSSAPI_CONTEXT = "sasl gssapi context";
+ private static final String SASL_GSSAPI_STATE = "sasl gssapi state";
+
+ // Server expects simple or SASL credentials, may return continuation-token.
+ // SASL: token --> counter token
+ private static final String BIND_STATE_SIMPLE = "simple";
+
+ // Server expects empty client ack, will return negotiation-token.
+ // SASL: client ACK --> nego token
+ private static final String BIND_STATE_WAIT_1 = "waiting 1";
+
+ // Server expects counter-negotition-token, should return SUCCESS if OK.
+ // SASL: nego token --> SUCCESS
+ private static final String BIND_STATE_WAIT_2 = "waiting 2";
+
+ // Server has bound, specifically the bind requires QoP processing on all messages (similar to SSL).
+ private static final String BIND_STATE_GSSAPI_BOUND = "gssapi bound";
+
+ private GSSCredential serviceCredential;
+
+
+ /**
+ * Creates a new instance of HandleGssapi.
+ */
+ public HandleGssapi()
+ {
+ getSubject();
+
+ log.debug( "Starting GSSAPI handler with credential " + serviceCredential.toString() );
+ }
+
+
+ private void getSubject()
+ {
+ KerberosPrincipal servicePrincipal = new KerberosPrincipal( "ldap/ldap.example.com@EXAMPLE.COM" );
+ char[] password = new String( "randall" ).toCharArray();
+ KerberosKey serviceKey = new KerberosKey( servicePrincipal, password, "DES" );
+ Subject subject = new Subject();
+ subject.getPrivateCredentials().add( serviceKey );
+
+ serviceCredential = ( GSSCredential ) Subject.doAs( subject, new PrivilegedAction()
+ {
+ public Object run()
+ {
+ return getGssCredential();
+ }
+ } );
+ }
+
+
+ private GSSCredential getGssCredential()
+ {
+ GSSCredential serverCredential = null;
+
+ try
+ {
+ GSSManager manager = GSSManager.getInstance();
+ Oid krb5Mechanism = new Oid( "1.2.840.113554.1.2.2" );
+ Oid krb5PrincipalNameType = new Oid( "1.2.840.113554.1.2.2.1" );
+ GSSName serverName = manager.createName( "ldap/ldap.example.com@EXAMPLE.COM", krb5PrincipalNameType );
+ serverCredential = manager.createCredential( serverName, GSSCredential.DEFAULT_LIFETIME, krb5Mechanism,
+ GSSCredential.ACCEPT_ONLY );
+ // context.requestMutualAuth(true);
+ // context.requestCredDeleg(true);
+ }
+ catch ( GSSException gsse )
+ {
+ log.error( "GSSException: " + gsse.getMessage() );
+ log.debug( "GSSException major: " + gsse.getMajorString() );
+ log.debug( "GSSException minor: " + gsse.getMinorString() );
+ }
+
+ return serverCredential;
+ }
+
+
+ public void execute( NextCommand next, IoSession session, Object message ) throws Exception
+ {
+ BindRequest request = ( BindRequest ) message;
+
+ if ( !session.containsAttribute( SASL_GSSAPI_STATE ) )
+ {
+ session.setAttribute( SASL_GSSAPI_STATE, BIND_STATE_SIMPLE );
+ }
+
+ if ( request.getSaslMechanism() != null && request.getSaslMechanism().equals( "GSSAPI" ) )
+ {
+ handleGssapi( next, session, message );
+ }
+ else
+ {
+ next.execute( session, message );
+ }
+ }
+
+
+ private void handleGssapi( NextCommand next, IoSession session, Object message ) throws Exception
+ {
+ String gssapiState = ( String ) session.getAttribute( SASL_GSSAPI_STATE );
+
+ BindRequest request = ( BindRequest ) message;
+ LdapResult result = request.getResultResponse().getLdapResult();
+
+ /**
+ * The server attempts to establish a security context. Establishment may result in
+ * tokens that the server must return to the client.
+ */
+ GSSContext context;
+ byte tokenBytes[] = null;
+
+ try
+ {
+ byte[] gssapiData = request.getCredentials();
+
+ if ( session.containsAttribute( SASL_GSSAPI_CONTEXT ) )
+ {
+ context = ( GSSContext ) session.getAttribute( SASL_GSSAPI_CONTEXT );
+ }
+ else
+ {
+ GSSManager manager = GSSManager.getInstance();
+ context = manager.createContext( serviceCredential );
+ tokenBytes = context.acceptSecContext( gssapiData, 0, gssapiData.length );
+ }
+
+ if ( context != null && !context.isEstablished() && gssapiState.equals( BIND_STATE_SIMPLE ) )
+ {
+ /**
+ * If a GSSAPI token remains, it must be returned to
+ * the client for the final leg of an authentication.
+ */
+ if ( tokenBytes != null && tokenBytes.length > 0 )
+ {
+ log.info( "Final leg GSSAPI token had length " + tokenBytes.length );
+ result.setResultCode( ResultCodeEnum.SASL_BIND_IN_PROGRESS );
+ BindResponse resp = ( BindResponse ) request.getResultResponse();
+ resp.setServerSaslCreds( tokenBytes );
+ session.write( resp );
+ log.debug( "Returning final authentication data to client to complete context." );
+ return;
+ }
+ }
+
+ /**
+ * If the context is established, we can attempt to retrieve the name of the "context
+ * initiator." In the case of the Kerberos mechanism, the context initiator is the
+ * Kerberos principal of the client. Additionally, the client may be delegating
+ * credentials.
+ */
+ if ( context != null && context.isEstablished() && gssapiState.equals( BIND_STATE_SIMPLE ) )
+ {
+ log.debug( "Context established, attempting Kerberos principal retrieval." );
+ session.setAttribute( SASL_GSSAPI_CONTEXT, context );
+
+ Subject subject = new Subject();
+ GSSName clientGSSName = context.getSrcName();
+
+ /*
+ * This is the principal name that will be used to bind to the DIT.
+ * Note that this principal has not yet completed binding.
+ */
+ session.setAttribute( Context.SECURITY_PRINCIPAL, clientGSSName.toString() );
+
+ Principal clientPrincipal = new KerberosPrincipal( clientGSSName.toString() );
+ subject.getPrincipals().add( clientPrincipal );
+ log.info( "Got client Kerberos principal: " + clientGSSName );
+
+ if ( context.getCredDelegState() )
+ {
+ GSSCredential delegateCredential = context.getDelegCred();
+ GSSName delegateGSSName = delegateCredential.getName();
+ Principal delegatePrincipal = new KerberosPrincipal( delegateGSSName.toString() );
+ subject.getPrincipals().add( delegatePrincipal );
+ subject.getPrivateCredentials().add( delegateCredential );
+ log.info( "Got delegated Kerberos principal: " + delegateGSSName );
+ }
+
+ session.setAttribute( SASL_GSSAPI_STATE, BIND_STATE_WAIT_1 );
+
+ /**
+ * If a GSSAPI token remains, it must be returned to
+ * the client for the final leg of an authentication.
+ */
+ if ( tokenBytes != null && tokenBytes.length > 0 )
+ {
+ log.info( "Final leg GSSAPI token had length " + tokenBytes.length );
+ result.setResultCode( ResultCodeEnum.SASL_BIND_IN_PROGRESS );
+ BindResponse resp = ( BindResponse ) request.getResultResponse();
+ resp.setServerSaslCreds( tokenBytes );
+ session.write( resp );
+ log.debug( "Returning final authentication data to client to complete context." );
+ return;
+ }
+ }
+
+ if ( context != null && context.isEstablished() && gssapiState.equals( BIND_STATE_WAIT_1 ) )
+ {
+ /*
+ * ... the server then constructs 4 octets of data, with
+ * the first octet containing a bit-mask specifying the
+ * security layers supported by the server and the second
+ * through fourth octets containing in network byte order
+ * the maximum size output_token the server is able to receive.
+ */
+ byte[] bitMaskAndLength =
+ { ( byte ) 0x06, ( byte ) 0x00, ( byte ) 0x00, ( byte ) 0xFF };
+
+ /*
+ * The first MessageProp argument is 0 to request the default Quality-of-Protection.
+ * The second argument is true to request privacy (encryption of the message).
+ *
+ * The server must then pass the plaintext to GSS_Wrap with conf_flag set to FALSE.
+ */
+ MessageProp prop = new MessageProp( 0, false );
+ byte[] token = context.wrap( bitMaskAndLength, 0, bitMaskAndLength.length, prop );
+
+ // Issue the generated output_message to the client in a challenge.
+ result.setResultCode( ResultCodeEnum.SASL_BIND_IN_PROGRESS );
+ BindResponse resp = ( BindResponse ) request.getResultResponse();
+ resp.setServerSaslCreds( token );
+ session.write( resp );
+ log.debug( "Returned QoP bitmask and requested maximum message length." );
+
+ session.setAttribute( SASL_GSSAPI_STATE, BIND_STATE_WAIT_2 );
+
+ return;
+ }
+
+ if ( context != null && context.isEstablished() && gssapiState.equals( BIND_STATE_WAIT_2 ) )
+ {
+ log.info( "Negotiated token had length " + request.getCredentials().length );
+
+ /*
+ * The first MessageProp argument is 0 to request the default Quality-of-Protection.
+ * The second argument is true to request privacy (encryption of the message).
+ */
+ MessageProp prop = new MessageProp( 0, false );
+
+ /*
+ * The server must then pass the resulting response to GSS_Unwrap and
+ * interpret the first octet of resulting cleartext as the bit-mask for
+ * the selected security layer, the second through fourth octets as the
+ * maximum size output_message to send to the client, and the remaining
+ * octets as the authorization identity.
+ */
+ byte[] token = context.unwrap( request.getCredentials(), 0, request.getCredentials().length, prop );
+
+ log.debug( "Unwrapped token length is " + token.length );
+
+ /*
+ * The security layers and their corresponding bit-masks are as follows:
+ * 1 No security layer.
+ * 2 Integrity protection. Sender calls GSS_Wrap with conf_flag set to FALSE.
+ * 4 Privacy protection. Sender calls GSS_Wrap with conf_flag set to TRUE.
+ */
+ // 1st octet is bit-mask of QoP.
+ ByteBuffer bitMaskAndLength = ByteBuffer.wrap( token );
+ int bitMask = bitMaskAndLength.get();
+ log.debug( "1st octet bitmask is QoP as int " + bitMask );
+
+ /*
+ * Note that SASL negotiates the maximum size of the output_message to send.
+ * Implementations can use the GSS_Wrap_size_limit call to determine the
+ * corresponding maximum size input_message.
+ */
+ // Bytes 2-4 are requested maximum message size.
+ byte b[] = new byte[3];
+ bitMaskAndLength.get( b );
+ int length = ( b[0] & 0xff ) << 16 | ( b[1] & 0xff ) << 8 | ( b[2] & 0xff );
+ log.debug( "Bytes 2-4 are requested maximum message length " + length );
+
+ log.debug( "QoP bitmask and requested maximum message length negotiation complete." );
+
+ /*
+ * The server must verify that the src_name is authorized to
+ * authenticate as the authorization identity. After these
+ * verifications, the authentication process is complete.
+ */
+
+ /**
+ * TODO - The SimpleAuthenticator is expecting at least an empty String for
+ * anonymous authentication credentials. Once an Authenticator is available
+ * for SASL/GSSAPI, we can revisit what it needs.
+ *
+ * The Context.SECURITY_PRINCIPAL and Context.SECURITY_CREDENTIALS will be
+ * used to get an LdapContext and truly bind to the DIT.
+ */
+ session.setAttribute( Context.SECURITY_CREDENTIALS, "" );
+
+ /**
+ * If we got here, we're ready to try getting an initial LDAP context.
+ */
+ session.setAttribute( SASL_GSSAPI_STATE, BIND_STATE_GSSAPI_BOUND );
+ next.execute( session, message );
+ }
+ }
+ catch ( GSSException gsse )
+ {
+ log.error( "GSSException: " + gsse.getMessage() );
+ log.debug( "GSSException major: " + gsse.getMajorString() );
+ log.debug( "GSSException minor: " + gsse.getMinorString() );
+
+ /*
+ * A bad key for the server will result in:
+ * Mechanism level: Integrity check on decrypted field failed (31)
+ */
+ result.setResultCode( ResultCodeEnum.INVALID_CREDENTIALS );
+ result.setErrorMessage( gsse.getMinorString() );
+ session.write( request.getResultResponse() );
+ return;
+ }
+ /*
+ catch (GSSException gsse) {
+ LOG.fatal(gsse.getMessage());
+
+ if( gsse.getMajor() == GSSException.DEFECTIVE_CREDENTIAL
+ || gsse.getMajor() == GSSException.CREDENTIALS_EXPIRED )
+ throw new InvalidCredentialsException(gsse.getMessage(),gsse);
+ if( gsse.getMajor() == GSSException.NO_CRED )
+ throw new CredentialsNotAvailableException(gsse.getMessage(),gsse);
+ if( gsse.getMajor() == GSSException.DEFECTIVE_TOKEN
+ || gsse.getMajor() == GSSException.DUPLICATE_TOKEN
+ || gsse.getMajor() == GSSException.OLD_TOKEN )
+ throw new AuthChallengeException(gsse.getMessage(),gsse);
+
+ throw new AuthenticationException(gsse.getMessage());
+ }
+ */
+ }
+}
Propchange: directory/sandbox/erodriguez/sasl-gssapi/src/main/java/org/apache/directory/server/saslgssapi/HandleGssapi.java
------------------------------------------------------------------------------
svn:eol-style = native
Added: directory/sandbox/erodriguez/sasl-gssapi/src/main/java/org/apache/directory/server/saslgssapi/SaslGssapiFilter.java
URL: http://svn.apache.org/viewvc/directory/sandbox/erodriguez/sasl-gssapi/src/main/java/org/apache/directory/server/saslgssapi/SaslGssapiFilter.java?view=auto&rev=537553
==============================================================================
--- directory/sandbox/erodriguez/sasl-gssapi/src/main/java/org/apache/directory/server/saslgssapi/SaslGssapiFilter.java (added)
+++ directory/sandbox/erodriguez/sasl-gssapi/src/main/java/org/apache/directory/server/saslgssapi/SaslGssapiFilter.java Sat May 12 21:59:16 2007
@@ -0,0 +1,167 @@
+/*
+ * 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.saslgssapi;
+
+
+import org.apache.mina.common.ByteBuffer;
+import org.apache.mina.common.IoFilterAdapter;
+import org.apache.mina.common.IoSession;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.MessageProp;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * An {@link IoFilterAdapter} that handles privacy and confidentiality protections
+ * for a SASL GSSAPI bound session.
+ *
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ * @version $Rev$, $Date$
+ */
+public class SaslGssapiFilter extends IoFilterAdapter
+{
+ private static final Logger log = LoggerFactory.getLogger( SaslGssapiFilter.class.getName() );
+
+ private static final String SASL_GSSAPI_CONTEXT = "sasl gssapi context";
+ private static final String SASL_GSSAPI_USE_SASL = "sasl gssapi use sasl";
+
+ /*
+ * The first MessageProp argument is 0 to request the default Quality-of-Protection.
+ * The second argument is false to not request privacy (encryption of the message).
+ */
+ private static MessageProp REQUEST_PRIVACY = new MessageProp( false );
+
+ /*
+ * The first MessageProp argument is 0 to request the default Quality-of-Protection.
+ * The second argument is true to request privacy (encryption of the message).
+ */
+ private static MessageProp REQUEST_CONFIDENTIALITY = new MessageProp( true );
+
+
+ public void messageReceived( NextFilter nextFilter, IoSession session, Object message ) throws GSSException
+ {
+ log.debug( "Message received: " + message );
+
+ /*
+ * Guard clause: check if in SASL GSSAPI bound mode.
+ */
+ Boolean useSasl = ( Boolean ) session.getAttribute( SASL_GSSAPI_USE_SASL );
+
+ if ( useSasl == null || !useSasl.booleanValue() )
+ {
+ log.debug( "Will not use SASL on received message." );
+ nextFilter.messageReceived( session, message );
+ return;
+ }
+
+ GSSContext context = getGssContext( session );
+
+ /*
+ * Get the buffer as bytes. First 4 bytes are length as int.
+ */
+ ByteBuffer buf = ( ByteBuffer ) message;
+ int bufferLength = buf.getInt();
+ byte[] bufferBytes = new byte[bufferLength];
+ buf.get( bufferBytes );
+
+ log.debug( "Will use SASL on received message of length: " + bufferLength );
+
+ /*
+ * Unwrap the data.
+ */
+ byte[] token = context.unwrap( bufferBytes, 0, bufferBytes.length, REQUEST_PRIVACY );
+
+ nextFilter.messageReceived( session, ByteBuffer.wrap( token ) );
+ }
+
+
+ public void filterWrite( NextFilter nextFilter, IoSession session, WriteRequest writeRequest ) throws GSSException
+ {
+ log.debug( "Filtering write request: " + writeRequest );
+
+ /*
+ * Guard clause: check if in SASL GSSAPI bound mode.
+ */
+ Boolean useSasl = ( Boolean ) session.getAttribute( SASL_GSSAPI_USE_SASL );
+
+ if ( useSasl == null || !useSasl.booleanValue() )
+ {
+ log.debug( "Will not use SASL on write request." );
+ nextFilter.filterWrite( session, writeRequest );
+ return;
+ }
+
+ GSSContext context = getGssContext( session );
+
+ /*
+ * Get the buffer as bytes.
+ */
+ ByteBuffer buf = ( ByteBuffer ) writeRequest.getMessage();
+ int bufferLength = buf.remaining();
+ byte[] bufferBytes = new byte[bufferLength];
+ buf.get( bufferBytes );
+
+ log.debug( "Will use SASL on to filter message of length: " + bufferLength );
+
+ /*
+ * Wrap the data.
+ */
+ byte[] token = context.wrap( bufferBytes, 0, bufferBytes.length, REQUEST_CONFIDENTIALITY );
+
+ /*
+ * Prepend 4 byte length.
+ */
+ ByteBuffer encryptedBuffer = ByteBuffer.allocate( 4 + token.length );
+ encryptedBuffer.putInt( token.length );
+ encryptedBuffer.put( token );
+ encryptedBuffer.position( 0 );
+ encryptedBuffer.limit( 4 + token.length );
+
+ log.debug( "Sending encrypted token of length " + token.length );
+
+ nextFilter.filterWrite( session, new WriteRequest( encryptedBuffer, writeRequest.getFuture() ) );
+ }
+
+
+ /**
+ * Helper method to get the {@link GSSContext} and perform basic checks.
+ *
+ * @param session The {@link IoSession}
+ * @return {@link GSSContext} The {@link GSSContext} stored in the session by the {@link BindHandler}.
+ */
+ private GSSContext getGssContext( IoSession session )
+ {
+ GSSContext context = null;
+
+ if ( session.containsAttribute( SASL_GSSAPI_CONTEXT ) )
+ {
+ context = ( GSSContext ) session.getAttribute( SASL_GSSAPI_CONTEXT );
+ }
+
+ if ( context == null )
+ {
+ throw new IllegalStateException();
+ }
+
+ return context;
+ }
+}
Propchange: directory/sandbox/erodriguez/sasl-gssapi/src/main/java/org/apache/directory/server/saslgssapi/SaslGssapiFilter.java
------------------------------------------------------------------------------
svn:eol-style = native