blob: 22e9f8f782bf887b2c4384915afb145f5ccd3a04 [file] [log] [blame]
/*
* 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.core.kerberos;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.directory.server.core.interceptor.BaseInterceptor;
import org.apache.directory.server.core.interceptor.Interceptor;
import org.apache.directory.server.core.interceptor.NextInterceptor;
import org.apache.directory.server.core.interceptor.context.AddOperationContext;
import org.apache.directory.server.core.interceptor.context.LookupOperationContext;
import org.apache.directory.server.core.interceptor.context.ModifyOperationContext;
import org.apache.directory.server.core.normalization.NormalizationInterceptor;
import org.apache.directory.server.core.authn.AuthenticationInterceptor;
import org.apache.directory.server.core.authz.AciAuthorizationInterceptor;
import org.apache.directory.server.core.authz.DefaultAuthorizationInterceptor;
import org.apache.directory.server.core.exception.ExceptionInterceptor;
import org.apache.directory.server.core.operational.OperationalAttributeInterceptor;
import org.apache.directory.server.core.schema.SchemaInterceptor;
import org.apache.directory.server.core.subtree.SubentryInterceptor;
import org.apache.directory.server.core.collective.CollectiveAttributeInterceptor;
import org.apache.directory.server.core.entry.ClonedServerEntry;
import org.apache.directory.server.core.entry.DefaultServerAttribute;
import org.apache.directory.server.core.entry.ServerAttribute;
import org.apache.directory.server.core.entry.ServerBinaryValue;
import org.apache.directory.server.core.entry.ServerEntry;
import org.apache.directory.server.core.entry.ServerModification;
import org.apache.directory.server.core.entry.ServerStringValue;
import org.apache.directory.server.core.event.EventInterceptor;
import org.apache.directory.server.core.trigger.TriggerInterceptor;
import org.apache.directory.server.kerberos.shared.crypto.encryption.EncryptionType;
import org.apache.directory.server.kerberos.shared.crypto.encryption.KerberosKeyFactory;
import org.apache.directory.server.kerberos.shared.crypto.encryption.RandomKeyFactory;
import org.apache.directory.server.kerberos.shared.exceptions.KerberosException;
import org.apache.directory.server.kerberos.shared.io.encoder.EncryptionKeyEncoder;
import org.apache.directory.server.kerberos.shared.messages.value.EncryptionKey;
import org.apache.directory.server.kerberos.shared.store.KerberosAttribute;
import org.apache.directory.server.schema.registries.AttributeTypeRegistry;
import org.apache.directory.server.schema.registries.Registries;
import org.apache.directory.shared.ldap.constants.SchemaConstants;
import org.apache.directory.shared.ldap.entry.EntryAttribute;
import org.apache.directory.shared.ldap.entry.Modification;
import org.apache.directory.shared.ldap.entry.ModificationOperation;
import org.apache.directory.shared.ldap.entry.Value;
import org.apache.directory.shared.ldap.exception.LdapAuthenticationException;
import org.apache.directory.shared.ldap.name.LdapDN;
import org.apache.directory.shared.ldap.util.StringTools;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An {@link Interceptor} that creates symmetric Kerberos keys for users. When a
* 'userPassword' is added or modified, the 'userPassword' and 'krb5PrincipalName'
* are used to derive Kerberos keys. If the 'userPassword' is the special keyword
* 'randomKey', a random key is generated and used as the Kerberos key.
*
* @org.apache.xbean.XBean
*
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
* @version $Rev$, $Date$
*/
public class KeyDerivationInterceptor extends BaseInterceptor
{
/** The log for this class. */
private static final Logger log = LoggerFactory.getLogger( KeyDerivationInterceptor.class );
/** The service name. */
public static final String NAME = "keyDerivationService";
/**
* Define the interceptors to bypass upon user lookup.
*/
private static final Collection<String> USERLOOKUP_BYPASS;
static
{
Set<String> c = new HashSet<String>();
c.add( NormalizationInterceptor.class.getName() );
c.add( AuthenticationInterceptor.class.getName() );
c.add( AciAuthorizationInterceptor.class.getName() );
c.add( DefaultAuthorizationInterceptor.class.getName() );
c.add( ExceptionInterceptor.class.getName() );
c.add( OperationalAttributeInterceptor.class.getName() );
c.add( SchemaInterceptor.class.getName() );
c.add( SubentryInterceptor.class.getName() );
c.add( CollectiveAttributeInterceptor.class.getName() );
c.add( EventInterceptor.class.getName() );
c.add( TriggerInterceptor.class.getName() );
USERLOOKUP_BYPASS = Collections.unmodifiableCollection( c );
}
/**
* Intercept the addition of the 'userPassword' and 'krb5PrincipalName' attributes. Use the 'userPassword'
* and 'krb5PrincipalName' attributes to derive Kerberos keys for the principal. If the 'userPassword' is
* the special keyword 'randomKey', set random keys for the principal. Set the key version number (kvno)
* to '0'.
*/
public void add( NextInterceptor next, AddOperationContext addContext ) throws Exception
{
LdapDN normName = addContext.getDn();
ServerEntry entry = addContext.getEntry();
if ( ( entry.get( SchemaConstants.USER_PASSWORD_AT ) != null ) &&
( entry.get( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT ) != null ) )
{
log.debug( "Adding the entry '{}' for DN '{}'.", entry, normName.getUpName() );
ServerBinaryValue userPassword = (ServerBinaryValue)entry.get( SchemaConstants.USER_PASSWORD_AT ).get();
String strUserPassword = StringTools.utf8ToString( userPassword.get() );
if ( log.isDebugEnabled() )
{
StringBuffer sb = new StringBuffer();
sb.append( "'" + strUserPassword + "' ( " );
sb.append( userPassword );
sb.append( " )" );
log.debug( "Adding Attribute id : 'userPassword', Values : [ {} ]", sb.toString() );
}
Value<?> principalNameValue = entry.get( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT ).get();
String principalName = (String)principalNameValue.get();
log.debug( "Got principal '{}' with userPassword '{}'.", principalName, strUserPassword );
Map<EncryptionType, EncryptionKey> keys = generateKeys( principalName, strUserPassword );
entry.put( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT, principalName );
entry.put( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT, "0" );
entry.put( getKeyAttribute( addContext.getSession().getDirectoryService().getRegistries(), keys ) );
log.debug( "Adding modified entry '{}' for DN '{}'.", entry, normName
.getUpName() );
}
next.add( addContext );
}
/**
* Intercept the modification of the 'userPassword' attribute. Perform a lookup to check for an
* existing principal name and key version number (kvno). If a 'krb5PrincipalName' is not in
* the modify request, attempt to use an existing 'krb5PrincipalName' attribute. If a kvno
* exists, increment the kvno; otherwise, set the kvno to '0'.
*
* If both a 'userPassword' and 'krb5PrincipalName' can be found, use the 'userPassword' and
* 'krb5PrincipalName' attributes to derive Kerberos keys for the principal.
*
* If the 'userPassword' is the special keyword 'randomKey', set random keys for the principal.
*/
public void modify( NextInterceptor next, ModifyOperationContext modContext ) throws Exception
{
ModifySubContext subContext = new ModifySubContext();
detectPasswordModification( modContext, subContext );
if ( subContext.getUserPassword() != null )
{
lookupPrincipalAttributes( modContext, subContext );
}
if ( subContext.isPrincipal() && subContext.hasValues() )
{
deriveKeys( modContext, subContext );
}
next.modify( modContext );
}
/**
* Detect password modification by checking the modify request for the 'userPassword'. Additionally,
* check to see if a 'krb5PrincipalName' was provided.
*
* @param modContext
* @param subContext
* @throws NamingException
*/
void detectPasswordModification( ModifyOperationContext modContext, ModifySubContext subContext )
throws Exception
{
List<Modification> mods = modContext.getModItems();
String operation = null;
// Loop over attributes being modified to pick out 'userPassword' and 'krb5PrincipalName'.
for ( Modification mod:mods )
{
if ( log.isDebugEnabled() )
{
switch ( mod.getOperation() )
{
case ADD_ATTRIBUTE:
operation = "Adding";
break;
case REMOVE_ATTRIBUTE:
operation = "Removing";
break;
case REPLACE_ATTRIBUTE:
operation = "Replacing";
break;
}
}
ServerAttribute attr = (ServerAttribute)mod.getAttribute();
if ( attr.instanceOf( SchemaConstants.USER_PASSWORD_AT ) )
{
Object firstValue = attr.get();
String password = null;
if ( firstValue instanceof ServerStringValue )
{
password = ((ServerStringValue)firstValue).get();
log.debug( "{} Attribute id : 'userPassword', Values : [ '{}' ]", operation, password );
}
else if ( firstValue instanceof ServerBinaryValue )
{
password = StringTools.utf8ToString( ((ServerBinaryValue)firstValue).get() );
if ( log.isDebugEnabled() )
{
StringBuffer sb = new StringBuffer();
sb.append( "'" + password + "' ( " );
sb.append( StringTools.dumpBytes( ((ServerBinaryValue)firstValue).get() ).trim() );
sb.append( " )" );
log.debug( "{} Attribute id : 'userPassword', Values : [ {} ]", operation, sb.toString() );
}
}
subContext.setUserPassword( password );
log.debug( "Got userPassword '{}'.", subContext.getUserPassword() );
}
if ( attr.instanceOf( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT ) )
{
subContext.setPrincipalName( attr.getString() );
log.debug( "Got principal '{}'.", subContext.getPrincipalName() );
}
}
}
/**
* Lookup the principal's attributes that are relevant to executing key derivation.
*
* @param modContext
* @param subContext
* @throws NamingException
*/
void lookupPrincipalAttributes( ModifyOperationContext modContext, ModifySubContext subContext )
throws Exception
{
LdapDN principalDn = modContext.getDn();
LookupOperationContext lookupContext = modContext.newLookupContext( principalDn );
lookupContext.setByPassed( USERLOOKUP_BYPASS );
lookupContext.setAttrsId( new String[]
{
SchemaConstants.OBJECT_CLASS_AT,
KerberosAttribute.KRB5_PRINCIPAL_NAME_AT,
KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT
} );
ClonedServerEntry userEntry = modContext.lookup( lookupContext );
if ( userEntry == null )
{
throw new LdapAuthenticationException( "Failed to authenticate user '" + principalDn + "'." );
}
EntryAttribute objectClass = userEntry.getOriginalEntry().get( SchemaConstants.OBJECT_CLASS_AT );
if ( !objectClass.contains( SchemaConstants.KRB5_PRINCIPAL_OC ) )
{
return;
}
else
{
subContext.isPrincipal( true );
log.debug( "DN {} is a Kerberos principal. Will attempt key derivation.", principalDn.getUpName() );
}
if ( subContext.getPrincipalName() == null )
{
EntryAttribute principalAttribute = userEntry.getOriginalEntry().get( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT );
String principalName = principalAttribute.getString();
subContext.setPrincipalName( principalName );
log.debug( "Found principal '{}' from lookup.", principalName );
}
EntryAttribute keyVersionNumberAttr = userEntry.getOriginalEntry().get( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT );
if ( keyVersionNumberAttr == null )
{
subContext.setNewKeyVersionNumber( 0 );
log.debug( "Key version number was null, setting to 0." );
}
else
{
int oldKeyVersionNumber = Integer.valueOf( keyVersionNumberAttr.getString() );
int newKeyVersionNumber = oldKeyVersionNumber + 1;
subContext.setNewKeyVersionNumber( newKeyVersionNumber );
log.debug( "Found key version number '{}', setting to '{}'.", oldKeyVersionNumber, newKeyVersionNumber );
}
}
/**
* Use the 'userPassword' and 'krb5PrincipalName' attributes to derive Kerberos keys for the principal.
*
* If the 'userPassword' is the special keyword 'randomKey', set random keys for the principal.
*
* @param modContext
* @param subContext
*/
void deriveKeys( ModifyOperationContext modContext, ModifySubContext subContext ) throws Exception
{
List<Modification> mods = modContext.getModItems();
String principalName = subContext.getPrincipalName();
String userPassword = subContext.getUserPassword();
int kvno = subContext.getNewKeyVersionNumber();
log.debug( "Got principal '{}' with userPassword '{}'.", principalName, userPassword );
Map<EncryptionType, EncryptionKey> keys = generateKeys( principalName, userPassword );
List<Modification> newModsList = new ArrayList<Modification>();
// Make sure we preserve any other modification items.
for ( Modification mod:mods )
{
newModsList.add( mod );
}
AttributeTypeRegistry atRegistry = modContext.getSession()
.getDirectoryService().getRegistries().getAttributeTypeRegistry();
// Add our modification items.
newModsList.add(
new ServerModification(
ModificationOperation.REPLACE_ATTRIBUTE,
new DefaultServerAttribute(
KerberosAttribute.KRB5_PRINCIPAL_NAME_AT,
atRegistry.lookup( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT ),
principalName ) ) );
newModsList.add(
new ServerModification(
ModificationOperation.REPLACE_ATTRIBUTE,
new DefaultServerAttribute(
KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT,
atRegistry.lookup( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ),
Integer.toString( kvno ) ) ) );
ServerAttribute attribute = getKeyAttribute( modContext.getSession()
.getDirectoryService().getRegistries(), keys );
newModsList.add( new ServerModification( ModificationOperation.REPLACE_ATTRIBUTE, attribute ) );
modContext.setModItems( newModsList );
}
private ServerAttribute getKeyAttribute( Registries registries, Map<EncryptionType, EncryptionKey> keys ) throws Exception
{
ServerAttribute keyAttribute =
new DefaultServerAttribute( KerberosAttribute.KRB5_KEY_AT,
registries.getAttributeTypeRegistry().lookup( KerberosAttribute.KRB5_KEY_AT ) );
Iterator<EncryptionKey> it = keys.values().iterator();
while ( it.hasNext() )
{
try
{
keyAttribute.add( EncryptionKeyEncoder.encode( it.next() ) );
}
catch ( IOException ioe )
{
log.error( "Error encoding EncryptionKey.", ioe );
}
}
return keyAttribute;
}
private Map<EncryptionType, EncryptionKey> generateKeys( String principalName, String userPassword )
{
if ( userPassword.equalsIgnoreCase( "randomKey" ) )
{
// Generate random key.
try
{
return RandomKeyFactory.getRandomKeys();
}
catch ( KerberosException ke )
{
log.debug( ke.getMessage(), ke );
return null;
}
}
else
{
// Derive key based on password and principal name.
return KerberosKeyFactory.getKerberosKeys( principalName, userPassword );
}
}
class ModifySubContext
{
private boolean isPrincipal = false;
private String principalName;
private String userPassword;
private int newKeyVersionNumber = -1;
boolean isPrincipal()
{
return isPrincipal;
}
void isPrincipal( boolean isPrincipal )
{
this.isPrincipal = isPrincipal;
}
String getPrincipalName()
{
return principalName;
}
void setPrincipalName( String principalName )
{
this.principalName = principalName;
}
String getUserPassword()
{
return userPassword;
}
void setUserPassword( String userPassword )
{
this.userPassword = userPassword;
}
int getNewKeyVersionNumber()
{
return newKeyVersionNumber;
}
void setNewKeyVersionNumber( int newKeyVersionNumber )
{
this.newKeyVersionNumber = newKeyVersionNumber;
}
boolean hasValues()
{
return userPassword != null && principalName != null && newKeyVersionNumber > -1;
}
}
}