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