1 /* 2 * Copyright (c) 2010, 2020, 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 if (referralsState.sendCanonicalize()) { 282 options.set(KDCOptions.CANONICALIZE, true); 283 } 284 extraPAs = new PAData[]{ new PAData(Krb5.PA_REQ_ENC_PA_REP, 285 new byte[]{}) }; 286 } else { 287 options.set(KDCOptions.CANONICALIZE, false); 288 } 289 return new KrbAsReq(key, 290 options, 291 refCname, 292 sname, 293 from, 294 till, 295 rtime, 296 eTypes, 297 addresses, 298 extraPAs); 299 } 300 301 /** 302 * Parses AS-REP, decrypts enc-part, retrieves ticket and session key 303 * @throws KrbException 304 * @throws Asn1Exception 305 * @throws IOException 306 */ resolve()307 private KrbAsReqBuilder resolve() 308 throws KrbException, Asn1Exception, IOException { 309 if (ktab != null) { 310 rep.decryptUsingKeyTab(ktab, req, cname); 311 } else { 312 rep.decryptUsingPassword(password, req, cname); 313 } 314 if (rep.getPA() != null) { 315 if (paList == null || paList.length == 0) { 316 paList = rep.getPA(); 317 } else { 318 int extraLen = rep.getPA().length; 319 if (extraLen > 0) { 320 int oldLen = paList.length; 321 paList = Arrays.copyOf(paList, paList.length + extraLen); 322 System.arraycopy(rep.getPA(), 0, paList, oldLen, extraLen); 323 } 324 } 325 } 326 return this; 327 } 328 329 /** 330 * Communication until AS-REP or non preauth-related KRB-ERROR received 331 * @throws KrbException 332 * @throws IOException 333 */ send()334 private KrbAsReqBuilder send() throws KrbException, IOException { 335 boolean preAuthFailedOnce = false; 336 KdcComm comm = null; 337 EncryptionKey pakey = null; 338 ReferralsState referralsState = new ReferralsState(this); 339 while (true) { 340 if (referralsState.refreshComm()) { 341 comm = new KdcComm(refCname.getRealmAsString()); 342 } 343 try { 344 req = build(pakey, referralsState); 345 rep = new KrbAsRep(comm.send(req.encoding())); 346 return this; 347 } catch (KrbException ke) { 348 if (!preAuthFailedOnce && ( 349 ke.returnCode() == Krb5.KDC_ERR_PREAUTH_FAILED || 350 ke.returnCode() == Krb5.KDC_ERR_PREAUTH_REQUIRED)) { 351 if (Krb5.DEBUG) { 352 System.out.println("KrbAsReqBuilder: " + 353 "PREAUTH FAILED/REQ, re-send AS-REQ"); 354 } 355 preAuthFailedOnce = true; 356 KRBError kerr = ke.getError(); 357 int paEType = PAData.getPreferredEType(kerr.getPA(), 358 EType.getDefaults("default_tkt_enctypes")[0]); 359 if (password == null) { 360 EncryptionKey[] ks = Krb5Util.keysFromJavaxKeyTab(ktab, cname); 361 pakey = EncryptionKey.findKey(paEType, ks); 362 if (pakey != null) pakey = (EncryptionKey)pakey.clone(); 363 for (EncryptionKey k: ks) k.destroy(); 364 } else { 365 pakey = EncryptionKey.acquireSecretKey(cname, 366 password, 367 paEType, 368 PAData.getSaltAndParams( 369 paEType, kerr.getPA())); 370 } 371 paList = kerr.getPA(); // Update current paList 372 } else { 373 if (referralsState.handleError(ke)) { 374 pakey = null; 375 preAuthFailedOnce = false; 376 continue; 377 } 378 throw ke; 379 } 380 } 381 } 382 } 383 384 static final class ReferralsState { 385 private static boolean canonicalizeConfig; 386 private boolean enabled; 387 private boolean sendCanonicalize; 388 private boolean isEnterpriseCname; 389 private int count; 390 private boolean refreshComm; 391 private KrbAsReqBuilder reqBuilder; 392 393 static { initStatic()394 initStatic(); 395 } 396 397 // Config may be refreshed while running so the setting 398 // value may need to be updated. See Config::refresh. initStatic()399 static void initStatic() { 400 canonicalizeConfig = false; 401 try { 402 canonicalizeConfig = Config.getInstance() 403 .getBooleanObject("libdefaults", "canonicalize") == 404 Boolean.TRUE; 405 } catch (KrbException e) { 406 if (Krb5.DEBUG) { 407 System.out.println("Exception in getting canonicalize," + 408 " using default value " + 409 Boolean.valueOf(canonicalizeConfig) + ": " + 410 e.getMessage()); 411 } 412 } 413 } 414 ReferralsState(KrbAsReqBuilder reqBuilder)415 ReferralsState(KrbAsReqBuilder reqBuilder) throws KrbException { 416 this.reqBuilder = reqBuilder; 417 sendCanonicalize = canonicalizeConfig; 418 isEnterpriseCname = reqBuilder.refCname.getNameType() == 419 PrincipalName.KRB_NT_ENTERPRISE; 420 updateStatus(); 421 if (!enabled && isEnterpriseCname) { 422 throw new KrbException("NT-ENTERPRISE principals only" + 423 " allowed when referrals are enabled."); 424 } 425 refreshComm = true; 426 } 427 updateStatus()428 private void updateStatus() { 429 enabled = !Config.DISABLE_REFERRALS && 430 (isEnterpriseCname || sendCanonicalize); 431 } 432 handleError(KrbException ke)433 boolean handleError(KrbException ke) throws RealmException { 434 if (enabled) { 435 if (ke.returnCode() == Krb5.KRB_ERR_WRONG_REALM) { 436 Realm referredRealm = ke.getError().getClientRealm(); 437 if (referredRealm != null && 438 !referredRealm.toString().isEmpty() && 439 count < Config.MAX_REFERRALS) { 440 // A valid referral was received while referrals 441 // were enabled. Change the cname realm to the referred 442 // realm and set refreshComm to send a new request. 443 reqBuilder.refCname = new PrincipalName( 444 reqBuilder.refCname.getNameType(), 445 reqBuilder.refCname.getNameStrings(), 446 referredRealm); 447 refreshComm = true; 448 count++; 449 return true; 450 } 451 } 452 if (count < Config.MAX_REFERRALS && sendCanonicalize) { 453 if (Krb5.DEBUG) { 454 System.out.println("KrbAsReqBuilder: AS-REQ failed." + 455 " Retrying with CANONICALIZE false."); 456 } 457 458 // Server returned an unexpected error with 459 // CANONICALIZE true. Retry with false. 460 sendCanonicalize = false; 461 462 // Setting CANONICALIZE to false may imply that referrals 463 // are now disabled (if cname is not of NT-ENTERPRISE type). 464 updateStatus(); 465 466 return true; 467 } 468 } 469 return false; 470 } 471 refreshComm()472 boolean refreshComm() { 473 boolean retRefreshComm = refreshComm; 474 refreshComm = false; 475 return retRefreshComm; 476 } 477 isEnabled()478 boolean isEnabled() { 479 return enabled; 480 } 481 sendCanonicalize()482 boolean sendCanonicalize() { 483 return sendCanonicalize; 484 } 485 } 486 487 /** 488 * Performs AS-REQ send and AS-REP receive. 489 * Maybe a state is needed here, to divide prepare process and getCreds. 490 * @throws KrbException 491 * @throws Asn1Exception 492 * @throws IOException 493 */ action()494 public KrbAsReqBuilder action() 495 throws KrbException, Asn1Exception, IOException { 496 checkState(State.INIT, "Cannot call action"); 497 state = State.REQ_OK; 498 return send().resolve(); 499 } 500 501 /** 502 * Gets Credentials object after action 503 */ getCreds()504 public Credentials getCreds() { 505 checkState(State.REQ_OK, "Cannot retrieve creds"); 506 return rep.getCreds(); 507 } 508 509 /** 510 * Gets another type of Credentials after action 511 */ getCCreds()512 public sun.security.krb5.internal.ccache.Credentials getCCreds() { 513 checkState(State.REQ_OK, "Cannot retrieve CCreds"); 514 return rep.getCCreds(); 515 } 516 517 /** 518 * Destroys the object and clears keys and password info. 519 */ destroy()520 public void destroy() { 521 state = State.DESTROYED; 522 if (password != null) { 523 Arrays.fill(password, (char)0); 524 } 525 } 526 527 /** 528 * Checks if the current state is the specified one. 529 * @param st the expected state 530 * @param msg error message if state is not correct 531 * @throws IllegalStateException if state is not correct 532 */ checkState(State st, String msg)533 private void checkState(State st, String msg) { 534 if (state != st) { 535 throw new IllegalStateException(msg + " at " + st + " state"); 536 } 537 } 538 } 539