001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied. See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 *
019 */
020 package org.apache.directory.server.kerberos.shared;
021
022 import java.net.InetAddress;
023 import java.text.ParseException;
024 import java.util.ArrayList;
025 import java.util.List;
026 import java.util.Set;
027
028 import javax.security.auth.kerberos.KerberosPrincipal;
029
030 import org.apache.directory.server.i18n.I18n;
031 import org.apache.directory.server.kerberos.shared.crypto.encryption.CipherTextHandler;
032 import org.apache.directory.server.kerberos.shared.crypto.encryption.EncryptionType;
033 import org.apache.directory.server.kerberos.shared.crypto.encryption.KeyUsage;
034 import org.apache.directory.server.kerberos.shared.exceptions.ErrorType;
035 import org.apache.directory.server.kerberos.shared.exceptions.KerberosException;
036 import org.apache.directory.server.kerberos.shared.messages.ApplicationRequest;
037 import org.apache.directory.server.kerberos.shared.messages.components.Authenticator;
038 import org.apache.directory.server.kerberos.shared.messages.components.EncTicketPart;
039 import org.apache.directory.server.kerberos.shared.messages.components.Ticket;
040 import org.apache.directory.server.kerberos.shared.messages.value.ApOptions;
041 import org.apache.directory.server.kerberos.shared.messages.value.EncryptionKey;
042 import org.apache.directory.server.kerberos.shared.messages.value.HostAddress;
043 import org.apache.directory.server.kerberos.shared.messages.value.KerberosTime;
044 import org.apache.directory.server.kerberos.shared.messages.value.PrincipalName;
045 import org.apache.directory.server.kerberos.shared.replay.ReplayCache;
046 import org.apache.directory.server.kerberos.shared.store.PrincipalStore;
047 import org.apache.directory.server.kerberos.shared.store.PrincipalStoreEntry;
048 import org.apache.directory.shared.ldap.util.StringTools;
049
050 /**
051 * An utility class for Kerberos.
052 *
053 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
054 */
055 public class KerberosUtils
056 {
057 /** A constant for integer optional values */
058 public static final int NULL = -1;
059
060 /** An empty list of principal names */
061 public static final List<String> EMPTY_PRINCIPAL_NAME = new ArrayList<String>();
062
063 /**
064 * Parse a KerberosPrincipal instance and return the names. The Principal name
065 * is described in RFC 1964 : <br/>
066 * <br/>
067 * This name type corresponds to the single-string representation of a<br/>
068 * Kerberos name. (Within the MIT Kerberos V5 implementation, such<br/>
069 * names are parseable with the krb5_parse_name() function.) The<br/>
070 * elements included within this name representation are as follows,<br/>
071 * proceeding from the beginning of the string:<br/>
072 * <br/>
073 * (1) One or more principal name components; if more than one<br/>
074 * principal name component is included, the components are<br/>
075 * separated by `/`. Arbitrary octets may be included within<br/>
076 * principal name components, with the following constraints and<br/>
077 * special considerations:<br/>
078 * <br/>
079 * (1a) Any occurrence of the characters `@` or `/` within a<br/>
080 * name component must be immediately preceded by the `\`<br/>
081 * quoting character, to prevent interpretation as a component<br/>
082 * or realm separator.<br/>
083 * <br/>
084 * (1b) The ASCII newline, tab, backspace, and null characters<br/>
085 * may occur directly within the component or may be<br/>
086 * represented, respectively, by `\n`, `\t`, `\b`, or `\0`.<br/>
087 * <br/>
088 * (1c) If the `\` quoting character occurs outside the contexts<br/>
089 * described in (1a) and (1b) above, the following character is<br/>
090 * interpreted literally. As a special case, this allows the<br/>
091 * doubled representation `\\` to represent a single occurrence<br/>
092 * of the quoting character.<br/>
093 * <br/>
094 * (1d) An occurrence of the `\` quoting character as the last<br/>
095 * character of a component is illegal.<br/>
096 * <br/>
097 * (2) Optionally, a `@` character, signifying that a realm name<br/>
098 * immediately follows. If no realm name element is included, the<br/>
099 * local realm name is assumed. The `/` , `:`, and null characters<br/>
100 * may not occur within a realm name; the `@`, newline, tab, and<br/>
101 * backspace characters may be included using the quoting<br/>
102 * conventions described in (1a), (1b), and (1c) above.<br/>
103 *
104 * @param principal The principal to be parsed
105 * @return The names as a List of nameComponent
106 *
107 * @throws ParseException if the name is not valid
108 */
109 public static List<String> getNames( KerberosPrincipal principal ) throws ParseException
110 {
111 if ( principal == null )
112 {
113 return EMPTY_PRINCIPAL_NAME;
114 }
115
116 String names = principal.getName();
117
118 if ( StringTools.isEmpty( names ) )
119 {
120 // Empty name...
121 return EMPTY_PRINCIPAL_NAME;
122 }
123
124 return getNames( names );
125 }
126
127 /**
128 * Parse a PrincipalName and return the names.
129 */
130 public static List<String> getNames( String principalNames ) throws ParseException
131 {
132 if ( principalNames == null )
133 {
134 return EMPTY_PRINCIPAL_NAME;
135 }
136
137 List<String> nameComponents = new ArrayList<String>();
138
139 // Start the parsing. Another State Machine :)
140 char[] chars = principalNames.toCharArray();
141
142 boolean escaped = false;
143 boolean done = false;
144 int start = 0;
145 int pos = 0;
146
147 for ( int i = 0; i < chars.length; i++ )
148 {
149 pos = i;
150
151 switch ( chars[i] )
152 {
153 case '\\' :
154 escaped = !escaped;
155 break;
156
157 case '/' :
158 if ( escaped )
159 {
160 escaped = false;
161 }
162 else
163 {
164 // We have a new name component
165 if ( i - start > 0 )
166 {
167 String nameComponent = new String( chars, start, i - start );
168 nameComponents.add( nameComponent );
169 start = i + 1;
170 }
171 else
172 {
173 throw new ParseException( I18n.err( I18n.ERR_628 ), i );
174 }
175 }
176
177 break;
178
179 case '@' :
180 if ( escaped )
181 {
182 escaped = false;
183 }
184 else
185 {
186 // We have reached the realm : let's get out
187 done = true;
188 // We have a new name component
189
190 if ( i - start > 0 )
191 {
192 String nameComponent = new String( chars, start, i - start );
193 nameComponents.add( nameComponent );
194 start = i + 1;
195 }
196 else
197 {
198 throw new ParseException( I18n.err( I18n.ERR_628 ), i );
199 }
200 }
201
202 break;
203
204 default :
205 }
206
207 if ( done )
208 {
209 break;
210 }
211 }
212
213 if ( escaped )
214 {
215 throw new ParseException( I18n.err( I18n.ERR_629 ), pos );
216 }
217
218 return nameComponents;
219 }
220
221
222 /**
223 * Constructs a KerberosPrincipal from a PrincipalName and an
224 * optional realm
225 *
226 * @param principal The principal name and type
227 * @param realm The optional realm
228 *
229 * @return A KerberosPrincipal
230 */
231 public static KerberosPrincipal getKerberosPrincipal( PrincipalName principal, String realm )
232 {
233 String name = principal.getNameString();
234
235 if ( !StringTools.isEmpty( realm ) )
236 {
237 name += '@' + realm;
238 }
239
240 return new KerberosPrincipal( name, principal.getNameType().getOrdinal() );
241 }
242
243
244 /**
245 * Get the matching encryption type from the configured types, searching
246 * into the requested types. We returns the first we find.
247 *
248 * @param requestedTypes The client encryption types
249 * @param configuredTypes The configured encryption types
250 * @return The first matching encryption type.
251 */
252 public static EncryptionType getBestEncryptionType( Set<EncryptionType> requestedTypes, Set<EncryptionType> configuredTypes )
253 {
254 for ( EncryptionType encryptionType:requestedTypes )
255 {
256 if ( configuredTypes.contains( encryptionType ) )
257 {
258 return encryptionType;
259 }
260 }
261
262 return null;
263 }
264
265
266 /**
267 * Build a list of encryptionTypes
268 *
269 * @param encryptionTypes The encryptionTypes
270 * @return A list comma separated of the encryptionTypes
271 */
272 public static String getEncryptionTypesString( Set<EncryptionType> encryptionTypes )
273 {
274 StringBuilder sb = new StringBuilder();
275 boolean isFirst = true;
276
277 for ( EncryptionType etype:encryptionTypes )
278 {
279 if ( isFirst )
280 {
281 isFirst = false;
282 }
283 else
284 {
285 sb.append( ", " );
286 }
287
288 sb.append( etype );
289 }
290
291 return sb.toString();
292 }
293
294
295 /**
296 * Get a PrincipalStoreEntry given a principal. The ErrorType is used to indicate
297 * whether any resulting error pertains to a server or client.
298 *
299 * @param principal
300 * @param store
301 * @param errorType
302 * @return The PrincipalStoreEntry
303 * @throws Exception
304 */
305 public static PrincipalStoreEntry getEntry( KerberosPrincipal principal, PrincipalStore store, ErrorType errorType )
306 throws KerberosException
307 {
308 PrincipalStoreEntry entry = null;
309
310 try
311 {
312 entry = store.getPrincipal( principal );
313 }
314 catch ( Exception e )
315 {
316 throw new KerberosException( errorType, e );
317 }
318
319 if ( entry == null )
320 {
321 throw new KerberosException( errorType );
322 }
323
324 if ( entry.getKeyMap() == null || entry.getKeyMap().isEmpty() )
325 {
326 throw new KerberosException( ErrorType.KDC_ERR_NULL_KEY );
327 }
328
329 return entry;
330 }
331
332
333 /**
334 * Verifies an AuthHeader using guidelines from RFC 1510 section A.10., "KRB_AP_REQ verification."
335 *
336 * @param authHeader
337 * @param ticket
338 * @param serverKey
339 * @param clockSkew
340 * @param replayCache
341 * @param emptyAddressesAllowed
342 * @param clientAddress
343 * @param lockBox
344 * @param authenticatorKeyUsage
345 * @param isValidate
346 * @return The authenticator.
347 * @throws KerberosException
348 */
349 public static Authenticator verifyAuthHeader( ApplicationRequest authHeader, Ticket ticket, EncryptionKey serverKey,
350 long clockSkew, ReplayCache replayCache, boolean emptyAddressesAllowed, InetAddress clientAddress,
351 CipherTextHandler lockBox, KeyUsage authenticatorKeyUsage, boolean isValidate ) throws KerberosException
352 {
353 if ( authHeader.getProtocolVersionNumber() != KerberosConstants.KERBEROS_V5 )
354 {
355 throw new KerberosException( ErrorType.KRB_AP_ERR_BADVERSION );
356 }
357
358 if ( authHeader.getMessageType() != KerberosMessageType.AP_REQ )
359 {
360 throw new KerberosException( ErrorType.KRB_AP_ERR_MSG_TYPE );
361 }
362
363 if ( authHeader.getTicket().getTktVno() != KerberosConstants.KERBEROS_V5 )
364 {
365 throw new KerberosException( ErrorType.KRB_AP_ERR_BADVERSION );
366 }
367
368 EncryptionKey ticketKey = null;
369
370 if ( authHeader.getOption( ApOptions.USE_SESSION_KEY ) )
371 {
372 ticketKey = authHeader.getTicket().getEncTicketPart().getSessionKey();
373 }
374 else
375 {
376 ticketKey = serverKey;
377 }
378
379 if ( ticketKey == null )
380 {
381 // TODO - check server key version number, skvno; requires store
382 if ( false )
383 {
384 throw new KerberosException( ErrorType.KRB_AP_ERR_BADKEYVER );
385 }
386
387 throw new KerberosException( ErrorType.KRB_AP_ERR_NOKEY );
388 }
389
390 EncTicketPart encPart = ( EncTicketPart ) lockBox.unseal( EncTicketPart.class, ticketKey, ticket.getEncPart(),
391 KeyUsage.NUMBER2 );
392 ticket.setEncTicketPart( encPart );
393
394 Authenticator authenticator = ( Authenticator ) lockBox.unseal( Authenticator.class, ticket.getEncTicketPart().getSessionKey(),
395 authHeader.getEncPart(), authenticatorKeyUsage );
396
397 if ( !authenticator.getClientPrincipal().getName().equals( ticket.getEncTicketPart().getClientPrincipal().getName() ) )
398 {
399 throw new KerberosException( ErrorType.KRB_AP_ERR_BADMATCH );
400 }
401
402 if ( ticket.getEncTicketPart().getClientAddresses() != null )
403 {
404 if ( !ticket.getEncTicketPart().getClientAddresses().contains( new HostAddress( clientAddress ) ) )
405 {
406 throw new KerberosException( ErrorType.KRB_AP_ERR_BADADDR );
407 }
408 }
409 else
410 {
411 if ( !emptyAddressesAllowed )
412 {
413 throw new KerberosException( ErrorType.KRB_AP_ERR_BADADDR );
414 }
415 }
416
417 KerberosPrincipal serverPrincipal = ticket.getServerPrincipal();
418 KerberosPrincipal clientPrincipal = authenticator.getClientPrincipal();
419 KerberosTime clientTime = authenticator.getClientTime();
420 int clientMicroSeconds = authenticator.getClientMicroSecond();
421
422 if ( replayCache.isReplay( serverPrincipal, clientPrincipal, clientTime, clientMicroSeconds ) )
423 {
424 throw new KerberosException( ErrorType.KRB_AP_ERR_REPEAT );
425 }
426
427 replayCache.save( serverPrincipal, clientPrincipal, clientTime, clientMicroSeconds );
428
429 if ( !authenticator.getClientTime().isInClockSkew( clockSkew ) )
430 {
431 throw new KerberosException( ErrorType.KRB_AP_ERR_SKEW );
432 }
433
434 /*
435 * "The server computes the age of the ticket: local (server) time minus
436 * the starttime inside the Ticket. If the starttime is later than the
437 * current time by more than the allowable clock skew, or if the INVALID
438 * flag is set in the ticket, the KRB_AP_ERR_TKT_NYV error is returned."
439 */
440 KerberosTime startTime = ( ticket.getEncTicketPart().getStartTime() != null ) ? ticket.getEncTicketPart().getStartTime() : ticket.getEncTicketPart().getAuthTime();
441
442 KerberosTime now = new KerberosTime();
443 boolean isValidStartTime = startTime.lessThan( now );
444
445 if ( !isValidStartTime || ( ticket.getEncTicketPart().getFlags().isInvalid() && !isValidate ) )
446 {
447 // it hasn't yet become valid
448 throw new KerberosException( ErrorType.KRB_AP_ERR_TKT_NYV );
449 }
450
451 // TODO - doesn't take into account skew
452 if ( !ticket.getEncTicketPart().getEndTime().greaterThan( now ) )
453 {
454 throw new KerberosException( ErrorType.KRB_AP_ERR_TKT_EXPIRED );
455 }
456
457 authHeader.setOption( ApOptions.MUTUAL_REQUIRED );
458
459 return authenticator;
460 }
461 }