blob: b1396fea587302f983c032c980ea9bf741cb1930 [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.shared.kerberos;
import static org.apache.directory.shared.kerberos.codec.types.EncryptionType.DES3_CBC_MD5;
import static org.apache.directory.shared.kerberos.codec.types.EncryptionType.DES3_CBC_SHA1;
import static org.apache.directory.shared.kerberos.codec.types.EncryptionType.DES3_CBC_SHA1_KD;
import static org.apache.directory.shared.kerberos.codec.types.EncryptionType.DES_CBC_CRC;
import static org.apache.directory.shared.kerberos.codec.types.EncryptionType.DES_CBC_MD4;
import static org.apache.directory.shared.kerberos.codec.types.EncryptionType.DES_CBC_MD5;
import static org.apache.directory.shared.kerberos.codec.types.EncryptionType.DES_EDE3_CBC_ENV_OID;
import static org.apache.directory.shared.kerberos.codec.types.EncryptionType.DSAWITHSHA1_CMSOID;
import static org.apache.directory.shared.kerberos.codec.types.EncryptionType.MD5WITHRSAENCRYPTION_CMSOID;
import static org.apache.directory.shared.kerberos.codec.types.EncryptionType.RC2CBC_ENVOID;
import static org.apache.directory.shared.kerberos.codec.types.EncryptionType.RC4_HMAC;
import static org.apache.directory.shared.kerberos.codec.types.EncryptionType.RSAENCRYPTION_ENVOID;
import static org.apache.directory.shared.kerberos.codec.types.EncryptionType.RSAES_OAEP_ENV_OID;
import static org.apache.directory.shared.kerberos.codec.types.EncryptionType.SHA1WITHRSAENCRYPTION_CMSOID;
import java.net.InetAddress;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import javax.security.auth.kerberos.KerberosPrincipal;
import org.apache.directory.api.util.Strings;
import org.apache.directory.server.i18n.I18n;
import org.apache.directory.server.kerberos.protocol.codec.KerberosDecoder;
import org.apache.directory.server.kerberos.shared.crypto.encryption.CipherTextHandler;
import org.apache.directory.server.kerberos.shared.crypto.encryption.KeyUsage;
import org.apache.directory.server.kerberos.shared.replay.ReplayCache;
import org.apache.directory.server.kerberos.shared.store.PrincipalStore;
import org.apache.directory.server.kerberos.shared.store.PrincipalStoreEntry;
import org.apache.directory.shared.kerberos.codec.options.ApOptions;
import org.apache.directory.shared.kerberos.codec.types.EncryptionType;
import org.apache.directory.shared.kerberos.components.EncTicketPart;
import org.apache.directory.shared.kerberos.components.EncryptionKey;
import org.apache.directory.shared.kerberos.components.HostAddress;
import org.apache.directory.shared.kerberos.components.PrincipalName;
import org.apache.directory.shared.kerberos.exceptions.ErrorType;
import org.apache.directory.shared.kerberos.exceptions.KerberosException;
import org.apache.directory.shared.kerberos.messages.ApReq;
import org.apache.directory.shared.kerberos.messages.Authenticator;
import org.apache.directory.shared.kerberos.messages.Ticket;
/**
* An utility class for Kerberos.
*
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
*/
public class KerberosUtils
{
/** A constant for integer optional values */
public static final int NULL = -1;
/** An empty list of principal names */
public static final List<String> EMPTY_PRINCIPAL_NAME = new ArrayList<String>();
/**
* an order preserved map containing cipher names to the corresponding algorithm
* names in the descending order of strength
*/
private static final Map<String, String> cipherAlgoMap = new LinkedHashMap<String, String>();
public static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone( "UTC" );
/** Defines a default date format with a "yyyyMMddHHmmss'Z'" pattern */
public static final SimpleDateFormat UTC_DATE_FORMAT = new SimpleDateFormat( "yyyyMMddHHmmss'Z'" );
private static final Set<EncryptionType> oldEncTypes = new HashSet<EncryptionType>();
static
{
UTC_DATE_FORMAT.setTimeZone( UTC_TIME_ZONE );
cipherAlgoMap.put( "rc4", "ArcFourHmac" );
cipherAlgoMap.put( "aes256", "AES256" );
cipherAlgoMap.put( "aes128", "AES128" );
cipherAlgoMap.put( "des3", "DESede" );
cipherAlgoMap.put( "des", "DES" );
oldEncTypes.add( DES_CBC_CRC );
oldEncTypes.add( DES_CBC_MD4 );
oldEncTypes.add( DES_CBC_MD5 );
oldEncTypes.add( DES_EDE3_CBC_ENV_OID );
oldEncTypes.add( DES3_CBC_MD5 );
oldEncTypes.add( DES3_CBC_SHA1 );
oldEncTypes.add( DES3_CBC_SHA1_KD );
oldEncTypes.add( DSAWITHSHA1_CMSOID );
oldEncTypes.add( MD5WITHRSAENCRYPTION_CMSOID );
oldEncTypes.add( SHA1WITHRSAENCRYPTION_CMSOID );
oldEncTypes.add( RC2CBC_ENVOID );
oldEncTypes.add( RSAENCRYPTION_ENVOID );
oldEncTypes.add( RSAES_OAEP_ENV_OID );
oldEncTypes.add( RC4_HMAC );
}
/**
* Parse a KerberosPrincipal instance and return the names. The Principal name
* is described in RFC 1964 : <br/>
* <br/>
* This name type corresponds to the single-string representation of a<br/>
* Kerberos name. (Within the MIT Kerberos V5 implementation, such<br/>
* names are parseable with the krb5_parse_name() function.) The<br/>
* elements included within this name representation are as follows,<br/>
* proceeding from the beginning of the string:<br/>
* <br/>
* (1) One or more principal name components; if more than one<br/>
* principal name component is included, the components are<br/>
* separated by `/`. Arbitrary octets may be included within<br/>
* principal name components, with the following constraints and<br/>
* special considerations:<br/>
* <br/>
* (1a) Any occurrence of the characters `@` or `/` within a<br/>
* name component must be immediately preceded by the `\`<br/>
* quoting character, to prevent interpretation as a component<br/>
* or realm separator.<br/>
* <br/>
* (1b) The ASCII newline, tab, backspace, and null characters<br/>
* may occur directly within the component or may be<br/>
* represented, respectively, by `\n`, `\t`, `\b`, or `\0`.<br/>
* <br/>
* (1c) If the `\` quoting character occurs outside the contexts<br/>
* described in (1a) and (1b) above, the following character is<br/>
* interpreted literally. As a special case, this allows the<br/>
* doubled representation `\\` to represent a single occurrence<br/>
* of the quoting character.<br/>
* <br/>
* (1d) An occurrence of the `\` quoting character as the last<br/>
* character of a component is illegal.<br/>
* <br/>
* (2) Optionally, a `@` character, signifying that a realm name<br/>
* immediately follows. If no realm name element is included, the<br/>
* local realm name is assumed. The `/` , `:`, and null characters<br/>
* may not occur within a realm name; the `@`, newline, tab, and<br/>
* backspace characters may be included using the quoting<br/>
* conventions described in (1a), (1b), and (1c) above.<br/>
*
* @param principal The principal to be parsed
* @return The names as a List of nameComponent
*
* @throws ParseException if the name is not valid
*/
public static List<String> getNames( KerberosPrincipal principal ) throws ParseException
{
if ( principal == null )
{
return EMPTY_PRINCIPAL_NAME;
}
String names = principal.getName();
if ( Strings.isEmpty( names ) )
{
// Empty name...
return EMPTY_PRINCIPAL_NAME;
}
return getNames( names );
}
/**
* Parse a PrincipalName and return the names.
*/
public static List<String> getNames( String principalNames ) throws ParseException
{
if ( principalNames == null )
{
return EMPTY_PRINCIPAL_NAME;
}
List<String> nameComponents = new ArrayList<String>();
// Start the parsing. Another State Machine :)
char[] chars = principalNames.toCharArray();
boolean escaped = false;
boolean done = false;
int start = 0;
int pos = 0;
for ( int i = 0; i < chars.length; i++ )
{
pos = i;
switch ( chars[i] )
{
case '\\':
escaped = !escaped;
break;
case '/':
if ( escaped )
{
escaped = false;
}
else
{
// We have a new name component
if ( i - start > 0 )
{
String nameComponent = new String( chars, start, i - start );
nameComponents.add( nameComponent );
start = i + 1;
}
else
{
throw new ParseException( I18n.err( I18n.ERR_628 ), i );
}
}
break;
case '@':
if ( escaped )
{
escaped = false;
}
else
{
// We have reached the realm : let's get out
done = true;
}
break;
default:
}
if ( done )
{
// We have a new name component
if ( i - start > 0 )
{
String nameComponent = new String( chars, start, i - start );
nameComponents.add( nameComponent );
start = i + 1;
}
else
{
throw new ParseException( I18n.err( I18n.ERR_628 ), i );
}
break;
}
else if ( i + 1 == chars.length )
{
// We have a new name component
String nameComponent = new String( chars, start, i - start + 1 );
nameComponents.add( nameComponent );
break;
}
}
if ( escaped )
{
throw new ParseException( I18n.err( I18n.ERR_629 ), pos );
}
return nameComponents;
}
/**
* Constructs a KerberosPrincipal from a PrincipalName and an
* optional realm
*
* @param principal The principal name and type
* @param realm The optional realm
*
* @return A KerberosPrincipal
*/
public static KerberosPrincipal getKerberosPrincipal( PrincipalName principal, String realm )
{
String name = principal.getNameString();
if ( !Strings.isEmpty( realm ) )
{
name += '@' + realm;
}
return new KerberosPrincipal( name, principal.getNameType().getValue() );
}
/**
* Get the matching encryption type from the configured types, searching
* into the requested types. We returns the first we find.
*
* @param requestedTypes The client encryption types
* @param configuredTypes The configured encryption types
* @return The first matching encryption type.
*/
public static EncryptionType getBestEncryptionType( Set<EncryptionType> requestedTypes,
Set<EncryptionType> configuredTypes )
{
for ( EncryptionType encryptionType : configuredTypes )
{
if ( requestedTypes.contains( encryptionType ) )
{
return encryptionType;
}
}
return null;
}
/**
* Build a list of encryptionTypes
*
* @param encryptionTypes The encryptionTypes
* @return A list comma separated of the encryptionTypes
*/
public static String getEncryptionTypesString( Set<EncryptionType> encryptionTypes )
{
StringBuilder sb = new StringBuilder();
boolean isFirst = true;
for ( EncryptionType etype : encryptionTypes )
{
if ( isFirst )
{
isFirst = false;
}
else
{
sb.append( ", " );
}
sb.append( etype );
}
return sb.toString();
}
public static boolean isKerberosString( byte[] value )
{
if ( value == null )
{
return false;
}
for ( byte b : value )
{
if ( ( b < 0x20 ) || ( b > 0x7E ) )
{
return false;
}
}
return true;
}
public static String getAlgoNameFromEncType( EncryptionType encType )
{
String cipherName = encType.getName().toLowerCase();
for ( String c : cipherAlgoMap.keySet() )
{
if ( cipherName.startsWith( c ) )
{
return cipherAlgoMap.get( c );
}
}
throw new IllegalArgumentException( "Unknown algorithm name for the encryption type " + encType );
}
/**
* Order a list of EncryptionType in a decreasing strength order
*
* @param etypes The ETypes to order
* @return A list of ordered ETypes. he strongest is on the left.
*/
public static Set<EncryptionType> orderEtypesByStrength( Set<EncryptionType> etypes )
{
Set<EncryptionType> ordered = new LinkedHashSet<EncryptionType>( etypes.size() );
for ( String algo : cipherAlgoMap.values() )
{
for ( EncryptionType encType : etypes )
{
String foundAlgo = getAlgoNameFromEncType( encType );
if ( algo.equals( foundAlgo ) )
{
ordered.add( encType );
}
}
}
return ordered;
}
/**
* Get a PrincipalStoreEntry given a principal. The ErrorType is used to indicate
* whether any resulting error pertains to a server or client.
*/
public static PrincipalStoreEntry getEntry( KerberosPrincipal principal, PrincipalStore store, ErrorType errorType )
throws KerberosException
{
PrincipalStoreEntry entry = null;
try
{
entry = store.getPrincipal( principal );
}
catch ( Exception e )
{
throw new KerberosException( errorType, e );
}
if ( entry == null )
{
throw new KerberosException( errorType );
}
if ( entry.getKeyMap() == null || entry.getKeyMap().isEmpty() )
{
throw new KerberosException( ErrorType.KDC_ERR_NULL_KEY );
}
return entry;
}
/**
* Verifies an AuthHeader using guidelines from RFC 1510 section A.10., "KRB_AP_REQ verification."
*
* @param authHeader
* @param ticket
* @param serverKey
* @param clockSkew
* @param replayCache
* @param emptyAddressesAllowed
* @param clientAddress
* @param lockBox
* @param authenticatorKeyUsage
* @param isValidate
* @return The authenticator.
* @throws KerberosException
*/
public static Authenticator verifyAuthHeader( ApReq authHeader, Ticket ticket, EncryptionKey serverKey,
long clockSkew, ReplayCache replayCache, boolean emptyAddressesAllowed, InetAddress clientAddress,
CipherTextHandler lockBox, KeyUsage authenticatorKeyUsage, boolean isValidate ) throws KerberosException
{
if ( authHeader.getProtocolVersionNumber() != KerberosConstants.KERBEROS_V5 )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_BADVERSION );
}
if ( authHeader.getMessageType() != KerberosMessageType.AP_REQ )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_MSG_TYPE );
}
if ( authHeader.getTicket().getTktVno() != KerberosConstants.KERBEROS_V5 )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_BADVERSION );
}
EncryptionKey ticketKey = null;
if ( authHeader.getOption( ApOptions.USE_SESSION_KEY ) )
{
ticketKey = authHeader.getTicket().getEncTicketPart().getKey();
}
else
{
ticketKey = serverKey;
}
if ( ticketKey == null )
{
// TODO - check server key version number, skvno; requires store
// if ( false )
// {
// throw new KerberosException( ErrorType.KRB_AP_ERR_BADKEYVER );
// }
throw new KerberosException( ErrorType.KRB_AP_ERR_NOKEY );
}
byte[] encTicketPartData = lockBox.decrypt( ticketKey, ticket.getEncPart(),
KeyUsage.AS_OR_TGS_REP_TICKET_WITH_SRVKEY );
EncTicketPart encPart = KerberosDecoder.decodeEncTicketPart( encTicketPartData );
ticket.setEncTicketPart( encPart );
byte[] authenticatorData = lockBox.decrypt( ticket.getEncTicketPart().getKey(), authHeader.getAuthenticator(),
authenticatorKeyUsage );
Authenticator authenticator = KerberosDecoder.decodeAuthenticator( authenticatorData );
if ( !authenticator.getCName().getNameString().equals( ticket.getEncTicketPart().getCName().getNameString() ) )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_BADMATCH );
}
if ( ticket.getEncTicketPart().getClientAddresses() != null )
{
if ( !ticket.getEncTicketPart().getClientAddresses().contains( new HostAddress( clientAddress ) ) )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_BADADDR );
}
}
else
{
if ( !emptyAddressesAllowed )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_BADADDR );
}
}
KerberosPrincipal serverPrincipal = getKerberosPrincipal( ticket.getSName(), ticket.getRealm() );
KerberosPrincipal clientPrincipal = getKerberosPrincipal( authenticator.getCName(), authenticator.getCRealm() );
KerberosTime clientTime = authenticator.getCtime();
int clientMicroSeconds = authenticator.getCusec();
if ( replayCache != null )
{
if ( replayCache.isReplay( serverPrincipal, clientPrincipal, clientTime, clientMicroSeconds ) )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_REPEAT );
}
replayCache.save( serverPrincipal, clientPrincipal, clientTime, clientMicroSeconds );
}
if ( !authenticator.getCtime().isInClockSkew( clockSkew ) )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_SKEW );
}
/*
* "The server computes the age of the ticket: local (server) time minus
* the starttime inside the Ticket. If the starttime is later than the
* current time by more than the allowable clock skew, or if the INVALID
* flag is set in the ticket, the KRB_AP_ERR_TKT_NYV error is returned."
*/
KerberosTime startTime = ( ticket.getEncTicketPart().getStartTime() != null ) ? ticket.getEncTicketPart()
.getStartTime() : ticket.getEncTicketPart().getAuthTime();
KerberosTime now = new KerberosTime();
boolean isValidStartTime = startTime.lessThan( now );
if ( !isValidStartTime || ( ticket.getEncTicketPart().getFlags().isInvalid() && !isValidate ) )
{
// it hasn't yet become valid
throw new KerberosException( ErrorType.KRB_AP_ERR_TKT_NYV );
}
// TODO - doesn't take into account skew
if ( !ticket.getEncTicketPart().getEndTime().greaterThan( now ) )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_TKT_EXPIRED );
}
authHeader.getApOptions().set( ApOptions.MUTUAL_REQUIRED );
return authenticator;
}
/**
* checks if the given encryption type is *new* (ref sec#3.1.3 of rfc4120)
*
* @param eType the encryption type
* @return true if the encryption type is new, false otherwise
*/
public static boolean isNewEncryptionType( EncryptionType eType )
{
return !oldEncTypes.contains( eType );
}
/**
* Verifies an AuthHeader using guidelines from RFC 1510 section A.10., "KRB_AP_REQ verification."
*
* @param authHeader
* @param ticket
* @param serverKey
* @param clockSkew
* @param replayCache
* @param emptyAddressesAllowed
* @param clientAddress
* @param lockBox
* @param authenticatorKeyUsage
* @param isValidate
* @return The authenticator.
* @throws KerberosException
*
public static Authenticator verifyAuthHeader( ApplicationRequest authHeader, Ticket ticket, EncryptionKey serverKey,
long clockSkew, ReplayCache replayCache, boolean emptyAddressesAllowed, InetAddress clientAddress,
CipherTextHandler lockBox, KeyUsage authenticatorKeyUsage, boolean isValidate ) throws KerberosException
{
if ( authHeader.getProtocolVersionNumber() != KerberosConstants.KERBEROS_V5 )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_BADVERSION );
}
if ( authHeader.getMessageType() != KerberosMessageType.AP_REQ )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_MSG_TYPE );
}
if ( authHeader.getTicket().getTktVno() != KerberosConstants.KERBEROS_V5 )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_BADVERSION );
}
EncryptionKey ticketKey = null;
if ( authHeader.getOption( ApOptions.USE_SESSION_KEY ) )
{
ticketKey = authHeader.getTicket().getEncTicketPart().getSessionKey();
}
else
{
ticketKey = serverKey;
}
if ( ticketKey == null )
{
// TODO - check server key version number, skvno; requires store
if ( false )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_BADKEYVER );
}
throw new KerberosException( ErrorType.KRB_AP_ERR_NOKEY );
}
EncTicketPart encPart = ( EncTicketPart ) lockBox.unseal( EncTicketPart.class, ticketKey, ticket.getEncPart(),
KeyUsage.NUMBER2 );
ticket.setEncTicketPart( encPart );
Authenticator authenticator = ( Authenticator ) lockBox.unseal( Authenticator.class, ticket.getEncTicketPart().getSessionKey(),
authHeader.getEncPart(), authenticatorKeyUsage );
if ( !authenticator.getClientPrincipal().getName().equals( ticket.getEncTicketPart().getClientPrincipal().getName() ) )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_BADMATCH );
}
if ( ticket.getEncTicketPart().getClientAddresses() != null )
{
if ( !ticket.getEncTicketPart().getClientAddresses().contains( new HostAddress( clientAddress ) ) )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_BADADDR );
}
}
else
{
if ( !emptyAddressesAllowed )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_BADADDR );
}
}
KerberosPrincipal serverPrincipal = ticket.getServerPrincipal();
KerberosPrincipal clientPrincipal = authenticator.getClientPrincipal();
KerberosTime clientTime = authenticator.getClientTime();
int clientMicroSeconds = authenticator.getClientMicroSecond();
if ( replayCache.isReplay( serverPrincipal, clientPrincipal, clientTime, clientMicroSeconds ) )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_REPEAT );
}
replayCache.save( serverPrincipal, clientPrincipal, clientTime, clientMicroSeconds );
if ( !authenticator.getClientTime().isInClockSkew( clockSkew ) )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_SKEW );
}
/*
* "The server computes the age of the ticket: local (server) time minus
* the starttime inside the Ticket. If the starttime is later than the
* current time by more than the allowable clock skew, or if the INVALID
* flag is set in the ticket, the KRB_AP_ERR_TKT_NYV error is returned."
*
KerberosTime startTime = ( ticket.getEncTicketPart().getStartTime() != null ) ? ticket.getEncTicketPart().getStartTime() : ticket.getEncTicketPart().getAuthTime();
KerberosTime now = new KerberosTime();
boolean isValidStartTime = startTime.lessThan( now );
if ( !isValidStartTime || ( ticket.getEncTicketPart().getFlags().isInvalid() && !isValidate ) )
{
// it hasn't yet become valid
throw new KerberosException( ErrorType.KRB_AP_ERR_TKT_NYV );
}
// TODO - doesn't take into account skew
if ( !ticket.getEncTicketPart().getEndTime().greaterThan( now ) )
{
throw new KerberosException( ErrorType.KRB_AP_ERR_TKT_EXPIRED );
}
authHeader.setOption( ApOptions.MUTUAL_REQUIRED );
return authenticator;
}*/
}