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 }