Feedback |
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.
The applications for this tutorial are named SampleClient and SampleServer.
Here is a summary of execution of the SampleClient and SampleServer applications:
- Run the SampleServer application. SampleServer
- Reads its argument, the port number that it should listen on for client connections.
- Creates a ServerSocket for listening for client connections on that port.
- Listens for a connection.
- Run the SampleClient application (possibly on a different machine). SampleClient
- 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.
- Attempts a socket connection with the SampleServer, using the host and port it was passed as arguments.
- 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.
- SampleClient and SampleServer each instantiate a GSSContext and follow a protocol for establishing a shared context that will enable subsequent secure data exchanges.
- SampleClient and SampleServer can now securely exchange messages.
- 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 entire code for both the SampleClient and SampleServer programs resides in their
main
methods and can be broken down into the following subparts:
- Obtain the Command-Line Arguments
- Establish a Socket Connection for Transfers Between SampleClient and SampleServer
- Establish a Security Context
- Securely Exchange Messages
- 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:
- A service principal name -- The name of the Kerberos principal that represents SampleServer. (See Kerberos User and Service Principal Names.)
- A host name -- The machine on which SampleServer is running.
- 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 viaSocket 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'sacceptSecContext
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 callsacceptSecContext
, passing it the token. TheacceptSecContext
method may in turn return a token. If it does, the acceptor should send that token to the initiator, which should then callinitSecContext
again and pass it this token. Each timeinitSecContext
oracceptSecContext
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
orinitSecContext
). This continues until the context is fully established (which is the case when the context'sisEstablished
method returnstrue
).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
- Instantiates a GSSContext.
- Sets the desired optional features on the context.
- 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
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 namedserver
. The GSSManagermanager
is used to instantiate a GSSName by calling one of itscreateName
methods. SampleClient calls thecreateName
method with the following signature:GSSName createName(String nameStr, Oid nameType);SampleClient passes the
server
String for thenameStr
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 theserver
String; it is in the appropriate format for a Kerberos Version 5 name. Thus, SampleClient passes anull
for the Oid. Here is the call:GSSName serverName = manager.createName(server, null);The
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 tocreateContext
.The
GSSCredential myCred
ArgumentThe third argument to the GSSManager
createContext
method is a GSSCredential representing the caller's credentials. If you passnull
for this argument, as SampleClient does, the default credentials are used.The
int lifetime
ArgumentThe final argument to the GSSManager
createContext
method is anint
specifying the desired lifetime, in seconds, for the context that is created. SampleClient passesGSSContext.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. Mostrequest
methods take aboolean
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 theget
methods.SampleClient requests the following:
- 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.
- 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 thewrap
method requests privacy.
- Integrity. This requests integrity for the
wrap
andgetMIC
methods. When integrity is requested, a cryptographic tag known as a Message Integrity Code (MIC) will be generated when calling those methods. WhengetMIC
is called, the generated MIC appears in the returned token. Whenwrap
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 laterNote: 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
- Calls the context's
initSecContext
method. If this is the first call, the method is passed anull
token. Otherwise, it is passed the token most recently sent to SampleClient by SampleServer (a token generated by a SampleServer call toacceptSecContext
).
- Sends the token returned by
initSecContext
(if any) to SampleServer. The first call toinitSecContext
always produces a token. The last call might not return a token.
- 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 DataOutputStreamoutStream
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
- Instantiates a GSSContext.
- 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:
- Receives a token from SampleClient. This token is the result of a SampleClient
initSecContext
call.
- Calls the context's
acceptSecContext
method, passing it the token just received.
- 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 DataOutputStreamoutStream
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
andgetMIC
. There are actually twowrap
methods (and twogetMIC
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 (ininBuf
), the offset intoinBuf
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 theunwrap
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 (ininMsg
), the offset intoinMsg
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 theverifyMIC
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 thewrap
andunwrap
methods. But there could be situations where the message and the MIC are received separately.The signature for the
verifyMIC
corresponding to thegetMIC
shown above isvoid verifyMIC (byte[] inToken, int tokOffset, int tokLen, byte[] inMsg, int msgOffset, int msgLen, MessageProp msgProp);This verifies the MIC contained in the
inToken
(of lengthtokLen
, starting at offsettokOffset
) over the message contained ininMsg
(of lengthmsgLen
, starting at offsetmsgOffset
). 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 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:
- SampleClient calls
wrap
to encrypt and calculate a MIC for a message.
- SampleClient sends the token returned from
wrap
to SampleServer.
- SampleServer calls
unwrap
to obtain the original message and verify its integrity.
- SampleServer calls
getMIC
to calculate a MIC on the decrypted message.
- SampleServer sends the token returned by
getMIC
(which contains the MIC) to SampleClient.
- 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
- close the socket connection and
- release system resources and cryptographic information stored in the context object and then invalidate the context.
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.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:
- When you run SampleServer, and SampleClient attempts a connection to it, the underlying Kerberos mechanism will attempt to authenticate to the Kerberos KDC. It prompts you to log in. You should log in as the appropriate service principal.
- When you run SampleClient, one of the arguments is the service principal name. This is needed so SampleClient can initiate establishment of a security context with the appropriate service.
- If the SampleClient and SampleServer programs were run with a security manager (they're not for this tutorial), the client and server policy files would each require a ServicePermission with name equal to the service principal name and action equal to "initiate" or "accept" (for initiating or accepting establishment of a security context).
Throughout this document, and in the accompanying login configuration file,
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.service_principal@your_realmIn 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.COMSuch 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 asftp/host1@realm
andftp/host2@realm
on machineshost1
andhost2
, 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 acreateName
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
andcom.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. Thecom.sun.security.jgss.initiate
entry specifies the configuration for the client side and thecom.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
tofalse
, 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:
- Prepare SampleServer for Execution
- Prepare SampleClient for Execution
- Execute SampleServer
- Execute SampleClient
Prepare SampleServer for Execution
To prepare SampleServer for execution, do the following:
- Copy the following files into a directory accessible by the machine on which you will run SampleServer:
- The SampleServer.java source file.
- The bcsLogin.conf login configuration file.
- Compile
SampleServer.java
:javac SampleServer.javaPrepare SampleClient for Execution
To prepare SampleClient for execution, do the following:
- Copy the following files into a directory accessible by the machine on which you will run SampleClient:
- The SampleClient.java source file.
- The bcsLogin.conf login configuration file.
- Compile
SampleClient.java
:javac SampleClient.javaExecute 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
- by
-Djava.security.krb5.realm=<your_realm>
that your Kerberos realm is the one specified. For example, if your realm is "KRBNT-OPERATIONS.ABC.COM" you'd put-Djava.security.krb5.realm=KRBNT-OPERATIONS.ABC.COM
.
- by
-Djava.security.krb5.kdc=<your_kdc>
that your Kerberos KDC is the one specified. For example, if your KDC is "samplekdc.abc.com" you'd put-Djava.security.krb5.kdc=samplekdc.abc.com
.
- by
-Djavax.security.auth.useSubjectCredsOnly=false
that the underlying mechanism can decide how to get credentials. See The useSubjectCredsOnly System Property.
- by
-Djava.security.auth.login.config=bcsLogin.conf
that the login configuration file to be used isbcsLogin.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.
Execute SampleClient
To execute SampleClient, first go to the directory in which you have prepared SampleClient for execution. Execute SampleClient, specifying
- by
-Djava.security.krb5.realm=<your_realm>
that your Kerberos realm is the one specified.
- by
-Djava.security.krb5.kdc=<your_kdc>
that your Kerberos KDC is the one specified.
- by
-Djavax.security.auth.useSubjectCredsOnly=false
that the underlying mechanism can decide how to get credentials.
- by
-Djava.security.auth.login.config=bcsLogin.conf
that the login configuration file to be used isbcsLogin.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.
Feedback |