KDC.java revision 3909
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/**
* A KDC server.
* <p>
* Features:
* <ol>
* <li> Supports TCP and UDP
* <li> Supports AS-REQ and TGS-REQ
* <li> Principal db and other settings hard coded in application
* <li> Options, say, request preauth or not
* </ol>
* Side effects:
* <ol>
* <li> The Sun-internal class <code>sun.security.krb5.Config</code> is a
* singleton and initialized according to Kerberos settings (krb5.conf and
* java.security.krb5.* system properties). This means once it's initialized
* it will not automatically notice any changes to these settings (or file
* changes of krb5.conf). The KDC class normally does not touch these
* settings (except for the <code>writeKtab()</code> method). However, to make
* sure nothing ever goes wrong, if you want to make any changes to these
* settings after calling a KDC method, call <code>Config.refresh()</code> to
* make sure your changes are reflected in the <code>Config</code> object.
* </ol>
* System properties recognized:
* <ul>
* <li>test.kdc.save.ccache
* </ul>
* Support policies:
* <ul>
* <li>ok-as-delegate
* </ul>
* Issues and TODOs:
* <ol>
* <li> Generates krb5.conf to be used on another machine, currently the kdc is
* always localhost
* <li> More options to KDC, say, error output, say, response nonce !=
* request nonce
* </ol>
* Note: This program uses internal krb5 classes (including reflection to
* access private fields and methods).
* <p>
* Usages:
* <p>
* 1. Init and start the KDC:
* <pre>
* KDC kdc = KDC.create("REALM.NAME", port, isDaemon);
* KDC kdc = KDC.create("REALM.NAME");
* </pre>
* Here, <code>port</code> is the UDP and TCP port number the KDC server
* listens on. If zero, a random port is chosen, which you can use getPort()
* later to retrieve the value.
* <p>
* If <code>isDaemon</code> is true, the KDC worker threads will be daemons.
* <p>
* The shortcut <code>KDC.create("REALM.NAME")</code> has port=0 and
* isDaemon=false, and is commonly used in an embedded KDC.
* <p>
* 2. Adding users:
* <pre>
* kdc.addPrincipal(String principal_name, char[] password);
* kdc.addPrincipalRandKey(String principal_name);
* </pre>
* generates a random key. To expose this key, call <code>writeKtab()</code> to
* save the keys into a keytab file.
* <p>
* Note that you need to add the principal name krbtgt/REALM.NAME yourself.
* <p>
* Note that you can safely add a principal at any time after the KDC is
* started and before a user requests info on this principal.
* <p>
* 3. Other public methods:
* <ul>
* <li> <code>getPort</code>: Returns the port number the KDC uses
* <li> <code>getRealm</code>: Returns the realm name
* <li> <code>writeKtab</code>: Writes all principals' keys into a keytab file
* <li> <code>saveConfig</code>: Saves a krb5.conf file to access this KDC
* <li> <code>setOption</code>: Sets various options
* </ul>
* Read the javadoc for details. Lazy developer can use <code>OneKDC</code>
* directly.
*/
public class KDC {
// Under the hood.
// The random generator to generate random keys (including session keys)
// Principal db. principal -> pass. A case-insensitive TreeMap is used
// so that even if the client provides a name with different case, the KDC
// can still locate the principal and give back correct salt.
// Realm name
// KDC
// Service port number
private int port;
// Options
/**
* Option names, to be expanded forever.
*/
public static enum Option {
/**
* Whether pre-authentication is required. Default Boolean.TRUE
*/
/**
* Only issue TGT in RC4
*/
/**
* Use RC4 as the first in preauth
*/
/**
* Use only one preauth, so that some keys are not easy to generate
*/
};
static {
}
/**
* A standalone KDC server.
*/
}
/**
* Creates and starts a KDC running as a daemon on a random port.
* @param realm the realm name
* @return the running KDC instance
* @throws java.io.IOException for any socket creation error
*/
}
return k;
}
/**
* Creates and starts a KDC server.
* @param realm the realm name
* @param port the TCP and UDP port to listen to. A random port will to
* chosen if zero.
* @param asDaemon if true, KDC threads will be daemons. Otherwise, not.
* @return the running KDC instance
* @throws java.io.IOException for any socket creation error
*/
}
/**
* Sets an option
* @param key the option name
* @param obj the value
*/
}
/**
* Write all principals' keys from multiple KDCsinto one keytab file.
* Note that the keys for the krbtgt principals will not be written.
* <p>
* Attention: This method references krb5.conf settings. If you need to
* setup krb5.conf later, please call <code>Config.refresh()</code> after
* the new setting. For example:
* <pre>
* System.setProperty("java.security.krb5.conf", "/home/mykrb5.conf");
* Config.refresh();
* </pre>
*
* Inside this method there are 2 places krb5.conf is used:
* <ol>
* <li> (Fatal) Generating keys: EncryptionKey.acquireSecretKeys
* <li> (Has workaround) Creating PrincipalName
* </ol>
* @param tab The keytab filename to write to.
* @throws java.io.IOException for any file output error
* name error.
*/
throws IOException, KrbException {
}
}
}
/**
* Write a ktab for this KDC.
*/
}
/**
* Adds a new principal to this realm with a given password.
* @param user the principal's name. For a service principal, use the
* @param pass the password for the principal
*/
}
}
/**
* Adds a new principal to this realm with a random password
* @param user the principal's name. For a service principal, use the
*/
}
/**
* Returns the name of this realm
* @return the name of this realm
*/
return realm;
}
/**
* Returns the name of kdc
* @return the name of kdc
*/
return kdc;
}
/**
* Writes a krb5.conf for one or more KDC that includes KDC locations for
* each realm and the default realm name. You can also add extra strings
* into the file. The method should be called like:
* <pre>
* KDC.saveConfig("krb5.conf", kdc1, kdc2, ..., line1, line2, ...);
* </pre>
* Here you can provide one or more kdc# and zero or more line# arguments.
* The line# will be put after [libdefaults] and before [realms]. Therefore
* stanzas as well. Note that a newline character will be appended to
* each line# argument.
* <p>
* For example:
* <pre>
* KDC.saveConfig("krb5.conf", this);
* </pre>
* generates:
* <pre>
* [libdefaults]
* default_realm = REALM.NAME
*
* [realms]
* REALM.NAME = {
* kdc = host:port_number
* }
* </pre>
*
* Another example:
* <pre>
* KDC.saveConfig("krb5.conf", kdc1, kdc2, "forwardable = true", "",
* "[domain_realm]",
* ".kdc1.com = KDC1.NAME");
* </pre>
* generates:
* <pre>
* [libdefaults]
* default_realm = KDC1.NAME
* forwardable = true
*
* [domain_realm]
* .kdc1.com = KDC1.NAME
*
* [realms]
* KDC1.NAME = {
* kdc = host:port1
* }
* KDC2.NAME = {
* kdc = host:port2
* }
* </pre>
* @param file the name of the file to write into
* @param kdc the first (and default) KDC
* @param more more KDCs or extra lines (in their appearing order) to
* insert into the krb5.conf file. This method reads each argument's type
* to determine what it's for. This argument can be empty.
* @throws java.io.IOException for any file output error
*/
throws IOException {
if (o instanceof String) {
}
}
if (o instanceof KDC) {
}
}
}
/**
* Returns the service port of the KDC server.
* @return the KDC service port
*/
public int getPort() {
return port;
}
// Private helper methods
/**
* Private constructor, cannot be called outside.
* @param realm
*/
}
/**
* A constructor that starts the KDC service also.
*/
throws IOException {
}
/**
* Generates a 32-char random password
* @return the password
*/
private static char[] randomPassword() {
char[] pass = new char[32];
for (int i=0; i<31; i++)
// The last char cannot be a number, otherwise, keyForUser()
// believes it's a sign of kvno
return pass;
}
/**
* Generates a random key for the given encryption type.
* @param eType the encryption type
* @return the generated key
* @throws sun.security.krb5.KrbException for unknown/unsupported etype
*/
throws KrbException {
// Is 32 enough for AES256? I should have generated the keys directly
// but different cryptos have different rules on what keys are valid.
char[] pass = randomPassword();
switch (eType) {
default: algo = "DES"; break;
}
}
/**
* Returns the password for a given principal
* @param p principal
* @return the password
* @throws sun.security.krb5.KrbException when the principal is not inside
* the database.
*/
throws KrbException {
if (p.getRealmString() == null) {
}
throw new KrbException(server?
}
return pass;
}
/**
* Returns the salt string for the principal.
* @param p principal
* @return the salt
*/
if (p.getRealmString() == null) {
}
try {
// Find the principal name with correct case.
} catch (RealmException re) {
// Won't happen
}
}
String s = p.getRealmString();
for (String n: p.getNameStrings()) {
s += n;
}
return s;
}
/**
* Returns the key for a given principal of the given encryption type
* @param p the principal
* @param etype the encryption type
* @param server looking for a server principal?
* @return the key
* @throws sun.security.krb5.KrbException for unknown/unsupported etype
*/
throws KrbException {
try {
// Do not call EncryptionKey.acquireSecretKeys(), otherwise
// the krb5.conf config file would be loaded.
// For service whose password ending with a number, use it as kvno.
// Kvno must be postive.
}
}
return new EncryptionKey(EncryptionKeyDotStringToKey(
} catch (KrbException ke) {
throw ke;
} catch (Exception e) {
throw new RuntimeException(e); // should not happen
}
}
} else {
}
}
/**
*
* A system property named test.kdc.policy.RULE will be consulted.
* If it's unset, returns false. If its value is "", any pair is
* matched. Otherwise, it should contains the server name matched.
*
* TODO: client name is not used currently.
*
* @param c client name
* @param s server name
* @param rule rule name
* @return if a match is found
*/
boolean result = false;
result = false;
result = true;
} else {
result = true;
break;
}
}
}
if (result) {
}
return result;
}
/**
* Processes an incoming request and generates a response.
* @param in the request
* @return the response
* @throws java.lang.Exception for various errors
*/
return processAsReq(in);
else
return processTgsReq(in);
}
/**
* Processes a TGS_REQ and generates a TGS_REP (or KRB_ERROR)
* @param in the request
* @return the response
* @throws java.lang.Exception for various errors
*/
try {
" sends TGS-REQ for " +
} else {
}
}
}
}
// Session key for original ticket, TGT
// Session key for session with the service
// Check time, TODO
}
}
}
//renew = new KerberosTime(new Date().getTime() + 1000 * 3600 * 24 * 7);
}
}
}
}
}
key,
new KerberosTime(new Date()),
null);
}
);
key,
new LastReq(new LastReqEntry[]{
}),
// Next 5 and last MUST be same with ticket
new KerberosTime(new Date()),
);
EncryptedData edata = new EncryptedData(ckey, enc_part.asn1Encode(), KeyUsage.KU_ENC_TGS_REP_PART_SESSKEY);
t,
edata);
return out.toByteArray();
} catch (KrbException ke) {
new KerberosTime(new Date()),
0,
ke.returnCode(),
null);
}
return kerr.asn1Encode();
}
}
/**
* Processes a AS_REQ and generates a AS_REP (or KRB_ERROR)
* @param in the request
* @return the response
* @throws java.lang.Exception for various errors
*/
try {
" sends AS-REQ for " +
boolean found = false;
found = true;
break;
}
}
if (!found) {
}
}
}
}
// Session key
// Check time, TODO
}
//body.from
}
//renew = new KerberosTime(new Date().getTime() + 1000 * 3600 * 24 * 7);
}
}
}
}
// Creating PA-DATA
break;
}
};
}
epas[i],
null).asn1Encode());
}
boolean allOld = true;
for (int i: eTypes) {
if (i == EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96 ||
allOld = false;
break;
}
}
if (allOld) {
epas[i],
).asn1Encode());
}
eid = new DerOutputStream();
}
}
} else {
try {
} catch (Exception e) {
}
}
key,
new KerberosTime(new Date()),
null);
);
key,
new LastReq(new LastReqEntry[]{
}),
// Next 5 and last MUST be same with ticket
new KerberosTime(new Date()),
);
t,
edata);
// Added feature:
// Write the current issuing TGT into a ccache file specified
// by the system property below.
throw new IOException("Unable to create the cache file " +
ccache);
}
}
return result;
} catch (KrbException ke) {
}
}
new KerberosTime(new Date()),
0,
ke.returnCode(),
eData);
}
return kerr.asn1Encode();
}
}
/**
* Generates a line for a KDC to put inside [realms] of krb5.conf
* @param kdc the KDC
* @return REALM.NAME = { kdc = host:port }
*/
}
/**
* Start the KDC service. This server listens on both UDP and TCP using
* the same port number. It uses three threads to deal with requests.
* They can be set to daemon threads if requested.
* @param port the port number to listen to. If zero, a random available
* port no less than 8000 will be chosen and used.
* @param asDaemon true if the KDC threads should be daemons
* @throws java.io.IOException for any communication error
*/
if (port > 0) {
} else {
while (true) {
// Try to find a port number that's both TCP and UDP free
try {
break;
} catch (Exception e) {
}
}
}
// The UDP consumer
public void run() {
while (true) {
try {
byte[] inbuf = new byte[8192];
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
// The TCP consumer
public void run() {
while (true) {
try {
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
// The dispatcher
public void run() {
while (true) {
try {
} catch (Exception e) {
}
}
}
};
}
public void terminate() {
try {
} catch (Exception e) {
// OK
}
}
/**
* Helper class to encapsulate a job in a KDC.
*/
private static class Job {
byte[] token; // The received request at creation time and
// the response at send time
Socket s; // The TCP socket from where the request comes
boolean useTCP; // Whether TCP or UDP is used
// Creates a job object for TCP
useTCP = true;
this.s = s;
}
// Creates a job object for UDP
useTCP = false;
}
// Sends the output back to the client
void send() {
try {
if (useTCP) {
s.close();
} else {
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static class KDCNameService implements NameServiceDescriptor {
throws UnknownHostException {
// Everything is localhost
return new InetAddress[]{
};
}
throws UnknownHostException {
// No reverse lookup, PrincipalName use original string
throw new UnknownHostException();
}
};
return ns;
}
public String getProviderName() {
return "mock";
}
return "ns";
}
}
// Calling private methods thru reflections
private static final Field getPADataField;
private static final Method stringToKey;
static {
try {
ctorEncryptedData.setAccessible(true);
getPADataField.setAccessible(true);
getEType.setAccessible(true);
"stringToKey",
stringToKey.setAccessible(true);
} catch (NoSuchFieldException nsfe) {
throw new AssertionError(nsfe);
} catch (NoSuchMethodException nsme) {
throw new AssertionError(nsme);
}
}
try {
} catch (Exception e) {
throw new AssertionError(e);
}
}
try {
} catch (Exception e) {
throw new AssertionError(e);
}
}
try {
} catch (Exception e) {
throw new AssertionError(e);
}
}
try {
return (byte[])stringToKey.invoke(
} catch (InvocationTargetException ex) {
} catch (Exception e) {
throw new AssertionError(e);
}
}
}