This tutorial presents two sample applications demonstrating the use of the Java GSS-API for secure exchanges of messages between communicating applications, in this case a client application and a server application.
Java GSS-API uses what is called a "security mechanism" to provide these services. The GSS-API implementation available with the Java 2 Standard Edition platform contains support for the Kerberos V5 mechanism in addition to any other vendor-specific choices. We use the Kerberos V5 mechanism for this tutorial.
In order to perform authentication between the client and server and to establish cryptographic keys for secure communication, a GSS-API mechanism needs access to certain credentials for the local entity on each side of the connection. In our case, the credential used on the client side consists of a Kerberos ticket, and on the server side, it consists of a long-term Kerberos secret key. Kerberos tickets can optionally include the host address and IPv4 and IPv6 host addresses are both supported. Java GSS-API requires that the mechanism obtain these credentials from the Subject associated with the thread's access control context.
To populate a Subject with such credentials, client and server applications typically will first perform JAAS authentication using a Kerberos module. The JAAS Authentication tutorial demonstrates how to do this. The JAAS Authorization tutorial then demonstrates how to associate the authenticated Subject with the thread's access control context. A utility has also been written as a convenience to automatically perform those operations on your behalf. The Use of JAAS Login Utility tutorial demonstrates how to use the Login utility.
For this tutorial, we will not have the client and server
perform JAAS authentication, nor will we have them use the Login
utility. Instead, we will rely on setting the system property
javax.security.auth.useSubjectCredsOnly
to
false
, which allows us to relax the restriction of
requiring a GSS mechanism to obtain necessary credentials from an
existing Subject, set up by JAAS. See
The useSubjectCredsOnly System
Property.
Note: This is a simplified introductory tutorial. For example, we do not include any policy files or run the sample code using a security manager. In real life, code using Java GSS-API should be run with a security manager, so that security-sensitive operations would not be allowed unless the required permissions were explicitly granted.
There is another tutorial, Use of JAAS Login Utility and Java GSS-API for Secure Message Exchanges, that is just like the tutorial you are reading except that it utilizes the Login utility, policy files, and a more complex login configuration file (A login configuration file, required whenever JAAS authentication is done, specifies the desired authentication module).
As with all tutorials in this series, the underlying technology used to support authentication and secure communication for the applications in this tutorial is Kerberos V5. See Kerberos Requirements.
If you want to first see the tutorial code in action, you can
skip directly to Running the SampleClient and
SampleServer Programs and then go back to the other sections
to learn more.
The applications for this tutorial are named SampleClient and SampleServer.
Here is a summary of execution of the SampleClient and SampleServer applications:
The actual code and further details are presented in the following sections.
The entire code for both the SampleClient and SampleServer programs resides in their
main
methods and can be broken down into the
following subparts:
Note: The Java GSS-API classes utilized by these programs
(GSSManager, GSSContext, GSSName, GSSCredential, MessageProp, and
Oid) are found in the org.ietf.jgss
package.
The first thing both our client and server main
methods do is read the command-line arguments.
SampleClient expects three arguments:
Here is the code for reading the command-line arguments:
if (args.length < 3) { System.out.println("Usage: java <options> Login SampleClient " + " <servicePrincipal> <hostName> <port>"); System.exit(-1); } String server = args[0]; String hostName = args[1]; int port = Integer.parseInt(args[2]);
SampleServer expects just one argument:
Here is the code for reading the command-line argument:
if (args.length != 1) { System.out.println( "Usage: java <options> Login SampleServer <localPort>"); System.exit(-1); } int localPort = Integer.parseInt(args[0]);
Java GSS-API provides methods for creating and interpreting tokens (opaque byte data). The tokens contain messages to be securely exchanged between two peers, but the method of actual token transfer is up to the peers. For our SampleClient and SampleServer applications, we establish a socket connection between the client and server and exchange data using the socket input and output streams.
SampleClient was passed as arguments the name of the host machine SampleServer is running on, as well as the port number on which SampleServer will be listening for connections, so SampleClient has all it needs to establish a socket connection with SampleServer. It uses the following code to set up the connection and initialize a DataInputStream and a DataOutputStream for future data exchanges:
Socket socket = new Socket(hostName, port); DataInputStream inStream = new DataInputStream(socket.getInputStream()); DataOutputStream outStream = new DataOutputStream(socket.getOutputStream()); System.out.println("Connected to server " + socket.getInetAddress());
The SampleServer application was passed as an argument the port number to be used for listening for connections from clients. It creates a ServerSocket for listening on that port:
ServerSocket ss = new ServerSocket(localPort);
The ServerSocket can then wait for and accept a connection from a client, and then initialize a DataInputStream and a DataOutputStream for future data exchanges with the client :
Socket socket = ss.accept(); DataInputStream inStream = new DataInputStream(socket.getInputStream()); DataOutputStream outStream = new DataOutputStream(socket.getOutputStream()); System.out.println("Got connection from client " + socket.getInetAddress());
The accept
method waits until a client (in our
case, SampleClient) requests a connection on the host and port of
the SampleServer, which SampleClient does via
Socket socket = new Socket(hostName, port);
When the connection is requested and established, the
accept
method returns a new Socket object bound to a
new port. The server can communicate with the client over this
new socket and continue to listen for other client connection
requests on the ServerSocket bound to the original port. Thus, a
server program typically has a loop which can handle multiple
connection requests.
The basic loop structure for our SampleServer is the following:
while (true) { Socket socket = ss.accept(); <Establish input and output streams for the connection> <Establish a context with the client> <Exchange messages with the client>; <Clean up>; }
Client connections are queued at the original port, so with this program structure used by SampleServer, the interaction with the first client making a connection has to complete before the next connection can be accepted. The server could actually service multiple clients simultaneously through the use of threads - one thread per client connection, as in
while (true) { <accept a connection>; <create a thread to handle the client>; }
Before two applications can use Java GSS-API to securely exchange messages between them, they must establish a joint security context using their credentials. (Note: In the case of SampleClient, the credentials were established when the Login utility authenticated the user on whose behalf the SampleClient was run, and similarly for SampleServer.) The security context encapsulates shared state information that might include, for example, cryptographic keys. One use of such keys might be to encrypt messages to be exchanged, if encryption is requested.
As part of the security context establishment, the context initiator (in our case, SampleClient) is authenticated to the acceptor (SampleServer), and may require that the acceptor also be authenticated back to the initiator, in which case we say that "mutual authentication" took place.
Both applications create and use a GSSContext object to establish and maintain the shared information that makes up the security context.
The instantiation of the context object is done differently by
the context initiator and the context acceptor. After the
initiator instantiates a GSSContext, it may choose to set various
context options that will determine the characteristics of the
desired security context, for example, specifying whether or not
mutual authentication should take place. After all the desired
characteristics have been set, the initiator calls the
initSecContext
method, which produces a token
required by the acceptor's acceptSecContext
method.
While Java GSS-API methods exist for preparing tokens to be
exchanged between applications, it is the responsibility of
the applications to actually transfer the tokens between
them. So after the initiator has received a token from its
call to initSecContext
, it sends that token to the
acceptor. The acceptor calls acceptSecContext
,
passing it the token. The acceptSecContext
method
may in turn return a token. If it does, the acceptor should send
that token to the initiator, which should then call
initSecContext
again and pass it this token. Each
time initSecContext
or acceptSecContext
returns a token, the application that called the method should
send the token to its peer and that peer should pass the token to
its appropriate method (acceptSecContext
or
initSecContext
). This continues until the context is
fully established (which is the case when the context's
isEstablished
method returns true
).
The context establishment code for our sample applications is described in the following:
In our client/server scenario, SampleClient is the context initiator. Here are the basic steps it takes to establish a security context. It
initSecContext
, sending any returned token to
SampleServer, and receiving a token (if any) from
SampleServer.A GSSContext is created by instantiating a
GSSManager and then calling one of its
createContext
methods. The GSSManager class serves
as a factory for other important GSS API classes. It can create
instances of classes implementing the GSSContext, GSSCredential,
and GSSName interfaces.
SampleClient obtains an instance of the default GSSManager
subclass by calling the GSSManager static method
getInstance
:
GSSManager manager = GSSManager.getInstance();
The default GSSManager subclass is one whose
create*
methods (createContext
, etc.)
return classes whose implementations support Kerberos as the
underlying technology.
The GSSManager factory method for creating a context on the initiator's side has the following signature:
GSSContext createContext(GSSName peer, Oid mech, GSSCredential myCred, int lifetime);
The arguments are described below, followed by the complete
call to createContext
.
GSSName peer
ArgumentThe peer in our client/server paradigm is the server. For the
peer
argument, we need a GSSName for the
service principal representing the server. (See Kerberos User and Service Principal Names.) A
String for the service principal name is passed as the first
argument to SampleClient, which places the argument into its
local String variable named server
. The GSSManager
manager
is used to instantiate a GSSName by calling
one of its createName
methods. SampleClient calls
the createName
method with the following
signature:
GSSName createName(String nameStr, Oid nameType);
SampleClient passes the server
String for the
nameStr
argument.
The second argument is an Oid. An Oid represents a
Universal Object Identifier. Oids are hierarchically
globally-interpretable identifiers used within the GSS-API
framework to identify mechanisms and name types. The structure
and encoding of Oids is defined in the ISOIEC-8824 and
ISOIEC-8825 standards. The Oid passed to the
createName
method is specifically a name type Oid
(not a mechanism Oid).
In GSS-API, string names are often mapped from a
mechanism-independent format into a mechanism-specific format.
Usually, an Oid specifies what name format the string is in so
that the mechanism knows how to do this mapping. Passing in a
null
Oid indicates that the name is already in a
native format that the mechanism uses. This is the case for the
server
String; it is in the appropriate format for a
Kerberos Version 5 name. Thus, SampleClient passes a
null
for the Oid. Here is the call:
GSSName serverName = manager.createName(server, null);
Oid mech
ArgumentThe second argument to the GSSManager
createContext
method is an Oid representing the
mechanism to be used for the authentication between the client
and the server during context establishment and for subsequent
secure communication between them.
Our tutorial will use Kerberos V5 as the security mechanism. The Oid for the Kerberos V5 mechanism is defined in RFC 1964 as "1.2.840.113554.1.2.2" so we create such an Oid:
Oid krb5Oid = new Oid("1.2.840.113554.1.2.2");
SampleClient passes krb5Oid
as the second
argument to createContext
.
GSSCredential myCred
ArgumentThe third argument to the GSSManager
createContext
method is a GSSCredential
representing the caller's credentials. If you pass
null
for this argument, as SampleClient does, the
default credentials are used.
int lifetime
ArgumentThe final argument to the GSSManager
createContext
method is an int
specifying the desired lifetime, in seconds, for the context that
is created. SampleClient passes
GSSContext.DEFAULT_LIFETIME
to request a default
lifetime.
Now that we have all the required arguments, here is the call SampleClient makes to create a GSSContext:
GSSContext context = manager.createContext(serverName, krb5Oid, null, GSSContext.DEFAULT_LIFETIME);
After instantiating a context, and prior to actually
establishing the context with the context acceptor, the context
initiator may choose to set various options that determine the
desired security context characteristics. Each such option is set
by calling a request
method on the instantiated
context. Most request
methods take a
boolean
argument for indicating whether or not the
feature is requested. It is not always possible for a request to
be satisfied, so whether or not it was can be determined after
context establishment by calling one of the get
methods.
SampleClient requests the following:
wrap
. Encryption is actually used only
if the MessageProp object passed to the wrap
method
requests privacy.wrap
and getMIC
methods. When integrity
is requested, a cryptographic tag known as a Message Integrity
Code (MIC) will be generated when calling those methods. When
getMIC
is called, the generated MIC appears in the
returned token. When wrap
is called, the MIC is
packaged together with the message (the original message or the
result of encrypting the message, depending on whether
confidentiality was applied) all as part of one token. You can
subsequently verify the MIC against the message to ensure that
the message has not been modified in transit.The SampleClient code for making these requests on the
GSSException context
is the following:
context.requestMutualAuth(true); // Mutual authentication context.requestConf(true); // Will use encryption later context.requestInteg(true); // Will use integrity later
Note: When using the default GSSManager implementation and the Kerberos mechanism, these requests will always be granted.
After SampleClient has instantiated a GSSContext and specified the desired context options, it can actually establish the security context with SampleServer. To do so, SampleClient has a loop. Each loop iteration
initSecContext
method. If
this is the first call, the method is passed a null
token. Otherwise, it is passed the token most recently sent to
SampleClient by SampleServer (a token generated by a SampleServer
call to acceptSecContext
).initSecContext
(if
any) to SampleServer. The first call to
initSecContext
always produces a token. The last
call might not return a token.The tokens returned by initSecContext
or received
from SampleServer are placed in a byte array. Tokens should be
treated by SampleClient and SampleServer as opaque data to be
passed between them and interpreted by Java GSS-API methods.
The initSecContext
arguments are a byte array
containing a token, the starting offset into that array of where
the token begins, and the token length. For the first call,
SampleClient passes a null token, since no token has yet been
received from SampleServer.
To exchange tokens with SampleServer, SampleClient uses the
DataInputStream inStream
and DataOutputStream
outStream
it previously set up using the input and
output streams for the socket connection made with SampleServer.
Note that whenever a token is written, the number of bytes in the
token is written first, followed by the token itself. The reasons
are discussed in the introduction to the The SampleClient and SampleServer Message
Exchanges section.
Here is the SampleClient context establishment loop, followed by code displaying information about who the client and server are and whether or not mutual authentication actually took place:
byte[] token = new byte[0]; while (!context.isEstablished()) { // token is ignored on the first call token = context.initSecContext(token, 0, token.length); // Send a token to the server if one was generated by // initSecContext if (token != null) { System.out.println("Will send token of size " + token.length + " from initSecContext."); outStream.writeInt(token.length); outStream.write(token); outStream.flush(); } // If the client is done with context establishment // then there will be no more tokens to read in this loop if (!context.isEstablished()) { token = new byte[inStream.readInt()]; System.out.println("Will read input token of size " + token.length + " for processing by initSecContext"); inStream.readFully(token); } } System.out.println("Context Established! "); System.out.println("Client is " + context.getSrcName()); System.out.println("Server is " + context.getTargName()); if (context.getMutualAuthState()) System.out.println("Mutual authentication took place!");
In our client/server scenario, SampleServer is the context acceptor. Here are the basic steps it takes to establish a security context. It
acceptSecContext
and passing
it the token, and sending any returned token to
SampleClient.As described in SampleClient
GSSContext Instantiation, a GSSContext is created by
instantiating a GSSManager and then calling one of its
createContext
methods.
Like SampleClient, SampleServer obtains an instance of the
default GSSManager subclass by calling the GSSManager static
method getInstance
:
GSSManager manager = GSSManager.getInstance();
The GSSManager factory method for creating a context on the acceptor's side has the following signature:
GSSContext createContext(GSSCredential myCred);
If you pass null
for the GSSCredential argument,
as SampleServer does, the default credentials are used. The
context is instantiated via the following:
GSSContext context = manager.createContext((GSSCredential)null);
After SampleServer has instantiated a GSSContext, it can establish the security context with SampleClient. To do so, SampleServer has a loop that continues until the context is established. Each loop iteration does the following:
initSecContext
call.acceptSecContext
method,
passing it the token just received.acceptSecContext
returns a token, then
SampleServer sends this token to SampleClient and then starts the
next loop iteration if the context is not yet established.The tokens returned by acceptSecContext
or
received from SampleClient are placed in a byte array.
The acceptSecContext
arguments are a byte array
containing a token, the starting offset into that array of where
the token begins, and the token length.
To exchange tokens with SampleClient, SampleServer uses the
DataInputStream inStream
and DataOutputStream
outStream
it previously set up using the input and
output streams for the socket connection made with
SampleClient.
Here is the SampleServer context establishment loop:
byte[] token = null; while (!context.isEstablished()) { token = new byte[inStream.readInt()]; System.out.println("Will read input token of size " + token.length + " for processing by acceptSecContext"); inStream.readFully(token); token = context.acceptSecContext(token, 0, token.length); // Send a token to the peer if one was generated by // acceptSecContext if (token != null) { System.out.println("Will send token of size " + token.length + " from acceptSecContext."); outStream.writeInt(token.length); outStream.write(token); outStream.flush(); } } System.out.print("Context Established! "); System.out.println("Client is " + context.getSrcName()); System.out.println("Server is " + context.getTargName()); if (context.getMutualAuthState()) System.out.println("Mutual authentication took place!");
Once a security context has been established between SampleClient and SampleServer, they can use the context to securely exchange messages.
Two types of methods exist for preparing messages for secure
exchange: wrap
and getMIC
. There are
actually two wrap
methods (and two
getMIC
methods), where the differences between the
two are the indication of where the input message is (a byte
array or an input stream) and where the output should go (to a
byte array return value or to an output stream).
These methods for preparing messages for exchange, and the corresponding methods for interpretation by the peer of the resulting tokens, are described below.
The wrap
method is the primary method for message
exchanges.
The signature for the wrap
method called by
SampleClient is the following:
byte[] wrap (byte[] inBuf, int offset, interface len, MessageProp msgProp)
You pass wrap
a message (in inBuf
),
the offset into inBuf
where the message begins
(offset
), and the length of the message
(len
). You also pass a MessageProp, which is used to
indicate the desired QOP (Quality-of-Protection) and to specify
whether or not privacy (encryption) is desired. A QOP value
selects the cryptographic integrity and encryption (if requested)
algorithm(s) to be used. The algorithms corresponding to various
QOP values are specified by the provider of the underlying
mechanism. For example, the values for Kerberos V5 are defined in
RFC 1964 in
section 4.2. It is common to specify 0 as the QOP value to
request the default QOP.
The wrap
method returns a token containing the
message and a cryptographic Message Integrity Code (MIC) over it.
The message placed in the token will be encrypted if the
MessageProp indicates privacy is desired. You do not need to know
the format of the returned token; it should be treated as opaque
data. You send the returned token to your peer application, which
calls the unwrap
method to "unwrap" the token to get
the original message and to verify its integrity.
If you simply want to get a token containing a cryptographic
Message Integrity Code (MIC) for a supplied message, you call
getMIC
. A sample reason you might want to do this is
to confirm with your peer that you both have the same data, by
just transporting a MIC for that data without incurring the cost
of transporting the data itself to each other.
The signature for the getMIC
method called by
SampleServer is the following:
byte[] getMIC (byte[] inMsg, int offset, int len, MessageProp msgProp)
You pass getMIC
a message (in
inMsg
), the offset into inMsg
where the
message begins (offset
), and the length of the
message (len
). You also pass a MessageProp,
which is used to indicate the desired QOP
(Quality-of-Protection). It is common to specify 0 as the QOP
value to request the default QOP.
If you have a token created by getMIC
and the
message used to calculate the MIC (or a message purported to be
the message on which the MIC was calculated), you can call the
verifyMIC
method to verify the MIC for the message.
If the verification is successful (that is, if a GSSException is
not thrown), it proves that the message is exactly the same as it
was when the MIC was calculated. A peer receiving a message from
an application typically expects a MIC as well, so that they can
verify the MIC and be assured the message has not been modified
or corrupted in transit. Note: If you know ahead of time that you
will want the MIC as well as the message then it is more
convenient to use the wrap
and unwrap
methods. But there could be situations where the message and the
MIC are received separately.
The signature for the verifyMIC
corresponding to
the getMIC
shown above is
void verifyMIC (byte[] inToken, int tokOffset, int tokLen, byte[] inMsg, int msgOffset, int msgLen, MessageProp msgProp);
This verifies the MIC contained in the inToken
(of length tokLen
, starting at offset
tokOffset
) over the message contained in
inMsg
(of length msgLen
, starting at
offset msgOffset
). The MessageProp is used by the
underlying mechanism to return information to the caller, such as
the QOP indicating the strength of protection that was applied to
the message.
The message exchanges between SampleClient and SampleServer are summarized below, followed by the coding details.
These steps are the "standard" steps used for verifying a GSS-API client and server. A group at MIT has written a GSS-API client and a GSS-API server that have become fairly popular test programs for checking interoperability between different implementations of the GSS-API library. (These GSS-API sample applications can be downloaded as a part of the Kerberos distribution available from MIT at http://web.mit.edu/kerberos.) This client and server from MIT follow the protocol that once the context is established, the client sends a message across and it expects back the MIC on that message. If you implement a GSS-API library, it is common practice to test it by running either the client or server using your library implementation against a corresponding peer server or client that uses another GSS-API library implementation. If both library implementations conform to the standards, then the two peers will be able to communicate successfully.
One implication of testing your client or server against ones written in C (like the MIT ones) is the way tokens must be exchanged. C implementations of GSS-API do not include stream-based methods. In the absence of stream-based methods on your peer, when you write a token you must first write the number of bytes and then write the token. Similarly, when you are reading a token, you first read the number of bytes and then read the token. This is what SampleClient and SampleServer do.
Here is the summary of the SampleClient and SampleServer message exchanges:
wrap
to encrypt and calculate
a MIC for a message.wrap
to SampleServer.unwrap
to obtain the original
message and verify its integrity.getMIC
to calculate a MIC on
the decrypted message.getMIC
(which contains the MIC) to SampleClient.verifyMIC
to verify that the
MIC sent by SampleServer is a valid MIC for the original
message.The SampleClient code for encrypting a message, calculating a MIC for it, and sending the result to SampleServer is the following:
byte[] messageBytes = "Hello There!\0".getBytes(); /* * The first MessageProp argument is 0 to request * the default Quality-of-Protection. * The second argument is true to request * privacy (encryption of the message). */ MessageProp prop = new MessageProp(0, true); /* * Encrypt the data and send it across. Integrity protection * is always applied, irrespective of encryption. */ token = context.wrap(messageBytes, 0, messageBytes.length, prop); System.out.println("Will send wrap token of size " + token.length); outStream.writeInt(token.length); outStream.write(token); outStream.flush();
The following SampleServer code reads the wrapped token sent by SampleClient and "unwraps" it to obtain the original message and have its integrity verified. The unwrapping in this case includes decryption since the message was encrypted.
Note: Here the integrity check is expected to succeed. But
note that in general if an integrity check fails it signifies
that the message was changed in transit. If the
unwrap
method encounters an integrity check failure,
it throws a GSSException with major error code
GSSException.BAD_MIC.
/* * Create a MessageProp which unwrap will use to return * information such as the Quality-of-Protection that was * applied to the wrapped token, whether or not it was * encrypted, etc. Since the initial MessageProp values * are ignored, it doesn't matter what they are set to. */ MessageProp prop = new MessageProp(0, false); /* * Read the token. This uses the same token byte array * as that used during context establishment. */ token = new byte[inStream.readInt()]; System.out.println("Will read token of size " + token.length); inStream.readFully(token); byte[] bytes = context.unwrap(token, 0, token.length, prop); String str = new String(bytes); System.out.println("Received data \"" + str + "\" of length " + str.length()); System.out.println("Encryption applied: " + prop.getPrivacy());
Next, SampleServer generates a MIC for the decrypted message and sends it to SampleClient. This is not really necessary but simply illustrates generating a MIC on the decrypted message, which should be exactly the same as the original message SampleClient wrapped and sent to SampleServer. When SampleServer generates this and sends it to SampleClient, and SampleClient verifies it, this proves to SampleClient that the decrypted message SampleServer has is in fact exactly the same as the original message from SampleClient.
/* * First reset the QOP of the MessageProp to 0 * to ensure the default Quality-of-Protection * is applied. */ prop.setQOP(0); token = context.getMIC(bytes, 0, bytes.length, prop); System.out.println("Will send MIC token of size " + token.length); outStream.writeInt(token.length); outStream.write(token); outStream.flush();
The following SampleClient code reads the MIC calculated by SampleServer on the decrypted message and then verifies that the MIC is a MIC for the original message, proving that the decrypted message SampleServer has is the same as the original message:
token = new byte[inStream.readInt()]; System.out.println("Will read token of size " + token.length); inStream.readFully(token); /* * Recall messageBytes is the byte array containing * the original message and prop is the MessageProp * already instantiated by SampleClient. */ context.verifyMIC(token, 0, token.length, messageBytes, 0, messageBytes.length, prop); System.out.println("Verified received MIC for message.");
socket.close(); context.dispose();
Since the underlying authentication and secure communication technology used by this tutorial is Kerberos V5, we use Kerberos-style principal names wherever a user or service is called for.
For example, when you run SampleClient you are asked to provide your user name. Your Kerberos-style user name is simply the user name you were assigned for Kerberos authentication. It consists of a base user name (like "mjones") followed by an "@" and your realm (like "mjones@KRBNT-OPERATIONS.EXAMPLE.COM").
A server program like SampleServer is typically considered to offer a "service" and to be run on behalf of a particular "service principal." A service principal name for SampleServer is needed in several places:
Throughout this document, and in the accompanying login configuration file,
service_principal@your_realmis used as a placeholder to be replaced by the actual name to be used in your environment. Any Kerberos principal can actually be used for the service principal name. So for the purposes of trying out this tutorial, you could use your user name as both the client user name and the service principal name.
In a production environment, system administrators typically like servers to be run as specific principals only and may assign a particular name to be used. Often the Kerberos-style service principal name assigned is of the form
service_name/machine_name@realm;
For example, an nfs service run on a machine named "raven" in the realm named "KRBNT-OPERATIONS.EXAMPLE.COM" could have the service principal name
nfs/raven@KRBNT-OPERATIONS.EXAMPLE.COM
Such multi-component names are not required, however.
Single-component names, just like those of user principals, can
be used. For example, an installation might use the same ftp
service principal ftp@realm
for all ftp servers in
that realm, while another installation might have different ftp
principals for different ftp servers, such as
ftp/host1@realm
and ftp/host2@realm
on
machines host1
and host2
,
respectively.
If the realm of a user or service principal name is the default realm (see Kerberos Requirements), you can leave off the realm when you are logging into Kerberos (that is, when you are prompted for your username). Thus, for example, if your user name is "mjones@KRBNT-OPERATIONS.EXAMPLE.COM", and you run SampleClient, when it requests your user name you could just specify "mjones", leaving off the realm. The name is interpreted in the context of being a Kerberos principal name and the default realm is appended, as needed.
You can also leave off the realm if a principal name will be
converted to a GSSName by a GSSManager createName
method. For example, when you run SampleClient, one of the
arguments is the server service principal name. You can specify
the name without including the realm, because SampleClient passes
the name to such a createName
method, which appends
the default realm as needed.
It is recommended that you always include realms when principal names are used in login configuration files and policy files, because the behavior of the parsers for such files may be implementation-dependent; they may or may not append the default realm before such names are utilized and subsequent actions may fail if there is no realm in the name.
For this tutorial, we are letting the underlying Kerberos mechanism obtain credentials of the users running SampleClient and SampleServer, rather than invoking JAAS methods directly (as in the JAAS Authentication and JAAS Authorization tutorials) or indirectly (for example, via the Login utility described in the Use of JAAS Login Utility tutorial and in the Use of JAAS Login Utility and Java GSS-API for Secure Message Exchanges tutorial).
The default Kerberos mechanism implementation supplied by Sun Microsystems actually prompts for a Kerberos name and password and authenticates the specified user (or service) to the Kerberos KDC. The mechanism relies on JAAS to perform this authentication.
JAAS supports a pluggable authentication framework, meaning that any type of authentication module can be plugged under a calling application. A login configuration specifies the login module to be used for a particular application. The default JAAS implementation from Sun Microsystems requires that the login configuration information be specified in a file. (Note: Some other vendors might not have file-based implementations.) See JAAS Login Configuration File for information as to what a login configuration file is, what it contains, and how to specify which login configuration file should be used.
For this tutorial, the Kerberos login module
com.sun.security.auth.module.Krb5LoginModule
is
specified in the configuration file. This login module prompts
for a Kerberos name and password and attempts to authenticate to
the Kerberos KDC.
Both SampleClient and SampleServer can use the same login configuration file, if that file contains two entries, one entry for the client side and one for the server side.
The bcsLogin.conf login configuration file used for this tutorial is the following:
com.sun.security.jgss.initiate { com.sun.security.auth.module.Krb5LoginModule required; }; com.sun.security.jgss.accept { com.sun.security.auth.module.Krb5LoginModule required storeKey=true };
Entries with these two names
(com.sun.security.jgss.initiate
and
com.sun.security.jgss.accept
) are used by Sun
implementations of GSS-API mechanisms when they need new
credentials. Since the mechanism used in this tutorial is the
Kerberos V5 mechanism, a Kerberos login module will need to be
invoked in order to obtain these credentials. Thus we list
Krb5LoginModule as a required module in these entries. The
com.sun.security.jgss.initiate
entry specifies the
configuration for the client side and the
com.sun.security.jgss.accept
entry for the server
side.
The Krb5LoginModule succeeds only if the attempt to log in to the Kerberos KDC as a specified entity is successful. When running SampleClient or SampleServer, the user will be prompted for a name and password.
The SampleServer entry storeKey=true
indicates that a secret key should be calculated from the
password provided during login and it should be stored in the
private credentials of the Subject created as a result of login.
This key is subsequently utilized during mutual authentication
when establishing a security context between SampleClient and
SampleServer.
For information about all the possible options that can be passed to Krb5LoginModule, see the Krb5LoginModule documentation.
For this tutorial, we set the system property
javax.security.auth.useSubjectCredsOnly
to
false
, which allows us to relax the usual
restriction of requiring a GSS mechanism to obtain necessary
credentials from an existing Subject,
set up by JAAS. When this restriction is relaxed, it allows the
mechanism to obtain credentials from some vendor-specific
location. For example, some vendors might choose to use the
operating system's cache if one exists, while others might choose
to read from a protected file on disk.
When this restriction is relaxed, Sun Microsystem's Kerberos
mechanism still looks for the credentials in the Subject
associated with the thread's access control context, but if it
doesn't find any there, it performs JAAS authentication using a
Kerberos module to obtain new ones. The Kerberos module prompts
you for a Kerberos principal name and password. Note that
Kerberos mechanism implementations from other vendors may behave
differently when this property is set to false
.
Consult their documentation to determine their implementation's
behavior.
To execute the SampleClient and SampleServer programs, do the following:
To prepare SampleServer for execution, do the following:
SampleServer.java
:
javac SampleServer.java
To prepare SampleClient for execution, do the following:
SampleClient.java
:
javac SampleClient.java
It is important to execute SampleServer before SampleClient because SampleClient will try to make a socket connection to SampleServer and that will fail if SampleServer is not yet running and accepting socket connections.
To execute SampleServer, be sure to run it on the machine it is expected to be run on. This machine name (host name) is specified as an argument to SampleClient. The service principal name appears in several places, including the login configuration file and the policy files.
Go to the directory in which you have prepared SampleServer for execution. Execute SampleServer, specifying
-Djava.security.krb5.realm=<your_realm>
that your Kerberos realm is the one specified. For example, if
your realm is "KRBNT-OPERATIONS.EXAMPLE.COM" you'd put
-Djava.security.krb5.realm=KRBNT-OPERATIONS.EXAMPLE.COM
.-Djava.security.krb5.kdc=<your_kdc>
that your Kerberos KDC is the one specified. For example, if your
KDC is "samplekdc.example.com" you'd put
-Djava.security.krb5.kdc=samplekdc.example.com
.-Djavax.security.auth.useSubjectCredsOnly=false
that
the underlying mechanism can decide how to get credentials. See
The useSubjectCredsOnly System
Property.-Djava.security.auth.login.config=bcsLogin.conf
that
the login configuration file to be used is
bcsLogin.conf
.The only argument required by SampleServer is one specifying the port number to be used for listening for client connections. Choose a high port number unlikely to be used for anything else. An example would be something like 4444.
Below is the full command to use for both Microsoft Windows and Unix systems.
Important: In this command, you must replace
<port_number>
with an appropriate port number,
<your_realm>
with your Kerberos realm, and
<your_kdc>
with your Kerberos KDC.
Here is the command:
java -Djava.security.krb5.realm=<your_realm> -Djava.security.krb5.kdc=<your_kdc> -Djavax.security.auth.useSubjectCredsOnly=false -Djava.security.auth.login.config=bcsLogin.conf SampleServer <port_number>
The full command should appear on one line (or, on UNIX systems, on multiple lines where each line but the last is terminated with " \" indicating that there is more to come). Multiple lines are used here just for legibility. Since this command is very long, you may need to place it in a .bat file (for Windows) or a .sh file (for UNIX) and then run that file to execute the command.
The SampleServer
code will listen for socket
connections on the specified port. When prompted, type the
Kerberos name and password for the service principal. The
underlying Kerberos authentication mechanism specified in the
login configuration file will log the service principal into
Kerberos.
For login troubleshooting suggestions, see Troubleshooting.
To execute SampleClient, first go to the directory in which you have prepared SampleClient for execution. Execute SampleClient, specifying
-Djava.security.krb5.realm=<your_realm>
that your Kerberos realm is the one specified.-Djava.security.krb5.kdc=<your_kdc>
that your Kerberos KDC is the one specified.-Djavax.security.auth.useSubjectCredsOnly=false
that
the underlying mechanism can decide how to get credentials.-Djava.security.auth.login.config=bcsLogin.conf
that
the login configuration file to be used is
bcsLogin.conf
.The SampleClient arguments are (1) the Kerberos name of the service principal that represents SampleServer, (2) the name of the host (machine) on which SampleServer is running, and (3) the port number on which SampleServer is listening for client connections.
Below is the full command to use for both Windows and Unix systems.
Important: In this command, you must replace
<service_principal>
,
<host>
, <port_number>
,
<your_realm>
, and
<your_kdc>
with appropriate values (and
note that the port number must be the same as the port number
passed as an argument to SampleServer). These values need not be
placed in quotes.
Here is the command:
java -Djava.security.krb5.realm=<your_realm> -Djava.security.krb5.kdc=<your_kdc> -Djavax.security.auth.useSubjectCredsOnly=false -Djava.security.auth.login.config=bcsLogin.conf SampleClient <service_principal> <host> <port_number>
Type the full command on one line. Multiple lines are used here for legibility. As with the command for executing SampleServer, if the command is too long to type directly into your command window, place it in a .bat file (Windows) or a .sh file (UNIX) and then execute that file.
When prompted, type your Kerberos user name and password. The underlying Kerberos authentication mechanism specified in the login configuration file will log you into Kerberos. The SampleClient code requests a socket connection with SampleServer. Once SampleServer accepts the connection, SampleClient and SampleServer establish a shared context and then exchange messages as described in this tutorial.
For login troubleshooting suggestions, see Troubleshooting.