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.