1 /* 2 * Copyright (c) 2008, 2010, 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 import sun.net.spi.nameservice.NameService; 34 import sun.net.spi.nameservice.NameServiceDescriptor; 35 import sun.security.krb5.*; 36 import sun.security.krb5.internal.*; 37 import sun.security.krb5.internal.ccache.CredentialsCache; 38 import sun.security.krb5.internal.crypto.KeyUsage; 39 import sun.security.krb5.internal.ktab.KeyTab; 40 import sun.security.util.DerInputStream; 41 import sun.security.util.DerOutputStream; 42 import sun.security.util.DerValue; 43 44 /** 45 * A KDC server. 46 * <p> 47 * Features: 48 * <ol> 49 * <li> Supports TCP and UDP 50 * <li> Supports AS-REQ and TGS-REQ 51 * <li> Principal db and other settings hard coded in application 52 * <li> Options, say, request preauth or not 53 * </ol> 54 * Side effects: 55 * <ol> 56 * <li> The Sun-internal class <code>sun.security.krb5.Config</code> is a 57 * singleton and initialized according to Kerberos settings (krb5.conf and 58 * java.security.krb5.* system properties). This means once it's initialized 59 * it will not automatically notice any changes to these settings (or file 60 * changes of krb5.conf). The KDC class normally does not touch these 61 * settings (except for the <code>writeKtab()</code> method). However, to make 62 * sure nothing ever goes wrong, if you want to make any changes to these 63 * settings after calling a KDC method, call <code>Config.refresh()</code> to 64 * make sure your changes are reflected in the <code>Config</code> object. 65 * </ol> 66 * System properties recognized: 67 * <ul> 68 * <li>test.kdc.save.ccache 69 * </ul> 70 * Support policies: 71 * <ul> 72 * <li>ok-as-delegate 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 */ 125 public class KDC { 126 127 // Under the hood. 128 129 // The random generator to generate random keys (including session keys) 130 private static SecureRandom secureRandom = new SecureRandom(); 131 132 // Principal db. principal -> pass. A case-insensitive TreeMap is used 133 // so that even if the client provides a name with different case, the KDC 134 // can still locate the principal and give back correct salt. 135 private TreeMap<String,char[]> passwords = new TreeMap<> 136 (String.CASE_INSENSITIVE_ORDER); 137 138 // Realm name 139 private String realm; 140 // KDC 141 private String kdc; 142 // Service port number 143 private int port; 144 // The request/response job queue 145 private BlockingQueue<Job> q = new ArrayBlockingQueue<>(100); 146 // Options 147 private Map<Option,Object> options = new HashMap<>(); 148 149 private Thread thread1, thread2, thread3; 150 DatagramSocket u1 = null; 151 ServerSocket t1 = null; 152 153 /** 154 * Option names, to be expanded forever. 155 */ 156 public static enum Option { 157 /** 158 * Whether pre-authentication is required. Default Boolean.TRUE 159 */ 160 PREAUTH_REQUIRED, 161 /** 162 * Only issue TGT in RC4 163 */ 164 ONLY_RC4_TGT, 165 /** 166 * Use RC4 as the first in preauth 167 */ 168 RC4_FIRST_PREAUTH, 169 /** 170 * Use only one preauth, so that some keys are not easy to generate 171 */ 172 ONLY_ONE_PREAUTH, 173 }; 174 175 static { 176 System.setProperty("sun.net.spi.nameservice.provider.1", "ns,mock"); 177 } 178 179 /** 180 * A standalone KDC server. 181 */ 182 public static void main(String[] args) throws Exception { 183 KDC kdc = create("RABBIT.HOLE", "kdc.rabbit.hole", 0, false); 184 kdc.addPrincipal("dummy", "bogus".toCharArray()); 185 kdc.addPrincipal("foo", "bar".toCharArray()); 186 kdc.addPrincipalRandKey("krbtgt/RABBIT.HOLE"); 187 kdc.addPrincipalRandKey("server/host.rabbit.hole"); 188 kdc.addPrincipalRandKey("backend/host.rabbit.hole"); 189 KDC.saveConfig("krb5.conf", kdc, "forwardable = true"); 190 } 191 192 /** 193 * Creates and starts a KDC running as a daemon on a random port. 194 * @param realm the realm name 195 * @return the running KDC instance 196 * @throws java.io.IOException for any socket creation error 197 */ 198 public static KDC create(String realm) throws IOException { 199 return create(realm, "kdc." + realm.toLowerCase(), 0, true); 200 } 201 202 public static KDC existing(String realm, String kdc, int port) { 203 KDC k = new KDC(realm, kdc); 204 k.port = port; 205 return k; 206 } 207 208 /** 209 * Creates and starts a KDC server. 210 * @param realm the realm name 211 * @param port the TCP and UDP port to listen to. A random port will to 212 * chosen if zero. 213 * @param asDaemon if true, KDC threads will be daemons. Otherwise, not. 214 * @return the running KDC instance 215 * @throws java.io.IOException for any socket creation error 216 */ 217 public static KDC create(String realm, String kdc, int port, boolean asDaemon) throws IOException { 218 return new KDC(realm, kdc, port, asDaemon); 219 } 220 221 /** 222 * Sets an option 223 * @param key the option name 224 * @param obj the value 225 */ 226 public void setOption(Option key, Object value) { 227 options.put(key, value); 228 } 229 230 /** 231 * Write all principals' keys from multiple KDCsinto one keytab file. 232 * Note that the keys for the krbtgt principals will not be written. 233 * <p> 234 * Attention: This method references krb5.conf settings. If you need to 235 * setup krb5.conf later, please call <code>Config.refresh()</code> after 236 * the new setting. For example: 237 * <pre> 238 * KDC.writeKtab("/etc/kdc/ktab", kdc); // Config is initialized, 239 * System.setProperty("java.security.krb5.conf", "/home/mykrb5.conf"); 240 * Config.refresh(); 241 * </pre> 242 * 243 * Inside this method there are 2 places krb5.conf is used: 244 * <ol> 245 * <li> (Fatal) Generating keys: EncryptionKey.acquireSecretKeys 246 * <li> (Has workaround) Creating PrincipalName 247 * </ol> 248 * @param tab The keytab filename to write to. 249 * @throws java.io.IOException for any file output error 250 * @throws sun.security.krb5.KrbException for any realm and/or principal 251 * name error. 252 */ 253 public static void writeMultiKtab(String tab, KDC... kdcs) 254 throws IOException, KrbException { 255 KeyTab ktab = KeyTab.create(tab); 256 for (KDC kdc: kdcs) { 257 for (String name : kdc.passwords.keySet()) { 258 ktab.addEntry(new PrincipalName(name, 259 name.indexOf('/') < 0 ? 260 PrincipalName.KRB_NT_UNKNOWN : 261 PrincipalName.KRB_NT_SRV_HST), 262 kdc.passwords.get(name), -1, true); 263 } 264 } 265 ktab.save(); 266 } 267 268 /** 269 * Write a ktab for this KDC. 270 */ 271 public void writeKtab(String tab) throws IOException, KrbException { 272 KDC.writeMultiKtab(tab, this); 273 } 274 275 /** 276 * Adds a new principal to this realm with a given password. 277 * @param user the principal's name. For a service principal, use the 278 * form of host/f.q.d.n 279 * @param pass the password for the principal 280 */ 281 public void addPrincipal(String user, char[] pass) { 282 if (user.indexOf('@') < 0) { 283 user = user + "@" + realm; 284 } 285 passwords.put(user, pass); 286 } 287 288 /** 289 * Adds a new principal to this realm with a random password 290 * @param user the principal's name. For a service principal, use the 291 * form of host/f.q.d.n 292 */ 293 public void addPrincipalRandKey(String user) { 294 addPrincipal(user, randomPassword()); 295 } 296 297 /** 298 * Returns the name of this realm 299 * @return the name of this realm 300 */ 301 public String getRealm() { 302 return realm; 303 } 304 305 /** 306 * Returns the name of kdc 307 * @return the name of kdc 308 */ 309 public String getKDC() { 310 return kdc; 311 } 312 313 /** 314 * Writes a krb5.conf for one or more KDC that includes KDC locations for 315 * each realm and the default realm name. You can also add extra strings 316 * into the file. The method should be called like: 317 * <pre> 318 * KDC.saveConfig("krb5.conf", kdc1, kdc2, ..., line1, line2, ...); 319 * </pre> 320 * Here you can provide one or more kdc# and zero or more line# arguments. 321 * The line# will be put after [libdefaults] and before [realms]. Therefore 322 * you can append new lines into [libdefaults] and/or create your new 323 * stanzas as well. Note that a newline character will be appended to 324 * each line# argument. 325 * <p> 326 * For example: 327 * <pre> 328 * KDC.saveConfig("krb5.conf", this); 329 * </pre> 330 * generates: 331 * <pre> 332 * [libdefaults] 333 * default_realm = REALM.NAME 334 * 335 * [realms] 336 * REALM.NAME = { 337 * kdc = host:port_number 338 * } 339 * </pre> 340 * 341 * Another example: 342 * <pre> 343 * KDC.saveConfig("krb5.conf", kdc1, kdc2, "forwardable = true", "", 344 * "[domain_realm]", 345 * ".kdc1.com = KDC1.NAME"); 346 * </pre> 347 * generates: 348 * <pre> 349 * [libdefaults] 350 * default_realm = KDC1.NAME 351 * forwardable = true 352 * 353 * [domain_realm] 354 * .kdc1.com = KDC1.NAME 355 * 356 * [realms] 357 * KDC1.NAME = { 358 * kdc = host:port1 359 * } 360 * KDC2.NAME = { 361 * kdc = host:port2 362 * } 363 * </pre> 364 * @param file the name of the file to write into 365 * @param kdc the first (and default) KDC 366 * @param more more KDCs or extra lines (in their appearing order) to 367 * insert into the krb5.conf file. This method reads each argument's type 368 * to determine what it's for. This argument can be empty. 369 * @throws java.io.IOException for any file output error 370 */ 371 public static void saveConfig(String file, KDC kdc, Object... more) 372 throws IOException { 373 File f = new File(file); 374 StringBuffer sb = new StringBuffer(); 375 sb.append("[libdefaults]\ndefault_realm = "); 376 sb.append(kdc.realm); 377 sb.append("\n"); 378 for (Object o: more) { 379 if (o instanceof String) { 380 sb.append(o); 381 sb.append("\n"); 382 } 383 } 384 sb.append("\n[realms]\n"); 385 sb.append(realmLineForKDC(kdc)); 386 for (Object o: more) { 387 if (o instanceof KDC) { 388 sb.append(realmLineForKDC((KDC)o)); 389 } 390 } 391 FileOutputStream fos = new FileOutputStream(f); 392 fos.write(sb.toString().getBytes()); 393 fos.close(); 394 } 395 396 /** 397 * Returns the service port of the KDC server. 398 * @return the KDC service port 399 */ 400 public int getPort() { 401 return port; 402 } 403 404 // Private helper methods 405 406 /** 407 * Private constructor, cannot be called outside. 408 * @param realm 409 */ 410 private KDC(String realm, String kdc) { 411 this.realm = realm; 412 this.kdc = kdc; 413 } 414 415 /** 416 * A constructor that starts the KDC service also. 417 */ 418 protected KDC(String realm, String kdc, int port, boolean asDaemon) 419 throws IOException { 420 this(realm, kdc); 421 startServer(port, asDaemon); 422 } 423 /** 424 * Generates a 32-char random password 425 * @return the password 426 */ 427 private static char[] randomPassword() { 428 char[] pass = new char[32]; 429 for (int i=0; i<31; i++) 430 pass[i] = (char)secureRandom.nextInt(); 431 // The last char cannot be a number, otherwise, keyForUser() 432 // believes it's a sign of kvno 433 pass[31] = 'Z'; 434 return pass; 435 } 436 437 /** 438 * Generates a random key for the given encryption type. 439 * @param eType the encryption type 440 * @return the generated key 441 * @throws sun.security.krb5.KrbException for unknown/unsupported etype 442 */ 443 private static EncryptionKey generateRandomKey(int eType) 444 throws KrbException { 445 // Is 32 enough for AES256? I should have generated the keys directly 446 // but different cryptos have different rules on what keys are valid. 447 char[] pass = randomPassword(); 448 String algo; 449 switch (eType) { 450 case EncryptedData.ETYPE_DES_CBC_MD5: algo = "DES"; break; 451 case EncryptedData.ETYPE_DES3_CBC_HMAC_SHA1_KD: algo = "DESede"; break; 452 case EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96: algo = "AES128"; break; 453 case EncryptedData.ETYPE_ARCFOUR_HMAC: algo = "ArcFourHMAC"; break; 454 case EncryptedData.ETYPE_AES256_CTS_HMAC_SHA1_96: algo = "AES256"; break; 455 default: algo = "DES"; break; 456 } 457 return new EncryptionKey(pass, "NOTHING", algo); // Silly 458 } 459 460 /** 461 * Returns the password for a given principal 462 * @param p principal 463 * @return the password 464 * @throws sun.security.krb5.KrbException when the principal is not inside 465 * the database. 466 */ 467 private char[] getPassword(PrincipalName p, boolean server) 468 throws KrbException { 469 String pn = p.toString(); 470 if (p.getRealmString() == null) { 471 pn = pn + "@" + getRealm(); 472 } 473 char[] pass = passwords.get(pn); 474 if (pass == null) { 475 throw new KrbException(server? 476 Krb5.KDC_ERR_S_PRINCIPAL_UNKNOWN: 477 Krb5.KDC_ERR_C_PRINCIPAL_UNKNOWN); 478 } 479 return pass; 480 } 481 482 /** 483 * Returns the salt string for the principal. 484 * @param p principal 485 * @return the salt 486 */ 487 private String getSalt(PrincipalName p) { 488 String pn = p.toString(); 489 if (p.getRealmString() == null) { 490 pn = pn + "@" + getRealm(); 491 } 492 if (passwords.containsKey(pn)) { 493 try { 494 // Find the principal name with correct case. 495 p = new PrincipalName(passwords.ceilingEntry(pn).getKey()); 496 } catch (RealmException re) { 497 // Won't happen 498 } 499 } 500 String s = p.getRealmString(); 501 if (s == null) s = getRealm(); 502 for (String n: p.getNameStrings()) { 503 s += n; 504 } 505 return s; 506 } 507 508 /** 509 * Returns the key for a given principal of the given encryption type 510 * @param p the principal 511 * @param etype the encryption type 512 * @param server looking for a server principal? 513 * @return the key 514 * @throws sun.security.krb5.KrbException for unknown/unsupported etype 515 */ 516 private EncryptionKey keyForUser(PrincipalName p, int etype, boolean server) 517 throws KrbException { 518 try { 519 // Do not call EncryptionKey.acquireSecretKeys(), otherwise 520 // the krb5.conf config file would be loaded. 521 Integer kvno = null; 522 // For service whose password ending with a number, use it as kvno. 523 // Kvno must be postive. 524 if (p.toString().indexOf('/') > 0) { 525 char[] pass = getPassword(p, server); 526 if (Character.isDigit(pass[pass.length-1])) { 527 kvno = pass[pass.length-1] - '0'; 528 } 529 } 530 return new EncryptionKey(EncryptionKeyDotStringToKey( 531 getPassword(p, server), getSalt(p), null, etype), 532 etype, kvno); 533 } catch (KrbException ke) { 534 throw ke; 535 } catch (Exception e) { 536 throw new RuntimeException(e); // should not happen 537 } 538 } 539 540 private Map<String,String> policies = new HashMap<>(); 541 542 public void setPolicy(String rule, String value) { 543 if (value == null) { 544 policies.remove(rule); 545 } else { 546 policies.put(rule, value); 547 } 548 } 549 /** 550 * If the provided client/server pair matches a rule 551 * 552 * A system property named test.kdc.policy.RULE will be consulted. 553 * If it's unset, returns false. If its value is "", any pair is 554 * matched. Otherwise, it should contains the server name matched. 555 * 556 * TODO: client name is not used currently. 557 * 558 * @param c client name 559 * @param s server name 560 * @param rule rule name 561 * @return if a match is found 562 */ 563 private boolean configMatch(String c, String s, String rule) { 564 String policy = policies.get(rule); 565 boolean result = false; 566 if (policy == null) { 567 result = false; 568 } else if (policy.length() == 0) { 569 result = true; 570 } else { 571 String[] names = policy.split("\\s+"); 572 for (String name: names) { 573 if (name.equals(s)) { 574 result = true; 575 break; 576 } 577 } 578 } 579 if (result) { 580 System.out.printf(">>>> Policy match result (%s vs %s on %s) %b\n", 581 c, s, rule, result); 582 } 583 return result; 584 } 585 586 587 /** 588 * Processes an incoming request and generates a response. 589 * @param in the request 590 * @return the response 591 * @throws java.lang.Exception for various errors 592 */ 593 private byte[] processMessage(byte[] in) throws Exception { 594 if ((in[0] & 0x1f) == Krb5.KRB_AS_REQ) 595 return processAsReq(in); 596 else 597 return processTgsReq(in); 598 } 599 600 /** 601 * Processes a TGS_REQ and generates a TGS_REP (or KRB_ERROR) 602 * @param in the request 603 * @return the response 604 * @throws java.lang.Exception for various errors 605 */ 606 private byte[] processTgsReq(byte[] in) throws Exception { 607 TGSReq tgsReq = new TGSReq(in); 608 try { 609 System.out.println(realm + "> " + tgsReq.reqBody.cname + 610 " sends TGS-REQ for " + 611 tgsReq.reqBody.sname); 612 KDCReqBody body = tgsReq.reqBody; 613 int[] eTypes = KDCReqBodyDotEType(body); 614 int e2 = eTypes[0]; // etype for outgoing session key 615 int e3 = eTypes[0]; // etype for outgoing ticket 616 617 PAData[] pas = kDCReqDotPAData(tgsReq); 618 619 Ticket tkt = null; 620 EncTicketPart etp = null; 621 if (pas == null || pas.length == 0) { 622 throw new KrbException(Krb5.KDC_ERR_PADATA_TYPE_NOSUPP); 623 } else { 624 for (PAData pa: pas) { 625 if (pa.getType() == Krb5.PA_TGS_REQ) { 626 APReq apReq = new APReq(pa.getValue()); 627 EncryptedData ed = apReq.authenticator; 628 tkt = apReq.ticket; 629 int te = tkt.encPart.getEType(); 630 tkt.sname.setRealm(tkt.realm); 631 EncryptionKey kkey = keyForUser(tkt.sname, te, true); 632 byte[] bb = tkt.encPart.decrypt(kkey, KeyUsage.KU_TICKET); 633 DerInputStream derIn = new DerInputStream(bb); 634 DerValue der = derIn.getDerValue(); 635 etp = new EncTicketPart(der.toByteArray()); 636 } 637 } 638 if (tkt == null) { 639 throw new KrbException(Krb5.KDC_ERR_PADATA_TYPE_NOSUPP); 640 } 641 } 642 643 // Session key for original ticket, TGT 644 EncryptionKey ckey = etp.key; 645 646 // Session key for session with the service 647 EncryptionKey key = generateRandomKey(e2); 648 649 // Check time, TODO 650 KerberosTime till = body.till; 651 if (till == null) { 652 throw new KrbException(Krb5.KDC_ERR_NEVER_VALID); // TODO 653 } else if (till.isZero()) { 654 till = new KerberosTime(new Date().getTime() + 1000 * 3600 * 11); 655 } 656 657 boolean[] bFlags = new boolean[Krb5.TKT_OPTS_MAX+1]; 658 if (body.kdcOptions.get(KDCOptions.FORWARDABLE)) { 659 bFlags[Krb5.TKT_OPTS_FORWARDABLE] = true; 660 } 661 if (body.kdcOptions.get(KDCOptions.FORWARDED) || 662 etp.flags.get(Krb5.TKT_OPTS_FORWARDED)) { 663 bFlags[Krb5.TKT_OPTS_FORWARDED] = true; 664 } 665 if (body.kdcOptions.get(KDCOptions.RENEWABLE)) { 666 bFlags[Krb5.TKT_OPTS_RENEWABLE] = true; 667 //renew = new KerberosTime(new Date().getTime() + 1000 * 3600 * 24 * 7); 668 } 669 if (body.kdcOptions.get(KDCOptions.PROXIABLE)) { 670 bFlags[Krb5.TKT_OPTS_PROXIABLE] = true; 671 } 672 if (body.kdcOptions.get(KDCOptions.POSTDATED)) { 673 bFlags[Krb5.TKT_OPTS_POSTDATED] = true; 674 } 675 if (body.kdcOptions.get(KDCOptions.ALLOW_POSTDATE)) { 676 bFlags[Krb5.TKT_OPTS_MAY_POSTDATE] = true; 677 } 678 679 if (configMatch("", body.sname.getNameString(), "ok-as-delegate")) { 680 bFlags[Krb5.TKT_OPTS_DELEGATE] = true; 681 } 682 bFlags[Krb5.TKT_OPTS_INITIAL] = true; 683 684 TicketFlags tFlags = new TicketFlags(bFlags); 685 EncTicketPart enc = new EncTicketPart( 686 tFlags, 687 key, 688 etp.crealm, 689 etp.cname, 690 new TransitedEncoding(1, new byte[0]), // TODO 691 new KerberosTime(new Date()), 692 body.from, 693 till, body.rtime, 694 body.addresses, 695 null); 696 EncryptionKey skey = keyForUser(body.sname, e3, true); 697 if (skey == null) { 698 throw new KrbException(Krb5.KDC_ERR_SUMTYPE_NOSUPP); // TODO 699 } 700 Ticket t = new Ticket( 701 body.crealm, 702 body.sname, 703 new EncryptedData(skey, enc.asn1Encode(), KeyUsage.KU_TICKET) 704 ); 705 EncTGSRepPart enc_part = new EncTGSRepPart( 706 key, 707 new LastReq(new LastReqEntry[]{ 708 new LastReqEntry(0, new KerberosTime(new Date().getTime() - 10000)) 709 }), 710 body.getNonce(), // TODO: detect replay 711 new KerberosTime(new Date().getTime() + 1000 * 3600 * 24), 712 // Next 5 and last MUST be same with ticket 713 tFlags, 714 new KerberosTime(new Date()), 715 body.from, 716 till, body.rtime, 717 body.crealm, 718 body.sname, 719 body.addresses 720 ); 721 EncryptedData edata = new EncryptedData(ckey, enc_part.asn1Encode(), KeyUsage.KU_ENC_TGS_REP_PART_SESSKEY); 722 TGSRep tgsRep = new TGSRep(null, 723 etp.crealm, 724 etp.cname, 725 t, 726 edata); 727 System.out.println(" Return " + tgsRep.cname 728 + " ticket for " + tgsRep.ticket.sname); 729 730 DerOutputStream out = new DerOutputStream(); 731 out.write(DerValue.createTag(DerValue.TAG_APPLICATION, 732 true, (byte)Krb5.KRB_TGS_REP), tgsRep.asn1Encode()); 733 return out.toByteArray(); 734 } catch (KrbException ke) { 735 ke.printStackTrace(System.out); 736 KRBError kerr = ke.getError(); 737 KDCReqBody body = tgsReq.reqBody; 738 System.out.println(" Error " + ke.returnCode() 739 + " " +ke.returnCodeMessage()); 740 if (kerr == null) { 741 kerr = new KRBError(null, null, null, 742 new KerberosTime(new Date()), 743 0, 744 ke.returnCode(), 745 body.crealm, body.cname, 746 new Realm(getRealm()), body.sname, 747 KrbException.errorMessage(ke.returnCode()), 748 null); 749 } 750 return kerr.asn1Encode(); 751 } 752 } 753 754 /** 755 * Processes a AS_REQ and generates a AS_REP (or KRB_ERROR) 756 * @param in the request 757 * @return the response 758 * @throws java.lang.Exception for various errors 759 */ 760 private byte[] processAsReq(byte[] in) throws Exception { 761 ASReq asReq = new ASReq(in); 762 int[] eTypes = null; 763 List<PAData> outPAs = new ArrayList<>(); 764 765 try { 766 System.out.println(realm + "> " + asReq.reqBody.cname + 767 " sends AS-REQ for " + 768 asReq.reqBody.sname); 769 770 KDCReqBody body = asReq.reqBody; 771 body.cname.setRealm(getRealm()); 772 773 eTypes = KDCReqBodyDotEType(body); 774 int eType = eTypes[0]; 775 776 EncryptionKey ckey = keyForUser(body.cname, eType, false); 777 EncryptionKey skey = keyForUser(body.sname, eType, true); 778 779 if (options.containsKey(KDC.Option.ONLY_RC4_TGT)) { 780 int tgtEType = EncryptedData.ETYPE_ARCFOUR_HMAC; 781 boolean found = false; 782 for (int i=0; i<eTypes.length; i++) { 783 if (eTypes[i] == tgtEType) { 784 found = true; 785 break; 786 } 787 } 788 if (!found) { 789 throw new KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP); 790 } 791 skey = keyForUser(body.sname, tgtEType, true); 792 } 793 if (ckey == null) { 794 throw new KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP); 795 } 796 if (skey == null) { 797 throw new KrbException(Krb5.KDC_ERR_SUMTYPE_NOSUPP); // TODO 798 } 799 800 // Session key 801 EncryptionKey key = generateRandomKey(eType); 802 // Check time, TODO 803 KerberosTime till = body.till; 804 if (till == null) { 805 throw new KrbException(Krb5.KDC_ERR_NEVER_VALID); // TODO 806 } else if (till.isZero()) { 807 till = new KerberosTime(new Date().getTime() + 1000 * 3600 * 11); 808 } 809 //body.from 810 boolean[] bFlags = new boolean[Krb5.TKT_OPTS_MAX+1]; 811 if (body.kdcOptions.get(KDCOptions.FORWARDABLE)) { 812 bFlags[Krb5.TKT_OPTS_FORWARDABLE] = true; 813 } 814 if (body.kdcOptions.get(KDCOptions.RENEWABLE)) { 815 bFlags[Krb5.TKT_OPTS_RENEWABLE] = true; 816 //renew = new KerberosTime(new Date().getTime() + 1000 * 3600 * 24 * 7); 817 } 818 if (body.kdcOptions.get(KDCOptions.PROXIABLE)) { 819 bFlags[Krb5.TKT_OPTS_PROXIABLE] = true; 820 } 821 if (body.kdcOptions.get(KDCOptions.POSTDATED)) { 822 bFlags[Krb5.TKT_OPTS_POSTDATED] = true; 823 } 824 if (body.kdcOptions.get(KDCOptions.ALLOW_POSTDATE)) { 825 bFlags[Krb5.TKT_OPTS_MAY_POSTDATE] = true; 826 } 827 bFlags[Krb5.TKT_OPTS_INITIAL] = true; 828 829 // Creating PA-DATA 830 int[] epas = eTypes; 831 if (options.containsKey(KDC.Option.RC4_FIRST_PREAUTH)) { 832 for (int i=1; i<epas.length; i++) { 833 if (epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC) { 834 epas[i] = epas[0]; 835 epas[0] = EncryptedData.ETYPE_ARCFOUR_HMAC; 836 break; 837 } 838 }; 839 } else if (options.containsKey(KDC.Option.ONLY_ONE_PREAUTH)) { 840 epas = new int[] { eTypes[0] }; 841 } 842 843 DerValue[] pas = new DerValue[epas.length]; 844 for (int i=0; i<epas.length; i++) { 845 pas[i] = new DerValue(new ETypeInfo2( 846 epas[i], 847 epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC ? 848 null : getSalt(body.cname), 849 null).asn1Encode()); 850 } 851 DerOutputStream eid = new DerOutputStream(); 852 eid.putSequence(pas); 853 854 outPAs.add(new PAData(Krb5.PA_ETYPE_INFO2, eid.toByteArray())); 855 856 boolean allOld = true; 857 for (int i: eTypes) { 858 if (i == EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96 || 859 i == EncryptedData.ETYPE_AES256_CTS_HMAC_SHA1_96) { 860 allOld = false; 861 break; 862 } 863 } 864 if (allOld) { 865 for (int i=0; i<epas.length; i++) { 866 pas[i] = new DerValue(new ETypeInfo( 867 epas[i], 868 epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC ? 869 null : getSalt(body.cname) 870 ).asn1Encode()); 871 } 872 eid = new DerOutputStream(); 873 eid.putSequence(pas); 874 outPAs.add(new PAData(Krb5.PA_ETYPE_INFO, eid.toByteArray())); 875 } 876 877 PAData[] inPAs = kDCReqDotPAData(asReq); 878 if (inPAs == null || inPAs.length == 0) { 879 Object preauth = options.get(Option.PREAUTH_REQUIRED); 880 if (preauth == null || preauth.equals(Boolean.TRUE)) { 881 throw new KrbException(Krb5.KDC_ERR_PREAUTH_REQUIRED); 882 } 883 } else { 884 try { 885 EncryptedData data = newEncryptedData(new DerValue(inPAs[0].getValue())); 886 EncryptionKey pakey = keyForUser(body.cname, data.getEType(), false); 887 data.decrypt(pakey, KeyUsage.KU_PA_ENC_TS); 888 } catch (Exception e) { 889 throw new KrbException(Krb5.KDC_ERR_PREAUTH_FAILED); 890 } 891 bFlags[Krb5.TKT_OPTS_PRE_AUTHENT] = true; 892 } 893 894 TicketFlags tFlags = new TicketFlags(bFlags); 895 EncTicketPart enc = new EncTicketPart( 896 tFlags, 897 key, 898 body.crealm, 899 body.cname, 900 new TransitedEncoding(1, new byte[0]), 901 new KerberosTime(new Date()), 902 body.from, 903 till, body.rtime, 904 body.addresses, 905 null); 906 Ticket t = new Ticket( 907 body.crealm, 908 body.sname, 909 new EncryptedData(skey, enc.asn1Encode(), KeyUsage.KU_TICKET) 910 ); 911 EncASRepPart enc_part = new EncASRepPart( 912 key, 913 new LastReq(new LastReqEntry[]{ 914 new LastReqEntry(0, new KerberosTime(new Date().getTime() - 10000)) 915 }), 916 body.getNonce(), // TODO: detect replay? 917 new KerberosTime(new Date().getTime() + 1000 * 3600 * 24), 918 // Next 5 and last MUST be same with ticket 919 tFlags, 920 new KerberosTime(new Date()), 921 body.from, 922 till, body.rtime, 923 body.crealm, 924 body.sname, 925 body.addresses 926 ); 927 EncryptedData edata = new EncryptedData(ckey, enc_part.asn1Encode(), KeyUsage.KU_ENC_AS_REP_PART); 928 ASRep asRep = new ASRep( 929 outPAs.toArray(new PAData[outPAs.size()]), 930 body.crealm, 931 body.cname, 932 t, 933 edata); 934 935 System.out.println(" Return " + asRep.cname 936 + " ticket for " + asRep.ticket.sname); 937 938 DerOutputStream out = new DerOutputStream(); 939 out.write(DerValue.createTag(DerValue.TAG_APPLICATION, 940 true, (byte)Krb5.KRB_AS_REP), asRep.asn1Encode()); 941 byte[] result = out.toByteArray(); 942 943 // Added feature: 944 // Write the current issuing TGT into a ccache file specified 945 // by the system property below. 946 String ccache = System.getProperty("test.kdc.save.ccache"); 947 if (ccache != null) { 948 asRep.encKDCRepPart = enc_part; 949 sun.security.krb5.internal.ccache.Credentials credentials = 950 new sun.security.krb5.internal.ccache.Credentials(asRep); 951 asReq.reqBody.cname.setRealm(getRealm()); 952 CredentialsCache cache = 953 CredentialsCache.create(asReq.reqBody.cname, ccache); 954 if (cache == null) { 955 throw new IOException("Unable to create the cache file " + 956 ccache); 957 } 958 cache.update(credentials); 959 cache.save(); 960 new File(ccache).deleteOnExit(); 961 } 962 963 return result; 964 } catch (KrbException ke) { 965 ke.printStackTrace(System.out); 966 KRBError kerr = ke.getError(); 967 KDCReqBody body = asReq.reqBody; 968 System.out.println(" Error " + ke.returnCode() 969 + " " +ke.returnCodeMessage()); 970 byte[] eData = null; 971 if (kerr == null) { 972 if (ke.returnCode() == Krb5.KDC_ERR_PREAUTH_REQUIRED || 973 ke.returnCode() == Krb5.KDC_ERR_PREAUTH_FAILED) { 974 DerOutputStream bytes = new DerOutputStream(); 975 bytes.write(new PAData(Krb5.PA_ENC_TIMESTAMP, new byte[0]).asn1Encode()); 976 for (PAData p: outPAs) { 977 bytes.write(p.asn1Encode()); 978 } 979 DerOutputStream temp = new DerOutputStream(); 980 temp.write(DerValue.tag_Sequence, bytes); 981 eData = temp.toByteArray(); 982 } 983 kerr = new KRBError(null, null, null, 984 new KerberosTime(new Date()), 985 0, 986 ke.returnCode(), 987 body.crealm, body.cname, 988 new Realm(getRealm()), body.sname, 989 KrbException.errorMessage(ke.returnCode()), 990 eData); 991 } 992 return kerr.asn1Encode(); 993 } 994 } 995 996 /** 997 * Generates a line for a KDC to put inside [realms] of krb5.conf 998 * @param kdc the KDC 999 * @return REALM.NAME = { kdc = host:port } 1000 */ 1001 private static String realmLineForKDC(KDC kdc) { 1002 return String.format(" %s = {\n kdc = %s:%d\n }\n", 1003 kdc.realm, 1004 kdc.kdc, 1005 kdc.port); 1006 } 1007 1008 /** 1009 * Start the KDC service. This server listens on both UDP and TCP using 1010 * the same port number. It uses three threads to deal with requests. 1011 * They can be set to daemon threads if requested. 1012 * @param port the port number to listen to. If zero, a random available 1013 * port no less than 8000 will be chosen and used. 1014 * @param asDaemon true if the KDC threads should be daemons 1015 * @throws java.io.IOException for any communication error 1016 */ 1017 protected void startServer(int port, boolean asDaemon) throws IOException { 1018 if (port > 0) { 1019 u1 = new DatagramSocket(port, InetAddress.getByName("127.0.0.1")); 1020 t1 = new ServerSocket(port); 1021 } else { 1022 while (true) { 1023 // Try to find a port number that's both TCP and UDP free 1024 try { 1025 port = 8000 + new java.util.Random().nextInt(10000); 1026 u1 = null; 1027 u1 = new DatagramSocket(port, InetAddress.getByName("127.0.0.1")); 1028 t1 = new ServerSocket(port); 1029 break; 1030 } catch (Exception e) { 1031 if (u1 != null) u1.close(); 1032 } 1033 } 1034 } 1035 final DatagramSocket udp = u1; 1036 final ServerSocket tcp = t1; 1037 System.out.println("Start KDC on " + port); 1038 1039 this.port = port; 1040 1041 // The UDP consumer 1042 thread1 = new Thread() { 1043 public void run() { 1044 while (true) { 1045 try { 1046 byte[] inbuf = new byte[8192]; 1047 DatagramPacket p = new DatagramPacket(inbuf, inbuf.length); 1048 udp.receive(p); 1049 System.out.println("-----------------------------------------------"); 1050 System.out.println(">>>>> UDP packet received"); 1051 q.put(new Job(processMessage(Arrays.copyOf(inbuf, p.getLength())), udp, p)); 1052 } catch (Exception e) { 1053 e.printStackTrace(); 1054 } 1055 } 1056 } 1057 }; 1058 thread1.setDaemon(asDaemon); 1059 thread1.start(); 1060 1061 // The TCP consumer 1062 thread2 = new Thread() { 1063 public void run() { 1064 while (true) { 1065 try { 1066 Socket socket = tcp.accept(); 1067 System.out.println("-----------------------------------------------"); 1068 System.out.println(">>>>> TCP connection established"); 1069 DataInputStream in = new DataInputStream(socket.getInputStream()); 1070 DataOutputStream out = new DataOutputStream(socket.getOutputStream()); 1071 byte[] token = new byte[in.readInt()]; 1072 in.readFully(token); 1073 q.put(new Job(processMessage(token), socket, out)); 1074 } catch (Exception e) { 1075 e.printStackTrace(); 1076 } 1077 } 1078 } 1079 }; 1080 thread2.setDaemon(asDaemon); 1081 thread2.start(); 1082 1083 // The dispatcher 1084 thread3 = new Thread() { 1085 public void run() { 1086 while (true) { 1087 try { 1088 q.take().send(); 1089 } catch (Exception e) { 1090 } 1091 } 1092 } 1093 }; 1094 thread3.setDaemon(true); 1095 thread3.start(); 1096 } 1097 1098 public void terminate() { 1099 try { 1100 thread1.stop(); 1101 thread2.stop(); 1102 thread3.stop(); 1103 u1.close(); 1104 t1.close(); 1105 } catch (Exception e) { 1106 // OK 1107 } 1108 } 1109 /** 1110 * Helper class to encapsulate a job in a KDC. 1111 */ 1112 private static class Job { 1113 byte[] token; // The received request at creation time and 1114 // the response at send time 1115 Socket s; // The TCP socket from where the request comes 1116 DataOutputStream out; // The OutputStream of the TCP socket 1117 DatagramSocket s2; // The UDP socket from where the request comes 1118 DatagramPacket dp; // The incoming UDP datagram packet 1119 boolean useTCP; // Whether TCP or UDP is used 1120 1121 // Creates a job object for TCP 1122 Job(byte[] token, Socket s, DataOutputStream out) { 1123 useTCP = true; 1124 this.token = token; 1125 this.s = s; 1126 this.out = out; 1127 } 1128 1129 // Creates a job object for UDP 1130 Job(byte[] token, DatagramSocket s2, DatagramPacket dp) { 1131 useTCP = false; 1132 this.token = token; 1133 this.s2 = s2; 1134 this.dp = dp; 1135 } 1136 1137 // Sends the output back to the client 1138 void send() { 1139 try { 1140 if (useTCP) { 1141 System.out.println(">>>>> TCP request honored"); 1142 out.writeInt(token.length); 1143 out.write(token); 1144 s.close(); 1145 } else { 1146 System.out.println(">>>>> UDP request honored"); 1147 s2.send(new DatagramPacket(token, token.length, dp.getAddress(), dp.getPort())); 1148 } 1149 } catch (Exception e) { 1150 e.printStackTrace(); 1151 } 1152 } 1153 } 1154 1155 public static class KDCNameService implements NameServiceDescriptor { 1156 @Override 1157 public NameService createNameService() throws Exception { 1158 NameService ns = new NameService() { 1159 @Override 1160 public InetAddress[] lookupAllHostAddr(String host) 1161 throws UnknownHostException { 1162 // Everything is localhost 1163 return new InetAddress[]{ 1164 InetAddress.getByAddress(host, new byte[]{127,0,0,1}) 1165 }; 1166 } 1167 @Override 1168 public String getHostByAddr(byte[] addr) 1169 throws UnknownHostException { 1170 // No reverse lookup, PrincipalName use original string 1171 throw new UnknownHostException(); 1172 } 1173 }; 1174 return ns; 1175 } 1176 1177 @Override 1178 public String getProviderName() { 1179 return "mock"; 1180 } 1181 1182 @Override 1183 public String getType() { 1184 return "ns"; 1185 } 1186 } 1187 1188 // Calling private methods thru reflections 1189 private static final Field getPADataField; 1190 private static final Field getEType; 1191 private static final Constructor<EncryptedData> ctorEncryptedData; 1192 private static final Method stringToKey; 1193 1194 static { 1195 try { 1196 ctorEncryptedData = EncryptedData.class.getDeclaredConstructor(DerValue.class); 1197 ctorEncryptedData.setAccessible(true); 1198 getPADataField = KDCReq.class.getDeclaredField("pAData"); 1199 getPADataField.setAccessible(true); 1200 getEType = KDCReqBody.class.getDeclaredField("eType"); 1201 getEType.setAccessible(true); 1202 stringToKey = EncryptionKey.class.getDeclaredMethod( 1203 "stringToKey", 1204 char[].class, String.class, byte[].class, Integer.TYPE); 1205 stringToKey.setAccessible(true); 1206 } catch (NoSuchFieldException nsfe) { 1207 throw new AssertionError(nsfe); 1208 } catch (NoSuchMethodException nsme) { 1209 throw new AssertionError(nsme); 1210 } 1211 } 1212 private EncryptedData newEncryptedData(DerValue der) { 1213 try { 1214 return ctorEncryptedData.newInstance(der); 1215 } catch (Exception e) { 1216 throw new AssertionError(e); 1217 } 1218 } 1219 private static PAData[] kDCReqDotPAData(KDCReq req) { 1220 try { 1221 return (PAData[])getPADataField.get(req); 1222 } catch (Exception e) { 1223 throw new AssertionError(e); 1224 } 1225 } 1226 private static int[] KDCReqBodyDotEType(KDCReqBody body) { 1227 try { 1228 return (int[]) getEType.get(body); 1229 } catch (Exception e) { 1230 throw new AssertionError(e); 1231 } 1232 } 1233 private static byte[] EncryptionKeyDotStringToKey(char[] password, String salt, 1234 byte[] s2kparams, int keyType) throws KrbCryptoException { 1235 try { 1236 return (byte[])stringToKey.invoke( 1237 null, password, salt, s2kparams, keyType); 1238 } catch (InvocationTargetException ex) { 1239 throw (KrbCryptoException)ex.getCause(); 1240 } catch (Exception e) { 1241 throw new AssertionError(e); 1242 } 1243 } 1244 }