Use of JavaTM 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. Kerberos tickets can
optionally include the host address and IPv4 and IPv6 host addresses
are both supported.
Java GSS-API requires that the mechanism obtain these credentials
from the Subject associated with the thread's access control context.
To populate a Subject with such credentials, client and server
applications typically will first perform JAAS authentication using a
Kerberos module. The JAAS Authentication tutorial
demonstrates how to do this. The
JAAS Authorization tutorial then demonstrates
how to associate the authenticated Subject with the thread's access
control context. A utility has also been written
as a convenience to automatically perform those operations
on your behalf. The Use of JAAS Login Utility
tutorial demonstrates how to use the Login utility.
For this tutorial, we will not have the client and server perform
JAAS authentication, nor will we have them use the Login utility.
Instead, we will rely on setting the system property
javax.security.auth.useSubjectCredsOnly to false,
which allows us to relax the restriction of requiring a GSS mechanism to obtain
necessary credentials from an existing
Subject, set up by JAAS.
See The useSubjectCredsOnly System Property.
Note: This is a simplified introductory tutorial.
For example, we do not include any policy files or run the sample code
using a security manager. In real life, code using Java GSS-API
should be run with a security manager, so that security-sensitive
operations would not be allowed unless the required permissions were
explicitly granted.
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.
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 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:
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.
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 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:
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:
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:
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:
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 the wrap method requests privacy.
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
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).
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.
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
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:
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 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.
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 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();
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:
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,
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 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:
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:
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 is bcsLogin.conf.
The only argument required by SampleServer is one
specifying the port number to be used for listening for client
connections. Choose a high port number unlikely to be used for anything else.
An example would be something like 4444.
Below is the full command to use for both Microsoft Windows and Unix systems.
Important: In this command, you must replace
<port_number> with an appropriate port number,
<your_realm> with your Kerberos realm,
and <your_kdc> with your Kerberos KDC.
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.
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.
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.