Tutorials Introduction and TOC Next Tutorial Feedback

Use of Java GSS-API for Secure Message Exchanges Without JAAS Programming



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. 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.


Overview of the Client And Server Applications

The applications for this tutorial are named SampleClient and SampleServer.

Here is a summary of execution of the SampleClient and SampleServer applications:

  1. Run the SampleServer application. SampleServer

    1. Reads its argument, the port number that it should listen on for client connections.

    2. Creates a ServerSocket for listening for client connections on that port.

    3. Listens for a connection.

  2. Run the SampleClient application (possibly on a different machine). SampleClient

    1. Reads its arguments: (1) The name of the Kerberos principal that represents SampleServer. (See Kerberos User and Service Principal Names.), (2) the name of the host (machine) on which SampleServer is running, and (3) the port number on which SampleServer listens for client connections.

    2. Attempts a socket connection with the SampleServer, using the host and port it was passed as arguments.

  3. The socket connection is accepted by SampleServer and both applications initialize a DataInputStream and a DataOutputStream from the socket input and output streams, to be used for future data exchanges.

  4. SampleClient and SampleServer each instantiate a GSSContext and follow a protocol for establishing a shared context that will enable subsequent secure data exchanges.

  5. SampleClient and SampleServer can now securely exchange messages.

  6. When SampleClient and SampleServer are done exchanging messages, they perform clean-up operations.

The actual code and further details are presented in the following sections.

The SampleClient and SampleServer Code

The entire code for both the SampleClient and SampleServer programs resides in their main methods and can be broken down into the following subparts:

  1. Obtain the Command-Line Arguments

  2. Establish a Socket Connection for Transfers Between SampleClient and SampleServer

  3. Establish a Security Context

  4. Securely Exchange Messages

  5. Clean Up

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.

Obtaining the Command-Line Arguments

The first thing both our client and server main methods do is read the command-line arguments.

Arguments Read By SampleClient

SampleClient expects three arguments:

  1. A service principal name -- The name of the Kerberos principal that represents SampleServer. (See Kerberos User and Service Principal Names.)

  2. A host name -- The machine on which SampleServer is running.

  3. A port number -- The port number of the port on which SampleServer listens for connections.

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]);

Argument Read By SampleServer

SampleServer expects just one argument:

  • A local port number -- The port number used by SampleServer for listening for connections with clients. This number should be the same as the port number specified when running the SampleClient program.

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]);

Establishing a Socket Connection for Message Exchanges

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 Code For Socket Connection

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());

SampleServer Code For Socket Connection

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>;
}

Establishing a Security Context

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:

Context Establishment by SampleClient

In our client/server scenario, SampleClient is the context initiator. Here are the basic steps it takes to establish a security context. It

  1. Instantiates a GSSContext.

  2. Sets the desired optional features on the context.

  3. Loops while the context is not yet established, each time calling initSecContext, sending any returned token to SampleServer, and receiving a token (if any) from SampleServer.

SampleClient GSSContext Instantiation

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.

The GSSName peer Argument

The 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);

The Oid mech Argument

The 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.

The GSSCredential myCred Argument

The 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.

The int lifetime Argument

The 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.

The Complete createContext Call

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);

SampleClient Setting of Desired Options

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:

  1. Mutual authentication. The context initiator is always authenticated to the acceptor. If the initiator requests mutual authentication, then the acceptor is also authenticated to the initiator.

  2. Confidentiality. Requesting confidentiality means that you request the enabling of encryption for the context method named wrap. Encryption is actually used only if the MessageProp object passed to the wrap method requests privacy.

  3. Integrity. This requests integrity for the 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.

SampleClient Context Establishment Loop

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

  1. Calls the context's 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).

  2. Sends the token returned by initSecContext (if any) to SampleServer. The first call to initSecContext always produces a token. The last call might not return a token.

  3. Checks to see if the context is established. If not, SampleClient receives another token from SampleServer and then starts the next loop iteration.

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!");

Context Establishment by SampleServer

In our client/server scenario, SampleServer is the context acceptor. Here are the basic steps it takes to establish a security context. It

  1. Instantiates a GSSContext.

  2. Loops while the context is not yet established, each time receiving a token from SampleClient, calling acceptSecContext and passing it the token, and sending any returned token to SampleClient.

SampleServer GSSContext Instantiation

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);

SampleServer Context Establishment Loop

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:

  1. Receives a token from SampleClient. This token is the result of a SampleClient initSecContext call.

  2. Calls the context's acceptSecContext method, passing it the token just received.

  3. If 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!");

Exchanging Messages Securely

Once a security context has been established between SampleClient and SampleServer, they can use the context to securely exchange messages.

GSSContext Methods for Message Exchange

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.

wrap

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.

getMIC

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 SampleClient and SampleServer Message Exchanges

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 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:

  1. SampleClient calls wrap to encrypt and calculate a MIC for a message.

  2. SampleClient sends the token returned from wrap to SampleServer.

  3. SampleServer calls unwrap to obtain the original message and verify its integrity.

  4. SampleServer calls getMIC to calculate a MIC on the decrypted message.

  5. SampleServer sends the token returned by getMIC (which contains the MIC) to SampleClient.

  6. SampleClient calls verifyMIC to verify that the MIC sent by SampleServer is a valid MIC for the original message.

SampleClient Code to Encrypt the Message and Send It

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();

SampleServer Code to Unwrap Token, Calculate MIC, and Send It

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();

SampleClient Code to Verify the MIC

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.");

Clean Up

When SampleClient and SampleServer have finished exchanging messages, they need to perform cleanup operations. Both contain the following code to

socket.close();
context.dispose();

Kerberos User and Service Principal Names

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.ABC.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_realm
is 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.ABC.COM" could have the service principal name

nfs/raven@KRBNT-OPERATIONS.ABC.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.

When the Realm is Required in Principal Names

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.ABC.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.

The Login Configuration File

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.

The useSubjectCredsOnly System Property

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.

Running the SampleClient and SampleServer Programs

To execute the SampleClient and SampleServer programs, do the following:

Prepare SampleServer for Execution

To prepare SampleServer for execution, do the following:

  1. Copy the following files into a directory accessible by the machine on which you will run SampleServer:

  2. Compile SampleServer.java:
    javac SampleServer.java
    

Prepare SampleClient for Execution

To prepare SampleClient for execution, do the following:

  1. Copy the following files into a directory accessible by the machine on which you will run SampleClient:

  2. Compile SampleClient.java:
    javac SampleClient.java
    

Execute SampleServer

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

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, 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.

Execute SampleClient

To execute SampleClient, first go to the directory in which you have prepared SampleClient for execution. Execute SampleClient, specifying

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, 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.



Tutorials Introduction and TOC Next Tutorial Feedback