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