1 /* 2 * Copyright (c) 2008, 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. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 import jdk.test.lib.Platform; 25 26 import java.lang.reflect.Constructor; 27 import java.lang.reflect.Field; 28 import java.lang.reflect.InvocationTargetException; 29 import java.net.*; 30 import java.io.*; 31 import java.lang.reflect.Method; 32 import java.nio.file.Files; 33 import java.nio.file.Paths; 34 import java.util.*; 35 import java.util.concurrent.*; 36 import java.util.stream.Collectors; 37 import java.util.stream.Stream; 38 39 import sun.security.krb5.*; 40 import sun.security.krb5.internal.*; 41 import sun.security.krb5.internal.ccache.CredentialsCache; 42 import sun.security.krb5.internal.crypto.EType; 43 import sun.security.krb5.internal.crypto.KeyUsage; 44 import sun.security.krb5.internal.ktab.KeyTab; 45 import sun.security.util.DerInputStream; 46 import sun.security.util.DerOutputStream; 47 import sun.security.util.DerValue; 48 49 /** 50 * A KDC server. 51 * 52 * Note: By setting the system property native.kdc.path to a native 53 * krb5 installation, this class starts a native KDC with the 54 * given realm and host. It can also add new principals and save keytabs. 55 * Other features might not be available. 56 * <p> 57 * Features: 58 * <ol> 59 * <li> Supports TCP and UDP 60 * <li> Supports AS-REQ and TGS-REQ 61 * <li> Principal db and other settings hard coded in application 62 * <li> Options, say, request preauth or not 63 * </ol> 64 * Side effects: 65 * <ol> 66 * <li> The Sun-internal class <code>sun.security.krb5.Config</code> is a 67 * singleton and initialized according to Kerberos settings (krb5.conf and 68 * java.security.krb5.* system properties). This means once it's initialized 69 * it will not automatically notice any changes to these settings (or file 70 * changes of krb5.conf). The KDC class normally does not touch these 71 * settings (except for the <code>writeKtab()</code> method). However, to make 72 * sure nothing ever goes wrong, if you want to make any changes to these 73 * settings after calling a KDC method, call <code>Config.refresh()</code> to 74 * make sure your changes are reflected in the <code>Config</code> object. 75 * </ol> 76 * System properties recognized: 77 * <ul> 78 * <li>test.kdc.save.ccache 79 * </ul> 80 * Issues and TODOs: 81 * <ol> 82 * <li> Generates krb5.conf to be used on another machine, currently the kdc is 83 * always localhost 84 * <li> More options to KDC, say, error output, say, response nonce != 85 * request nonce 86 * </ol> 87 * Note: This program uses internal krb5 classes (including reflection to 88 * access private fields and methods). 89 * <p> 90 * Usages: 91 * <p> 92 * 1. Init and start the KDC: 93 * <pre> 94 * KDC kdc = KDC.create("REALM.NAME", port, isDaemon); 95 * KDC kdc = KDC.create("REALM.NAME"); 96 * </pre> 97 * Here, <code>port</code> is the UDP and TCP port number the KDC server 98 * listens on. If zero, a random port is chosen, which you can use getPort() 99 * later to retrieve the value. 100 * <p> 101 * If <code>isDaemon</code> is true, the KDC worker threads will be daemons. 102 * <p> 103 * The shortcut <code>KDC.create("REALM.NAME")</code> has port=0 and 104 * isDaemon=false, and is commonly used in an embedded KDC. 105 * <p> 106 * 2. Adding users: 107 * <pre> 108 * kdc.addPrincipal(String principal_name, char[] password); 109 * kdc.addPrincipalRandKey(String principal_name); 110 * </pre> 111 * A service principal's name should look like "host/f.q.d.n". The second form 112 * generates a random key. To expose this key, call <code>writeKtab()</code> to 113 * save the keys into a keytab file. 114 * <p> 115 * Note that you need to add the principal name krbtgt/REALM.NAME yourself. 116 * <p> 117 * Note that you can safely add a principal at any time after the KDC is 118 * started and before a user requests info on this principal. 119 * <p> 120 * 3. Other public methods: 121 * <ul> 122 * <li> <code>getPort</code>: Returns the port number the KDC uses 123 * <li> <code>getRealm</code>: Returns the realm name 124 * <li> <code>writeKtab</code>: Writes all principals' keys into a keytab file 125 * <li> <code>saveConfig</code>: Saves a krb5.conf file to access this KDC 126 * <li> <code>setOption</code>: Sets various options 127 * </ul> 128 * Read the javadoc for details. Lazy developer can use <code>OneKDC</code> 129 * directly. 130 */ 131 public class KDC { 132 133 public static final int DEFAULT_LIFETIME = 39600; 134 public static final int DEFAULT_RENEWTIME = 86400; 135 136 public static final String NOT_EXISTING_HOST = "not.existing.host"; 137 138 // What etypes the KDC supports. Comma-separated strings. Null for all. 139 // Please note native KDCs might use different names. 140 private static final String SUPPORTED_ETYPES 141 = System.getProperty("kdc.supported.enctypes"); 142 143 // The native KDC 144 private final NativeKdc nativeKdc; 145 146 // The native KDC process 147 private Process kdcProc = null; 148 149 // Under the hood. 150 151 // Principal db. principal -> pass. A case-insensitive TreeMap is used 152 // so that even if the client provides a name with different case, the KDC 153 // can still locate the principal and give back correct salt. 154 private TreeMap<String,char[]> passwords = new TreeMap<> 155 (String.CASE_INSENSITIVE_ORDER); 156 157 // Non default salts. Precisely, there should be different salts for 158 // different etypes, pretend they are the same at the moment. 159 private TreeMap<String,String> salts = new TreeMap<> 160 (String.CASE_INSENSITIVE_ORDER); 161 162 // Non default s2kparams for newer etypes. Precisely, there should be 163 // different s2kparams for different etypes, pretend they are the same 164 // at the moment. 165 private TreeMap<String,byte[]> s2kparamses = new TreeMap<> 166 (String.CASE_INSENSITIVE_ORDER); 167 168 // Realm name 169 private String realm; 170 // KDC 171 private String kdc; 172 // Service port number 173 private int port; 174 // The request/response job queue 175 private BlockingQueue<Job> q = new ArrayBlockingQueue<>(100); 176 // Options 177 private Map<Option,Object> options = new HashMap<>(); 178 // Realm-specific krb5.conf settings 179 private List<String> conf = new ArrayList<>(); 180 181 private Thread thread1, thread2, thread3; 182 private volatile boolean udpConsumerReady = false; 183 private volatile boolean tcpConsumerReady = false; 184 private volatile boolean dispatcherReady = false; 185 DatagramSocket u1 = null; 186 ServerSocket t1 = null; 187 188 public static enum KtabMode { APPEND, EXISTING }; 189 190 /** 191 * Option names, to be expanded forever. 192 */ 193 public static enum Option { 194 /** 195 * Whether pre-authentication is required. Default Boolean.TRUE 196 */ 197 PREAUTH_REQUIRED, 198 /** 199 * Only issue TGT in RC4 200 */ 201 ONLY_RC4_TGT, 202 /** 203 * Use RC4 as the first in preauth 204 */ 205 RC4_FIRST_PREAUTH, 206 /** 207 * Use only one preauth, so that some keys are not easy to generate 208 */ 209 ONLY_ONE_PREAUTH, 210 /** 211 * Set all name-type to a value in response 212 */ 213 RESP_NT, 214 /** 215 * Multiple ETYPE-INFO-ENTRY with same etype but different salt 216 */ 217 DUP_ETYPE, 218 /** 219 * What backend server can be delegated to 220 */ 221 OK_AS_DELEGATE, 222 /** 223 * Allow S4U2self, List<String> of middle servers. 224 * If not set, means KDC does not understand S4U2self at all, therefore 225 * would ignore any PA-FOR-USER request and send a ticket using the 226 * cname of teh requestor. If set, it returns FORWARDABLE tickets to 227 * a server with its name in the list 228 */ 229 ALLOW_S4U2SELF, 230 /** 231 * Allow S4U2proxy, Map<String,List<String>> of middle servers to 232 * backends. If not set or a backend not in a server's list, 233 * Krb5.KDC_ERR_POLICY will be send for S4U2proxy request. 234 */ 235 ALLOW_S4U2PROXY, 236 /** 237 * Sensitive accounts can never be delegated. 238 */ 239 SENSITIVE_ACCOUNTS, 240 /** 241 * If true, will check if TGS-REQ contains a non-null addresses field. 242 */ 243 CHECK_ADDRESSES, 244 }; 245 246 /** 247 * A standalone KDC server. 248 */ 249 public static void main(String[] args) throws Exception { 250 int port = args.length > 0 ? Integer.parseInt(args[0]) : 0; 251 KDC kdc = create("RABBIT.HOLE", "kdc.rabbit.hole", port, false); 252 kdc.addPrincipal("dummy", "bogus".toCharArray()); 253 kdc.addPrincipal("foo", "bar".toCharArray()); 254 kdc.addPrincipalRandKey("krbtgt/RABBIT.HOLE"); 255 kdc.addPrincipalRandKey("server/host.rabbit.hole"); 256 kdc.addPrincipalRandKey("backend/host.rabbit.hole"); 257 KDC.saveConfig("krb5.conf", kdc, "forwardable = true"); 258 } 259 260 /** 261 * Creates and starts a KDC running as a daemon on a random port. 262 * @param realm the realm name 263 * @return the running KDC instance 264 * @throws java.io.IOException for any socket creation error 265 */ 266 public static KDC create(String realm) throws IOException { 267 return create(realm, "kdc." + realm.toLowerCase(Locale.US), 0, true); 268 } 269 270 public static KDC existing(String realm, String kdc, int port) { 271 KDC k = new KDC(realm, kdc); 272 k.port = port; 273 return k; 274 } 275 276 /** 277 * Creates and starts a KDC server. 278 * @param realm the realm name 279 * @param port the TCP and UDP port to listen to. A random port will to 280 * chosen if zero. 281 * @param asDaemon if true, KDC threads will be daemons. Otherwise, not. 282 * @return the running KDC instance 283 * @throws java.io.IOException for any socket creation error 284 */ 285 public static KDC create(String realm, String kdc, int port, 286 boolean asDaemon) throws IOException { 287 return new KDC(realm, kdc, port, asDaemon); 288 } 289 290 /** 291 * Sets an option 292 * @param key the option name 293 * @param value the value 294 */ 295 public void setOption(Option key, Object value) { 296 if (value == null) { 297 options.remove(key); 298 } else { 299 options.put(key, value); 300 } 301 } 302 303 /** 304 * Writes or appends keys into a keytab. 305 * <p> 306 * Attention: This is the most basic one of a series of methods below on 307 * keytab creation or modification. All these methods reference krb5.conf 308 * settings. If you need to modify krb5.conf or switch to another krb5.conf 309 * later, please call <code>Config.refresh()</code> again. For example: 310 * <pre> 311 * kdc.writeKtab("/etc/kdc/ktab", true); // Config is initialized, 312 * System.setProperty("java.security.krb5.conf", "/home/mykrb5.conf"); 313 * Config.refresh(); 314 * </pre> 315 * Inside this method there are 2 places krb5.conf is used: 316 * <ol> 317 * <li> (Fatal) Generating keys: EncryptionKey.acquireSecretKeys 318 * <li> (Has workaround) Creating PrincipalName 319 * </ol> 320 * @param tab the keytab file name 321 * @param append true if append, otherwise, overwrite. 322 * @param names the names to write into, write all if names is empty 323 */ 324 public void writeKtab(String tab, boolean append, String... names) 325 throws IOException, KrbException { 326 KeyTab ktab = null; 327 if (nativeKdc == null) { 328 ktab = append ? KeyTab.getInstance(tab) : KeyTab.create(tab); 329 } 330 Iterable<String> entries = 331 (names.length != 0) ? Arrays.asList(names): passwords.keySet(); 332 for (String name : entries) { 333 if (name.indexOf('@') < 0) { 334 name = name + "@" + realm; 335 } 336 if (nativeKdc == null) { 337 char[] pass = passwords.get(name); 338 int kvno = 0; 339 if (Character.isDigit(pass[pass.length - 1])) { 340 kvno = pass[pass.length - 1] - '0'; 341 } 342 PrincipalName pn = new PrincipalName(name, 343 name.indexOf('/') < 0 ? 344 PrincipalName.KRB_NT_UNKNOWN : 345 PrincipalName.KRB_NT_SRV_HST); 346 ktab.addEntry(pn, 347 getSalt(pn), 348 pass, 349 kvno, 350 true); 351 } else { 352 nativeKdc.ktadd(name, tab); 353 } 354 } 355 if (nativeKdc == null) { 356 ktab.save(); 357 } 358 } 359 360 /** 361 * Writes all principals' keys from multiple KDCs into one keytab file. 362 * @throws java.io.IOException for any file output error 363 * @throws sun.security.krb5.KrbException for any realm and/or principal 364 * name error. 365 */ 366 public static void writeMultiKtab(String tab, KDC... kdcs) 367 throws IOException, KrbException { 368 KeyTab.create(tab).save(); // Empty the old keytab 369 appendMultiKtab(tab, kdcs); 370 } 371 372 /** 373 * Appends all principals' keys from multiple KDCs to one keytab file. 374 */ 375 public static void appendMultiKtab(String tab, KDC... kdcs) 376 throws IOException, KrbException { 377 for (KDC kdc: kdcs) { 378 kdc.writeKtab(tab, true); 379 } 380 } 381 382 /** 383 * Write a ktab for this KDC. 384 */ 385 public void writeKtab(String tab) throws IOException, KrbException { 386 writeKtab(tab, false); 387 } 388 389 /** 390 * Appends keys in this KDC to a ktab. 391 */ 392 public void appendKtab(String tab) throws IOException, KrbException { 393 writeKtab(tab, true); 394 } 395 396 /** 397 * Adds a new principal to this realm with a given password. 398 * @param user the principal's name. For a service principal, use the 399 * form of host/f.q.d.n 400 * @param pass the password for the principal 401 */ 402 public void addPrincipal(String user, char[] pass) { 403 addPrincipal(user, pass, null, null); 404 } 405 406 /** 407 * Adds a new principal to this realm with a given password. 408 * @param user the principal's name. For a service principal, use the 409 * form of host/f.q.d.n 410 * @param pass the password for the principal 411 * @param salt the salt, or null if a default value will be used 412 * @param s2kparams the s2kparams, or null if a default value will be used 413 */ 414 public void addPrincipal( 415 String user, char[] pass, String salt, byte[] s2kparams) { 416 if (user.indexOf('@') < 0) { 417 user = user + "@" + realm; 418 } 419 if (nativeKdc != null) { 420 if (!user.equals("krbtgt/" + realm)) { 421 nativeKdc.addPrincipal(user, new String(pass)); 422 } 423 passwords.put(user, new char[0]); 424 } else { 425 passwords.put(user, pass); 426 if (salt != null) { 427 salts.put(user, salt); 428 } 429 if (s2kparams != null) { 430 s2kparamses.put(user, s2kparams); 431 } 432 } 433 } 434 435 /** 436 * Adds a new principal to this realm with a random password 437 * @param user the principal's name. For a service principal, use the 438 * form of host/f.q.d.n 439 */ 440 public void addPrincipalRandKey(String user) { 441 addPrincipal(user, randomPassword()); 442 } 443 444 /** 445 * Returns the name of this realm 446 * @return the name of this realm 447 */ 448 public String getRealm() { 449 return realm; 450 } 451 452 /** 453 * Returns the name of kdc 454 * @return the name of kdc 455 */ 456 public String getKDC() { 457 return kdc; 458 } 459 460 /** 461 * Add realm-specific krb5.conf setting 462 */ 463 public void addConf(String s) { 464 conf.add(s); 465 } 466 467 /** 468 * Writes a krb5.conf for one or more KDC that includes KDC locations for 469 * each realm and the default realm name. You can also add extra strings 470 * into the file. The method should be called like: 471 * <pre> 472 * KDC.saveConfig("krb5.conf", kdc1, kdc2, ..., line1, line2, ...); 473 * </pre> 474 * Here you can provide one or more kdc# and zero or more line# arguments. 475 * The line# will be put after [libdefaults] and before [realms]. Therefore 476 * you can append new lines into [libdefaults] and/or create your new 477 * stanzas as well. Note that a newline character will be appended to 478 * each line# argument. 479 * <p> 480 * For example: 481 * <pre> 482 * KDC.saveConfig("krb5.conf", this); 483 * </pre> 484 * generates: 485 * <pre> 486 * [libdefaults] 487 * default_realm = REALM.NAME 488 * 489 * [realms] 490 * REALM.NAME = { 491 * kdc = host:port_number 492 * # realm-specific settings 493 * } 494 * </pre> 495 * 496 * Another example: 497 * <pre> 498 * KDC.saveConfig("krb5.conf", kdc1, kdc2, "forwardable = true", "", 499 * "[domain_realm]", 500 * ".kdc1.com = KDC1.NAME"); 501 * </pre> 502 * generates: 503 * <pre> 504 * [libdefaults] 505 * default_realm = KDC1.NAME 506 * forwardable = true 507 * 508 * [domain_realm] 509 * .kdc1.com = KDC1.NAME 510 * 511 * [realms] 512 * KDC1.NAME = { 513 * kdc = host:port1 514 * } 515 * KDC2.NAME = { 516 * kdc = host:port2 517 * } 518 * </pre> 519 * @param file the name of the file to write into 520 * @param kdc the first (and default) KDC 521 * @param more more KDCs or extra lines (in their appearing order) to 522 * insert into the krb5.conf file. This method reads each argument's type 523 * to determine what it's for. This argument can be empty. 524 * @throws java.io.IOException for any file output error 525 */ 526 public static void saveConfig(String file, KDC kdc, Object... more) 527 throws IOException { 528 StringBuffer sb = new StringBuffer(); 529 sb.append("[libdefaults]\ndefault_realm = "); 530 sb.append(kdc.realm); 531 sb.append("\n"); 532 for (Object o : more) { 533 if (o instanceof String) { 534 sb.append(o); 535 sb.append("\n"); 536 } 537 } 538 sb.append("\n[realms]\n"); 539 sb.append(kdc.realmLine()); 540 for (Object o : more) { 541 if (o instanceof KDC) { 542 sb.append(((KDC) o).realmLine()); 543 } 544 } 545 Files.write(Paths.get(file), sb.toString().getBytes()); 546 } 547 548 /** 549 * Returns the service port of the KDC server. 550 * @return the KDC service port 551 */ 552 public int getPort() { 553 return port; 554 } 555 556 // Private helper methods 557 558 /** 559 * Private constructor, cannot be called outside. 560 * @param realm 561 */ 562 private KDC(String realm, String kdc) { 563 this.realm = realm; 564 this.kdc = kdc; 565 this.nativeKdc = null; 566 } 567 568 /** 569 * A constructor that starts the KDC service also. 570 */ 571 protected KDC(String realm, String kdc, int port, boolean asDaemon) 572 throws IOException { 573 this.realm = realm; 574 this.kdc = kdc; 575 this.nativeKdc = NativeKdc.get(this); 576 startServer(port, asDaemon); 577 } 578 /** 579 * Generates a 32-char random password 580 * @return the password 581 */ 582 private static char[] randomPassword() { 583 char[] pass = new char[32]; 584 Random r = new Random(); 585 for (int i=0; i<31; i++) 586 pass[i] = (char)('a' + r.nextInt(26)); 587 // The last char cannot be a number, otherwise, keyForUser() 588 // believes it's a sign of kvno 589 pass[31] = 'Z'; 590 return pass; 591 } 592 593 /** 594 * Generates a random key for the given encryption type. 595 * @param eType the encryption type 596 * @return the generated key 597 * @throws sun.security.krb5.KrbException for unknown/unsupported etype 598 */ 599 private static EncryptionKey generateRandomKey(int eType) 600 throws KrbException { 601 return genKey0(randomPassword(), "NOTHING", null, eType, null); 602 } 603 604 /** 605 * Returns the password for a given principal 606 * @param p principal 607 * @return the password 608 * @throws sun.security.krb5.KrbException when the principal is not inside 609 * the database. 610 */ 611 private char[] getPassword(PrincipalName p, boolean server) 612 throws KrbException { 613 String pn = p.toString(); 614 if (p.getRealmString() == null) { 615 pn = pn + "@" + getRealm(); 616 } 617 char[] pass = passwords.get(pn); 618 if (pass == null) { 619 throw new KrbException(server? 620 Krb5.KDC_ERR_S_PRINCIPAL_UNKNOWN: 621 Krb5.KDC_ERR_C_PRINCIPAL_UNKNOWN, pn.toString()); 622 } 623 return pass; 624 } 625 626 /** 627 * Returns the salt string for the principal. 628 * @param p principal 629 * @return the salt 630 */ 631 protected String getSalt(PrincipalName p) { 632 String pn = p.toString(); 633 if (p.getRealmString() == null) { 634 pn = pn + "@" + getRealm(); 635 } 636 if (salts.containsKey(pn)) { 637 return salts.get(pn); 638 } 639 if (passwords.containsKey(pn)) { 640 try { 641 // Find the principal name with correct case. 642 p = new PrincipalName(passwords.ceilingEntry(pn).getKey()); 643 } catch (RealmException re) { 644 // Won't happen 645 } 646 } 647 String s = p.getRealmString(); 648 if (s == null) s = getRealm(); 649 for (String n: p.getNameStrings()) { 650 s += n; 651 } 652 return s; 653 } 654 655 /** 656 * Returns the s2kparams for the principal given the etype. 657 * @param p principal 658 * @param etype encryption type 659 * @return the s2kparams, might be null 660 */ 661 protected byte[] getParams(PrincipalName p, int etype) { 662 switch (etype) { 663 case EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96: 664 case EncryptedData.ETYPE_AES256_CTS_HMAC_SHA1_96: 665 case EncryptedData.ETYPE_AES128_CTS_HMAC_SHA256_128: 666 case EncryptedData.ETYPE_AES256_CTS_HMAC_SHA384_192: 667 String pn = p.toString(); 668 if (p.getRealmString() == null) { 669 pn = pn + "@" + getRealm(); 670 } 671 if (s2kparamses.containsKey(pn)) { 672 return s2kparamses.get(pn); 673 } 674 if (etype < EncryptedData.ETYPE_AES128_CTS_HMAC_SHA256_128) { 675 return new byte[]{0, 0, 0x10, 0}; 676 } else { 677 return new byte[]{0, 0, (byte) 0x80, 0}; 678 } 679 default: 680 return null; 681 } 682 } 683 684 /** 685 * Returns the key for a given principal of the given encryption type 686 * @param p the principal 687 * @param etype the encryption type 688 * @param server looking for a server principal? 689 * @return the key 690 * @throws sun.security.krb5.KrbException for unknown/unsupported etype 691 */ 692 private EncryptionKey keyForUser(PrincipalName p, int etype, boolean server) 693 throws KrbException { 694 try { 695 // Do not call EncryptionKey.acquireSecretKeys(), otherwise 696 // the krb5.conf config file would be loaded. 697 Integer kvno = null; 698 // For service whose password ending with a number, use it as kvno. 699 // Kvno must be postive. 700 if (p.toString().indexOf('/') > 0) { 701 char[] pass = getPassword(p, server); 702 if (Character.isDigit(pass[pass.length-1])) { 703 kvno = pass[pass.length-1] - '0'; 704 } 705 } 706 return genKey0(getPassword(p, server), getSalt(p), 707 getParams(p, etype), etype, kvno); 708 } catch (KrbException ke) { 709 throw ke; 710 } catch (Exception e) { 711 throw new RuntimeException(e); // should not happen 712 } 713 } 714 715 /** 716 * Returns a KerberosTime. 717 * 718 * @param offset offset from NOW in seconds 719 */ 720 private static KerberosTime timeAfter(int offset) { 721 return new KerberosTime(new Date().getTime() + offset * 1000L); 722 } 723 724 /** 725 * Generates key from password. 726 */ 727 private static EncryptionKey genKey0( 728 char[] pass, String salt, byte[] s2kparams, 729 int etype, Integer kvno) throws KrbException { 730 return new EncryptionKey(EncryptionKeyDotStringToKey( 731 pass, salt, s2kparams, etype), 732 etype, kvno); 733 } 734 735 /** 736 * Processes an incoming request and generates a response. 737 * @param in the request 738 * @return the response 739 * @throws java.lang.Exception for various errors 740 */ 741 protected byte[] processMessage(byte[] in) throws Exception { 742 if ((in[0] & 0x1f) == Krb5.KRB_AS_REQ) 743 return processAsReq(in); 744 else 745 return processTgsReq(in); 746 } 747 748 /** 749 * Processes a TGS_REQ and generates a TGS_REP (or KRB_ERROR) 750 * @param in the request 751 * @return the response 752 * @throws java.lang.Exception for various errors 753 */ 754 protected byte[] processTgsReq(byte[] in) throws Exception { 755 TGSReq tgsReq = new TGSReq(in); 756 PrincipalName service = tgsReq.reqBody.sname; 757 if (options.containsKey(KDC.Option.RESP_NT)) { 758 service = new PrincipalName((int)options.get(KDC.Option.RESP_NT), 759 service.getNameStrings(), service.getRealm()); 760 } 761 try { 762 System.out.println(realm + "> " + tgsReq.reqBody.cname + 763 " sends TGS-REQ for " + 764 service + ", " + tgsReq.reqBody.kdcOptions); 765 KDCReqBody body = tgsReq.reqBody; 766 int[] eTypes = filterSupported(KDCReqBodyDotEType(body)); 767 if (eTypes.length == 0) { 768 throw new KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP); 769 } 770 int e2 = eTypes[0]; // etype for outgoing session key 771 int e3 = eTypes[0]; // etype for outgoing ticket 772 773 PAData[] pas = KDCReqDotPAData(tgsReq); 774 775 Ticket tkt = null; 776 EncTicketPart etp = null; 777 778 PrincipalName cname = null; 779 boolean allowForwardable = true; 780 781 if (pas == null || pas.length == 0) { 782 throw new KrbException(Krb5.KDC_ERR_PADATA_TYPE_NOSUPP); 783 } else { 784 PrincipalName forUserCName = null; 785 for (PAData pa: pas) { 786 if (pa.getType() == Krb5.PA_TGS_REQ) { 787 APReq apReq = new APReq(pa.getValue()); 788 EncryptedData ed = apReq.authenticator; 789 tkt = apReq.ticket; 790 int te = tkt.encPart.getEType(); 791 EncryptionKey kkey = keyForUser(tkt.sname, te, true); 792 byte[] bb = tkt.encPart.decrypt(kkey, KeyUsage.KU_TICKET); 793 DerInputStream derIn = new DerInputStream(bb); 794 DerValue der = derIn.getDerValue(); 795 etp = new EncTicketPart(der.toByteArray()); 796 // Finally, cname will be overwritten by PA-FOR-USER 797 // if it exists. 798 cname = etp.cname; 799 System.out.println(realm + "> presenting a ticket of " 800 + etp.cname + " to " + tkt.sname); 801 } else if (pa.getType() == Krb5.PA_FOR_USER) { 802 if (options.containsKey(Option.ALLOW_S4U2SELF)) { 803 PAForUserEnc p4u = new PAForUserEnc( 804 new DerValue(pa.getValue()), null); 805 forUserCName = p4u.name; 806 System.out.println(realm + "> See PA_FOR_USER " 807 + " in the name of " + p4u.name); 808 } 809 } 810 } 811 if (forUserCName != null) { 812 List<String> names = (List<String>) 813 options.get(Option.ALLOW_S4U2SELF); 814 if (!names.contains(cname.toString())) { 815 // Mimic the normal KDC behavior. When a server is not 816 // allowed to send S4U2self, do not send an error. 817 // Instead, send a ticket which is useless later. 818 allowForwardable = false; 819 } 820 cname = forUserCName; 821 } 822 if (tkt == null) { 823 throw new KrbException(Krb5.KDC_ERR_PADATA_TYPE_NOSUPP); 824 } 825 } 826 827 // Session key for original ticket, TGT 828 EncryptionKey ckey = etp.key; 829 830 // Session key for session with the service 831 EncryptionKey key = generateRandomKey(e2); 832 833 // Check time, TODO 834 KerberosTime from = body.from; 835 KerberosTime till = body.till; 836 if (from == null || from.isZero()) { 837 from = timeAfter(0); 838 } 839 if (till == null) { 840 throw new KrbException(Krb5.KDC_ERR_NEVER_VALID); // TODO 841 } else if (till.isZero()) { 842 till = timeAfter(DEFAULT_LIFETIME); 843 } 844 845 boolean[] bFlags = new boolean[Krb5.TKT_OPTS_MAX+1]; 846 if (body.kdcOptions.get(KDCOptions.FORWARDABLE) 847 && allowForwardable) { 848 List<String> sensitives = (List<String>) 849 options.get(Option.SENSITIVE_ACCOUNTS); 850 if (sensitives != null && sensitives.contains(cname.toString())) { 851 // Cannot make FORWARDABLE 852 } else { 853 bFlags[Krb5.TKT_OPTS_FORWARDABLE] = true; 854 } 855 } 856 // We do not request for addresses for FORWARDED tickets 857 if (options.containsKey(Option.CHECK_ADDRESSES) 858 && body.kdcOptions.get(KDCOptions.FORWARDED) 859 && body.addresses != null) { 860 throw new KrbException(Krb5.KDC_ERR_BADOPTION); 861 } 862 if (body.kdcOptions.get(KDCOptions.FORWARDED) || 863 etp.flags.get(Krb5.TKT_OPTS_FORWARDED)) { 864 bFlags[Krb5.TKT_OPTS_FORWARDED] = true; 865 } 866 if (body.kdcOptions.get(KDCOptions.RENEWABLE)) { 867 bFlags[Krb5.TKT_OPTS_RENEWABLE] = true; 868 //renew = timeAfter(3600 * 24 * 7); 869 } 870 if (body.kdcOptions.get(KDCOptions.PROXIABLE)) { 871 bFlags[Krb5.TKT_OPTS_PROXIABLE] = true; 872 } 873 if (body.kdcOptions.get(KDCOptions.POSTDATED)) { 874 bFlags[Krb5.TKT_OPTS_POSTDATED] = true; 875 } 876 if (body.kdcOptions.get(KDCOptions.ALLOW_POSTDATE)) { 877 bFlags[Krb5.TKT_OPTS_MAY_POSTDATE] = true; 878 } 879 if (body.kdcOptions.get(KDCOptions.CNAME_IN_ADDL_TKT)) { 880 if (!options.containsKey(Option.ALLOW_S4U2PROXY)) { 881 // Don't understand CNAME_IN_ADDL_TKT 882 throw new KrbException(Krb5.KDC_ERR_BADOPTION); 883 } else { 884 Map<String,List<String>> map = (Map<String,List<String>>) 885 options.get(Option.ALLOW_S4U2PROXY); 886 Ticket second = KDCReqBodyDotFirstAdditionalTicket(body); 887 EncryptionKey key2 = keyForUser( 888 second.sname, second.encPart.getEType(), true); 889 byte[] bb = second.encPart.decrypt(key2, KeyUsage.KU_TICKET); 890 DerInputStream derIn = new DerInputStream(bb); 891 DerValue der = derIn.getDerValue(); 892 EncTicketPart tktEncPart = new EncTicketPart(der.toByteArray()); 893 if (!tktEncPart.flags.get(Krb5.TKT_OPTS_FORWARDABLE)) { 894 //throw new KrbException(Krb5.KDC_ERR_BADOPTION); 895 } 896 PrincipalName client = tktEncPart.cname; 897 System.out.println(realm + "> and an additional ticket of " 898 + client + " to " + second.sname); 899 if (map.containsKey(cname.toString())) { 900 if (map.get(cname.toString()).contains(service.toString())) { 901 System.out.println(realm + "> S4U2proxy OK"); 902 } else { 903 throw new KrbException(Krb5.KDC_ERR_BADOPTION); 904 } 905 } else { 906 throw new KrbException(Krb5.KDC_ERR_BADOPTION); 907 } 908 cname = client; 909 } 910 } 911 912 String okAsDelegate = (String)options.get(Option.OK_AS_DELEGATE); 913 if (okAsDelegate != null && ( 914 okAsDelegate.isEmpty() || 915 okAsDelegate.contains(service.getNameString()))) { 916 bFlags[Krb5.TKT_OPTS_DELEGATE] = true; 917 } 918 bFlags[Krb5.TKT_OPTS_INITIAL] = true; 919 920 KerberosTime renewTill = etp.renewTill; 921 if (renewTill != null && body.kdcOptions.get(KDCOptions.RENEW)) { 922 // till should never pass renewTill 923 if (till.greaterThan(renewTill)) { 924 till = renewTill; 925 } 926 if (System.getProperty("test.set.null.renew") != null) { 927 // Testing 8186576, see NullRenewUntil.java. 928 renewTill = null; 929 } 930 } 931 932 TicketFlags tFlags = new TicketFlags(bFlags); 933 EncTicketPart enc = new EncTicketPart( 934 tFlags, 935 key, 936 cname, 937 new TransitedEncoding(1, new byte[0]), // TODO 938 timeAfter(0), 939 from, 940 till, renewTill, 941 body.addresses != null ? body.addresses 942 : etp.caddr, 943 null); 944 EncryptionKey skey = keyForUser(service, e3, true); 945 if (skey == null) { 946 throw new KrbException(Krb5.KDC_ERR_SUMTYPE_NOSUPP); // TODO 947 } 948 Ticket t = new Ticket( 949 System.getProperty("test.kdc.diff.sname") != null ? 950 new PrincipalName("xx" + service.toString()) : 951 service, 952 new EncryptedData(skey, enc.asn1Encode(), KeyUsage.KU_TICKET) 953 ); 954 EncTGSRepPart enc_part = new EncTGSRepPart( 955 key, 956 new LastReq(new LastReqEntry[] { 957 new LastReqEntry(0, timeAfter(-10)) 958 }), 959 body.getNonce(), // TODO: detect replay 960 timeAfter(3600 * 24), 961 // Next 5 and last MUST be same with ticket 962 tFlags, 963 timeAfter(0), 964 from, 965 till, renewTill, 966 service, 967 body.addresses 968 ); 969 EncryptedData edata = new EncryptedData(ckey, enc_part.asn1Encode(), 970 KeyUsage.KU_ENC_TGS_REP_PART_SESSKEY); 971 TGSRep tgsRep = new TGSRep(null, 972 cname, 973 t, 974 edata); 975 System.out.println(" Return " + tgsRep.cname 976 + " ticket for " + tgsRep.ticket.sname + ", flags " 977 + tFlags); 978 979 DerOutputStream out = new DerOutputStream(); 980 out.write(DerValue.createTag(DerValue.TAG_APPLICATION, 981 true, (byte)Krb5.KRB_TGS_REP), tgsRep.asn1Encode()); 982 return out.toByteArray(); 983 } catch (KrbException ke) { 984 ke.printStackTrace(System.out); 985 KRBError kerr = ke.getError(); 986 KDCReqBody body = tgsReq.reqBody; 987 System.out.println(" Error " + ke.returnCode() 988 + " " +ke.returnCodeMessage()); 989 if (kerr == null) { 990 kerr = new KRBError(null, null, null, 991 timeAfter(0), 992 0, 993 ke.returnCode(), 994 body.cname, 995 service, 996 KrbException.errorMessage(ke.returnCode()), 997 null); 998 } 999 return kerr.asn1Encode(); 1000 } 1001 } 1002 1003 /** 1004 * Processes a AS_REQ and generates a AS_REP (or KRB_ERROR) 1005 * @param in the request 1006 * @return the response 1007 * @throws java.lang.Exception for various errors 1008 */ 1009 protected byte[] processAsReq(byte[] in) throws Exception { 1010 ASReq asReq = new ASReq(in); 1011 int[] eTypes = null; 1012 List<PAData> outPAs = new ArrayList<>(); 1013 1014 PrincipalName service = asReq.reqBody.sname; 1015 if (options.containsKey(KDC.Option.RESP_NT)) { 1016 service = new PrincipalName((int)options.get(KDC.Option.RESP_NT), 1017 service.getNameStrings(), 1018 Realm.getDefault()); 1019 } 1020 try { 1021 System.out.println(realm + "> " + asReq.reqBody.cname + 1022 " sends AS-REQ for " + 1023 service + ", " + asReq.reqBody.kdcOptions); 1024 1025 KDCReqBody body = asReq.reqBody; 1026 1027 eTypes = filterSupported(KDCReqBodyDotEType(body)); 1028 if (eTypes.length == 0) { 1029 throw new KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP); 1030 } 1031 int eType = eTypes[0]; 1032 1033 EncryptionKey ckey = keyForUser(body.cname, eType, false); 1034 EncryptionKey skey = keyForUser(service, eType, true); 1035 1036 if (options.containsKey(KDC.Option.ONLY_RC4_TGT)) { 1037 int tgtEType = EncryptedData.ETYPE_ARCFOUR_HMAC; 1038 boolean found = false; 1039 for (int i=0; i<eTypes.length; i++) { 1040 if (eTypes[i] == tgtEType) { 1041 found = true; 1042 break; 1043 } 1044 } 1045 if (!found) { 1046 throw new KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP); 1047 } 1048 skey = keyForUser(service, tgtEType, true); 1049 } 1050 if (ckey == null) { 1051 throw new KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP); 1052 } 1053 if (skey == null) { 1054 throw new KrbException(Krb5.KDC_ERR_SUMTYPE_NOSUPP); // TODO 1055 } 1056 1057 // Session key 1058 EncryptionKey key = generateRandomKey(eType); 1059 // Check time, TODO 1060 KerberosTime from = body.from; 1061 KerberosTime till = body.till; 1062 KerberosTime rtime = body.rtime; 1063 if (from == null || from.isZero()) { 1064 from = timeAfter(0); 1065 } 1066 if (till == null) { 1067 throw new KrbException(Krb5.KDC_ERR_NEVER_VALID); // TODO 1068 } else if (till.isZero()) { 1069 till = timeAfter(DEFAULT_LIFETIME); 1070 } else if (till.greaterThan(timeAfter(24 * 3600)) 1071 && System.getProperty("test.kdc.force.till") == null) { 1072 // If till is more than 1 day later, make it renewable 1073 till = timeAfter(DEFAULT_LIFETIME); 1074 body.kdcOptions.set(KDCOptions.RENEWABLE, true); 1075 if (rtime == null) rtime = till; 1076 } 1077 if (rtime == null && body.kdcOptions.get(KDCOptions.RENEWABLE)) { 1078 rtime = timeAfter(DEFAULT_RENEWTIME); 1079 } 1080 //body.from 1081 boolean[] bFlags = new boolean[Krb5.TKT_OPTS_MAX+1]; 1082 if (body.kdcOptions.get(KDCOptions.FORWARDABLE)) { 1083 List<String> sensitives = (List<String>) 1084 options.get(Option.SENSITIVE_ACCOUNTS); 1085 if (sensitives != null 1086 && sensitives.contains(body.cname.toString())) { 1087 // Cannot make FORWARDABLE 1088 } else { 1089 bFlags[Krb5.TKT_OPTS_FORWARDABLE] = true; 1090 } 1091 } 1092 if (body.kdcOptions.get(KDCOptions.RENEWABLE)) { 1093 bFlags[Krb5.TKT_OPTS_RENEWABLE] = true; 1094 //renew = timeAfter(3600 * 24 * 7); 1095 } 1096 if (body.kdcOptions.get(KDCOptions.PROXIABLE)) { 1097 bFlags[Krb5.TKT_OPTS_PROXIABLE] = true; 1098 } 1099 if (body.kdcOptions.get(KDCOptions.POSTDATED)) { 1100 bFlags[Krb5.TKT_OPTS_POSTDATED] = true; 1101 } 1102 if (body.kdcOptions.get(KDCOptions.ALLOW_POSTDATE)) { 1103 bFlags[Krb5.TKT_OPTS_MAY_POSTDATE] = true; 1104 } 1105 bFlags[Krb5.TKT_OPTS_INITIAL] = true; 1106 1107 // Creating PA-DATA 1108 DerValue[] pas2 = null, pas = null; 1109 if (options.containsKey(KDC.Option.DUP_ETYPE)) { 1110 int n = (Integer)options.get(KDC.Option.DUP_ETYPE); 1111 switch (n) { 1112 case 1: // customer's case in 7067974 1113 pas2 = new DerValue[] { 1114 new DerValue(new ETypeInfo2(1, null, null).asn1Encode()), 1115 new DerValue(new ETypeInfo2(1, "", null).asn1Encode()), 1116 new DerValue(new ETypeInfo2( 1117 1, realm, new byte[]{1}).asn1Encode()), 1118 }; 1119 pas = new DerValue[] { 1120 new DerValue(new ETypeInfo(1, null).asn1Encode()), 1121 new DerValue(new ETypeInfo(1, "").asn1Encode()), 1122 new DerValue(new ETypeInfo(1, realm).asn1Encode()), 1123 }; 1124 break; 1125 case 2: // we still reject non-null s2kparams and prefer E2 over E 1126 pas2 = new DerValue[] { 1127 new DerValue(new ETypeInfo2( 1128 1, realm, new byte[]{1}).asn1Encode()), 1129 new DerValue(new ETypeInfo2(1, null, null).asn1Encode()), 1130 new DerValue(new ETypeInfo2(1, "", null).asn1Encode()), 1131 }; 1132 pas = new DerValue[] { 1133 new DerValue(new ETypeInfo(1, realm).asn1Encode()), 1134 new DerValue(new ETypeInfo(1, null).asn1Encode()), 1135 new DerValue(new ETypeInfo(1, "").asn1Encode()), 1136 }; 1137 break; 1138 case 3: // but only E is wrong 1139 pas = new DerValue[] { 1140 new DerValue(new ETypeInfo(1, realm).asn1Encode()), 1141 new DerValue(new ETypeInfo(1, null).asn1Encode()), 1142 new DerValue(new ETypeInfo(1, "").asn1Encode()), 1143 }; 1144 break; 1145 case 4: // we also ignore rc4-hmac 1146 pas = new DerValue[] { 1147 new DerValue(new ETypeInfo(23, "ANYTHING").asn1Encode()), 1148 new DerValue(new ETypeInfo(1, null).asn1Encode()), 1149 new DerValue(new ETypeInfo(1, "").asn1Encode()), 1150 }; 1151 break; 1152 case 5: // "" should be wrong, but we accept it now 1153 // See s.s.k.internal.PAData$SaltAndParams 1154 pas = new DerValue[] { 1155 new DerValue(new ETypeInfo(1, "").asn1Encode()), 1156 new DerValue(new ETypeInfo(1, null).asn1Encode()), 1157 }; 1158 break; 1159 } 1160 } else { 1161 int[] epas = eTypes; 1162 if (options.containsKey(KDC.Option.RC4_FIRST_PREAUTH)) { 1163 for (int i=1; i<epas.length; i++) { 1164 if (epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC) { 1165 epas[i] = epas[0]; 1166 epas[0] = EncryptedData.ETYPE_ARCFOUR_HMAC; 1167 break; 1168 } 1169 }; 1170 } else if (options.containsKey(KDC.Option.ONLY_ONE_PREAUTH)) { 1171 epas = new int[] { eTypes[0] }; 1172 } 1173 pas2 = new DerValue[epas.length]; 1174 for (int i=0; i<epas.length; i++) { 1175 pas2[i] = new DerValue(new ETypeInfo2( 1176 epas[i], 1177 epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC ? 1178 null : getSalt(body.cname), 1179 getParams(body.cname, epas[i])).asn1Encode()); 1180 } 1181 boolean allOld = true; 1182 for (int i: eTypes) { 1183 if (i >= EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96 && 1184 i != EncryptedData.ETYPE_ARCFOUR_HMAC) { 1185 allOld = false; 1186 break; 1187 } 1188 } 1189 if (allOld) { 1190 pas = new DerValue[epas.length]; 1191 for (int i=0; i<epas.length; i++) { 1192 pas[i] = new DerValue(new ETypeInfo( 1193 epas[i], 1194 epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC ? 1195 null : getSalt(body.cname) 1196 ).asn1Encode()); 1197 } 1198 } 1199 } 1200 1201 DerOutputStream eid; 1202 if (pas2 != null) { 1203 eid = new DerOutputStream(); 1204 eid.putSequence(pas2); 1205 outPAs.add(new PAData(Krb5.PA_ETYPE_INFO2, eid.toByteArray())); 1206 } 1207 if (pas != null) { 1208 eid = new DerOutputStream(); 1209 eid.putSequence(pas); 1210 outPAs.add(new PAData(Krb5.PA_ETYPE_INFO, eid.toByteArray())); 1211 } 1212 1213 PAData[] inPAs = KDCReqDotPAData(asReq); 1214 if (inPAs == null || inPAs.length == 0) { 1215 Object preauth = options.get(Option.PREAUTH_REQUIRED); 1216 if (preauth == null || preauth.equals(Boolean.TRUE)) { 1217 throw new KrbException(Krb5.KDC_ERR_PREAUTH_REQUIRED); 1218 } 1219 } else { 1220 try { 1221 EncryptedData data = newEncryptedData( 1222 new DerValue(inPAs[0].getValue())); 1223 EncryptionKey pakey 1224 = keyForUser(body.cname, data.getEType(), false); 1225 data.decrypt(pakey, KeyUsage.KU_PA_ENC_TS); 1226 } catch (Exception e) { 1227 KrbException ke = new KrbException(Krb5.KDC_ERR_PREAUTH_FAILED); 1228 ke.initCause(e); 1229 throw ke; 1230 } 1231 bFlags[Krb5.TKT_OPTS_PRE_AUTHENT] = true; 1232 } 1233 1234 TicketFlags tFlags = new TicketFlags(bFlags); 1235 EncTicketPart enc = new EncTicketPart( 1236 tFlags, 1237 key, 1238 body.cname, 1239 new TransitedEncoding(1, new byte[0]), 1240 timeAfter(0), 1241 from, 1242 till, rtime, 1243 body.addresses, 1244 null); 1245 Ticket t = new Ticket( 1246 service, 1247 new EncryptedData(skey, enc.asn1Encode(), KeyUsage.KU_TICKET) 1248 ); 1249 EncASRepPart enc_part = new EncASRepPart( 1250 key, 1251 new LastReq(new LastReqEntry[]{ 1252 new LastReqEntry(0, timeAfter(-10)) 1253 }), 1254 body.getNonce(), // TODO: detect replay? 1255 timeAfter(3600 * 24), 1256 // Next 5 and last MUST be same with ticket 1257 tFlags, 1258 timeAfter(0), 1259 from, 1260 till, rtime, 1261 service, 1262 body.addresses 1263 ); 1264 EncryptedData edata = new EncryptedData(ckey, enc_part.asn1Encode(), 1265 KeyUsage.KU_ENC_AS_REP_PART); 1266 ASRep asRep = new ASRep( 1267 outPAs.toArray(new PAData[outPAs.size()]), 1268 body.cname, 1269 t, 1270 edata); 1271 1272 System.out.println(" Return " + asRep.cname 1273 + " ticket for " + asRep.ticket.sname + ", flags " 1274 + tFlags); 1275 1276 DerOutputStream out = new DerOutputStream(); 1277 out.write(DerValue.createTag(DerValue.TAG_APPLICATION, 1278 true, (byte)Krb5.KRB_AS_REP), asRep.asn1Encode()); 1279 byte[] result = out.toByteArray(); 1280 1281 // Added feature: 1282 // Write the current issuing TGT into a ccache file specified 1283 // by the system property below. 1284 String ccache = System.getProperty("test.kdc.save.ccache"); 1285 if (ccache != null) { 1286 asRep.encKDCRepPart = enc_part; 1287 sun.security.krb5.internal.ccache.Credentials credentials = 1288 new sun.security.krb5.internal.ccache.Credentials(asRep); 1289 CredentialsCache cache = 1290 CredentialsCache.create(asReq.reqBody.cname, ccache); 1291 if (cache == null) { 1292 throw new IOException("Unable to create the cache file " + 1293 ccache); 1294 } 1295 cache.update(credentials); 1296 cache.save(); 1297 } 1298 1299 return result; 1300 } catch (KrbException ke) { 1301 ke.printStackTrace(System.out); 1302 KRBError kerr = ke.getError(); 1303 KDCReqBody body = asReq.reqBody; 1304 System.out.println(" Error " + ke.returnCode() 1305 + " " +ke.returnCodeMessage()); 1306 byte[] eData = null; 1307 if (kerr == null) { 1308 if (ke.returnCode() == Krb5.KDC_ERR_PREAUTH_REQUIRED || 1309 ke.returnCode() == Krb5.KDC_ERR_PREAUTH_FAILED) { 1310 DerOutputStream bytes = new DerOutputStream(); 1311 bytes.write(new PAData(Krb5.PA_ENC_TIMESTAMP, new byte[0]).asn1Encode()); 1312 for (PAData p: outPAs) { 1313 bytes.write(p.asn1Encode()); 1314 } 1315 DerOutputStream temp = new DerOutputStream(); 1316 temp.write(DerValue.tag_Sequence, bytes); 1317 eData = temp.toByteArray(); 1318 } 1319 kerr = new KRBError(null, null, null, 1320 timeAfter(0), 1321 0, 1322 ke.returnCode(), 1323 body.cname, 1324 service, 1325 KrbException.errorMessage(ke.returnCode()), 1326 eData); 1327 } 1328 return kerr.asn1Encode(); 1329 } 1330 } 1331 1332 private int[] filterSupported(int[] input) { 1333 int count = 0; 1334 for (int i = 0; i < input.length; i++) { 1335 if (!EType.isSupported(input[i])) { 1336 continue; 1337 } 1338 if (SUPPORTED_ETYPES != null) { 1339 boolean supported = false; 1340 for (String se : SUPPORTED_ETYPES.split(",")) { 1341 if (Config.getType(se) == input[i]) { 1342 supported = true; 1343 break; 1344 } 1345 } 1346 if (!supported) { 1347 continue; 1348 } 1349 } 1350 if (count != i) { 1351 input[count] = input[i]; 1352 } 1353 count++; 1354 } 1355 if (count != input.length) { 1356 input = Arrays.copyOf(input, count); 1357 } 1358 return input; 1359 } 1360 1361 /** 1362 * Generates a line for a KDC to put inside [realms] of krb5.conf 1363 * @return REALM.NAME = { kdc = host:port etc } 1364 */ 1365 private String realmLine() { 1366 StringBuilder sb = new StringBuilder(); 1367 sb.append(realm).append(" = {\n kdc = ") 1368 .append(kdc).append(':').append(port).append('\n'); 1369 for (String s: conf) { 1370 sb.append(" ").append(s).append('\n'); 1371 } 1372 return sb.append("}\n").toString(); 1373 } 1374 1375 /** 1376 * Start the KDC service. This server listens on both UDP and TCP using 1377 * the same port number. It uses three threads to deal with requests. 1378 * They can be set to daemon threads if requested. 1379 * @param port the port number to listen to. If zero, a random available 1380 * port no less than 8000 will be chosen and used. 1381 * @param asDaemon true if the KDC threads should be daemons 1382 * @throws java.io.IOException for any communication error 1383 */ 1384 protected void startServer(int port, boolean asDaemon) throws IOException { 1385 if (nativeKdc != null) { 1386 startNativeServer(port, asDaemon); 1387 } else { 1388 startJavaServer(port, asDaemon); 1389 } 1390 } 1391 1392 private void startNativeServer(int port, boolean asDaemon) throws IOException { 1393 nativeKdc.prepare(); 1394 nativeKdc.init(); 1395 kdcProc = nativeKdc.kdc(); 1396 } 1397 1398 private void startJavaServer(int port, boolean asDaemon) throws IOException { 1399 if (port > 0) { 1400 u1 = new DatagramSocket(port, InetAddress.getByName("127.0.0.1")); 1401 t1 = new ServerSocket(port); 1402 } else { 1403 while (true) { 1404 // Try to find a port number that's both TCP and UDP free 1405 try { 1406 port = 8000 + new java.util.Random().nextInt(10000); 1407 u1 = null; 1408 u1 = new DatagramSocket(port, InetAddress.getByName("127.0.0.1")); 1409 t1 = new ServerSocket(port); 1410 break; 1411 } catch (Exception e) { 1412 if (u1 != null) u1.close(); 1413 } 1414 } 1415 } 1416 final DatagramSocket udp = u1; 1417 final ServerSocket tcp = t1; 1418 System.out.println("Start KDC on " + port); 1419 1420 this.port = port; 1421 1422 // The UDP consumer 1423 thread1 = new Thread() { 1424 public void run() { 1425 udpConsumerReady = true; 1426 while (true) { 1427 try { 1428 byte[] inbuf = new byte[8192]; 1429 DatagramPacket p = new DatagramPacket(inbuf, inbuf.length); 1430 udp.receive(p); 1431 System.out.println("-----------------------------------------------"); 1432 System.out.println(">>>>> UDP packet received"); 1433 q.put(new Job(processMessage(Arrays.copyOf(inbuf, p.getLength())), udp, p)); 1434 } catch (Exception e) { 1435 e.printStackTrace(); 1436 } 1437 } 1438 } 1439 }; 1440 thread1.setDaemon(asDaemon); 1441 thread1.start(); 1442 1443 // The TCP consumer 1444 thread2 = new Thread() { 1445 public void run() { 1446 tcpConsumerReady = true; 1447 while (true) { 1448 try { 1449 Socket socket = tcp.accept(); 1450 System.out.println("-----------------------------------------------"); 1451 System.out.println(">>>>> TCP connection established"); 1452 DataInputStream in = new DataInputStream(socket.getInputStream()); 1453 DataOutputStream out = new DataOutputStream(socket.getOutputStream()); 1454 int len = in.readInt(); 1455 if (len > 65535) { 1456 throw new Exception("Huge request not supported"); 1457 } 1458 byte[] token = new byte[len]; 1459 in.readFully(token); 1460 q.put(new Job(processMessage(token), socket, out)); 1461 } catch (Exception e) { 1462 e.printStackTrace(); 1463 } 1464 } 1465 } 1466 }; 1467 thread2.setDaemon(asDaemon); 1468 thread2.start(); 1469 1470 // The dispatcher 1471 thread3 = new Thread() { 1472 public void run() { 1473 dispatcherReady = true; 1474 while (true) { 1475 try { 1476 q.take().send(); 1477 } catch (Exception e) { 1478 } 1479 } 1480 } 1481 }; 1482 thread3.setDaemon(true); 1483 thread3.start(); 1484 1485 // wait for the KDC is ready 1486 try { 1487 while (!isReady()) { 1488 Thread.sleep(100); 1489 } 1490 } catch(InterruptedException e) { 1491 throw new IOException(e); 1492 } 1493 } 1494 1495 public void kinit(String user, String ccache) throws Exception { 1496 if (user.indexOf('@') < 0) { 1497 user = user + "@" + realm; 1498 } 1499 if (nativeKdc != null) { 1500 nativeKdc.kinit(user, ccache); 1501 } else { 1502 Context.fromUserPass(user, passwords.get(user), false) 1503 .ccache(ccache); 1504 } 1505 } 1506 1507 boolean isReady() { 1508 return udpConsumerReady && tcpConsumerReady && dispatcherReady; 1509 } 1510 1511 public void terminate() { 1512 if (nativeKdc != null) { 1513 System.out.println("Killing kdc..."); 1514 kdcProc.destroyForcibly(); 1515 System.out.println("Done"); 1516 } else { 1517 try { 1518 thread1.stop(); 1519 thread2.stop(); 1520 thread3.stop(); 1521 u1.close(); 1522 t1.close(); 1523 } catch (Exception e) { 1524 // OK 1525 } 1526 } 1527 } 1528 1529 public static KDC startKDC(final String host, final String krbConfFileName, 1530 final String realm, final Map<String, String> principals, 1531 final String ktab, final KtabMode mode) { 1532 1533 KDC kdc; 1534 try { 1535 kdc = KDC.create(realm, host, 0, true); 1536 kdc.setOption(KDC.Option.PREAUTH_REQUIRED, Boolean.FALSE); 1537 if (krbConfFileName != null) { 1538 KDC.saveConfig(krbConfFileName, kdc); 1539 } 1540 1541 // Add principals 1542 if (principals != null) { 1543 principals.forEach((name, password) -> { 1544 if (password == null || password.isEmpty()) { 1545 System.out.println(String.format( 1546 "KDC:add a principal '%s' with a random " + 1547 "password", name)); 1548 kdc.addPrincipalRandKey(name); 1549 } else { 1550 System.out.println(String.format( 1551 "KDC:add a principal '%s' with '%s' password", 1552 name, password)); 1553 kdc.addPrincipal(name, password.toCharArray()); 1554 } 1555 }); 1556 } 1557 1558 // Create or append keys to existing keytab file 1559 if (ktab != null) { 1560 File ktabFile = new File(ktab); 1561 switch(mode) { 1562 case APPEND: 1563 if (ktabFile.exists()) { 1564 System.out.println(String.format( 1565 "KDC:append keys to an exising keytab " 1566 + "file %s", ktab)); 1567 kdc.appendKtab(ktab); 1568 } else { 1569 System.out.println(String.format( 1570 "KDC:create a new keytab file %s", ktab)); 1571 kdc.writeKtab(ktab); 1572 } 1573 break; 1574 case EXISTING: 1575 System.out.println(String.format( 1576 "KDC:use an existing keytab file %s", ktab)); 1577 break; 1578 default: 1579 throw new RuntimeException(String.format( 1580 "KDC:unsupported keytab mode: %s", mode)); 1581 } 1582 } 1583 1584 System.out.println(String.format( 1585 "KDC: started on %s:%s with '%s' realm", 1586 host, kdc.getPort(), realm)); 1587 } catch (Exception e) { 1588 throw new RuntimeException("KDC: unexpected exception", e); 1589 } 1590 1591 return kdc; 1592 } 1593 1594 /** 1595 * Helper class to encapsulate a job in a KDC. 1596 */ 1597 private static class Job { 1598 byte[] token; // The received request at creation time and 1599 // the response at send time 1600 Socket s; // The TCP socket from where the request comes 1601 DataOutputStream out; // The OutputStream of the TCP socket 1602 DatagramSocket s2; // The UDP socket from where the request comes 1603 DatagramPacket dp; // The incoming UDP datagram packet 1604 boolean useTCP; // Whether TCP or UDP is used 1605 1606 // Creates a job object for TCP 1607 Job(byte[] token, Socket s, DataOutputStream out) { 1608 useTCP = true; 1609 this.token = token; 1610 this.s = s; 1611 this.out = out; 1612 } 1613 1614 // Creates a job object for UDP 1615 Job(byte[] token, DatagramSocket s2, DatagramPacket dp) { 1616 useTCP = false; 1617 this.token = token; 1618 this.s2 = s2; 1619 this.dp = dp; 1620 } 1621 1622 // Sends the output back to the client 1623 void send() { 1624 try { 1625 if (useTCP) { 1626 System.out.println(">>>>> TCP request honored"); 1627 out.writeInt(token.length); 1628 out.write(token); 1629 s.close(); 1630 } else { 1631 System.out.println(">>>>> UDP request honored"); 1632 s2.send(new DatagramPacket(token, token.length, dp.getAddress(), dp.getPort())); 1633 } 1634 } catch (Exception e) { 1635 e.printStackTrace(); 1636 } 1637 } 1638 } 1639 1640 /** 1641 * A native KDC using the binaries in nativePath. Attention: 1642 * this is using binaries, not an existing KDC instance. 1643 * An implementation of this takes care of configuration, 1644 * principal db managing and KDC startup. 1645 */ 1646 static abstract class NativeKdc { 1647 1648 protected Map<String,String> env; 1649 protected String nativePath; 1650 protected String base; 1651 protected String realm; 1652 protected int port; 1653 1654 NativeKdc(String nativePath, KDC kdc) { 1655 if (kdc.port == 0) { 1656 kdc.port = 8000 + new java.util.Random().nextInt(10000); 1657 } 1658 this.nativePath = nativePath; 1659 this.realm = kdc.realm; 1660 this.port = kdc.port; 1661 this.base = Paths.get("" + port).toAbsolutePath().toString(); 1662 } 1663 1664 // Add a new principal 1665 abstract void addPrincipal(String user, String pass); 1666 // Add a keytab entry 1667 abstract void ktadd(String user, String ktab); 1668 // Initialize KDC 1669 abstract void init(); 1670 // Start kdc 1671 abstract Process kdc(); 1672 // Configuration 1673 abstract void prepare(); 1674 // Fill ccache 1675 abstract void kinit(String user, String ccache); 1676 1677 static NativeKdc get(KDC kdc) { 1678 String prop = System.getProperty("native.kdc.path"); 1679 if (prop == null) { 1680 return null; 1681 } else if (Files.exists(Paths.get(prop, "sbin/krb5kdc"))) { 1682 return new MIT(true, prop, kdc); 1683 } else if (Files.exists(Paths.get(prop, "kdc/krb5kdc"))) { 1684 return new MIT(false, prop, kdc); 1685 } else if (Files.exists(Paths.get(prop, "libexec/kdc"))) { 1686 return new Heimdal(prop, kdc); 1687 } else { 1688 throw new IllegalArgumentException("Strange " + prop); 1689 } 1690 } 1691 1692 Process run(boolean wait, String... cmd) { 1693 try { 1694 System.out.println("Running " + cmd2str(env, cmd)); 1695 ProcessBuilder pb = new ProcessBuilder(); 1696 pb.inheritIO(); 1697 pb.environment().putAll(env); 1698 Process p = pb.command(cmd).start(); 1699 if (wait) { 1700 if (p.waitFor() < 0) { 1701 throw new RuntimeException("exit code is not null"); 1702 } 1703 return null; 1704 } else { 1705 return p; 1706 } 1707 } catch (Exception e) { 1708 throw new RuntimeException(e); 1709 } 1710 } 1711 1712 private String cmd2str(Map<String,String> env, String... cmd) { 1713 return env.entrySet().stream().map(e -> e.getKey()+"="+e.getValue()) 1714 .collect(Collectors.joining(" ")) + " " + 1715 Stream.of(cmd).collect(Collectors.joining(" ")); 1716 } 1717 } 1718 1719 // Heimdal KDC. Build your own and run "make install" to nativePath. 1720 static class Heimdal extends NativeKdc { 1721 1722 Heimdal(String nativePath, KDC kdc) { 1723 super(nativePath, kdc); 1724 this.env = Map.of( 1725 "KRB5_CONFIG", base + "/krb5.conf", 1726 "KRB5_TRACE", "/dev/stderr", 1727 Platform.sharedLibraryPathVariableName(), nativePath + "/lib"); 1728 } 1729 1730 @Override 1731 public void addPrincipal(String user, String pass) { 1732 run(true, nativePath + "/bin/kadmin", "-l", "-r", realm, 1733 "add", "-p", pass, "--use-defaults", user); 1734 } 1735 1736 @Override 1737 public void ktadd(String user, String ktab) { 1738 run(true, nativePath + "/bin/kadmin", "-l", "-r", realm, 1739 "ext_keytab", "-k", ktab, user); 1740 } 1741 1742 @Override 1743 public void init() { 1744 run(true, nativePath + "/bin/kadmin", "-l", "-r", realm, 1745 "init", "--realm-max-ticket-life=1day", 1746 "--realm-max-renewable-life=1month", realm); 1747 } 1748 1749 @Override 1750 public Process kdc() { 1751 return run(false, nativePath + "/libexec/kdc", 1752 "--addresses=127.0.0.1", "-P", "" + port); 1753 } 1754 1755 @Override 1756 public void prepare() { 1757 try { 1758 Files.createDirectory(Paths.get(base)); 1759 Files.write(Paths.get(base + "/krb5.conf"), Arrays.asList( 1760 "[libdefaults]", 1761 "default_realm = " + realm, 1762 "default_keytab_name = FILE:" + base + "/krb5.keytab", 1763 "forwardable = true", 1764 "dns_lookup_kdc = no", 1765 "dns_lookup_realm = no", 1766 "dns_canonicalize_hostname = false", 1767 "\n[realms]", 1768 realm + " = {", 1769 " kdc = localhost:" + port, 1770 "}", 1771 "\n[kdc]", 1772 "db-dir = " + base, 1773 "database = {", 1774 " label = {", 1775 " dbname = " + base + "/current-db", 1776 " realm = " + realm, 1777 " mkey_file = " + base + "/mkey.file", 1778 " acl_file = " + base + "/heimdal.acl", 1779 " log_file = " + base + "/current.log", 1780 " }", 1781 "}", 1782 SUPPORTED_ETYPES == null ? "" 1783 : ("\n[kadmin]\ndefault_keys = " 1784 + (SUPPORTED_ETYPES + ",") 1785 .replaceAll(",", ":pw-salt ")), 1786 "\n[logging]", 1787 "kdc = 0-/FILE:" + base + "/messages.log", 1788 "krb5 = 0-/FILE:" + base + "/messages.log", 1789 "default = 0-/FILE:" + base + "/messages.log" 1790 )); 1791 } catch (IOException e) { 1792 throw new UncheckedIOException(e); 1793 } 1794 } 1795 1796 @Override 1797 void kinit(String user, String ccache) { 1798 String tmpName = base + "/" + user + "." + 1799 System.identityHashCode(this) + ".keytab"; 1800 ktadd(user, tmpName); 1801 run(true, nativePath + "/bin/kinit", 1802 "-f", "-t", tmpName, "-c", ccache, user); 1803 } 1804 } 1805 1806 // MIT krb5 KDC. Make your own exploded (install == false), or 1807 // "make install" into nativePath (install == true). 1808 static class MIT extends NativeKdc { 1809 1810 private boolean install; // "make install" or "make" 1811 1812 MIT(boolean install, String nativePath, KDC kdc) { 1813 super(nativePath, kdc); 1814 this.install = install; 1815 this.env = Map.of( 1816 "KRB5_KDC_PROFILE", base + "/kdc.conf", 1817 "KRB5_CONFIG", base + "/krb5.conf", 1818 "KRB5_TRACE", "/dev/stderr", 1819 Platform.sharedLibraryPathVariableName(), nativePath + "/lib"); 1820 } 1821 1822 @Override 1823 public void addPrincipal(String user, String pass) { 1824 run(true, nativePath + 1825 (install ? "/sbin/" : "/kadmin/cli/") + "kadmin.local", 1826 "-q", "addprinc -pw " + pass + " " + user); 1827 } 1828 1829 @Override 1830 public void ktadd(String user, String ktab) { 1831 run(true, nativePath + 1832 (install ? "/sbin/" : "/kadmin/cli/") + "kadmin.local", 1833 "-q", "ktadd -k " + ktab + " -norandkey " + user); 1834 } 1835 1836 @Override 1837 public void init() { 1838 run(true, nativePath + 1839 (install ? "/sbin/" : "/kadmin/dbutil/") + "kdb5_util", 1840 "create", "-s", "-W", "-P", "olala"); 1841 } 1842 1843 @Override 1844 public Process kdc() { 1845 return run(false, nativePath + 1846 (install ? "/sbin/" : "/kdc/") + "krb5kdc", 1847 "-n"); 1848 } 1849 1850 @Override 1851 public void prepare() { 1852 try { 1853 Files.createDirectory(Paths.get(base)); 1854 Files.write(Paths.get(base + "/kdc.conf"), Arrays.asList( 1855 "[kdcdefaults]", 1856 "\n[realms]", 1857 realm + "= {", 1858 " kdc_listen = " + this.port, 1859 " kdc_tcp_listen = " + this.port, 1860 " database_name = " + base + "/principal", 1861 " key_stash_file = " + base + "/.k5.ATHENA.MIT.EDU", 1862 SUPPORTED_ETYPES == null ? "" 1863 : (" supported_enctypes = " 1864 + (SUPPORTED_ETYPES + ",") 1865 .replaceAll(",", ":normal ")), 1866 "}" 1867 )); 1868 Files.write(Paths.get(base + "/krb5.conf"), Arrays.asList( 1869 "[libdefaults]", 1870 "default_realm = " + realm, 1871 "default_keytab_name = FILE:" + base + "/krb5.keytab", 1872 "forwardable = true", 1873 "dns_lookup_kdc = no", 1874 "dns_lookup_realm = no", 1875 "dns_canonicalize_hostname = false", 1876 "\n[realms]", 1877 realm + " = {", 1878 " kdc = localhost:" + port, 1879 "}", 1880 "\n[logging]", 1881 "kdc = FILE:" + base + "/krb5kdc.log" 1882 )); 1883 } catch (IOException e) { 1884 throw new UncheckedIOException(e); 1885 } 1886 } 1887 1888 @Override 1889 void kinit(String user, String ccache) { 1890 String tmpName = base + "/" + user + "." + 1891 System.identityHashCode(this) + ".keytab"; 1892 ktadd(user, tmpName); 1893 run(true, nativePath + 1894 (install ? "/bin/" : "/clients/kinit/") + "kinit", 1895 "-f", "-t", tmpName, "-c", ccache, user); 1896 } 1897 } 1898 1899 // Calling private methods thru reflections 1900 private static final Field getPADataField; 1901 private static final Field getEType; 1902 private static final Constructor<EncryptedData> ctorEncryptedData; 1903 private static final Method stringToKey; 1904 private static final Field getAddlTkt; 1905 1906 static { 1907 try { 1908 ctorEncryptedData = EncryptedData.class.getDeclaredConstructor(DerValue.class); 1909 ctorEncryptedData.setAccessible(true); 1910 getPADataField = KDCReq.class.getDeclaredField("pAData"); 1911 getPADataField.setAccessible(true); 1912 getEType = KDCReqBody.class.getDeclaredField("eType"); 1913 getEType.setAccessible(true); 1914 stringToKey = EncryptionKey.class.getDeclaredMethod( 1915 "stringToKey", 1916 char[].class, String.class, byte[].class, Integer.TYPE); 1917 stringToKey.setAccessible(true); 1918 getAddlTkt = KDCReqBody.class.getDeclaredField("additionalTickets"); 1919 getAddlTkt.setAccessible(true); 1920 } catch (NoSuchFieldException nsfe) { 1921 throw new AssertionError(nsfe); 1922 } catch (NoSuchMethodException nsme) { 1923 throw new AssertionError(nsme); 1924 } 1925 } 1926 private EncryptedData newEncryptedData(DerValue der) { 1927 try { 1928 return ctorEncryptedData.newInstance(der); 1929 } catch (Exception e) { 1930 throw new AssertionError(e); 1931 } 1932 } 1933 private static PAData[] KDCReqDotPAData(KDCReq req) { 1934 try { 1935 return (PAData[])getPADataField.get(req); 1936 } catch (Exception e) { 1937 throw new AssertionError(e); 1938 } 1939 } 1940 private static int[] KDCReqBodyDotEType(KDCReqBody body) { 1941 try { 1942 return (int[]) getEType.get(body); 1943 } catch (Exception e) { 1944 throw new AssertionError(e); 1945 } 1946 } 1947 private static byte[] EncryptionKeyDotStringToKey(char[] password, String salt, 1948 byte[] s2kparams, int keyType) throws KrbCryptoException { 1949 try { 1950 return (byte[])stringToKey.invoke( 1951 null, password, salt, s2kparams, keyType); 1952 } catch (InvocationTargetException ex) { 1953 throw (KrbCryptoException)ex.getCause(); 1954 } catch (Exception e) { 1955 throw new AssertionError(e); 1956 } 1957 } 1958 private static Ticket KDCReqBodyDotFirstAdditionalTicket(KDCReqBody body) { 1959 try { 1960 return ((Ticket[])getAddlTkt.get(body))[0]; 1961 } catch (Exception e) { 1962 throw new AssertionError(e); 1963 } 1964 } 1965 }