View Javadoc

1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *  
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *  
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License. 
18   *  
19   */
20  package org.apache.directory.server.kerberos.shared;
21  
22  import java.net.InetAddress;
23  import java.text.ParseException;
24  import java.util.ArrayList;
25  import java.util.List;
26  import java.util.Set;
27  
28  import javax.security.auth.kerberos.KerberosPrincipal;
29  
30  import org.apache.directory.server.kerberos.shared.crypto.encryption.CipherTextHandler;
31  import org.apache.directory.server.kerberos.shared.crypto.encryption.EncryptionType;
32  import org.apache.directory.server.kerberos.shared.crypto.encryption.KeyUsage;
33  import org.apache.directory.server.kerberos.shared.exceptions.ErrorType;
34  import org.apache.directory.server.kerberos.shared.exceptions.KerberosException;
35  import org.apache.directory.server.kerberos.shared.messages.ApplicationRequest;
36  import org.apache.directory.server.kerberos.shared.messages.components.Authenticator;
37  import org.apache.directory.server.kerberos.shared.messages.components.EncTicketPart;
38  import org.apache.directory.server.kerberos.shared.messages.components.Ticket;
39  import org.apache.directory.server.kerberos.shared.messages.value.ApOptions;
40  import org.apache.directory.server.kerberos.shared.messages.value.EncryptionKey;
41  import org.apache.directory.server.kerberos.shared.messages.value.HostAddress;
42  import org.apache.directory.server.kerberos.shared.messages.value.KerberosTime;
43  import org.apache.directory.server.kerberos.shared.messages.value.PrincipalName;
44  import org.apache.directory.server.kerberos.shared.replay.ReplayCache;
45  import org.apache.directory.server.kerberos.shared.store.PrincipalStore;
46  import org.apache.directory.server.kerberos.shared.store.PrincipalStoreEntry;
47  import org.apache.directory.shared.ldap.util.StringTools;
48  
49  /**
50   * An utility class for Kerberos.
51   *
52   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
53   */
54  public class KerberosUtils
55  {
56      /** A constant for integer optional values */
57      public static final int NULL = -1;
58  
59      /** An empty list of principal names */
60      public static final List<String> EMPTY_PRINCIPAL_NAME = new ArrayList<String>();
61      
62      /**
63       * Parse a KerberosPrincipal instance and return the names. The Principal name
64       * is described in RFC 1964 : <br/>
65       * <br/>
66       * This name type corresponds to the single-string representation of a<br/>
67       * Kerberos name.  (Within the MIT Kerberos V5 implementation, such<br/>
68       * names are parseable with the krb5_parse_name() function.)  The<br/>
69       * elements included within this name representation are as follows,<br/>
70       * proceeding from the beginning of the string:<br/>
71       * <br/>
72       *  (1) One or more principal name components; if more than one<br/>
73       *  principal name component is included, the components are<br/>
74       *  separated by `/`.  Arbitrary octets may be included within<br/>
75       *  principal name components, with the following constraints and<br/>
76       *  special considerations:<br/>
77       * <br/>
78       *     (1a) Any occurrence of the characters `@` or `/` within a<br/>
79       *     name component must be immediately preceded by the `\`<br/>
80       *     quoting character, to prevent interpretation as a component<br/>
81       *     or realm separator.<br/>
82       * <br/>
83       *     (1b) The ASCII newline, tab, backspace, and null characters<br/>
84       *     may occur directly within the component or may be<br/>
85       *     represented, respectively, by `\n`, `\t`, `\b`, or `\0`.<br/>
86       * <br/>
87       *     (1c) If the `\` quoting character occurs outside the contexts<br/>
88       *     described in (1a) and (1b) above, the following character is<br/>
89       *     interpreted literally.  As a special case, this allows the<br/>
90       *     doubled representation `\\` to represent a single occurrence<br/>
91       *     of the quoting character.<br/>
92       * <br/>
93       *     (1d) An occurrence of the `\` quoting character as the last<br/>
94       *     character of a component is illegal.<br/>
95       * <br/>
96       *  (2) Optionally, a `@` character, signifying that a realm name<br/>
97       *  immediately follows. If no realm name element is included, the<br/>
98       *  local realm name is assumed.  The `/` , `:`, and null characters<br/>
99       *  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 }