1 /* 2 * Copyright (c) 2010, 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package sun.security.krb5; 27 28 import java.io.IOException; 29 import java.util.Arrays; 30 import javax.security.auth.kerberos.KeyTab; 31 import sun.security.jgss.krb5.Krb5Util; 32 import sun.security.krb5.internal.HostAddresses; 33 import sun.security.krb5.internal.KDCOptions; 34 import sun.security.krb5.internal.KRBError; 35 import sun.security.krb5.internal.KerberosTime; 36 import sun.security.krb5.internal.Krb5; 37 import sun.security.krb5.internal.PAData; 38 import sun.security.krb5.internal.crypto.EType; 39 40 /** 41 * A manager class for AS-REQ communications. 42 * 43 * This class does: 44 * 1. Gather information to create AS-REQ 45 * 2. Create and send AS-REQ 46 * 3. Receive AS-REP and KRB-ERROR (-KRB_ERR_RESPONSE_TOO_BIG) and parse them 47 * 4. Emit credentials and secret keys (for JAAS storeKey=true with password) 48 * 49 * This class does not: 50 * 1. Deal with real communications (KdcComm does it, and TGS-REQ) 51 * a. Name of KDCs for a realm 52 * b. Server availability, timeout, UDP or TCP 53 * d. KRB_ERR_RESPONSE_TOO_BIG 54 * 2. Stores its own copy of password, this means: 55 * a. Do not change/wipe it before Builder finish 56 * b. Builder will not wipe it for you 57 * 58 * With this class: 59 * 1. KrbAsReq has only one constructor 60 * 2. Krb5LoginModule and Kinit call a single builder 61 * 3. Better handling of sensitive info 62 * 63 * @since 1.7 64 */ 65 66 public final class KrbAsReqBuilder { 67 68 // Common data for AS-REQ fields 69 private KDCOptions options; 70 private PrincipalName cname; 71 private PrincipalName refCname; // May be changed by referrals 72 private PrincipalName sname; 73 private KerberosTime from; 74 private KerberosTime till; 75 private KerberosTime rtime; 76 private HostAddresses addresses; 77 78 // Secret source: can't be changed once assigned, only one (of the two 79 // sources) can be set to non-null 80 private final char[] password; 81 private final KeyTab ktab; 82 83 // Used to create a ENC-TIMESTAMP in the 2nd AS-REQ 84 private PAData[] paList; // PA-DATA from both KRB-ERROR and AS-REP. 85 // Used by getKeys() only. 86 // Only AS-REP should be enough per RFC, 87 // combined in case etypes are different. 88 89 // The generated and received: 90 private KrbAsReq req; 91 private KrbAsRep rep; 92 93 private static enum State { 94 INIT, // Initialized, can still add more initialization info 95 REQ_OK, // AS-REQ performed 96 DESTROYED, // Destroyed, not usable anymore 97 } 98 private State state; 99 100 // Called by other constructors init(PrincipalName cname)101 private void init(PrincipalName cname) 102 throws KrbException { 103 this.cname = cname; 104 this.refCname = cname; 105 state = State.INIT; 106 } 107 108 /** 109 * Creates a builder to be used by {@code cname} with existing keys. 110 * 111 * @param cname the client of the AS-REQ. Must not be null. Might have no 112 * realm, where default realm will be used. This realm will be the target 113 * realm for AS-REQ. I believe a client should only get initial TGT from 114 * its own realm. 115 * @param ktab must not be null. If empty, might be quite useless. 116 * This argument will neither be modified nor stored by the method. 117 * @throws KrbException 118 */ KrbAsReqBuilder(PrincipalName cname, KeyTab ktab)119 public KrbAsReqBuilder(PrincipalName cname, KeyTab ktab) 120 throws KrbException { 121 init(cname); 122 this.ktab = ktab; 123 this.password = null; 124 } 125 126 /** 127 * Creates a builder to be used by {@code cname} with a known password. 128 * 129 * @param cname the client of the AS-REQ. Must not be null. Might have no 130 * realm, where default realm will be used. This realm will be the target 131 * realm for AS-REQ. I believe a client should only get initial TGT from 132 * its own realm. 133 * @param pass must not be null. This argument will neither be modified 134 * nor stored by the method. 135 * @throws KrbException 136 */ KrbAsReqBuilder(PrincipalName cname, char[] pass)137 public KrbAsReqBuilder(PrincipalName cname, char[] pass) 138 throws KrbException { 139 init(cname); 140 this.password = pass.clone(); 141 this.ktab = null; 142 } 143 144 /** 145 * Retrieves an array of secret keys for the client. This is used when 146 * the client supplies password but need keys to act as an acceptor. For 147 * an initiator, it must be called after AS-REQ is performed (state is OK). 148 * For an acceptor, it can be called when this KrbAsReqBuilder object is 149 * constructed (state is INIT). 150 * @param isInitiator if the caller is an initiator 151 * @return generated keys from password. PA-DATA from server might be used. 152 * All "default_tkt_enctypes" keys will be generated, Never null. 153 * @throws IllegalStateException if not constructed from a password 154 * @throws KrbException 155 */ getKeys(boolean isInitiator)156 public EncryptionKey[] getKeys(boolean isInitiator) throws KrbException { 157 checkState(isInitiator?State.REQ_OK:State.INIT, "Cannot get keys"); 158 if (password != null) { 159 int[] eTypes = EType.getDefaults("default_tkt_enctypes"); 160 EncryptionKey[] result = new EncryptionKey[eTypes.length]; 161 162 /* 163 * Returns an array of keys. Before KrbAsReqBuilder, all etypes 164 * use the same salt which is either the default one or a new salt 165 * coming from PA-DATA. After KrbAsReqBuilder, each etype uses its 166 * own new salt from PA-DATA. For an etype with no PA-DATA new salt 167 * at all, what salt should it use? 168 * 169 * Commonly, the stored keys are only to be used by an acceptor to 170 * decrypt service ticket in AP-REQ. Most impls only allow keys 171 * from a keytab on acceptor, but unfortunately (?) Java supports 172 * acceptor using password. In this case, if the service ticket is 173 * encrypted using an etype which we don't have PA-DATA new salt, 174 * using the default salt might be wrong (say, case-insensitive 175 * user name). Instead, we would use the new salt of another etype. 176 */ 177 178 String salt = null; // the saved new salt 179 try { 180 for (int i=0; i<eTypes.length; i++) { 181 // First round, only calculate those have a PA entry 182 PAData.SaltAndParams snp = 183 PAData.getSaltAndParams(eTypes[i], paList); 184 if (snp != null) { 185 // Never uses a salt for rc4-hmac, it does not use 186 // a salt at all 187 if (eTypes[i] != EncryptedData.ETYPE_ARCFOUR_HMAC && 188 snp.salt != null) { 189 salt = snp.salt; 190 } 191 result[i] = EncryptionKey.acquireSecretKey(cname, 192 password, 193 eTypes[i], 194 snp); 195 } 196 } 197 // No new salt from PA, maybe empty, maybe only rc4-hmac 198 if (salt == null) salt = cname.getSalt(); 199 for (int i=0; i<eTypes.length; i++) { 200 // Second round, calculate those with no PA entry 201 if (result[i] == null) { 202 result[i] = EncryptionKey.acquireSecretKey(password, 203 salt, 204 eTypes[i], 205 null); 206 } 207 } 208 } catch (IOException ioe) { 209 KrbException ke = new KrbException(Krb5.ASN1_PARSE_ERROR); 210 ke.initCause(ioe); 211 throw ke; 212 } 213 return result; 214 } else { 215 throw new IllegalStateException("Required password not provided"); 216 } 217 } 218 219 /** 220 * Sets or clears options. If cleared, default options will be used 221 * at creation time. 222 * @param options 223 */ setOptions(KDCOptions options)224 public void setOptions(KDCOptions options) { 225 checkState(State.INIT, "Cannot specify options"); 226 this.options = options; 227 } 228 setTill(KerberosTime till)229 public void setTill(KerberosTime till) { 230 checkState(State.INIT, "Cannot specify till"); 231 this.till = till; 232 } 233 setRTime(KerberosTime rtime)234 public void setRTime(KerberosTime rtime) { 235 checkState(State.INIT, "Cannot specify rtime"); 236 this.rtime = rtime; 237 } 238 239 /** 240 * Sets or clears target. If cleared, KrbAsReq might choose krbtgt 241 * for cname realm 242 * @param sname 243 */ setTarget(PrincipalName sname)244 public void setTarget(PrincipalName sname) { 245 checkState(State.INIT, "Cannot specify target"); 246 this.sname = sname; 247 } 248 249 /** 250 * Adds or clears addresses. KrbAsReq might add some if empty 251 * field not allowed 252 * @param addresses 253 */ setAddresses(HostAddresses addresses)254 public void setAddresses(HostAddresses addresses) { 255 checkState(State.INIT, "Cannot specify addresses"); 256 this.addresses = addresses; 257 } 258 259 /** 260 * Build a KrbAsReq object from all info fed above. Normally this method 261 * will be called twice: initial AS-REQ and second with pakey 262 * @param key null (initial AS-REQ) or pakey (with preauth) 263 * @return the KrbAsReq object 264 * @throws KrbException 265 * @throws IOException 266 */ build(EncryptionKey key, ReferralsState referralsState)267 private KrbAsReq build(EncryptionKey key, ReferralsState referralsState) 268 throws KrbException, IOException { 269 PAData[] extraPAs = null; 270 int[] eTypes; 271 if (password != null) { 272 eTypes = EType.getDefaults("default_tkt_enctypes"); 273 } else { 274 EncryptionKey[] ks = Krb5Util.keysFromJavaxKeyTab(ktab, cname); 275 eTypes = EType.getDefaults("default_tkt_enctypes", 276 ks); 277 for (EncryptionKey k: ks) k.destroy(); 278 } 279 options = (options == null) ? new KDCOptions() : options; 280 if (referralsState.isEnabled()) { 281 options.set(KDCOptions.CANONICALIZE, true); 282 extraPAs = new PAData[]{ new PAData(Krb5.PA_REQ_ENC_PA_REP, 283 new byte[]{}) }; 284 } else { 285 options.set(KDCOptions.CANONICALIZE, false); 286 } 287 return new KrbAsReq(key, 288 options, 289 refCname, 290 sname, 291 from, 292 till, 293 rtime, 294 eTypes, 295 addresses, 296 extraPAs); 297 } 298 299 /** 300 * Parses AS-REP, decrypts enc-part, retrieves ticket and session key 301 * @throws KrbException 302 * @throws Asn1Exception 303 * @throws IOException 304 */ resolve()305 private KrbAsReqBuilder resolve() 306 throws KrbException, Asn1Exception, IOException { 307 if (ktab != null) { 308 rep.decryptUsingKeyTab(ktab, req, cname); 309 } else { 310 rep.decryptUsingPassword(password, req, cname); 311 } 312 if (rep.getPA() != null) { 313 if (paList == null || paList.length == 0) { 314 paList = rep.getPA(); 315 } else { 316 int extraLen = rep.getPA().length; 317 if (extraLen > 0) { 318 int oldLen = paList.length; 319 paList = Arrays.copyOf(paList, paList.length + extraLen); 320 System.arraycopy(rep.getPA(), 0, paList, oldLen, extraLen); 321 } 322 } 323 } 324 return this; 325 } 326 327 /** 328 * Communication until AS-REP or non preauth-related KRB-ERROR received 329 * @throws KrbException 330 * @throws IOException 331 */ send()332 private KrbAsReqBuilder send() throws KrbException, IOException { 333 boolean preAuthFailedOnce = false; 334 KdcComm comm = null; 335 EncryptionKey pakey = null; 336 ReferralsState referralsState = new ReferralsState(); 337 while (true) { 338 if (referralsState.refreshComm()) { 339 comm = new KdcComm(refCname.getRealmAsString()); 340 } 341 try { 342 req = build(pakey, referralsState); 343 rep = new KrbAsRep(comm.send(req.encoding())); 344 return this; 345 } catch (KrbException ke) { 346 if (!preAuthFailedOnce && ( 347 ke.returnCode() == Krb5.KDC_ERR_PREAUTH_FAILED || 348 ke.returnCode() == Krb5.KDC_ERR_PREAUTH_REQUIRED)) { 349 if (Krb5.DEBUG) { 350 System.out.println("KrbAsReqBuilder: " + 351 "PREAUTH FAILED/REQ, re-send AS-REQ"); 352 } 353 preAuthFailedOnce = true; 354 KRBError kerr = ke.getError(); 355 int paEType = PAData.getPreferredEType(kerr.getPA(), 356 EType.getDefaults("default_tkt_enctypes")[0]); 357 if (password == null) { 358 EncryptionKey[] ks = Krb5Util.keysFromJavaxKeyTab(ktab, cname); 359 pakey = EncryptionKey.findKey(paEType, ks); 360 if (pakey != null) pakey = (EncryptionKey)pakey.clone(); 361 for (EncryptionKey k: ks) k.destroy(); 362 } else { 363 pakey = EncryptionKey.acquireSecretKey(cname, 364 password, 365 paEType, 366 PAData.getSaltAndParams( 367 paEType, kerr.getPA())); 368 } 369 paList = kerr.getPA(); // Update current paList 370 } else { 371 if (referralsState.handleError(ke)) { 372 pakey = null; 373 preAuthFailedOnce = false; 374 continue; 375 } 376 throw ke; 377 } 378 } 379 } 380 } 381 382 private final class ReferralsState { 383 private boolean enabled; 384 private int count; 385 private boolean refreshComm; 386 ReferralsState()387 ReferralsState() throws KrbException { 388 if (Config.DISABLE_REFERRALS) { 389 if (refCname.getNameType() == PrincipalName.KRB_NT_ENTERPRISE) { 390 throw new KrbException("NT-ENTERPRISE principals only allowed" + 391 " when referrals are enabled."); 392 } 393 enabled = false; 394 } else { 395 enabled = true; 396 } 397 refreshComm = true; 398 } 399 handleError(KrbException ke)400 boolean handleError(KrbException ke) throws RealmException { 401 if (enabled) { 402 if (ke.returnCode() == Krb5.KRB_ERR_WRONG_REALM) { 403 Realm referredRealm = ke.getError().getClientRealm(); 404 if (req.getMessage().reqBody.kdcOptions.get(KDCOptions.CANONICALIZE) && 405 referredRealm != null && referredRealm.toString().length() > 0 && 406 count < Config.MAX_REFERRALS) { 407 refCname = new PrincipalName(refCname.getNameType(), 408 refCname.getNameStrings(), referredRealm); 409 refreshComm = true; 410 count++; 411 return true; 412 } 413 } 414 if (count < Config.MAX_REFERRALS && 415 refCname.getNameType() != PrincipalName.KRB_NT_ENTERPRISE) { 416 // Server may raise an error if CANONICALIZE is true. 417 // Try CANONICALIZE false. 418 enabled = false; 419 return true; 420 } 421 } 422 return false; 423 } 424 refreshComm()425 boolean refreshComm() { 426 boolean retRefreshComm = refreshComm; 427 refreshComm = false; 428 return retRefreshComm; 429 } 430 isEnabled()431 boolean isEnabled() { 432 return enabled; 433 } 434 } 435 436 /** 437 * Performs AS-REQ send and AS-REP receive. 438 * Maybe a state is needed here, to divide prepare process and getCreds. 439 * @throws KrbException 440 * @throws Asn1Exception 441 * @throws IOException 442 */ action()443 public KrbAsReqBuilder action() 444 throws KrbException, Asn1Exception, IOException { 445 checkState(State.INIT, "Cannot call action"); 446 state = State.REQ_OK; 447 return send().resolve(); 448 } 449 450 /** 451 * Gets Credentials object after action 452 */ getCreds()453 public Credentials getCreds() { 454 checkState(State.REQ_OK, "Cannot retrieve creds"); 455 return rep.getCreds(); 456 } 457 458 /** 459 * Gets another type of Credentials after action 460 */ getCCreds()461 public sun.security.krb5.internal.ccache.Credentials getCCreds() { 462 checkState(State.REQ_OK, "Cannot retrieve CCreds"); 463 return rep.getCCreds(); 464 } 465 466 /** 467 * Destroys the object and clears keys and password info. 468 */ destroy()469 public void destroy() { 470 state = State.DESTROYED; 471 if (password != null) { 472 Arrays.fill(password, (char)0); 473 } 474 } 475 476 /** 477 * Checks if the current state is the specified one. 478 * @param st the expected state 479 * @param msg error message if state is not correct 480 * @throws IllegalStateException if state is not correct 481 */ checkState(State st, String msg)482 private void checkState(State st, String msg) { 483 if (state != st) { 484 throw new IllegalStateException(msg + " at " + st + " state"); 485 } 486 } 487 } 488