1/* 2 * Copyright (c) 2008, 2016, 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 24import java.lang.reflect.Constructor; 25import java.lang.reflect.Field; 26import java.lang.reflect.InvocationTargetException; 27import java.net.*; 28import java.io.*; 29import java.lang.reflect.Method; 30import java.security.SecureRandom; 31import java.time.Instant; 32import java.time.temporal.ChronoUnit; 33import java.time.temporal.TemporalAmount; 34import java.time.temporal.TemporalUnit; 35import java.util.*; 36import java.util.concurrent.*; 37 38import sun.security.krb5.*; 39import sun.security.krb5.internal.*; 40import sun.security.krb5.internal.ccache.CredentialsCache; 41import sun.security.krb5.internal.crypto.EType; 42import sun.security.krb5.internal.crypto.KeyUsage; 43import sun.security.krb5.internal.ktab.KeyTab; 44import sun.security.util.DerInputStream; 45import sun.security.util.DerOutputStream; 46import sun.security.util.DerValue; 47 48/** 49 * A KDC server. 50 * <p> 51 * Features: 52 * <ol> 53 * <li> Supports TCP and UDP 54 * <li> Supports AS-REQ and TGS-REQ 55 * <li> Principal db and other settings hard coded in application 56 * <li> Options, say, request preauth or not 57 * </ol> 58 * Side effects: 59 * <ol> 60 * <li> The Sun-internal class <code>sun.security.krb5.Config</code> is a 61 * singleton and initialized according to Kerberos settings (krb5.conf and 62 * java.security.krb5.* system properties). This means once it's initialized 63 * it will not automatically notice any changes to these settings (or file 64 * changes of krb5.conf). The KDC class normally does not touch these 65 * settings (except for the <code>writeKtab()</code> method). However, to make 66 * sure nothing ever goes wrong, if you want to make any changes to these 67 * settings after calling a KDC method, call <code>Config.refresh()</code> to 68 * make sure your changes are reflected in the <code>Config</code> object. 69 * </ol> 70 * System properties recognized: 71 * <ul> 72 * <li>test.kdc.save.ccache 73 * </ul> 74 * Issues and TODOs: 75 * <ol> 76 * <li> Generates krb5.conf to be used on another machine, currently the kdc is 77 * always localhost 78 * <li> More options to KDC, say, error output, say, response nonce != 79 * request nonce 80 * </ol> 81 * Note: This program uses internal krb5 classes (including reflection to 82 * access private fields and methods). 83 * <p> 84 * Usages: 85 * <p> 86 * 1. Init and start the KDC: 87 * <pre> 88 * KDC kdc = KDC.create("REALM.NAME", port, isDaemon); 89 * KDC kdc = KDC.create("REALM.NAME"); 90 * </pre> 91 * Here, <code>port</code> is the UDP and TCP port number the KDC server 92 * listens on. If zero, a random port is chosen, which you can use getPort() 93 * later to retrieve the value. 94 * <p> 95 * If <code>isDaemon</code> is true, the KDC worker threads will be daemons. 96 * <p> 97 * The shortcut <code>KDC.create("REALM.NAME")</code> has port=0 and 98 * isDaemon=false, and is commonly used in an embedded KDC. 99 * <p> 100 * 2. Adding users: 101 * <pre> 102 * kdc.addPrincipal(String principal_name, char[] password); 103 * kdc.addPrincipalRandKey(String principal_name); 104 * </pre> 105 * A service principal's name should look like "host/f.q.d.n". The second form 106 * generates a random key. To expose this key, call <code>writeKtab()</code> to 107 * save the keys into a keytab file. 108 * <p> 109 * Note that you need to add the principal name krbtgt/REALM.NAME yourself. 110 * <p> 111 * Note that you can safely add a principal at any time after the KDC is 112 * started and before a user requests info on this principal. 113 * <p> 114 * 3. Other public methods: 115 * <ul> 116 * <li> <code>getPort</code>: Returns the port number the KDC uses 117 * <li> <code>getRealm</code>: Returns the realm name 118 * <li> <code>writeKtab</code>: Writes all principals' keys into a keytab file 119 * <li> <code>saveConfig</code>: Saves a krb5.conf file to access this KDC 120 * <li> <code>setOption</code>: Sets various options 121 * </ul> 122 * Read the javadoc for details. Lazy developer can use <code>OneKDC</code> 123 * directly. 124 */ 125public class KDC { 126 127 public static final int DEFAULT_LIFETIME = 39600; 128 public static final int DEFAULT_RENEWTIME = 86400; 129 130 public static String NOT_EXISTING_HOST = "not.existing.host"; 131 132 // Under the hood. 133 134 // The random generator to generate random keys (including session keys) 135 private static SecureRandom secureRandom = new SecureRandom(); 136 137 // Principal db. principal -> pass. A case-insensitive TreeMap is used 138 // so that even if the client provides a name with different case, the KDC 139 // can still locate the principal and give back correct salt. 140 private TreeMap<String,char[]> passwords = new TreeMap<> 141 (String.CASE_INSENSITIVE_ORDER); 142 143 // Realm name 144 private String realm; 145 // KDC 146 private String kdc; 147 // Service port number 148 private int port; 149 // The request/response job queue 150 private BlockingQueue<Job> q = new ArrayBlockingQueue<>(100); 151 // Options 152 private Map<Option,Object> options = new HashMap<>(); 153 // Realm-specific krb5.conf settings 154 private List<String> conf = new ArrayList<>(); 155 156 private Thread thread1, thread2, thread3; 157 private volatile boolean udpConsumerReady = false; 158 private volatile boolean tcpConsumerReady = false; 159 private volatile boolean dispatcherReady = false; 160 DatagramSocket u1 = null; 161 ServerSocket t1 = null; 162 163 public static enum KtabMode { APPEND, EXISTING }; 164 165 /** 166 * Option names, to be expanded forever. 167 */ 168 public static enum Option { 169 /** 170 * Whether pre-authentication is required. Default Boolean.TRUE 171 */ 172 PREAUTH_REQUIRED, 173 /** 174 * Only issue TGT in RC4 175 */ 176 ONLY_RC4_TGT, 177 /** 178 * Use RC4 as the first in preauth 179 */ 180 RC4_FIRST_PREAUTH, 181 /** 182 * Use only one preauth, so that some keys are not easy to generate 183 */ 184 ONLY_ONE_PREAUTH, 185 /** 186 * Set all name-type to a value in response 187 */ 188 RESP_NT, 189 /** 190 * Multiple ETYPE-INFO-ENTRY with same etype but different salt 191 */ 192 DUP_ETYPE, 193 /** 194 * What backend server can be delegated to 195 */ 196 OK_AS_DELEGATE, 197 /** 198 * Allow S4U2self, List<String> of middle servers. 199 * If not set, means KDC does not understand S4U2self at all, therefore 200 * would ignore any PA-FOR-USER request and send a ticket using the 201 * cname of teh requestor. If set, it returns FORWARDABLE tickets to 202 * a server with its name in the list 203 */ 204 ALLOW_S4U2SELF, 205 /** 206 * Allow S4U2proxy, Map<String,List<String>> of middle servers to 207 * backends. If not set or a backend not in a server's list, 208 * Krb5.KDC_ERR_POLICY will be send for S4U2proxy request. 209 */ 210 ALLOW_S4U2PROXY, 211 /** 212 * Sensitive accounts can never be delegated. 213 */ 214 SENSITIVE_ACCOUNTS, 215 /** 216 * If true, will check if TGS-REQ contains a non-null addresses field. 217 */ 218 CHECK_ADDRESSES, 219 }; 220 221 static { 222 if (System.getProperty("jdk.net.hosts.file") == null) { 223 String hostsFileName = System.getProperty("test.src", ".") + "/TestHosts"; 224 System.setProperty("jdk.net.hosts.file", hostsFileName); 225 } 226 } 227 228 /** 229 * A standalone KDC server. 230 */ 231 public static void main(String[] args) throws Exception { 232 int port = args.length > 0 ? Integer.parseInt(args[0]) : 0; 233 KDC kdc = create("RABBIT.HOLE", "kdc.rabbit.hole", port, false); 234 kdc.addPrincipal("dummy", "bogus".toCharArray()); 235 kdc.addPrincipal("foo", "bar".toCharArray()); 236 kdc.addPrincipalRandKey("krbtgt/RABBIT.HOLE"); 237 kdc.addPrincipalRandKey("server/host.rabbit.hole"); 238 kdc.addPrincipalRandKey("backend/host.rabbit.hole"); 239 KDC.saveConfig("krb5.conf", kdc, "forwardable = true"); 240 } 241 242 /** 243 * Creates and starts a KDC running as a daemon on a random port. 244 * @param realm the realm name 245 * @return the running KDC instance 246 * @throws java.io.IOException for any socket creation error 247 */ 248 public static KDC create(String realm) throws IOException { 249 return create(realm, "kdc." + realm.toLowerCase(Locale.US), 0, true); 250 } 251 252 public static KDC existing(String realm, String kdc, int port) { 253 KDC k = new KDC(realm, kdc); 254 k.port = port; 255 return k; 256 } 257 258 /** 259 * Creates and starts a KDC server. 260 * @param realm the realm name 261 * @param port the TCP and UDP port to listen to. A random port will to 262 * chosen if zero. 263 * @param asDaemon if true, KDC threads will be daemons. Otherwise, not. 264 * @return the running KDC instance 265 * @throws java.io.IOException for any socket creation error 266 */ 267 public static KDC create(String realm, String kdc, int port, boolean asDaemon) throws IOException { 268 return new KDC(realm, kdc, port, asDaemon); 269 } 270 271 /** 272 * Sets an option 273 * @param key the option name 274 * @param value the value 275 */ 276 public void setOption(Option key, Object value) { 277 if (value == null) { 278 options.remove(key); 279 } else { 280 options.put(key, value); 281 } 282 } 283 284 /** 285 * Writes or appends keys into a keytab. 286 * <p> 287 * Attention: This is the most basic one of a series of methods below on 288 * keytab creation or modification. All these methods reference krb5.conf 289 * settings. If you need to modify krb5.conf or switch to another krb5.conf 290 * later, please call <code>Config.refresh()</code> again. For example: 291 * <pre> 292 * kdc.writeKtab("/etc/kdc/ktab", true); // Config is initialized, 293 * System.setProperty("java.security.krb5.conf", "/home/mykrb5.conf"); 294 * Config.refresh(); 295 * </pre> 296 * Inside this method there are 2 places krb5.conf is used: 297 * <ol> 298 * <li> (Fatal) Generating keys: EncryptionKey.acquireSecretKeys 299 * <li> (Has workaround) Creating PrincipalName 300 * </ol> 301 * @param tab the keytab file name 302 * @param append true if append, otherwise, overwrite. 303 * @param names the names to write into, write all if names is empty 304 */ 305 public void writeKtab(String tab, boolean append, String... names) 306 throws IOException, KrbException { 307 KeyTab ktab = append ? KeyTab.getInstance(tab) : KeyTab.create(tab); 308 Iterable<String> entries = 309 (names.length != 0) ? Arrays.asList(names): passwords.keySet(); 310 for (String name : entries) { 311 char[] pass = passwords.get(name); 312 int kvno = 0; 313 if (Character.isDigit(pass[pass.length-1])) { 314 kvno = pass[pass.length-1] - '0'; 315 } 316 PrincipalName pn = new PrincipalName(name, 317 name.indexOf('/') < 0 ? 318 PrincipalName.KRB_NT_UNKNOWN : 319 PrincipalName.KRB_NT_SRV_HST); 320 ktab.addEntry(pn, 321 getSalt(pn), 322 pass, 323 kvno, 324 true); 325 } 326 ktab.save(); 327 } 328 329 /** 330 * Writes all principals' keys from multiple KDCs into one keytab file. 331 * @throws java.io.IOException for any file output error 332 * @throws sun.security.krb5.KrbException for any realm and/or principal 333 * name error. 334 */ 335 public static void writeMultiKtab(String tab, KDC... kdcs) 336 throws IOException, KrbException { 337 KeyTab.create(tab).save(); // Empty the old keytab 338 appendMultiKtab(tab, kdcs); 339 } 340 341 /** 342 * Appends all principals' keys from multiple KDCs to one keytab file. 343 */ 344 public static void appendMultiKtab(String tab, KDC... kdcs) 345 throws IOException, KrbException { 346 for (KDC kdc: kdcs) { 347 kdc.writeKtab(tab, true); 348 } 349 } 350 351 /** 352 * Write a ktab for this KDC. 353 */ 354 public void writeKtab(String tab) throws IOException, KrbException { 355 writeKtab(tab, false); 356 } 357 358 /** 359 * Appends keys in this KDC to a ktab. 360 */ 361 public void appendKtab(String tab) throws IOException, KrbException { 362 writeKtab(tab, true); 363 } 364 365 /** 366 * Adds a new principal to this realm with a given password. 367 * @param user the principal's name. For a service principal, use the 368 * form of host/f.q.d.n 369 * @param pass the password for the principal 370 */ 371 public void addPrincipal(String user, char[] pass) { 372 if (user.indexOf('@') < 0) { 373 user = user + "@" + realm; 374 } 375 passwords.put(user, pass); 376 } 377 378 /** 379 * Adds a new principal to this realm with a random password 380 * @param user the principal's name. For a service principal, use the 381 * form of host/f.q.d.n 382 */ 383 public void addPrincipalRandKey(String user) { 384 addPrincipal(user, randomPassword()); 385 } 386 387 /** 388 * Returns the name of this realm 389 * @return the name of this realm 390 */ 391 public String getRealm() { 392 return realm; 393 } 394 395 /** 396 * Returns the name of kdc 397 * @return the name of kdc 398 */ 399 public String getKDC() { 400 return kdc; 401 } 402 403 /** 404 * Add realm-specific krb5.conf setting 405 */ 406 public void addConf(String s) { 407 conf.add(s); 408 } 409 410 /** 411 * Writes a krb5.conf for one or more KDC that includes KDC locations for 412 * each realm and the default realm name. You can also add extra strings 413 * into the file. The method should be called like: 414 * <pre> 415 * KDC.saveConfig("krb5.conf", kdc1, kdc2, ..., line1, line2, ...); 416 * </pre> 417 * Here you can provide one or more kdc# and zero or more line# arguments. 418 * The line# will be put after [libdefaults] and before [realms]. Therefore 419 * you can append new lines into [libdefaults] and/or create your new 420 * stanzas as well. Note that a newline character will be appended to 421 * each line# argument. 422 * <p> 423 * For example: 424 * <pre> 425 * KDC.saveConfig("krb5.conf", this); 426 * </pre> 427 * generates: 428 * <pre> 429 * [libdefaults] 430 * default_realm = REALM.NAME 431 * 432 * [realms] 433 * REALM.NAME = { 434 * kdc = host:port_number 435 * # realm-specific settings 436 * } 437 * </pre> 438 * 439 * Another example: 440 * <pre> 441 * KDC.saveConfig("krb5.conf", kdc1, kdc2, "forwardable = true", "", 442 * "[domain_realm]", 443 * ".kdc1.com = KDC1.NAME"); 444 * </pre> 445 * generates: 446 * <pre> 447 * [libdefaults] 448 * default_realm = KDC1.NAME 449 * forwardable = true 450 * 451 * [domain_realm] 452 * .kdc1.com = KDC1.NAME 453 * 454 * [realms] 455 * KDC1.NAME = { 456 * kdc = host:port1 457 * } 458 * KDC2.NAME = { 459 * kdc = host:port2 460 * } 461 * </pre> 462 * @param file the name of the file to write into 463 * @param kdc the first (and default) KDC 464 * @param more more KDCs or extra lines (in their appearing order) to 465 * insert into the krb5.conf file. This method reads each argument's type 466 * to determine what it's for. This argument can be empty. 467 * @throws java.io.IOException for any file output error 468 */ 469 public static void saveConfig(String file, KDC kdc, Object... more) 470 throws IOException { 471 File f = new File(file); 472 StringBuffer sb = new StringBuffer(); 473 sb.append("[libdefaults]\ndefault_realm = "); 474 sb.append(kdc.realm); 475 sb.append("\n"); 476 for (Object o: more) { 477 if (o instanceof String) { 478 sb.append(o); 479 sb.append("\n"); 480 } 481 } 482 sb.append("\n[realms]\n"); 483 sb.append(kdc.realmLine()); 484 for (Object o: more) { 485 if (o instanceof KDC) { 486 sb.append(((KDC)o).realmLine()); 487 } 488 } 489 FileOutputStream fos = new FileOutputStream(f); 490 fos.write(sb.toString().getBytes()); 491 fos.close(); 492 } 493 494 /** 495 * Returns the service port of the KDC server. 496 * @return the KDC service port 497 */ 498 public int getPort() { 499 return port; 500 } 501 502 // Private helper methods 503 504 /** 505 * Private constructor, cannot be called outside. 506 * @param realm 507 */ 508 private KDC(String realm, String kdc) { 509 this.realm = realm; 510 this.kdc = kdc; 511 } 512 513 /** 514 * A constructor that starts the KDC service also. 515 */ 516 protected KDC(String realm, String kdc, int port, boolean asDaemon) 517 throws IOException { 518 this(realm, kdc); 519 startServer(port, asDaemon); 520 } 521 /** 522 * Generates a 32-char random password 523 * @return the password 524 */ 525 private static char[] randomPassword() { 526 char[] pass = new char[32]; 527 for (int i=0; i<31; i++) 528 pass[i] = (char)secureRandom.nextInt(); 529 // The last char cannot be a number, otherwise, keyForUser() 530 // believes it's a sign of kvno 531 pass[31] = 'Z'; 532 return pass; 533 } 534 535 /** 536 * Generates a random key for the given encryption type. 537 * @param eType the encryption type 538 * @return the generated key 539 * @throws sun.security.krb5.KrbException for unknown/unsupported etype 540 */ 541 private static EncryptionKey generateRandomKey(int eType) 542 throws KrbException { 543 // Is 32 enough for AES256? I should have generated the keys directly 544 // but different cryptos have different rules on what keys are valid. 545 char[] pass = randomPassword(); 546 String algo; 547 switch (eType) { 548 case EncryptedData.ETYPE_DES_CBC_MD5: algo = "DES"; break; 549 case EncryptedData.ETYPE_DES3_CBC_HMAC_SHA1_KD: algo = "DESede"; break; 550 case EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96: algo = "AES128"; break; 551 case EncryptedData.ETYPE_ARCFOUR_HMAC: algo = "ArcFourHMAC"; break; 552 case EncryptedData.ETYPE_AES256_CTS_HMAC_SHA1_96: algo = "AES256"; break; 553 default: algo = "DES"; break; 554 } 555 return new EncryptionKey(pass, "NOTHING", algo); // Silly 556 } 557 558 /** 559 * Returns the password for a given principal 560 * @param p principal 561 * @return the password 562 * @throws sun.security.krb5.KrbException when the principal is not inside 563 * the database. 564 */ 565 private char[] getPassword(PrincipalName p, boolean server) 566 throws KrbException { 567 String pn = p.toString(); 568 if (p.getRealmString() == null) { 569 pn = pn + "@" + getRealm(); 570 } 571 char[] pass = passwords.get(pn); 572 if (pass == null) { 573 throw new KrbException(server? 574 Krb5.KDC_ERR_S_PRINCIPAL_UNKNOWN: 575 Krb5.KDC_ERR_C_PRINCIPAL_UNKNOWN, pn.toString()); 576 } 577 return pass; 578 } 579 580 /** 581 * Returns the salt string for the principal. 582 * @param p principal 583 * @return the salt 584 */ 585 protected String getSalt(PrincipalName p) { 586 String pn = p.toString(); 587 if (p.getRealmString() == null) { 588 pn = pn + "@" + getRealm(); 589 } 590 if (passwords.containsKey(pn)) { 591 try { 592 // Find the principal name with correct case. 593 p = new PrincipalName(passwords.ceilingEntry(pn).getKey()); 594 } catch (RealmException re) { 595 // Won't happen 596 } 597 } 598 String s = p.getRealmString(); 599 if (s == null) s = getRealm(); 600 for (String n: p.getNameStrings()) { 601 s += n; 602 } 603 return s; 604 } 605 606 /** 607 * Returns the key for a given principal of the given encryption type 608 * @param p the principal 609 * @param etype the encryption type 610 * @param server looking for a server principal? 611 * @return the key 612 * @throws sun.security.krb5.KrbException for unknown/unsupported etype 613 */ 614 private EncryptionKey keyForUser(PrincipalName p, int etype, boolean server) 615 throws KrbException { 616 try { 617 // Do not call EncryptionKey.acquireSecretKeys(), otherwise 618 // the krb5.conf config file would be loaded. 619 Integer kvno = null; 620 // For service whose password ending with a number, use it as kvno. 621 // Kvno must be postive. 622 if (p.toString().indexOf('/') > 0) { 623 char[] pass = getPassword(p, server); 624 if (Character.isDigit(pass[pass.length-1])) { 625 kvno = pass[pass.length-1] - '0'; 626 } 627 } 628 return new EncryptionKey(EncryptionKeyDotStringToKey( 629 getPassword(p, server), getSalt(p), null, etype), 630 etype, kvno); 631 } catch (KrbException ke) { 632 throw ke; 633 } catch (Exception e) { 634 throw new RuntimeException(e); // should not happen 635 } 636 } 637 638 /** 639 * Processes an incoming request and generates a response. 640 * @param in the request 641 * @return the response 642 * @throws java.lang.Exception for various errors 643 */ 644 protected byte[] processMessage(byte[] in) throws Exception { 645 if ((in[0] & 0x1f) == Krb5.KRB_AS_REQ) 646 return processAsReq(in); 647 else 648 return processTgsReq(in); 649 } 650 651 /** 652 * Processes a TGS_REQ and generates a TGS_REP (or KRB_ERROR) 653 * @param in the request 654 * @return the response 655 * @throws java.lang.Exception for various errors 656 */ 657 protected byte[] processTgsReq(byte[] in) throws Exception { 658 TGSReq tgsReq = new TGSReq(in); 659 PrincipalName service = tgsReq.reqBody.sname; 660 if (options.containsKey(KDC.Option.RESP_NT)) { 661 service = new PrincipalName((int)options.get(KDC.Option.RESP_NT), 662 service.getNameStrings(), service.getRealm()); 663 } 664 try { 665 System.out.println(realm + "> " + tgsReq.reqBody.cname + 666 " sends TGS-REQ for " + 667 service + ", " + tgsReq.reqBody.kdcOptions); 668 KDCReqBody body = tgsReq.reqBody; 669 int[] eTypes = KDCReqBodyDotEType(body); 670 int e2 = eTypes[0]; // etype for outgoing session key 671 int e3 = eTypes[0]; // etype for outgoing ticket 672 673 PAData[] pas = KDCReqDotPAData(tgsReq); 674 675 Ticket tkt = null; 676 EncTicketPart etp = null; 677 678 PrincipalName cname = null; 679 boolean allowForwardable = true; 680 681 if (pas == null || pas.length == 0) { 682 throw new KrbException(Krb5.KDC_ERR_PADATA_TYPE_NOSUPP); 683 } else { 684 PrincipalName forUserCName = null; 685 for (PAData pa: pas) { 686 if (pa.getType() == Krb5.PA_TGS_REQ) { 687 APReq apReq = new APReq(pa.getValue()); 688 EncryptedData ed = apReq.authenticator; 689 tkt = apReq.ticket; 690 int te = tkt.encPart.getEType(); 691 EncryptionKey kkey = keyForUser(tkt.sname, te, true); 692 byte[] bb = tkt.encPart.decrypt(kkey, KeyUsage.KU_TICKET); 693 DerInputStream derIn = new DerInputStream(bb); 694 DerValue der = derIn.getDerValue(); 695 etp = new EncTicketPart(der.toByteArray()); 696 // Finally, cname will be overwritten by PA-FOR-USER 697 // if it exists. 698 cname = etp.cname; 699 System.out.println(realm + "> presenting a ticket of " 700 + etp.cname + " to " + tkt.sname); 701 } else if (pa.getType() == Krb5.PA_FOR_USER) { 702 if (options.containsKey(Option.ALLOW_S4U2SELF)) { 703 PAForUserEnc p4u = new PAForUserEnc( 704 new DerValue(pa.getValue()), null); 705 forUserCName = p4u.name; 706 System.out.println(realm + "> presenting a PA_FOR_USER " 707 + " in the name of " + p4u.name); 708 } 709 } 710 } 711 if (forUserCName != null) { 712 List<String> names = (List<String>)options.get(Option.ALLOW_S4U2SELF); 713 if (!names.contains(cname.toString())) { 714 // Mimic the normal KDC behavior. When a server is not 715 // allowed to send S4U2self, do not send an error. 716 // Instead, send a ticket which is useless later. 717 allowForwardable = false; 718 } 719 cname = forUserCName; 720 } 721 if (tkt == null) { 722 throw new KrbException(Krb5.KDC_ERR_PADATA_TYPE_NOSUPP); 723 } 724 } 725 726 // Session key for original ticket, TGT 727 EncryptionKey ckey = etp.key; 728 729 // Session key for session with the service 730 EncryptionKey key = generateRandomKey(e2); 731 732 // Check time, TODO 733 KerberosTime till = body.till; 734 if (till == null) { 735 throw new KrbException(Krb5.KDC_ERR_NEVER_VALID); // TODO 736 } else if (till.isZero()) { 737 till = new KerberosTime(new Date().getTime() + 1000 * 3600 * 11); 738 } 739 740 boolean[] bFlags = new boolean[Krb5.TKT_OPTS_MAX+1]; 741 if (body.kdcOptions.get(KDCOptions.FORWARDABLE) 742 && allowForwardable) { 743 List<String> sensitives = (List<String>) 744 options.get(Option.SENSITIVE_ACCOUNTS); 745 if (sensitives != null && sensitives.contains(cname.toString())) { 746 // Cannot make FORWARDABLE 747 } else { 748 bFlags[Krb5.TKT_OPTS_FORWARDABLE] = true; 749 } 750 } 751 // We do not request for addresses for FORWARDED tickets 752 if (options.containsKey(Option.CHECK_ADDRESSES) 753 && body.kdcOptions.get(KDCOptions.FORWARDED) 754 && body.addresses != null) { 755 throw new KrbException(Krb5.KDC_ERR_BADOPTION); 756 } 757 if (body.kdcOptions.get(KDCOptions.FORWARDED) || 758 etp.flags.get(Krb5.TKT_OPTS_FORWARDED)) { 759 bFlags[Krb5.TKT_OPTS_FORWARDED] = true; 760 } 761 if (body.kdcOptions.get(KDCOptions.RENEWABLE)) { 762 bFlags[Krb5.TKT_OPTS_RENEWABLE] = true; 763 //renew = new KerberosTime(new Date().getTime() + 1000 * 3600 * 24 * 7); 764 } 765 if (body.kdcOptions.get(KDCOptions.PROXIABLE)) { 766 bFlags[Krb5.TKT_OPTS_PROXIABLE] = true; 767 } 768 if (body.kdcOptions.get(KDCOptions.POSTDATED)) { 769 bFlags[Krb5.TKT_OPTS_POSTDATED] = true; 770 } 771 if (body.kdcOptions.get(KDCOptions.ALLOW_POSTDATE)) { 772 bFlags[Krb5.TKT_OPTS_MAY_POSTDATE] = true; 773 } 774 if (body.kdcOptions.get(KDCOptions.CNAME_IN_ADDL_TKT)) { 775 if (!options.containsKey(Option.ALLOW_S4U2PROXY)) { 776 // Don't understand CNAME_IN_ADDL_TKT 777 throw new KrbException(Krb5.KDC_ERR_BADOPTION); 778 } else { 779 Map<String,List<String>> map = (Map<String,List<String>>) 780 options.get(Option.ALLOW_S4U2PROXY); 781 Ticket second = KDCReqBodyDotFirstAdditionalTicket(body); 782 EncryptionKey key2 = keyForUser(second.sname, second.encPart.getEType(), true); 783 byte[] bb = second.encPart.decrypt(key2, KeyUsage.KU_TICKET); 784 DerInputStream derIn = new DerInputStream(bb); 785 DerValue der = derIn.getDerValue(); 786 EncTicketPart tktEncPart = new EncTicketPart(der.toByteArray()); 787 if (!tktEncPart.flags.get(Krb5.TKT_OPTS_FORWARDABLE)) { 788 //throw new KrbException(Krb5.KDC_ERR_BADOPTION); 789 } 790 PrincipalName client = tktEncPart.cname; 791 System.out.println(realm + "> and an additional ticket of " 792 + client + " to " + second.sname); 793 if (map.containsKey(cname.toString())) { 794 if (map.get(cname.toString()).contains(service.toString())) { 795 System.out.println(realm + "> S4U2proxy OK"); 796 } else { 797 throw new KrbException(Krb5.KDC_ERR_BADOPTION); 798 } 799 } else { 800 throw new KrbException(Krb5.KDC_ERR_BADOPTION); 801 } 802 cname = client; 803 } 804 } 805 806 String okAsDelegate = (String)options.get(Option.OK_AS_DELEGATE); 807 if (okAsDelegate != null && ( 808 okAsDelegate.isEmpty() || 809 okAsDelegate.contains(service.getNameString()))) { 810 bFlags[Krb5.TKT_OPTS_DELEGATE] = true; 811 } 812 bFlags[Krb5.TKT_OPTS_INITIAL] = true; 813 814 TicketFlags tFlags = new TicketFlags(bFlags); 815 EncTicketPart enc = new EncTicketPart( 816 tFlags, 817 key, 818 cname, 819 new TransitedEncoding(1, new byte[0]), // TODO 820 new KerberosTime(new Date()), 821 body.from, 822 till, etp.renewTill, 823 body.addresses != null ? body.addresses 824 : etp.caddr, 825 null); 826 EncryptionKey skey = keyForUser(service, e3, true); 827 if (skey == null) { 828 throw new KrbException(Krb5.KDC_ERR_SUMTYPE_NOSUPP); // TODO 829 } 830 Ticket t = new Ticket( 831 service, 832 new EncryptedData(skey, enc.asn1Encode(), KeyUsage.KU_TICKET) 833 ); 834 EncTGSRepPart enc_part = new EncTGSRepPart( 835 key, 836 new LastReq(new LastReqEntry[]{ 837 new LastReqEntry(0, new KerberosTime(new Date().getTime() - 10000)) 838 }), 839 body.getNonce(), // TODO: detect replay 840 new KerberosTime(new Date().getTime() + 1000 * 3600 * 24), 841 // Next 5 and last MUST be same with ticket 842 tFlags, 843 new KerberosTime(new Date()), 844 body.from, 845 till, etp.renewTill, 846 service, 847 body.addresses 848 ); 849 EncryptedData edata = new EncryptedData(ckey, enc_part.asn1Encode(), KeyUsage.KU_ENC_TGS_REP_PART_SESSKEY); 850 TGSRep tgsRep = new TGSRep(null, 851 cname, 852 t, 853 edata); 854 System.out.println(" Return " + tgsRep.cname 855 + " ticket for " + tgsRep.ticket.sname + ", flags " 856 + tFlags); 857 858 DerOutputStream out = new DerOutputStream(); 859 out.write(DerValue.createTag(DerValue.TAG_APPLICATION, 860 true, (byte)Krb5.KRB_TGS_REP), tgsRep.asn1Encode()); 861 return out.toByteArray(); 862 } catch (KrbException ke) { 863 ke.printStackTrace(System.out); 864 KRBError kerr = ke.getError(); 865 KDCReqBody body = tgsReq.reqBody; 866 System.out.println(" Error " + ke.returnCode() 867 + " " +ke.returnCodeMessage()); 868 if (kerr == null) { 869 kerr = new KRBError(null, null, null, 870 new KerberosTime(new Date()), 871 0, 872 ke.returnCode(), 873 body.cname, 874 service, 875 KrbException.errorMessage(ke.returnCode()), 876 null); 877 } 878 return kerr.asn1Encode(); 879 } 880 } 881 882 /** 883 * Processes a AS_REQ and generates a AS_REP (or KRB_ERROR) 884 * @param in the request 885 * @return the response 886 * @throws java.lang.Exception for various errors 887 */ 888 protected byte[] processAsReq(byte[] in) throws Exception { 889 ASReq asReq = new ASReq(in); 890 int[] eTypes = null; 891 List<PAData> outPAs = new ArrayList<>(); 892 893 PrincipalName service = asReq.reqBody.sname; 894 if (options.containsKey(KDC.Option.RESP_NT)) { 895 service = new PrincipalName((int)options.get(KDC.Option.RESP_NT), 896 service.getNameStrings(), 897 Realm.getDefault()); 898 } 899 try { 900 System.out.println(realm + "> " + asReq.reqBody.cname + 901 " sends AS-REQ for " + 902 service + ", " + asReq.reqBody.kdcOptions); 903 904 KDCReqBody body = asReq.reqBody; 905 906 eTypes = KDCReqBodyDotEType(body); 907 int eType = eTypes[0]; 908 909 // Maybe server does not support aes256, but a kinit does 910 if (!EType.isSupported(eType)) { 911 if (eTypes.length < 2) { 912 throw new KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP); 913 } 914 eType = eTypes[1]; 915 } 916 917 EncryptionKey ckey = keyForUser(body.cname, eType, false); 918 EncryptionKey skey = keyForUser(service, eType, true); 919 920 if (options.containsKey(KDC.Option.ONLY_RC4_TGT)) { 921 int tgtEType = EncryptedData.ETYPE_ARCFOUR_HMAC; 922 boolean found = false; 923 for (int i=0; i<eTypes.length; i++) { 924 if (eTypes[i] == tgtEType) { 925 found = true; 926 break; 927 } 928 } 929 if (!found) { 930 throw new KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP); 931 } 932 skey = keyForUser(service, tgtEType, true); 933 } 934 if (ckey == null) { 935 throw new KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP); 936 } 937 if (skey == null) { 938 throw new KrbException(Krb5.KDC_ERR_SUMTYPE_NOSUPP); // TODO 939 } 940 941 // Session key 942 EncryptionKey key = generateRandomKey(eType); 943 // Check time, TODO 944 KerberosTime till = body.till; 945 KerberosTime rtime = body.rtime; 946 if (till == null) { 947 throw new KrbException(Krb5.KDC_ERR_NEVER_VALID); // TODO 948 } else if (till.isZero()) { 949 till = new KerberosTime( 950 new Date().getTime() + 1000 * DEFAULT_LIFETIME); 951 } else if (till.greaterThan(new KerberosTime(Instant.now() 952 .plus(1, ChronoUnit.DAYS)))) { 953 // If till is more than 1 day later, make it renewable 954 till = new KerberosTime( 955 new Date().getTime() + 1000 * DEFAULT_LIFETIME); 956 body.kdcOptions.set(KDCOptions.RENEWABLE, true); 957 if (rtime == null) rtime = till; 958 } 959 if (rtime == null && body.kdcOptions.get(KDCOptions.RENEWABLE)) { 960 rtime = new KerberosTime( 961 new Date().getTime() + 1000 * DEFAULT_RENEWTIME); 962 } 963 //body.from 964 boolean[] bFlags = new boolean[Krb5.TKT_OPTS_MAX+1]; 965 if (body.kdcOptions.get(KDCOptions.FORWARDABLE)) { 966 List<String> sensitives = (List<String>) 967 options.get(Option.SENSITIVE_ACCOUNTS); 968 if (sensitives != null && sensitives.contains(body.cname.toString())) { 969 // Cannot make FORWARDABLE 970 } else { 971 bFlags[Krb5.TKT_OPTS_FORWARDABLE] = true; 972 } 973 } 974 if (body.kdcOptions.get(KDCOptions.RENEWABLE)) { 975 bFlags[Krb5.TKT_OPTS_RENEWABLE] = true; 976 //renew = new KerberosTime(new Date().getTime() + 1000 * 3600 * 24 * 7); 977 } 978 if (body.kdcOptions.get(KDCOptions.PROXIABLE)) { 979 bFlags[Krb5.TKT_OPTS_PROXIABLE] = true; 980 } 981 if (body.kdcOptions.get(KDCOptions.POSTDATED)) { 982 bFlags[Krb5.TKT_OPTS_POSTDATED] = true; 983 } 984 if (body.kdcOptions.get(KDCOptions.ALLOW_POSTDATE)) { 985 bFlags[Krb5.TKT_OPTS_MAY_POSTDATE] = true; 986 } 987 bFlags[Krb5.TKT_OPTS_INITIAL] = true; 988 989 // Creating PA-DATA 990 DerValue[] pas2 = null, pas = null; 991 if (options.containsKey(KDC.Option.DUP_ETYPE)) { 992 int n = (Integer)options.get(KDC.Option.DUP_ETYPE); 993 switch (n) { 994 case 1: // customer's case in 7067974 995 pas2 = new DerValue[] { 996 new DerValue(new ETypeInfo2(1, null, null).asn1Encode()), 997 new DerValue(new ETypeInfo2(1, "", null).asn1Encode()), 998 new DerValue(new ETypeInfo2(1, realm, new byte[]{1}).asn1Encode()), 999 }; 1000 pas = new DerValue[] { 1001 new DerValue(new ETypeInfo(1, null).asn1Encode()), 1002 new DerValue(new ETypeInfo(1, "").asn1Encode()), 1003 new DerValue(new ETypeInfo(1, realm).asn1Encode()), 1004 }; 1005 break; 1006 case 2: // we still reject non-null s2kparams and prefer E2 over E 1007 pas2 = new DerValue[] { 1008 new DerValue(new ETypeInfo2(1, realm, new byte[]{1}).asn1Encode()), 1009 new DerValue(new ETypeInfo2(1, null, null).asn1Encode()), 1010 new DerValue(new ETypeInfo2(1, "", null).asn1Encode()), 1011 }; 1012 pas = new DerValue[] { 1013 new DerValue(new ETypeInfo(1, realm).asn1Encode()), 1014 new DerValue(new ETypeInfo(1, null).asn1Encode()), 1015 new DerValue(new ETypeInfo(1, "").asn1Encode()), 1016 }; 1017 break; 1018 case 3: // but only E is wrong 1019 pas = new DerValue[] { 1020 new DerValue(new ETypeInfo(1, realm).asn1Encode()), 1021 new DerValue(new ETypeInfo(1, null).asn1Encode()), 1022 new DerValue(new ETypeInfo(1, "").asn1Encode()), 1023 }; 1024 break; 1025 case 4: // we also ignore rc4-hmac 1026 pas = new DerValue[] { 1027 new DerValue(new ETypeInfo(23, "ANYTHING").asn1Encode()), 1028 new DerValue(new ETypeInfo(1, null).asn1Encode()), 1029 new DerValue(new ETypeInfo(1, "").asn1Encode()), 1030 }; 1031 break; 1032 case 5: // "" should be wrong, but we accept it now 1033 // See s.s.k.internal.PAData$SaltAndParams 1034 pas = new DerValue[] { 1035 new DerValue(new ETypeInfo(1, "").asn1Encode()), 1036 new DerValue(new ETypeInfo(1, null).asn1Encode()), 1037 }; 1038 break; 1039 } 1040 } else { 1041 int[] epas = eTypes; 1042 if (options.containsKey(KDC.Option.RC4_FIRST_PREAUTH)) { 1043 for (int i=1; i<epas.length; i++) { 1044 if (epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC) { 1045 epas[i] = epas[0]; 1046 epas[0] = EncryptedData.ETYPE_ARCFOUR_HMAC; 1047 break; 1048 } 1049 }; 1050 } else if (options.containsKey(KDC.Option.ONLY_ONE_PREAUTH)) { 1051 epas = new int[] { eTypes[0] }; 1052 } 1053 pas2 = new DerValue[epas.length]; 1054 for (int i=0; i<epas.length; i++) { 1055 pas2[i] = new DerValue(new ETypeInfo2( 1056 epas[i], 1057 epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC ? 1058 null : getSalt(body.cname), 1059 null).asn1Encode()); 1060 } 1061 boolean allOld = true; 1062 for (int i: eTypes) { 1063 if (i == EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96 || 1064 i == EncryptedData.ETYPE_AES256_CTS_HMAC_SHA1_96) { 1065 allOld = false; 1066 break; 1067 } 1068 } 1069 if (allOld) { 1070 pas = new DerValue[epas.length]; 1071 for (int i=0; i<epas.length; i++) { 1072 pas[i] = new DerValue(new ETypeInfo( 1073 epas[i], 1074 epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC ? 1075 null : getSalt(body.cname) 1076 ).asn1Encode()); 1077 } 1078 } 1079 } 1080 1081 DerOutputStream eid; 1082 if (pas2 != null) { 1083 eid = new DerOutputStream(); 1084 eid.putSequence(pas2); 1085 outPAs.add(new PAData(Krb5.PA_ETYPE_INFO2, eid.toByteArray())); 1086 } 1087 if (pas != null) { 1088 eid = new DerOutputStream(); 1089 eid.putSequence(pas); 1090 outPAs.add(new PAData(Krb5.PA_ETYPE_INFO, eid.toByteArray())); 1091 } 1092 1093 PAData[] inPAs = KDCReqDotPAData(asReq); 1094 if (inPAs == null || inPAs.length == 0) { 1095 Object preauth = options.get(Option.PREAUTH_REQUIRED); 1096 if (preauth == null || preauth.equals(Boolean.TRUE)) { 1097 throw new KrbException(Krb5.KDC_ERR_PREAUTH_REQUIRED); 1098 } 1099 } else { 1100 try { 1101 EncryptedData data = newEncryptedData(new DerValue(inPAs[0].getValue())); 1102 EncryptionKey pakey = keyForUser(body.cname, data.getEType(), false); 1103 data.decrypt(pakey, KeyUsage.KU_PA_ENC_TS); 1104 } catch (Exception e) { 1105 throw new KrbException(Krb5.KDC_ERR_PREAUTH_FAILED); 1106 } 1107 bFlags[Krb5.TKT_OPTS_PRE_AUTHENT] = true; 1108 } 1109 1110 TicketFlags tFlags = new TicketFlags(bFlags); 1111 EncTicketPart enc = new EncTicketPart( 1112 tFlags, 1113 key, 1114 body.cname, 1115 new TransitedEncoding(1, new byte[0]), 1116 new KerberosTime(new Date()), 1117 body.from, 1118 till, rtime, 1119 body.addresses, 1120 null); 1121 Ticket t = new Ticket( 1122 service, 1123 new EncryptedData(skey, enc.asn1Encode(), KeyUsage.KU_TICKET) 1124 ); 1125 EncASRepPart enc_part = new EncASRepPart( 1126 key, 1127 new LastReq(new LastReqEntry[]{ 1128 new LastReqEntry(0, new KerberosTime(new Date().getTime() - 10000)) 1129 }), 1130 body.getNonce(), // TODO: detect replay? 1131 new KerberosTime(new Date().getTime() + 1000 * 3600 * 24), 1132 // Next 5 and last MUST be same with ticket 1133 tFlags, 1134 new KerberosTime(new Date()), 1135 body.from, 1136 till, rtime, 1137 service, 1138 body.addresses 1139 ); 1140 EncryptedData edata = new EncryptedData(ckey, enc_part.asn1Encode(), KeyUsage.KU_ENC_AS_REP_PART); 1141 ASRep asRep = new ASRep( 1142 outPAs.toArray(new PAData[outPAs.size()]), 1143 body.cname, 1144 t, 1145 edata); 1146 1147 System.out.println(" Return " + asRep.cname 1148 + " ticket for " + asRep.ticket.sname + ", flags " 1149 + tFlags); 1150 1151 DerOutputStream out = new DerOutputStream(); 1152 out.write(DerValue.createTag(DerValue.TAG_APPLICATION, 1153 true, (byte)Krb5.KRB_AS_REP), asRep.asn1Encode()); 1154 byte[] result = out.toByteArray(); 1155 1156 // Added feature: 1157 // Write the current issuing TGT into a ccache file specified 1158 // by the system property below. 1159 String ccache = System.getProperty("test.kdc.save.ccache"); 1160 if (ccache != null) { 1161 asRep.encKDCRepPart = enc_part; 1162 sun.security.krb5.internal.ccache.Credentials credentials = 1163 new sun.security.krb5.internal.ccache.Credentials(asRep); 1164 CredentialsCache cache = 1165 CredentialsCache.create(asReq.reqBody.cname, ccache); 1166 if (cache == null) { 1167 throw new IOException("Unable to create the cache file " + 1168 ccache); 1169 } 1170 cache.update(credentials); 1171 cache.save(); 1172 } 1173 1174 return result; 1175 } catch (KrbException ke) { 1176 ke.printStackTrace(System.out); 1177 KRBError kerr = ke.getError(); 1178 KDCReqBody body = asReq.reqBody; 1179 System.out.println(" Error " + ke.returnCode() 1180 + " " +ke.returnCodeMessage()); 1181 byte[] eData = null; 1182 if (kerr == null) { 1183 if (ke.returnCode() == Krb5.KDC_ERR_PREAUTH_REQUIRED || 1184 ke.returnCode() == Krb5.KDC_ERR_PREAUTH_FAILED) { 1185 DerOutputStream bytes = new DerOutputStream(); 1186 bytes.write(new PAData(Krb5.PA_ENC_TIMESTAMP, new byte[0]).asn1Encode()); 1187 for (PAData p: outPAs) { 1188 bytes.write(p.asn1Encode()); 1189 } 1190 DerOutputStream temp = new DerOutputStream(); 1191 temp.write(DerValue.tag_Sequence, bytes); 1192 eData = temp.toByteArray(); 1193 } 1194 kerr = new KRBError(null, null, null, 1195 new KerberosTime(new Date()), 1196 0, 1197 ke.returnCode(), 1198 body.cname, 1199 service, 1200 KrbException.errorMessage(ke.returnCode()), 1201 eData); 1202 } 1203 return kerr.asn1Encode(); 1204 } 1205 } 1206 1207 /** 1208 * Generates a line for a KDC to put inside [realms] of krb5.conf 1209 * @return REALM.NAME = { kdc = host:port etc } 1210 */ 1211 private String realmLine() { 1212 StringBuilder sb = new StringBuilder(); 1213 sb.append(realm).append(" = {\n kdc = ") 1214 .append(kdc).append(':').append(port).append('\n'); 1215 for (String s: conf) { 1216 sb.append(" ").append(s).append('\n'); 1217 } 1218 return sb.append("}\n").toString(); 1219 } 1220 1221 /** 1222 * Start the KDC service. This server listens on both UDP and TCP using 1223 * the same port number. It uses three threads to deal with requests. 1224 * They can be set to daemon threads if requested. 1225 * @param port the port number to listen to. If zero, a random available 1226 * port no less than 8000 will be chosen and used. 1227 * @param asDaemon true if the KDC threads should be daemons 1228 * @throws java.io.IOException for any communication error 1229 */ 1230 protected void startServer(int port, boolean asDaemon) throws IOException { 1231 if (port > 0) { 1232 u1 = new DatagramSocket(port, InetAddress.getByName("127.0.0.1")); 1233 t1 = new ServerSocket(port); 1234 } else { 1235 while (true) { 1236 // Try to find a port number that's both TCP and UDP free 1237 try { 1238 port = 8000 + new java.util.Random().nextInt(10000); 1239 u1 = null; 1240 u1 = new DatagramSocket(port, InetAddress.getByName("127.0.0.1")); 1241 t1 = new ServerSocket(port); 1242 break; 1243 } catch (Exception e) { 1244 if (u1 != null) u1.close(); 1245 } 1246 } 1247 } 1248 final DatagramSocket udp = u1; 1249 final ServerSocket tcp = t1; 1250 System.out.println("Start KDC on " + port); 1251 1252 this.port = port; 1253 1254 // The UDP consumer 1255 thread1 = new Thread() { 1256 public void run() { 1257 udpConsumerReady = true; 1258 while (true) { 1259 try { 1260 byte[] inbuf = new byte[8192]; 1261 DatagramPacket p = new DatagramPacket(inbuf, inbuf.length); 1262 udp.receive(p); 1263 System.out.println("-----------------------------------------------"); 1264 System.out.println(">>>>> UDP packet received"); 1265 q.put(new Job(processMessage(Arrays.copyOf(inbuf, p.getLength())), udp, p)); 1266 } catch (Exception e) { 1267 e.printStackTrace(); 1268 } 1269 } 1270 } 1271 }; 1272 thread1.setDaemon(asDaemon); 1273 thread1.start(); 1274 1275 // The TCP consumer 1276 thread2 = new Thread() { 1277 public void run() { 1278 tcpConsumerReady = true; 1279 while (true) { 1280 try { 1281 Socket socket = tcp.accept(); 1282 System.out.println("-----------------------------------------------"); 1283 System.out.println(">>>>> TCP connection established"); 1284 DataInputStream in = new DataInputStream(socket.getInputStream()); 1285 DataOutputStream out = new DataOutputStream(socket.getOutputStream()); 1286 int len = in.readInt(); 1287 if (len > 65535) { 1288 throw new Exception("Huge request not supported"); 1289 } 1290 byte[] token = new byte[len]; 1291 in.readFully(token); 1292 q.put(new Job(processMessage(token), socket, out)); 1293 } catch (Exception e) { 1294 e.printStackTrace(); 1295 } 1296 } 1297 } 1298 }; 1299 thread2.setDaemon(asDaemon); 1300 thread2.start(); 1301 1302 // The dispatcher 1303 thread3 = new Thread() { 1304 public void run() { 1305 dispatcherReady = true; 1306 while (true) { 1307 try { 1308 q.take().send(); 1309 } catch (Exception e) { 1310 } 1311 } 1312 } 1313 }; 1314 thread3.setDaemon(true); 1315 thread3.start(); 1316 1317 // wait for the KDC is ready 1318 try { 1319 while (!isReady()) { 1320 Thread.sleep(100); 1321 } 1322 } catch(InterruptedException e) { 1323 throw new IOException(e); 1324 } 1325 } 1326 1327 boolean isReady() { 1328 return udpConsumerReady && tcpConsumerReady && dispatcherReady; 1329 } 1330 1331 public void terminate() { 1332 try { 1333 thread1.stop(); 1334 thread2.stop(); 1335 thread3.stop(); 1336 u1.close(); 1337 t1.close(); 1338 } catch (Exception e) { 1339 // OK 1340 } 1341 } 1342 1343 public static KDC startKDC(final String host, final String krbConfFileName, 1344 final String realm, final Map<String, String> principals, 1345 final String ktab, final KtabMode mode) { 1346 1347 KDC kdc; 1348 try { 1349 kdc = KDC.create(realm, host, 0, true); 1350 kdc.setOption(KDC.Option.PREAUTH_REQUIRED, Boolean.FALSE); 1351 if (krbConfFileName != null) { 1352 KDC.saveConfig(krbConfFileName, kdc); 1353 } 1354 1355 // Add principals 1356 if (principals != null) { 1357 principals.forEach((name, password) -> { 1358 if (password == null || password.isEmpty()) { 1359 System.out.println(String.format( 1360 "KDC:add a principal '%s' with a random " + 1361 "password", name)); 1362 kdc.addPrincipalRandKey(name); 1363 } else { 1364 System.out.println(String.format( 1365 "KDC:add a principal '%s' with '%s' password", 1366 name, password)); 1367 kdc.addPrincipal(name, password.toCharArray()); 1368 } 1369 }); 1370 } 1371 1372 // Create or append keys to existing keytab file 1373 if (ktab != null) { 1374 File ktabFile = new File(ktab); 1375 switch(mode) { 1376 case APPEND: 1377 if (ktabFile.exists()) { 1378 System.out.println(String.format( 1379 "KDC:append keys to an exising keytab " 1380 + "file %s", ktab)); 1381 kdc.appendKtab(ktab); 1382 } else { 1383 System.out.println(String.format( 1384 "KDC:create a new keytab file %s", ktab)); 1385 kdc.writeKtab(ktab); 1386 } 1387 break; 1388 case EXISTING: 1389 System.out.println(String.format( 1390 "KDC:use an existing keytab file %s", ktab)); 1391 break; 1392 default: 1393 throw new RuntimeException(String.format( 1394 "KDC:unsupported keytab mode: %s", mode)); 1395 } 1396 } 1397 1398 System.out.println(String.format( 1399 "KDC: started on %s:%s with '%s' realm", 1400 host, kdc.getPort(), realm)); 1401 } catch (Exception e) { 1402 throw new RuntimeException("KDC: unexpected exception", e); 1403 } 1404 1405 return kdc; 1406 } 1407 1408 /** 1409 * Helper class to encapsulate a job in a KDC. 1410 */ 1411 private static class Job { 1412 byte[] token; // The received request at creation time and 1413 // the response at send time 1414 Socket s; // The TCP socket from where the request comes 1415 DataOutputStream out; // The OutputStream of the TCP socket 1416 DatagramSocket s2; // The UDP socket from where the request comes 1417 DatagramPacket dp; // The incoming UDP datagram packet 1418 boolean useTCP; // Whether TCP or UDP is used 1419 1420 // Creates a job object for TCP 1421 Job(byte[] token, Socket s, DataOutputStream out) { 1422 useTCP = true; 1423 this.token = token; 1424 this.s = s; 1425 this.out = out; 1426 } 1427 1428 // Creates a job object for UDP 1429 Job(byte[] token, DatagramSocket s2, DatagramPacket dp) { 1430 useTCP = false; 1431 this.token = token; 1432 this.s2 = s2; 1433 this.dp = dp; 1434 } 1435 1436 // Sends the output back to the client 1437 void send() { 1438 try { 1439 if (useTCP) { 1440 System.out.println(">>>>> TCP request honored"); 1441 out.writeInt(token.length); 1442 out.write(token); 1443 s.close(); 1444 } else { 1445 System.out.println(">>>>> UDP request honored"); 1446 s2.send(new DatagramPacket(token, token.length, dp.getAddress(), dp.getPort())); 1447 } 1448 } catch (Exception e) { 1449 e.printStackTrace(); 1450 } 1451 } 1452 } 1453 1454 1455 // Calling private methods thru reflections 1456 private static final Field getPADataField; 1457 private static final Field getEType; 1458 private static final Constructor<EncryptedData> ctorEncryptedData; 1459 private static final Method stringToKey; 1460 private static final Field getAddlTkt; 1461 1462 static { 1463 try { 1464 ctorEncryptedData = EncryptedData.class.getDeclaredConstructor(DerValue.class); 1465 ctorEncryptedData.setAccessible(true); 1466 getPADataField = KDCReq.class.getDeclaredField("pAData"); 1467 getPADataField.setAccessible(true); 1468 getEType = KDCReqBody.class.getDeclaredField("eType"); 1469 getEType.setAccessible(true); 1470 stringToKey = EncryptionKey.class.getDeclaredMethod( 1471 "stringToKey", 1472 char[].class, String.class, byte[].class, Integer.TYPE); 1473 stringToKey.setAccessible(true); 1474 getAddlTkt = KDCReqBody.class.getDeclaredField("additionalTickets"); 1475 getAddlTkt.setAccessible(true); 1476 } catch (NoSuchFieldException nsfe) { 1477 throw new AssertionError(nsfe); 1478 } catch (NoSuchMethodException nsme) { 1479 throw new AssertionError(nsme); 1480 } 1481 } 1482 private EncryptedData newEncryptedData(DerValue der) { 1483 try { 1484 return ctorEncryptedData.newInstance(der); 1485 } catch (Exception e) { 1486 throw new AssertionError(e); 1487 } 1488 } 1489 private static PAData[] KDCReqDotPAData(KDCReq req) { 1490 try { 1491 return (PAData[])getPADataField.get(req); 1492 } catch (Exception e) { 1493 throw new AssertionError(e); 1494 } 1495 } 1496 private static int[] KDCReqBodyDotEType(KDCReqBody body) { 1497 try { 1498 return (int[]) getEType.get(body); 1499 } catch (Exception e) { 1500 throw new AssertionError(e); 1501 } 1502 } 1503 private static byte[] EncryptionKeyDotStringToKey(char[] password, String salt, 1504 byte[] s2kparams, int keyType) throws KrbCryptoException { 1505 try { 1506 return (byte[])stringToKey.invoke( 1507 null, password, salt, s2kparams, keyType); 1508 } catch (InvocationTargetException ex) { 1509 throw (KrbCryptoException)ex.getCause(); 1510 } catch (Exception e) { 1511 throw new AssertionError(e); 1512 } 1513 } 1514 private static Ticket KDCReqBodyDotFirstAdditionalTicket(KDCReqBody body) { 1515 try { 1516 return ((Ticket[])getAddlTkt.get(body))[0]; 1517 } catch (Exception e) { 1518 throw new AssertionError(e); 1519 } 1520 } 1521} 1522