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