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