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 }