|
|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| Set up the server, request a ticket, retrieve the
response
Faheem
Khan (mailto:fkhan872@yahoo.com?cc=&subject=Establish
secure communication with an e-bank) Freelance Consultant 18
February 2004
If you have participated in the first two lessons in this
series, you are now ready for the third and final project in which
you'll set up a KDC server, send a Kerberos ticket request to it, and
fetch its response. Of course, you'll then study the low level ASN1
processing methods required to process the KDC server's response in
order to fetch the ticket and the session key. Once you have the service
ticket, you'll send a request to the e-bank's business logic server to
establish a secure context. Finally, you'll see the actual secure
communication with the e-bank's business logic server.
As a refresher, the first article of this
series introduced a mobile banking MIDlet application and explained how
Kerberos can fulfill the security requirements of such an application. The
article also described the data formats that Kerberos uses to provide
security.
The second article of the
series demonstrated how to author ASN.1 data types in J2ME. You learned
how to use the Bouncy Castle cryptographic library for DES encryption and
for generating a secret Kerberos key from the user's password. You ended
by putting some of the pieces together and authoring a request for a
Kerberos ticket.
The Kerberos client that you are developing in this series of articles
does not require any particular Kerberos server; it will work with any KDC
implementation. The resources section contains links to some KDC servers
that you can use with the Kerberos client.
Whichever KDC server you choose, you must tell the server that the
users of your mobile banking MIDlet need not send pre-authentication data
(padata , the third field of the KDC-REQ
structure shown in Figure 2 of the first
article of this series), along with the request for a
TGT .
Sending the padata field is optional according to the
Kerberos specification. Therefore, KDC servers normally allow configuring
particular users so that the KDC server accepts TGT requests
without the padata field from the configured users. In an
attempt to reduce the processing burden on the Kerberos client, you must
tell the KDC server to accept TGT requests from e-bank's
mobile users without padata .
In this example, I used Microsoft's KDC server to try the J2ME-based
mobile banking application. The readme.txt file in the source code download of
this article contains instructions on how to set up the KDC server as well
as how to tell it to accept TGT requests without the
padata field. (I used this same KDC server to demonstrate
Single Sign-on in my article "Simplify enterprise Java authentication with
single sign-on." See Resources for a link.)
Sending the TGT request to the KDC
server After the KDC server is set up, you send the
TGT request to it. Have a look at the
getTicketResponse() method of Listing 1. It is the
same as the getTicketResponse() method of Listing 12 in the second
article of this series, with one difference: The method now includes
the J2ME code for sending the TGT request to the KDC server.
The new code is marked in Listing 1 so you can
track the new additions that were not in Listing 12.
In the NEW CODE section of Listing 1, I have
created a new Datagram object (dg ) over an existing
DatagramConnection object (dc ). Note that in the
last section of this article, the mobile banking MIDlet creates the
dc object that I am using here to create a
Datagram object.
After creating the dg object, the
getTicketResponse() method has called its send()
method to send the ticket request to the KDC server.
After sending the TGT request to the server, the
getTicketResponse() method of Listing 1 receives the
TGT response from the server. Upon receipt, it returns the
response to the calling application. Listing 1.
The getTicketResponse() method
public byte[] getTicketResponse( )
{
byte ticketRequest[];
byte msg_type[];
byte pvno[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, getIntegerBytes(5));
msg_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
2, getIntegerBytes(10));
byte kdc_options[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getBitStringBytes(new byte[5]));
byte generalStringSequence[] = getSequenceBytes (
getGeneralStringBytes (userName));
byte name_string[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, generalStringSequence);
byte name_type[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(ASN1DataTypes.NT_PRINCIPAL));
byte principalNameSequence [] = getSequenceBytes(
concatenateBytes (name_type, name_string));
byte cname[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,
1, principalNameSequence);
byte realm[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,
2, getGeneralStringBytes (realmName));
byte sgeneralStringSequence[] =
concatenateBytes(getGeneralStringBytes(kdcServiceName),
getGeneralStringBytes (realmName));
byte sname_string[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, getSequenceBytes(sgeneralStringSequence));
byte sname_type[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(ASN1DataTypes.NT_UNKNOWN));
byte sprincipalNameSequence [] = getSequenceBytes
(concatenateBytes (sname_type, sname_string));
byte sname[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,
3, sprincipalNameSequence);
byte till[] = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
5,
getGeneralizedTimeBytes (
new String("19700101000000Z").getBytes()));
byte nonce[] = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
7,
getIntegerBytes (getRandomNumber()));
byte etype[] = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
8,
getSequenceBytes(getIntegerBytes(3)));
byte req_body[] = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
4,
getSequenceBytes(
concatenateBytes(
kdc_options,
concatenateBytes(
cname,
concatenateBytes(
realm,
concatenateBytes(
sname,
concatenateBytes(
till,
concatenateBytes
(nonce, etype)
)
)
)
)
)
)
);
ticketRequest = getTagAndLengthBytes(
ASN1DataTypes.APPLICATION_TYPE,
10,
getSequenceBytes(
concatenateBytes(
pvno,
concatenateBytes
(msg_type, req_body)
)
)
);
/****** NEW CODE BEGINS ******/
try {
Datagram dg = dc.newDatagram(ticketRequest, ticketRequest.length);
dc.send(dg);
} catch (IllegalArgumentException il) {
il.printStackTrace();
}
catch (Exception io) {
io.printStackTrace();
}
byte ticketResponse[] = null;
try
{
Datagram dg = dc.newDatagram(700);
dc.receive(dg);
if (dg.getLength() > 0) {
ticketResponse = new byte[dg.getLength()];
System.arraycopy(dg.getData(), 0, ticketResponse, 0, dg.getLength());
} else
return null;
} catch (IOException ie){
ie.printStackTrace();
}
/****** NEW CODE ENDS ******/
return ticketResponse;
}//getTicketResponse
|
Processing a TGT
response You have received the TGT response
from the KDC . Now it is time to process the response to
extract the ticket and the session key from the
response.
Naturally, response processing includes some low level ASN.1 processing
(just like the low level ASN.1 authoring that you encountered while ticket
request authoring in the second article of this series). So I'll implement
and explain some low level ASN.1 processing methods as well as some low
level cryptographic support methods before starting to demonstrate how to
use the low level processing methods to extract the ticket
and the session key from the ticket response.
As before, the low-level ASN1 processing methods go in the
ASN1DataTypes class. The following methods are in the
ASN1DataTypes.java file in the source code download of
this article:
- isSequence()
- getIntegerValue()
- isASN1Structure()
- getNumberOfLengthBytes()
- getLength()
- getASN1Structure()
- getContents()
Following is an explanation of each of the low-level ASN.1 processing
methods listed above.
isSequence() The
isSequence() method shown in Listing 2 takes a single
byte as a parameter and checks whether the byte
is an ASN.1 SEQUENCE byte. If the byte value
represents a SEQUENCE it returns true, otherwise it returns a
false. Listing 2. The isSequence() method
public boolean isSequence(byte tagByte)
{
if (tagByte == (byte)0x30)
return true;
else
return false;
}//isSequence
|
getIntegerValue() The
getIntegerValue() method shown in Listing 3 takes just one
input parameter, which is a byte array representing the
contents of an ASN.1 INTEGER data type. It converts the input
byte array into a J2ME int data type and returns
the J2ME int . You need this method whenever you have
extracted the content bytes from an ASN.1 INTEGER and want to
know what integer value it represents. You also need this
method to convert length bytes into a J2ME int .
Note that the getIntegerValue() method is only designed to
handle positive integer values.
ASN.1 stores a positive INTEGER in the
most-significant-byte-first sequence. For example, the ASN.1
representation of 511 decimal is 0x01 0xFF . You
can write the complete bit representation of a decimal value (for
511 it is 1 11111111 ), then write the
hex value for each byte (for 511 it is
0x01, 0xFF ), and finally write the hex values as
most-significant-byte-first sequence.
On the other hand, an int in J2ME is always four bytes
long and the least significant byte occupies the right-most
position. The empty positions in positive integer values are
filled with zeros. For example, if you are to write a J2ME
int for 511 , it is 0x00 0x00 0x01
0xFF .
This means that when you are converting an ASN.1 INTEGER
into a positive J2ME int , you must correctly position every
byte of the input array to its correct position in the output
J2ME int .
For example, if the input bytes array contains two bytes of data
(0x01, 0xFF) , you must position the bytes in the output
int like so:
- You must write
0x00 in the left-most or most
significant position of the output int .
- Similarly, you must write
0x00 in the position adjacent
to the most significant byte of the output
int .
- The first byte
(0x01) of the input array goes into the
position adjacent to the least significant position of the output
int .
- The second byte
(0xFF) of the input array goes into the
least significant or right-most position of the output int .
The for loop in the getIntegerValue() method
calculates the correct position of every byte and then copies
the byte into its appropriate position.
Also note that just as a J2ME int is always four bytes,
the getIntegerValue() method only works for up to four
byte integer values. The humble J2ME-based Kerberos client
does not need to handle larger values. Listing 3.
The getIntegerValue() method
public int getIntegerValue(byte[] intValueAsBytes)
{
int intValue = 0;
int i = intValueAsBytes.length;
for (int y = 0; y < i; y++)
intValue |= ((int)intValueAsBytes[y] & 0xff) << ((i-(y+1)) * 8);
return intValue;
}//getIntegerValue()
|
isASN1Structure() The
isASN1Structure() method shown in Listing 4 analyzes
whether an input byte represents the tag byte (the first byte) of a
particular type of ASN.1 structure (that is, context specific,
application level, or universal type ) with a particular tag number.
The method takes three parameters. The first parameter
(tagByte ) is the input byte that you want to
analyze. The second and third parameters (tagType and
tagNumber ), respectively, represent the tag type and tag
number that you are searching for.
To check whether the tagByte is the required type of tag
with the required number, the isASN1Structure() method first
constructs a new temporary tag byte (tempTagByte ) using the
tagType and tagNumber parameters and then
compares tempTagByte with tagByte . If they are
the same, the method returns true; if not, it returns false. Listing 4. The isASN1Structure() method
public boolean isASN1Structure (byte tagByte, int tagType, int tagNumber)
{
byte tempTagByte = (byte) (tagType + tagNumber);
if (tagByte == tempTagByte)
return true;
else
return false;
}//isASN1Structure
|
getNumberOfLengthBytes() The
getNumberOfLengthBytes() method shown in Listing 5 takes one
parameter (firstLengthByte ) as a parameter. The
firstLengthByte parameter is the first length byte of an
ASN.1 structure. The getNumberOfLengthBytes() method
processes the first length byte to calculate the number of length bytes in
the ASN.1 structure. This is a utility method that other methods in the
ASN1DataTypes class use whenever there is a need to know the
number of length bytes of an ASN.1 structure.
The implementation strategy for the
getNumberOfLengthBytes() method in Listing 5 is as follows:
- Check if the most significant bit (bit 8) of
firstLengthByte is zero. The if (
(firstLengthByte)& (1<<8)==0) line in Listing
5 performs this task.
- In case the most significant bit is zero, the length bytes follow
the
single-byte length notation. Recall from part
1 of this series that there are two length notations -- the
single-byte and the multi-byte . There is
always one length byte in a single-byte length notation.
Therefore, in case the most significant bit is zero, you simply have to
return 1 as the number of length bytes.
- If the most significant bit of the
firstLengthByte is
1, it means the length bytes follow the multi-byte length
notation. In this case the else block Listing
5 receives control.
In multi-byte length format, the seven bits after the most
significant bit of firstLengthByte specify how many more
length bytes are to follow. For example, if the value of
firstLengthByte is 1000 0010 , the left-most 1
(the most-significant bit) tells that the length byte follows
multi-byte length notation. The other seven bits (000
0010 ) specify that there are two more length bytes. Therefore, the
getNumberOfLengthBytes() method in this case should return 3
(firstLengthBytes plus two more length bytes).
The first line in the else block of Listing 5
(firstLengthByte &= (byte)0x7f; ) removes the most
significant bit of the firstLengthByte .
The second line in the else block ( return
(int)firstLengthByte + 1; ) casts the firstLengthByte
as an integer , adds 1 to the resulting integer
value, and returns the integer value. Listing 5. The getNumberOfLengthBytes() method
public int getNumberOfLengthBytes (byte firstLengthByte) {
if ( (firstLengthByte & 1<<8) == 0 )
return 1;
else {
firstLengthByte &= (byte)0x7f;
return (int)firstLengthByte + 1;
}
}//getNumberOfLengthBytes
|
getLength() The purpose of
this method is to check how many bytes a particular ASN.1 structure has. A
processing application normally has a byte array consisting of a nested
hierarchy of many ASN.1 structures. The getLength() method
calculates the number of bytes that belong to a particular structure.
This method takes two parameters. The first parameter
(ASN1Structure ) is a byte array, which should contain at
least one complete ASN.1 structure, which itself contains the tag byte,
the length bytes, and the content bytes. The second parameter
(offset ) is a value offset into the
ASN1Structure byte array. This parameter specifies the start
of the ASN.1 structure contained in the ASN1Structure byte
array.
The getLength() method returns an integer value equal to
the total number of bytes in the ASN.1 structure starting at the
offset byte.
Look at Listing 6, which shows
the implementation of the getLength() method:
- The first step is to pass on the second byte of the ASN.1 structure
to the
getNumberOfLengthBytes() method. The ASN.1 structure
starts from the offset byte, so you expect that the offset
byte is actually the tag byte. As all Kerberos structures contain just
one tag byte, the second byte (the one just after the offset byte) is
the first length byte. The first length byte tells the total number of
length bytes, and the getNumberOfLengthBytes() method
returns the number of length bytes. The int numberOfLengthBytes =
getNumberOfLengthBytes(ASN1Structure [offset+1]); line performs
the task.
- If the
getNumberOfLengthBytes() method returns a value
larger then 1, you must deal with multi-byte length
notation. In this case, you read the length bytes starting from
offset + 2 (leaving the tag byte and the first length byte)
into a variable named lengthValueAsBytes . You then convert
the length value from ASN.1 bytes form into a J2ME int by
using the getIntegerValue() method. Finally, you add 1 to
the result (to compensate for the tag byte, which is not included in the
length value) before returning the length value to the calling
application.
- If the
getNumberOfLengthBytes() method returns 1, you
must deal with single-byte length notation. In this case,
you simply convert the first (and only) length byte into a J2ME
int , add 1 to it (to compensate for the tag byte, which is
not included in the length value), and return the resulting value to the
calling application.
Listing 6 The getLength() method
public int getLength (byte[] ASN1Structure, int offset) {
int structureLength;
int numberOfLengthBytes = getNumberOfLengthBytes(ASN1Structure[offset + 1]);
byte[] lengthValueAsBytes = new byte[numberOfLengthBytes - 1];
if (numberOfLengthBytes > 1)
{
for (int i=0; i < numberOfLengthBytes-1 ; i++)
lengthValueAsBytes[i]= ASN1Structure [offset + i + 2];
structureLength = getIntegerValue(lengthValueAsBytes);
}
else
structureLength = (int) (ASN1Structure[offset+1]);
structureLength += numberOfLengthBytes + 1;
return structureLength;
}//getLength()
|
getASN1Structure The
getASN1Structure() method of Listing 7 finds and
extracts a particular ASN.1 structure from a byte array consisting of a
series of ASN.1 structures. The method takes three parameters. The first
parameter (inputByteArray ) is the input byte array from which
you need to find the required ASN.1 structure. The second parameter is an
int , which specifies the type of tag that you want to find.
The third parameter specifies the tag number.
Have a look at the getASN1Strucute() method implementation
in Listing 7. You have
initialized an offset value to zero and entered into a
do-while loop.
Inside the do-while loop, you have read the first byte of
the input byte array into a byte named tagByte . You have then
used the isASN1Structure() method to check whether the first
byte of the input array is the required ASN.1 structure.
If the first byte represents the required structure, you have used the
getLength() method to find the required number of bytes to be
returned. You have then copied the required bytes into a byte array named
outputBytes and returned the bytes to the calling
application.
If the first byte does not represent the required structure, you want
to jump to the next structure. For this purpose, I have set the offset
value to the start of the next structure.
The do-while loop checks the next structure in its next
attempt and in this manner consumes the entire input byte array. If the
required structure is not found, the do-while loop exits and
the return is null. Listing 7. The
getASN1Structure() method
public byte[] getASN1Structure (byte[] inputByteArray, int tagType, int tagNumber)
{
byte tagByte;
int offset = 0;
do {
tagByte = inputByteArray[offset];
if (isASN1Structure(tagByte, tagType, tagNumber)) {
int lengthOfStructure = getLength(inputByteArray, offset);
byte[] outputBytes = new byte[lengthOfStructure];
for (int x =0; x < lengthOfStructure; x++)
outputBytes[x]= inputByteArray [x + offset];
return outputBytes;
}
else
offset += getLength(inputByteArray, offset);
} while (offset < inputByteArray.length);
return null;
}//getASN1Structure
|
getContents() The
getContents() method shown in Listing 8 takes an
ASN1Structure byte array and returns a byte array containing
the contents of the ASN1Structure .
The getContents() method assumes that the provided byte
array is a valid ASN1 structure, so it ignores the first byte of the
structure that represents the tag byte. It passes the second byte (that
is, the first length byte) to the getNumberOfLengthBytes()
method, which returns the number of length bytes in the ASN1Structure
input byte array.
It then constructs a new byte array named contentBytes and
copies the contents of the ASN1Structure into the
contentBytes array (leaving the tag and length bytes). Listing 8. The getContents() method
public byte[] getContents (byte[] ASN1Structure)
{
int numberOfLengthBytes = getNumberOfLengthBytes(ASN1Structure [1]);
byte[] contentBytes = new byte[ASN1Structure.length - (numberOfLengthBytes + 1)];
for (int x =0; x < contentBytes.length; x++)
contentBytes[x]= ASN1Structure [x + numberOfLengthBytes + 1];
return contentBytes;
}//getContents
|
Some low level cryptographic support
methods In addition to the low-level processing methods
described above, you also need some low-level cryptographic support
methods to process a ticket response. That's why, before explaining the
processing of the ticket response, I need to discuss the following methods
that provide cryptographic support in the Kerberos client:
- encrypt()
- decrypt()
- getMD5DigestValue()
- decryptAndVerifyDigest()
These methods are part of the KerberosClient class, which
you can find in the KerberosClient.java file, as well as in the source code download of
this article. Following is an explanation of each of these methods:
encrypt() The
encrypt() method shown in Listing 9 handles low
level cryptography and encrypts an input byte array.
This method takes three byte array parameters, that is, a cryptographic
key for encryption (keyBytes ), plain text data to be
encrypted (plainData ), and an initial vector or IV
(ivBytes ). It encrypts the plain text data using the key and
IV, and returns the encrypted form of the plain text data.
Notice from the encrypt() method in Listing 9 that I have
used the DESEngine , CBCBlockCipher ,
KeyParameter , and ParametersWithIV classes to
encrypt the plaintext data. These classes are part of the Bouncy Castle
cryptographic library discussed along with the getFinalKey()
method in Listing 11 of the second
article. Take a look back and compare the encrypt()
method of Listing 9 with the
getFinalKey() method of Listing 11 of the second
article. Note the following:
- The
getFinalKey() method uses a
ParametersWithIV class that wraps an initial vector. The
Kerberos specification requires using the encryption key as IV while
generating a secret key. Therefore, the encryption algorithm in the
getFinalKey() method uses the encryption key as an IV.
On the other hand, the encrypt() method is designed to
work both with and without an IV value. Higher level application logic
can use the encrypt() method either by supplying an IV value or omitting
it. If an application requires the encryption of data without an IV
value, it will pass null as the third parameter. In case IV is
present, the encrypt() method uses a ParametersWithIV
instance to initialize the CBCBlockCipher. Notice that inside the
if (ivBytes != null) block in Listing
9, I have passed a ParametersWithIV instance as the second parameter
to the cbcCipher.init() method call. If the third
parameter is null, the encrypt() method uses a KeyParameter
object to initialize the CBCBlockCipher object. Notice from Listing
9 that in the else block, I have passed a KeyParameter
instance as the second parameter to the cbcCipher.init()
method call.
- The
getFinalKey() method in in
the second article's Listing 11 returns the result of processing the
last block of input data. On the other hand, the encrypt()
method concatenates the result of every step of plaintext processing and
returns the concatenated form of all processed (encrypted) bytes.
Listing 9. The encrypt() method
public byte[] encrypt(byte[] keyBytes, byte[] plainData, byte[] ivBytes)
{
byte[] encryptedData = new byte[plainData.length];
CBCBlockCipher cbcCipher = new CBCBlockCipher(new DESEngine());
KeyParameter keyParameter = new KeyParameter(keyBytes);
if (ivBytes != null) {
ParametersWithIV kpWithIV = new ParametersWithIV (keyParameter, ivBytes);
cbcCipher.init(true, kpWithIV);
} else
cbcCipher.init(true, keyParameter);
int offset = 0;
int processedBytesLength = 0;
while (offset < encryptedData.length) {
try {
processedBytesLength = cbcCipher.processBlock( plainData,
offset,
encryptedData,
offset
);
offset += processedBytesLength;
} catch (Exception e) {
e.printStackTrace();
}//catch
}
return encryptedData;
}
|
decrypt() The
decrypt() method (shown in Listing 10) works
exactly the same way as the encrypt() method except that the
first parameter to the cbcCipher.init() method is
false for decryption (it was true for
encryption). Listing 10. The decrypt() method
public byte[] decrypt(byte[] keyBytes, byte[] encryptedData, byte[] ivBytes)
{
byte[] plainData = new byte[encryptedData.length];
CBCBlockCipher cbcCipher = new CBCBlockCipher(new DESEngine());
KeyParameter keyParameter = new KeyParameter(keyBytes);
if (ivBytes != null) {
ParametersWithIV kpWithIV = new ParametersWithIV (keyParameter, ivBytes);
cbcCipher.init(false, kpWithIV);
} else
cbcCipher.init(false, keyParameter);
int offset = 0;
int processedBytesLength = 0;
while (offset < encryptedData.length) {
try {
processedBytesLength = cbcCipher.processBlock( encryptedData,
offset,
plainData,
offset
);
offset += processedBytesLength;
} catch (Exception e) {
e.printStackTrace();
}//catch
}
return plainData;
}//decrypt()
|
getMD5DigestValue() The
getMD5DigestValue() method shown in Listing 11 takes an
input data byte array and returns the MD5 digest value calculated over the
input data.
The Bouncy Castle's cryptographic library contains MD5 digest support
in a class named MD5Digest . Digest calculation using the
MD5Digest class needs four steps:
- First, you instantiate an
MD5Digest object.
- Then, you call the
update() method of the
MD5Digest object, passing the data to be digested along
with the method call.
- Next, instantiate an output byte array to hold the MD5 digest value.
- Finally, you call the
doFinal() method of the
MD5Digest object, passing the output byte array along with
the method call. The doFinal() method calculates the digest
value and places it in the output byte array.
Listing 11. The getMD5DigestValue() method
public byte[] getMD5DigestValue (byte[] data)
{
MD5Digest digest = new MD5Digest();
digest.update (data, 0, data.length);
byte digestValue[] = new byte[digest.getDigestSize()];
digest.doFinal(digestValue, 0);
return digestValue;
}
|
decryptAndVerifyDigest() Recall from Figure 3 and Listing 2 of
the first article that the ticket response from the KDC server
contains a field named enc-part , which wraps an encrypted
data structure named EncryptedData . The
EncryptedData structure consists of three fields
(etype, kvno, and cipher ) as described in the explanation
accompanying Figure 3 in the first
article.
The decryptAndVerifyDigest() method shown in Listing 12 takes an
EncryptedData structure (essentially the contents of an
enc-part field) and a decryption key as parameters and
returns the plain text representation of the EncryptedData
structure. The decryption process takes the following steps:
Step 1: Notice from Listing 2 of the first
article that an EncryptedData structure is actually a
SEQUENCE of etype, kvno , and cipher
fields. Therefore, that first step is to check whether the input byte
array is a SEQUENCE . An isSequence() method call
can do this.
Step 2: If the input
byte array is a SEQUENCE , you need to tear the
SEQUENCE and extract its contents. A
getContents() method call extracts the SEQUENCE
contents.
From the SEQUENCE contents, you are interested in the
first field (etype , context-specific tag number 0), which
shows the type of encryption. You have used a
getASN1Structure() method call to fetch the
etype field from the SEQUENCE contents.
Step 3: You have called
the getContents() method to fetch the contents of the
etype field, which is an ASN.1 INTEGER . You have
again called the getContents() method to fetch the contents
of the INTEGER . The INTEGER contents are then
passed to the getIntegerValue() method, which returns the
J2ME int form of the INETGER contents. You store
the J2ME int value as a variable named eTypeValue . The
eTypeValue int specifies the type of encryption used in
authoring the EncryptedData structure.
Step 4: Recall that the
Kerberos client supports only one type of encryption -- DES-CBC -- whose
identifier is 3. Therefore, I have checked whether the
eTypeValue is 3. If it is not (that is, the server has used
some encryption algorithm other than DES-CBC), the Kerberos client cannot
handle its processing.
Step 5: The next step is
to extract the third field (cipher , context-specific tag
number 2) from the EncryptedData SEQUENCE
contents. A getASN1Structure() method call can perform this
job for you.
Step 6: Next, you
extract the contents of the cipher field by calling the
getContents() method. The contents of the cipher field are an
ASN.1 OCTET STRING . You again need to call the
getContents() method, which fetches the contents of the
OCTET STRING .
Step 7: The OCTET
STRING contents are in encrypted form, which you need to decrypt
using the decrypt() method discussed earlier.
Step 8: The decrypted
data byte array consists of three portions. The first portion consists of
the first eight bytes, which contain a random number called
confounder . The confounder bytes have no meaning; they just
help in making a hacker's job more difficult.
The 9th to 24th bytes of the decrypted data form the second portion,
which contains a 16 bytes MD5 digest value. The digest value is calculated
over the entire decrypted data with sixteen digest bytes (second portion)
zeroed out.
The third portion is the actual plain text data that you are looking
for.
Because the eighth step performs an integrity check, you have to zero
out the 9th to 24th bytes of the decrypted data, calculate an MD5 digest
value over the data, and match the digest value with the second portion
(9th to 24th byte). If the two digest values match, the integrity of the
message is verified.
Step 9: If the integrity
check is successful, you return the third portion (from 25th byte till the
end) of the decrypted data. Listing 12. The
decryptAndVerifyDigest() method
public byte[] decryptAndVerifyDigest (byte[] encryptedData, byte[] decryptionKey)
{
/****** Step 1: ******/
if (isSequence(encryptedData[0])) {
/****** Step 2: ******/
byte[] eType = getASN1Structure(getContents(encryptedData),
CONTEXT_SPECIFIC, 0);
if (eType != null) {
/****** Step 3: ******/
int eTypeValue = getIntegerValue(getContents(getContents(eType)));
/****** Step 4: ******/
if ( eTypeValue == 3) {
/****** Step 5: ******/
byte[] cipher = getASN1Structure(getContents(encryptedData),
CONTEXT_SPECIFIC, 2);
/****** Step 6: ******/
byte[] cipherText = getContents(getContents(cipher));
if (cipherText != null) {
/****** Step 7: ******/
byte[] plainData = decrypt(decryptionKey,
cipherText, null);
/****** Step 8: ******/
int data_offset = 24;
byte[] cipherCksum = new byte [16];
for (int i=8; i < data_offset; i++)
cipherCksum[i-8] = plainData[i];
for (int j=8; j < data_offset; j++)
plainData[j] = (byte) 0x00;
byte[] digestBytes = getMD5DigestValue(plainData);
for (int x =0; x < cipherCksum.length; x++) {
if (!(cipherCksum[x] == digestBytes[x]))
return null;
}
byte[] decryptedAndVerifiedData = new byte[plainData.length - data_offset];
/****** Step 9: ******/
for (int i=0; i < decryptedAndVerifiedData.length; i++)
decryptedAndVerifiedData[i] = plainData[i+data_offset];
return decryptedAndVerifiedData;
} else
return null;
} else
return null;
} else
return null;
} else
return null;
}//decryptAndVerifyDigest
|
Extracting the ticket and key from the
ticket response Having discussed low level ASN.1 processing
as well as low-level cryptographic support methods, you are now ready to
discuss how to use these methods to process the ticket response that you
fetched earlier using the getTicketResponse() method of Listing 1.
Take a look at the getTicketAndKey() method (which belongs
to the KerberosClient class) shown in Listing 13. This method
takes a ticket response byte array and a decryption key byte array as
parameters. The method extracts the ticket and the key from the ticket
response.
The getTicketAndKey() method returns an instance of a
class named TicketAndKey (a wrapper for the key and ticket
that you want to extract from the ticket response). I have shown the
TicketAndKey class in Listing 14. This class
has only four methods: two setter methods and two getter methods. The
setKey() and getKey() methods set and get the
key bytes respectively. The setTicket() and
getTicket() methods set and get the ticket bytes
respectively.
Now look at what's happening inside the getTicketAndKey()
method of Listing 13. Recall from
the discussion accompanying Figure 4 and Listing 2 in
the first article how a Kerberos key and ticket are stored in a ticket
response. Extracting the key from a ticket response is a lengthy process
involving these steps:
1. First, check whether
the ticketResponse byte array really contains a ticket
response. For this purpose, I have used the isASN1Structure()
method. If the isASN1Structure() method returns false, it
indicates that the input ticketResponse byte array is not a
valid ticket response. In this case, you are not going to do any further
processing and return null.
Notice from Listing 13 that I have
used two calls to the isASN1Structure() method. The first
isASN1Structure() method call takes "11" as the value of the
third parameter, while the second isASN1Structure() method
call takes "13" as the value of its third parameter. That's because "11"
is the application-specific tag number of a TGT response (Listing 2 of the first
article of this series) and "13" is the application-specific tag
number of a service ticket response (Listing 4 of the first
article of this series). If the ticketResponse byte array
is a TGT response or a service ticket response, one of the
two method calls returns true and you proceed with further processing. If
none of the methods return true, it indicates that the
ticketResponse byte array is not a ticket response and you
return null without any further processing.
2. The second step is to
extract the contents of the ticket response structure. For this purpose, I
have used a getContents() method call.
3. The contents of the
ticket response should be an ASN.1 SEQUENCE ; an
isSequence() method call checks this for you.
4. Next, I use the
getContents() method call to extract the contents of the
SEQUENCE .
5. The contents of the
SEQUENCE are the seven structures of the ticket response
(shown in Figure 3 and Listing 2 of the first
article). Out of these seven structures, you need just two: the ticket
and the enc-part.
Therefore, the fifth step is to extract the ticket field from the
SEQUENCE contents (using the getASN1Structure()
method call), extract the contents of the ticket field (using the
getContents() method call), and store the contents in the
TicketAndKey object that you created earlier. Notice that the
ticket field is the context-specific tag number 5, while the contents of
this field are the actual ticket, which starts with an application-level
tag number 1, as shown in Listing 3 and Figure 9 of
the first article.
6. Next, you have to
extract the key from the SEQUENCE contents that you got in
step 4. The key resides inside the enc-part field of the
SEQUENCE contents. Therefore, in step 6, I grab the
enc-part field from the SEQUENCE contents by
using a getASN1Structure() method call.
7. Once I have the
enc-part field, I need to get its contents by using the
getContents() method call. The contents of the
enc-part field form an EncryptedData structure.
8.You can pass the
EncryptedData structure to the
decryptAndVerifyDigest() method, which decrypts the
EncryptedData structure and performs a digest verification
check on the EncryptedData .
9. In the event that the
decryption and digest verification process is successful, the
decryptAndVerifyDigest() method extracts the ASN.1 data from
the decrypted form of the cipher data. The ASN.1 data should comply with
the structure that I presented in Figure 4 of the first
article. Notice that the key you require is the first field of the
structure shown in Figure 4 of the first
article. An application-level tag number "25" or "26" wraps the plain
text data. This structure is called EncKDCRepPart (encrypted
KDC reply part).
As a result, the next step is to check whether the data returned by the
decryptAndVerifyDigest() method is an application-level tag
number 25 or 26.
10. The next step is to
extract the contents of the EncKDCRepPart structure. A
getContents() method call extracts the required contents.
The EncKDCRepPart contents are a SEQUENCE , so
you also have to extract the SEQUENCE contents. Another
getContents() method call extracts the SEQUENCE
contents.
11. The first field of
the SEQUENCE contents (called key, with a context-specific
tag number 0) holds the key field. You can call the
getASN1Structure() method to extract the first field from the
SEQUENCE contents.
12. Next, you extract
the contents of the key field. A getConents() method call can
return those contents.
The contents of the key field form another ASN.1 structure called
EncryptionKey , which is a SEQUENCE of two
fields, namely keytype and keyvalue . Another
getContents() method call fetches the contents of the
SEQUENCE .
13. The session key you
require resides inside the second field (keyvalue ) of the
SEQUENCE contents. Therefore, you must call the
getASN1Structure() method to extract the
keyvalue field (context-specific tag number 1) from the
SEQUENCE contents.
14. You now have the
keyvalue field. You have to extract its contents by calling
the getContents() method. The keyvalue contents
are an OCTET STRING , so you must call the
getContents() method once again to fetch the OCTET
STRING contents, which is the required key that you were looking
for.
So you simply wrap the key bytes inside the KeyAndTicket
object (by calling its setKey() method) and return the
KeyAndTicket object. Listing 13. The
getTicketAndKey() method
public TicketAndKey getTicketAndKey( byte[] ticketResponse, byte[] decryptionKey)
{
TicketAndKey ticketAndKey = new TicketAndKey();
int offset = 0;
/***** Step 1:*****/
if ((isASN1Structure(ticketResponse[0], APPLICATION_TYPE, 11)) ||
(isASN1Structure(ticketResponse[0], APPLICATION_TYPE, 13))) {
try {
/***** Step 2:*****/
byte[] kdc_rep_sequence = getContents(ticketResponse);
/***** Step 3:*****/
if (isSequence(kdc_rep_sequence[0])) {
/***** Step 4:*****/
byte[] kdc_rep_sequenceContent = getContents(kdc_rep_sequence);
/***** Step 5:*****/
byte[] ticket = getContents(getASN1Structure(kdc_rep_sequenceContent,
CONTEXT_SPECIFIC, 5));
ticketAndKey.setTicket(ticket);
/***** Step 6:*****/
byte[] enc_part = getASN1Structure(kdc_rep_sequenceContent,
CONTEXT_SPECIFIC, 6);
if (enc_part!=null) {
/***** Step 7:*****/
byte[] enc_data_sequence = getContents(enc_part);
/***** Step 8:*****/
byte[] plainText = decryptAndVerifyDigest(enc_data_sequence,
decryptionKey);
if (plainText != null){
/***** Step 9:*****/
if ((isASN1Structure(plainText[0],APPLICATION_TYPE, 25)) ||
(isASN1Structure(plainText[0], APPLICATION_TYPE, 26))) {
/***** Step 10:*****/
byte[] enc_rep_part_content = getContents(getContents(plainText));
/***** Step 11:*****/
byte[] enc_key_structure = getASN1Structure(enc_rep_part_content,
CONTEXT_SPECIFIC, 0);
/***** Step 12:*****/
byte[] enc_key_sequence = getContents(getContents(enc_key_structure));
/***** Step 13:*****/
byte[] enc_key_val = getASN1Structure(enc_key_sequence,
CONTEXT_SPECIFIC, 1);
/***** Step 14:*****/
byte[] enc_key = getContents(getContents(enc_key_val));
ticketAndKey.setKey(enc_key);
return ticketAndKey;
} else
return null;
} else
return null;
} else
return null;
} else
return null;
} catch (Exception e) {
e.printStackTrace();
}
return null;
} else
return null;
}//getTicketAndKey()
|
Listing 14. The TicketAndKey Class
public class TicketAndKey
{
private byte[] key;
private byte[] ticket;
public void setKey(byte[] key)
{
this.key = key;
}//setKey()
public byte[] getKey()
{
return key;
}//getKey
public void setTicket(byte[] ticket)
{
this.ticket = ticket;
}//setTicket
public byte[] getTicket()
{
return ticket;
}//getTicket
}
|
Getting a service
ticket You have processed the TGT response to
fetch the TGT and the session key. Now it is time to use the
TGT and the session key to request a service ticket from the
KDC server. The request for a service ticket is similar to the request for
a TGT that I authored in Listing 1, except for one
difference: The optional padata field that I omitted from the
TGT request is not optional for a service ticket request.
Therefore, you need to include the padata field in the
service ticket request.
The padata field contains a SEQUENCE of two
fields, namely padata-type and padata-value . The
padata-value field can carry several types of data, which
means the accompanying padata-type field specifies the type
of data that the padata-value field carries.
I explained the structure of the padata field that goes
with a service ticket in Figure 5 of the first
article of this series. Recall that the padata field in a
service ticket request wraps an authentication header (a
KRB_AP_REQ structure), which in turn wraps the
TGT along with other data.
So, before you can start authoring a service ticket request, you must
author an authentication header. Here's a breakdown of how to do that.
Authoring an authentication
header I have included the following methods in the
KerberosClient class to author an authentication header:
- getMD5DigestValue()
- getChceksumBytes()
- authorDigestAndEncrypt()
- getAuthenticationHeader()
These four are helper methods. The fifth method,
(getAuthenticationHeader() ), uses the helper methods and
authors the authentication header.
authorDigestAndEncrypt() The
authorDigestAndEncrypt() method shown in Listing 15 takes a plain
text data byte array and an encryption key. This method calculates a
digest value over the plain text data, encrypts the plain text data, and
returns an EncryptedData structure exactly matching the
structure that I passed as input to the
decryptAndVerifyDigest() method of Listing 12.
You could say that the authorDigestAndEncrypt() method of
Listing 15 is exactly
opposite to the decryptAndVerifyDigest() method discussed
earlier. The authorDigestAndEncrypt() method takes as input
the plain text data that the decryptAndVerifyDigest() method
returned. Similarly, the EncryptedData structure that the
authorDigestAndEncrypt() method returns is what I sent as
input to the decryptAndVerifyDigest() method.
The authorDigestAndEncrypt() method implements the following strategy:
- First, you generate eight random bytes, which form the confounder.
- Next, you declare a byte array named
zeroedChecksum , of
sixteen bytes and initialize it to zero. This array of sixteen zeros
serves as a zeroed-out digest value.
- Third, you pad the input data byte array with extra bytes so that
the number of bytes in the array becomes a multiple of eight. You have
written a method named
getPaddedData() (shown in Listing
16), which takes a byte array and returns its padded form. Next, you
link the confounder (from step 1), the zeroed out digest (from step 2),
and the padded plain text byte array.
- The fourth step is to calculate the MD5 digest value over the
concatenated byte array of step 3.
- The fifth step is to place the digest bytes in their correct place.
The outcome of step 5 is the same as the outcome of step 3, except that
the zeroed out digest bytes are now replaced with the actual digest
value.
- Now you call the
encrypt() method to encrypt step 5's
byte array.
- Next, you author the
etype field (context-specific tag
number 0).
- Then, you wrap the encrypted byte array of step 6 into an
OCTET STRING by calling the
getOctetStringBytes() bytes. You then wrap the OCTET
STRING inside the cipher field (a context-specified
tag number 2).
- Finally, you link the
etype and cipher
fields, wrap the string into a SEQUENCE , and return the
SEQUENCE .
Listing 15. The authorDigestAndEncrypt()
method
public byte[] authorDigestAndEncrypt(byte[] key, byte[] data)
{
/****** Step 1: ******/
byte[] conFounder = concatenateBytes (getRandomNumber(), getRandomNumber());
/****** Step 2: ******/
byte[] zeroedChecksum = new byte[16];
/****** Step 3: ******/
byte[] paddedDataBytes = concatenateBytes (conFounder,
concatenateBytes(zeroedChecksum,
getPaddedData(data)
)
);
/****** Step 4: ******/
byte[] checksumBytes = getMD5DigestValue(paddedDataBytes);
/****** Step 5: ******/
for (int i=8; i < 24; i++)
paddedDataBytes[i] = checksumBytes[i-8];
/****** Step 6: ******/
byte[] encryptedData = encrypt(key, paddedDataBytes, null);
/****** Step 7: ******/
byte[] etype = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(3)
);
/****** Step 8: ******/
byte[] cipher = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
2, getOctetStringBytes(encryptedData)
);
/****** Step 9: ******/
byte[] ASN1_encryptedData = getSequenceBytes (
concatenateBytes(etype,cipher)
);
return ASN1_encryptedData;
}//authorDigestAndEncrypt
|
Listing 16. The getPaddedData() method
public byte[] getPaddedData(byte[] data) {
int numberToPad = 8 - ( data.length % 8 );
if (numberToPad > 0 && numberToPad != 8)
{
byte[] bytesPad = new byte[numberToPad];
for (int x = 0; x < numberToPad; x++)
bytesPad [x] = (byte)numberToPad;
return concatenateBytes(data, bytesPad);
}
else
return data;
}//getPaddedData()
|
getChecksumBytes() The
getChecksumBytes() method authors a structure called
Checksum , as shown in Listing 17. The Checksum
structure contains two fields, cksumtype and
checksum . Listing 17. The Checksum
structure
Checksum ::= SEQUENCE {
cksumtype[0] INTEGER,
checksum[1] OCTET STRING
}+
|
You need the Checksum structure in two places -- first while authoring
a service ticket response, and then while authoring a secure context
establishment request. The purpose of the Checksum structure is different
at the two occasions, which you elaborate while authoring the service
ticket and context establishment requests.
The getChecksumBytes() method shown in Listing 18 takes two
byte array parameters. The first parameter carries the
checksum field, while the second parameter carries the
cksumtype field.
The getChecksumBytes() method wraps the
cksumtype field in a context-specific tag number 0 (which
represents the cksumtype field as shown in Listing 17) and the
checksum field data in a context-specific tag number 1 (which
represents the checksum field also shown in Listing 17). It then
links the two fields, wraps that array in a SEQUENCE , and
returns the SEQUENCE . Listing 18.
The getChecksumBytes() method
public byte[] getChecksumBytes(byte[] cksumData, byte[] cksumType){
byte[] cksumBytes = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC, 3,
getSequenceBytes (
concatenateBytes (
getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0,
cksumType
),
getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC, 1,
getOctetStringBytes(cksumData)
)
)
)
);
return cksumBytes;
}//getChecksumBytes()
|
getAuthenticationHeader()
Recall the section "The request for a service ticket" in
the first article of this
series in which you learned that the KRB-AP-REQ structure
(also called an authentication header) wraps a Kerberos ticket.
Additionally, the authentication header also wraps an authenticator field,
which demonstrates a client's knowledge of the session or
sub-session key .
As shown in Figure 5 of the first
article, the authentication header consists of five fields, namely
pvno, msg-type, ap-options, ticket and
authenticator .
The getAuthenticationHeader() method of Listing 19 authors these
five fields one by one and then concatenates the various fields in the
correct order to form the complete authentication header. Listing 19. The getAuthenticationHeader() method
public byte[] getAuthenticationHeader( byte[] ticketContent,
String clientRealm,
String clientName,
byte[] checksumBytes,
byte[] encryptionKey,
int sequenceNumber
)
{
byte[] authenticator = null;
byte[] vno = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(5)
);
byte[] ap_req_msg_type = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
1, getIntegerBytes(14)
);
byte[] ap_options = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
2, getBitStringBytes(new byte[5])
);
byte[] ticket = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
3, ticketContent
);
byte[] realmName = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, getGeneralStringBytes(clientRealm)
);
byte[] generalStringSequence = getSequenceBytes(
getGeneralStringBytes (clientName)
);
byte[] name_string = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, generalStringSequence
);
byte[] name_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(ASN1DataTypes.NT_PRINCIPAL)
);
byte[] clientNameSequence = getSequenceBytes(
concatenateBytes (name_type, name_string)
);
byte[] cName = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
2, clientNameSequence);
byte[] cusec = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
4, getIntegerBytes(0)
);
byte[] ctime = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
5, getGeneralizedTimeBytes (
getUTCTimeString(System.currentTimeMillis()).getBytes()
)
);
if (sequenceNumber !=0 ) {
byte[] etype = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(3)
);
byte[] eKey = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
1, getOctetStringBytes(encryptionKey)
);
byte[] subKey_sequence = getSequenceBytes (concatenateBytes(etype, eKey));
byte[] subKey = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
6, subKey_sequence
);
byte[] sequenceNumberBytes = {
(byte)0xff,
(byte)0xff,
(byte)0xff,
(byte)0xff
};
sequenceNumberBytes[3] = (byte)sequenceNumber;
byte[] seqNumber = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
7, getIntegerBytes(sequenceNumberBytes)
);
authenticator = getTagAndLengthBytes(ASN1DataTypes.APPLICATION_TYPE,
2, getSequenceBytes(
concatenateBytes(vno,
concatenateBytes(realmName,
concatenateBytes(cName,
concatenateBytes(checksumBytes,
concatenateBytes(cusec,
concatenateBytes(ctime,
concatenateBytes(subKey,seqNumber)
)
)
)
)
)
)
)
);
} else {
authenticator = getTagAndLengthBytes(ASN1DataTypes.APPLICATION_TYPE,
2, getSequenceBytes(
concatenateBytes(vno,
concatenateBytes(realmName,
concatenateBytes(cName,
concatenateBytes(checksumBytes,
concatenateBytes(cusec,ctime)
)
)
)
)
)
);
}//if (sequenceNumber !=null)
byte[] enc_authenticator = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
4, authorDigestAndEncrypt(encryptionKey, authenticator)
);
byte[] ap_req = getTagAndLengthBytes (
ASN1DataTypes.APPLICATION_TYPE,
14, getSequenceBytes(
concatenateBytes (vno,
concatenateBytes(ap_req_msg_type,
concatenateBytes(ap_options,
concatenateBytes(ticket, enc_authenticator)
)
)
)
)
);
return ap_req;
}//getAuthenticationHeader
|
The getAuthenticationHeader() method takes a number of
input parameters:
- The byte array named
ticketContent contains the
Kerberos ticket (the TGT ) that the
getAuthenticationHeader() method wraps inside an
authentication header.
- The string type parameter named
clientRealm specifies
the name of the realm in which the Kerberos client (who is authoring
this request) is registered.
- The string type parameter named
clientName specifies
the name of the Kerberos client that is authoring this request.
- The
checksumBytes byte array carries a Checksum
structure along with the getChecksumBytes() method.
- The
encryptionKey byte array carries the encryption key
that you use to produce the encrypted portion of the authentication
header.
- The parameter named
sequenceNumber is an
integer value that identifies the sender's request number.
Recall from Figure 5 of the first
article that an authentication header consists of following fields:
- pvno
- msg-type
- ap-options
- ticket
- authenticator
Now let's see how the getAuthenticationHeader() method
implementation shown in Listing 19 authors the
different fields of the authentication header (the KRB-AP-REQ
structure):
You first want to author the pvno field, which has the
context-specific tag number 0 and wraps an ASN1 INTEGER with
a value of 5. A getTagAndLengthBytes() method call performs
this job. I have stored the pvno field in a byte array named
vno .
Similarly, you author the msg-type (context-specific tag
number 1) and ap-options fields (context-specific tag number
2) by making two calls to the getTagAndLengthBytes() method.
The next line (byte[] ticket =
getTagAndLengthBytes(ASN1DataTypes.Context_Specific, 3,
ticketContent) ) wraps the ticket structure inside context-specific
tag number 3, which is the fourth field of the authentication header.
Next, you must author the fifth field (named
authenticator , which has context-specific tag number 4) of
the authentication header. The authenticator field is an
EncryptedData structure. In plain text form, the
authenticator field is an Authenticator structure. Therefore,
you first author the complete Authenticator structure in
plain text form and pass on the plain text Authenticator to
the authorDigestAndEncrypt() method, which returns the
complete EncryptedData representation of the
Authenticator .
Notice from Listing 3 and Figure 5 of
the first article that the Authenticator structure in
plain text form consists of the following fields (omitting the last field,
which you don't need):
- authenticator-vno
- creal
- cname
- cksum
- cusec
- ctime
- subkey
- seq-number
I have already explained the meaning of each of these fields while
explaining Figure 5 of the first
article.
The authenticator-vno field is exactly the same as the
pvno field (the vno byte array discussed earlier
in this section, which contains context-specific tag number 0 with an
INTEGER value of 5). So I have reused the same byte array as
in the authenticator_vno field.
Now you come to the authoring of the crealm field, which
is similar to the realm field that I discussed in the section "Authoring
the request body" in the second article.
Similarly, the cname field is of PrincipalName
type, which was also explained in that section. I do not go into the
authoring details of crealm and cname fields
here.
The next task is to author the cksum field, which is of
type Checksum . The purpose of the cksum field in a service
ticket request is to cryptographically combine the authenticator with some
application data. Note the following three points:
- The authenticator structure contains the
cksum field.
- The
cksum field contains the cryptographic hash value
of some application data.
- The entire authenticator structure (including the
cksum
field) is encrypted using a secret key.
This certifies that the client who authored the authenticator and the
application data has the secret key, provided the cksum field
inside the authenticator matches the cryptographic checksum over the
application data.
The application that calls the getAuthenticationHeader()
method authors the Checksum structure (by calling the
getChecksumBytes() method) and passes on the
Checksum byte array as a value of the
checksumBytes parameter to the
getAuthenticationHeader() method.
As a result, you already have the Checksum structure in
the checksumBytes parameter. You just need to wrap the
checksumBytes in context-specific tag number 3 (which is the
tag number of cksum field in the authenticator structure).
Now you author the cusec field, which represents the
micro-second part of the client's time. The value of this field ranges
from 0 to 999999. This means you can supply a maximum value of 999999
micro-seconds in this field. However, MIDP does not contain any method
that can provide a time value that is more accurate than a millisecond.
Therefore, you cannot specify the micro-second part of the client's time.
You have simply passed a value of zero for this field.
In the Authenticator structure, you have two more fields to author --
subkey and seq-number . These two fields do not
have to be included in an Authenticator being authored for a
service ticket request, but you need them later when you use the same
getAuthenticationHeader() method to author a context
establishment request.
For the moment, just notice that you have simply checked whether the
sequenceNumber parameter is zero. It is zero for a service
ticket request and non-zero for a context-establishment request.
In case the sequenceNumber parameter is not zero, you have
authored the subkey and seq-number fields and
then linked the authenticator-vno, crealm, cname, cksum, cusec,
ctime, subkey and seq-number fields to form the byte
array, wrap the byte array in a SEQUENCE , and then wrap the
SEQUENCE into the Authenticator (application
level tag number 2).
If the sequenceNumber parameter is zero, you simply ignore
the subkey and seq-number fields, link the
authenticator-vno, crealm, cname, cksum, cusec, and
ctime fields to form the concatenated byte array, wrap the
byte array in a SEQUENCE , and then wrap the
SEQUENCE into the Authenticator (application
level tag number 2).
Next, you need to take the complete Authenticator
structure and pass it onto the authorDigestAndEncrypt()
method, which returns the complete EncryptedData
representation of the plain text Authenticator.
The next task is to concatenate the five fields of the authentication
header or KRB-AP-REQ structure (pvno, msg-type,
ap-options, ticket, authenticator ) into a concatenated array of
bytes, wrap the byte array into a SEQUECNE , and finally wrap
the SEQUENCE into an application-level tag number 14.
The authentication header is now complete, which you now return to the
calling application.
Authoring a service ticket
request I have discussed all the low-level methods you need
to author the request for a service ticket. You are going to use the same
getTicketResponse() method of Listing 1 that you used
to request a TGT for service ticket request authoring; you
just need to modify Listing 1 a bit so that
it will serve for both TGT and service ticket requests. Let's
see how this works.
Take a look at Listing 20 where you see
a modified form of the getTicketRespone() method of Listing
1. The modified form of contains some additional code as compared to Listing 1: Listing 20. The getTicketResponse() method
public byte[] getTicketResponse( String userName,
String serverName,
String realmName,
byte[] kerberosTicket,
byte[] key
)
{
byte ticketRequest[];
byte msg_type[];
byte pvno[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, getIntegerBytes(5));
msg_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
2, getIntegerBytes(10));
byte kdc_options[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getBitStringBytes(new byte[5]));
byte generalStringSequence[] = getSequenceBytes (
getGeneralStringBytes (userName));
byte name_string[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, generalStringSequence);
byte name_type[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(ASN1DataTypes.NT_PRINCIPAL));
byte principalNameSequence [] = getSequenceBytes(
concatenateBytes (name_type, name_string));
byte cname[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,
1, principalNameSequence);
byte realm[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,
2, getGeneralStringBytes (realmName));
byte sgeneralStringSequence[] = concatenateBytes(getGeneralStringBytes(serverName),
getGeneralStringBytes (realmName));
byte sname_string[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, getSequenceBytes(sgeneralStringSequence));
byte sname_type[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(ASN1DataTypes.NT_UNKNOWN));
byte sprincipalNameSequence [] = getSequenceBytes(
concatenateBytes (sname_type, sname_string)
);
byte sname[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,
3, sprincipalNameSequence);
byte till[] = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
5,
getGeneralizedTimeBytes (
new String("19700101000000Z").getBytes())
);
byte nonce[] = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
7,
getIntegerBytes (getRandomNumber())
);
byte etype[] = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
8,
getSequenceBytes(getIntegerBytes(3))
);
byte req_body[] = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
4,
getSequenceBytes(
concatenateBytes(kdc_options,
concatenateBytes(cname,
concatenateBytes(realm,
concatenateBytes(sname,
concatenateBytes(till,
concatenateBytes(nonce, etype)
)
)
)
)
)
)
);
if (kerberosTicket != null) {
msg_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
2, getIntegerBytes(12));
sname_string = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, getSequenceBytes(getGeneralStringBytes(serverName)));
sname_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(ASN1DataTypes.NT_UNKNOWN));
sprincipalNameSequence = getSequenceBytes(
concatenateBytes (sname_type, sname_string)
);
sname = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
3, sprincipalNameSequence
);
byte[] req_body_sequence = getSequenceBytes(
concatenateBytes(kdc_options,
concatenateBytes(realm,
concatenateBytes(sname,
concatenateBytes(till,
concatenateBytes(nonce, etype)
)
)
)
)
);
req_body = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
4, req_body_sequence
);
byte[] cksum = getChecksumBytes(
getMD5DigestValue(req_body_sequence),
getIntegerBytes(7)
);
byte[] authenticationHeader = getAuthenticationHeader(
kerberosTicket,
realmName,
userName,
cksum,
key,
0
);
byte[] padata_sequence = getSequenceBytes(concatenateBytes(
getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
1,getIntegerBytes(1)),
getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
2, getOctetStringBytes(authenticationHeader)
)
)
);
byte[] padata_sequences = getSequenceBytes(padata_sequence);
byte[] padata = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
3, padata_sequences
);
ticketRequest = getTagAndLengthBytes(
ASN1DataTypes.APPLICATION_TYPE,
12, getSequenceBytes(
concatenateBytes(pvno,
concatenateBytes(msg_type,
concatenateBytes(padata, req_body)
)
)
)
);
} else {
ticketRequest = getTagAndLengthBytes(
ASN1DataTypes.APPLICATION_TYPE,
10, getSequenceBytes(
concatenateBytes(pvno,
concatenateBytes(msg_type, req_body)
)
)
);
}
try {
Datagram dg = dc.newDatagram(ticketRequest, ticketRequest.length);
dc.send(dg);
} catch (IllegalArgumentException il) {
il.printStackTrace();
}
catch (Exception io) {
io.printStackTrace();
}
byte ticketResponse[] = null;
try {
Datagram dg = dc.newDatagram(700);
dc.receive(dg);
if (dg.getLength() > 0) {
ticketResponse = new byte[dg.getLength()];
System.arraycopy(dg.getData(), 0, ticketResponse, 0, dg.getLength());
} else
return null;
} catch (IOException ie){
ie.printStackTrace();
}
return ticketResponse;
}//getTicketResponse
|
The new getTicketResponse() method shown in Listing 20 takes five
parameters: userName, serverName, realmName, kerberosTicket ,
and key . To request a service ticket, pass a TGT
for the kerberosTicket byte array. On the other hand, you
don't have to pass a ticket when requesting a TGT , so you
will pass "null" for the kerberosTicket byte array.
The main difference between a TGT request and a service
ticket request is the padata field. This was explained in the
padata field of a service ticket request in the "The request
for a service ticket" section of the first article of this
series.
At the end of the getTicketResponse() , I have included an
if (kerberosTicket!=null) block. This block receives control
only when the kerberosTicket parameter is not null (it is
null in all TGT requests).
Inside the if (kerberosTicket!=null) block, I have
authored the padata field. As described in Figure 5 of the first
article, this padata field wraps an authentication
header, which the getAuthenticationHeader() method can author
for you.
You also learned from the getAuthenticationHeader() method
that in order to author an authentication header, you need a
Checksum structure, which the getChecksumBytes()
method can author for you.
Now, recall from the explanation of the getChecksumBytes()
method that in order to author a Checksum structure, you need
the data for the cksumtype and checksum fields.
Therefore, authoring an authentication header requires three steps:
- Author the data for the
cksumtype and
checksum fields. In the case of a service ticket request,
the data for the checksum field is simply the MD5 digest
value calculated over the SEQUENCE containing all the
sub-fields of the req-body field of the service ticket
request (notice in Figure
5 in first article, that req-body is the fourth field
of the service ticket request, just after the padata field,
which is the third field of the service ticket request). The data for
the cksumtype field is the ASN1 representation of the
integer 7. This value specifies the type of the checksum).
- Call the
getChecksumBytes() method and pass on the data
for the cksumtype and checksum fields. The
getChecksumBytes() method authors the complete
Checksum structure.
- Call the
getAuthenticationHeader() method and pass on
the Checksum structure along with the method call. The
getAuthenticationHeader() returns the authentication
header.
After authoring the authentication header, you must wrap it inside a
padata field. For this purpose, you have a few things to do:
- Wrap the authentication header in an
OCTET STRING by
calling the getOctetStringBytes() method that I described
in Listing
5 of the second article.
- Wrap the
OCTET STRING in the padata-value
field (context-specific tag number 2); a
getTagAndLengthBytes() method call can do this job.
- Another
getTagAndLengthBytes() method call authors the
padata-type field that accompanies the
padata-value you authored in step 2.
- Now, link the
padata-type and padata-value
fields.
- Put the linked byte array of step 4 into a
SEQUENCE .
This SEQUENCE represents a PADATA structure
(shown in Figure
5 and Listing 3 of the first article.
- The
padata field shown in Figure 5 and Listing
3 of the first article is a SEQUENCE of
PADATA structures. That means a single padata
field can hold a number of PADATA structures. However, you
have just one PADATA structure to be wrapped inside the
padata field, which means you simply wrap the
SEQUENCE of step 5 into another outer or higher level
SEQUENCE .
- The higher-level
SEQUENCE of step 6 represents the
SEQUENCE of PADATA structures that you can now
wrap inside the padata field (a context-specific tag number
3).
You can find all new code additions to the
getTicketResponse() method in the if
(kerberosTicket!=null) block at the end of Listing 20.
This ends the discussion on how to modify the existing
getTicketResponse() method to work for both TGT
and service ticket requests. The getTicketResponse() method
authors a service ticket request, sends the request to the
KDC , receives the service ticket response, and returns the
response to the calling application.
Extracting the service ticket and
sub-session key from the service ticket response A service
ticket response is similar to a TGT response. Recall that the
getTicketAndKey() method of Listing 13 parses a
TGT response to extract the TGT and the session
key. The same method also parses a service ticket response to extract the
service ticket and the sub-session key from the service ticket response.
So, you don't have to write any new code for extracting the service ticket
and the sub-session key.
Creating a secure communication
context Now you have the sub-session key and the service
ticket, the two things required to establish a secure communications
context with the e-bank's business logic server. At this point the
Kerberos client must author a context establishment request intended for
the e-bank's business logic server.
Refer to Figure 7 and Listing 5 in
the first article, which describe the message that a client sends to
the e-bank server to establish a secure context. The
createKerberosSession() method shown in Listing 21 handles all
aspects of establishing a secure communication context with the e-bank's
business logic server (this includes authoring the context establishment
request, sending the request to the server, fetching a response from the
server, parsing the response to check whether the remote server has agreed
to the context establishment request, and returning the outcome of this
effort to the calling application).
Take a look at the createKerberosSession() method of Listing 21, which takes
the following parameters:
- The
ticketContent byte array caries the service ticket
that you intend to use in order to establish a secure context.
- The
clientRealm string wraps the name of the
realm to which the requesting client belongs.
- The
clientName string specifies the name of the
requesting client.
- The
sequenceNumber parameter is an integer
that represents the sequence number of this message.
- encryptionKey: the sub-session key.
inStream and outStream are the input and
output streams that the createKerberosSession() method uses
to communicate with the e-bank's server.
As explained in the first article, you are going to use Java-GSS to
implement the e-bank's server-side logic. The GSS-Kerberos mechanism
dictates that a service ticket be wrapped inside an authentication header
and the authentication header itself wrapped inside the
InitialContextToken wrapper that you saw in Figure 7 and Listing 5 of
the first article.
You can use the getAuthenticationHeader() method of Listing 19 to wrap a
service ticket. Recall that I used the
getAuthenticationHeader() method to wrap a TGT
in the getTicketResponse() method of Listing 20.
In order to author an authentication header, you need a
Checksum . Recall from the discussion in the
getAuthenticationHeader() method of Listing 19 that the
purpose of a Checksum is to cryptographically bind an
authentication header with some application data. But, unlike the ticket
requesting an authentication header, the context establishing the
authentication header does not accompany the application data like you
might with an authentication header.
The GSS-Kerberos mechanism uses the Checksum structure for
a different purpose. Instead of binding the authentication header to some
application data, the GSS-Kerberos mechanism uses a Checksum
structure to bind a secure context with physical network addresses (that
is, the network addresses that a client might use to securely communicate
with the server). If you use this feature, use the secure context only
from the network address to which it is bound.
However, I don't want to use this feature in the sample mobile banking
application. That's why I just need to specify in the
Checksum structure that the secure context does not have any
network bindings. For this purpose, I have written a method named
getNoNetworkBindings() , as shown in Listing 22. The
getNoNetworkBindings() method is very simple. It simply
authors a hard-coded byte array that specifies that I don't require any
network bindings. It then calls the getChecksumBytes() method
to wrap the hard-coded array into a cksum field.
Once you have the no-network bindings Checksum byte array,
you can pass the array to the getAuthenticationHeader()
method, which returns the complete authentication header.
After authoring the authentication header, the
createKerberosSession() method of Listing 21 links the
authentication header byte array with a hard-coded byte array named
gssHeaderComponents . The gssHeaderComponents
byte array holds the byte representation of a GSS header that accompanies
an authentication header in a context establishment request.
Finally, you wrap the concatenated GSS header and authentication header
in an application-level tag number 0. It is a GSS requirement that all
context establishment requests be wrapped inside an application-level tag
number 0.
Now the context establishment request is complete. The next task is to
send the request over an output stream (the outStream
object). After sending the request, listen for and receive the response on
the inStream object.
When the createKerberosSession() method receives the
response, it checks whether the response has confirmed the creation of a
new context or whether it shows an error message. To perform this check,
you have to know the number of length bytes just after the starting tag
byte of the message. The GSS header bytes (which start just
after the length bytes) shows you the answer.
You are not interested in parsing the response for any further
processing. You're only interested in knowing whether the e-bank's server
has created a new session or denied one. If the e-bank's server confirms
the creation of a new session, the createKerberosSession()
method returns true ; if not, it returns false .
Listing 21. The createKerberosSession() method
public boolean createKerberosSession (
byte[] ticketContent,
String clientRealm,
String clientName,
int sequenceNumber,
byte[] encryptionKey,
DataInputStream inStream,
DataOutputStream outStream
)
{
byte[] cksum = getNoNetworkBindings();
if (sequenceNumber == 0)
sequenceNumber++;
byte[] authenticationHeader = getAuthenticationHeader(
ticketContent,
clientRealm,
clientName,
cksum,
encryptionKey,
sequenceNumber
);
byte[] gssHeaderComponents = {
(byte)0x6,
(byte)0x9,
(byte)0x2a,
(byte)0xffffff86,
(byte)0x48,
(byte)0xffffff86,
(byte)0xfffffff7,
(byte)0x12,
(byte)0x1,
(byte)0x2,
(byte)0x2,
(byte)0x1,
(byte)0x0
};
byte[] contextRequest = getTagAndLengthBytes(
ASN1DataTypes.APPLICATION_TYPE,
0, concatenateBytes (
gssHeaderComponents, authenticationHeader
)
);
try {
outStream.writeInt(contextRequest.length);
outStream.write(contextRequest );
outStream.flush();
byte[] ebankMessage = new byte[inStream.readInt()];
inStream.readFully(ebankMessage);
int respTokenNumber = getNumberOfLengthBytes (ebankMessage[1]);
respTokenNumber += 12;
byte KRB_AP_REP = (byte)0x02;
if (ebankMessage[respTokenNumber] == KRB_AP_REP){
return true;
} else
return false;
} catch (Exception io) {
io.printStackTrace();
}
return false;
}//createKerberosSession
|
Listing 22. The getNoNetworkBindings() method
public byte[] getNoNetworkBindings() {
byte[] bindingLength = { (byte)0x10, (byte)0x0, (byte)0x0, (byte)0x0};
byte[] bindingContent = new byte[16];
byte[] contextFlags_bytes = {
(byte)0x3e,
(byte)0x00,
(byte)0x00,
(byte)0x00
};
byte[] cksumBytes = concatenateBytes (
concatenateBytes(bindingLength,bindingContent),
contextFlags_bytes);
byte[] cksumType = {
(byte)0x2,
(byte)0x3,
(byte)0x0,
(byte)0x80,
(byte)0x3
};
byte[] cksum = getChecksumBytes(cksumBytes, cksumType);
return cksum;
}//getNoNetWorkBindings()
|
Sending a secure message to the e-bank's
business logic server If the
createKerberosSession() method returns true , you
know you have successfully established a secure session with the remote
Kerberos server. Now it's time to start exchanging messages with the
Kerberos server.
Look at the sendSecureMessage() method of Listing 23. This method
takes as parameters a plain text message, a cryptographic key, a sequence
number (which uniquely identifies the message being sent), and input and
output stream objects to exchange data with the server. The
sendSecureMessage() method authors a secure message, sends
the message to the server over the output stream, listens for a response
from the server, and returns the server's response.
The message sent to the server is secured using the sub-session key.
This means that only the intended recipient (the e-bank's business logic
server, which has the sub-session key) is capable of decrypting and
understanding the message. Moreover, the secure message contains message
integrity data, so the e-bank's server can verify the integrity of the
message coming from the client.
Let's have a look at how the sendSecureMessage() method
authors a secure GSS message from a plain text message.
A GSS secure message comes in the form of a token (an array of bytes
according to a token format). The token format consists of the following
components:
- A GSS header similar to the header that I talked about while
discussing the
createKerberosSession() method.
- An eight-byte token header. There can be several different types of
token in the GSS-Kerberos specification, where each token type is
identified by a unique header. The only type you are interested in is
the secure message token that you want to author in the
sendSecureMessage() method. A secure message token is
identified by the header with a value 0x02, 0x01, 0x00, 0x00,
0x00, 0x00, 0xff, and 0xff .
- An encrypted sequence number, which helps in detecting replay
attacks. For example, if some malicious hacker wants to replay (that is,
repeat) a money transfer instruction, he cannot author the correct
sequence number in encrypted form (unless, of course, he knows the
sub-session key).
- An encrypted digest value of the message.
- The encrypted form of the message.
The five fields listed above are linked together in their correct order
and then wrapped inside an ASN.1 application-level tag number 0. This
forms a complete GSS-Kerberos secure message token, as shown in Figure 1.
Figure 1.
In order to author the complete secure token shown in Figure 1, you must
author each of the five fields.
The first two fields have no dynamic content; they are the same in all
secure messages, so I have hard coded their values in Listing 23. The other
three fields must be dynamically computed according to the following
algorithm: 1. Add extra padding
bytes to the plain text message so that the number of bytes in the message
becomes an exact multiple of eight. 2. Generate an
eight-byte random number called confounder . Link the
confounder and the padded message of step 1. 3. Concatenate the token
header (second field of Figure 1) and the result
of step 2. Then calculate the 16-byte MD5 digest value over the result of
the linkage. 4. Encrypt the 16-byte
digest value of step 3 using the sub-session key. The
algorithm of encryption is DES-CBC with zero IV. The last eight bytes of
encrypted data (discarding the first eight bytes) form the fourth field
(encrypted digest value) of Figure 1. 5. Here is where you
must author an 8-byte sequence number in encrypted form (the third field
of Figure 1). The sequence
number is encrypted using the sub-session key and the last
eight bytes of the encrypted digest value of step 4 used in IV. 6. Now take the result
of step 2 (confounder and padded message linked together) and DES-CBC
encrypt it. For this encryption, you use a key generated by exclusively
ORing all bytes of the sub-session key with
0xF0 . The result of this encryption forms the fifth field, or
encrypted message, of Figure 1.
After authoring the individual fields, you link them in a byte array
and, finally, call the getTagAndLengthBytes() method to
prepend an application-level tag number 0 before the linked byte array.
You can track these steps in the sendSecureMessage()
method of Listing 23. After
authoring the secure message, you send the message to the server over the
output stream, listen for the server's response, and return the response
on receipt. Listing 23. The sendSecureMessage()
method
public byte[] sendSecureMessage( String message, byte[] sub_sessionKey,
int seqNumber,
DataInputStream inStream,
DataOutputStream outStream
)
{
byte[] gssHeaderComponents = {
(byte)0x6,
(byte)0x9,
(byte)0x2a,
(byte)0x86,
(byte)0x48,
(byte)0x86,
(byte)0xf7,
(byte)0x12,
(byte)0x01,
(byte)0x02,
(byte)0x02
};
byte[] tokenHeader = {
(byte)0x02,
(byte)0x01,
(byte)0x00,
(byte)0x00,
(byte)0x00,
(byte)0x00,
(byte)0xff,
(byte)0xff
};
try {
/***** Step 1: *****/
byte[] paddedDataBytes = getPaddedData (message.getBytes());
/***** Step 2: *****/
byte[] confounder = concatenateBytes (getRandomNumber(), getRandomNumber());
/***** Step 3: *****/
byte[] messageBytes = concatenateBytes(confounder, paddedDataBytes);
byte[] digestBytes = getMD5DigestValue(
concatenateBytes (tokenHeader,messageBytes));
CBCBlockCipher cipher = new CBCBlockCipher(new DESEngine());
KeyParameter kp = new KeyParameter(sub_sessionKey);
ParametersWithIV iv = new ParametersWithIV (kp, new byte[8]);
cipher.init(true, iv);
byte processedBlock[] = new byte[digestBytes.length];
byte message_cksum[] = new byte[8];
for(int x = 0; x < digestBytes.length/8; x ++) {
cipher.processBlock(digestBytes, x*8, processedBlock, x*8);
System.arraycopy(processedBlock, x*8, message_cksum, 0, 8);
iv = new ParametersWithIV (kp, message_cksum);
cipher.init (true, iv);
}
/***** Step 4: *****/
byte[] sequenceNumber = {
(byte)0xff,
(byte)0xff,
(byte)0xff,
(byte)0xff,
(byte)0x00,
(byte)0x00,
(byte)0x00,
(byte)0x00
};
sequenceNumber[0] = (byte)seqNumber;
/***** Step 5: *****/
byte[] encryptedSeqNumber = encrypt(sub_sessionKey, sequenceNumber, message_cksum);
/***** Step 6: *****/
byte[] encryptedMessage = encrypt(getContextKey(sub_sessionKey),
messageBytes, new byte[8]);
byte[] messageToken = getTagAndLengthBytes (
ASN1DataTypes.APPLICATION_TYPE,
0,
concatenateBytes (
gssHeaderComponents, concatenateBytes (
tokenHeader, concatenateBytes (
encryptedSeqNumber, concatenateBytes (
message_cksum, encryptedMessage
)
)
)
)
);
/***** Step 7: *****/
outStream.writeInt(messageToken.length);
outStream.write(messageToken);
outStream.flush();
/***** Step 8: *****/
byte[] responseToken = new byte[inStream.readInt()];
inStream.readFully(responseToken);
return responseToken;
} catch(IOException ie){
ie.printStackTrace();
} catch(Exception e){
e.printStackTrace();
}
return null;
}//sendSecureMessage
public byte[] getContextKey(byte keyValue[])
{
for (int i =0; i < keyValue.length; i++)
keyValue[i] ^= 0xf0;
return keyValue;
}//getContextKey
|
Decoding the server message
The server message returned by the
sendSecureMessage() method is secure, just like the one you
authored and sent to the server. It follows the same token format as shown
in Figure 1, meaning that
only the client possessing the sub-session key can decrypt
the message.
I have written a method named decodeSecureMessage() (shown
in Listing 24) that takes a
secure message along with a decryption key, decrypts the message, and
returns the plain text form of the message. The decode algorithm is like
this:
- The first step is to separate the encrypted portion of the message
(the fifth field shown in Figure 24) from the token headers. The length
of token headers is fixed, so only the number of length bytes can vary,
depending on the total length of the message. Therefore, you just have
to read the number of length bytes and copy the encrypted portion of the
message into a separate byte array accordingly.
- The second step is to read the message checksum (the fourth field of
Figure
1)
- Now you decrypt the encrypted message using the decryption key.
- Next, take the token header (the second field of Figure
1), link it with the decrypted message, and then take the MD5 digest
value of the linked byte array.
- At this point you encrypt the MD5 digest value.
- You must compare the eight-byte message checksum of step 2 with the
last eight bytes of the MD5 digest value of step 5. If they match, the
integrity check is verified.
- With this verification, you remove the cofounder (the first eight
bytes of the decrypted message) and return the rest of the message
(which is the required plain text message).
Listing 24. The decodeSecureMessage() method
public String decodeSecureMessage (byte[] message, byte[] decryptionKey){
int msg_tagAndHeaderLength = 36;
int msg_lengthBytes = getNumberOfLengthBytes (message[1]);
int encryptedMsg_offset = msg_tagAndHeaderLength + msg_lengthBytes;
byte[] encryptedMessage = new byte[message.length - encryptedMsg_offset];
System.arraycopy(message, encryptedMsg_offset,
encryptedMessage, 0,
encryptedMessage.length);
byte[] msg_checksum = new byte[8];
System.arraycopy(message, (encryptedMsg_offset-8),
msg_checksum, 0,
msg_checksum.length);
byte[] decryptedMsg = decrypt (decryptionKey, encryptedMessage, new byte[8]);
byte[] tokenHeader = {
(byte)0x2,
(byte)0x1,
(byte)0x0,
(byte)0x0,
(byte)0x0,
(byte)0x0,
(byte)0xff,
(byte)0xff
};
byte[] msg_digest = getMD5DigestValue (concatenateBytes(tokenHeader,decryptedMsg));
byte[] decMsg_checksum = new byte[8];
try {
CBCBlockCipher cipher = new CBCBlockCipher(new DESEngine());
KeyParameter kp = new KeyParameter(getContextKey(decryptionKey));
ParametersWithIV iv = new ParametersWithIV (kp, decMsg_checksum);
cipher.init(true, iv);
byte[] processedBlock = new byte[msg_digest.length];
for(int x = 0; x < msg_digest.length/8; x ++) {
cipher.processBlock(msg_digest, x*8, processedBlock, x*8);
System.arraycopy(processedBlock, x*8, decMsg_checksum, 0, 8);
iv = new ParametersWithIV (kp, decMsg_checksum);
cipher.init (true, iv);
}
} catch(java.lang.IllegalArgumentException il){
il.printStackTrace();
}
for (int x = 0; x < msg_checksum.length; x++) {
if (!(msg_checksum[x] == decMsg_checksum[x]))
return null;
}
return new String (decryptedMsg,
msg_checksum.length,
decryptedMsg.length - msg_checksum.length);
}//decodeSecureMessage()
public byte[] getContextKey(byte keyValue[])
{
for (int i =0; i < keyValue.length; i++)
keyValue[i] ^= 0xf0;
return keyValue;
}//getContextKey
|
The sample mobile banking
application You have completed all phases of secure Kerberos
messaging required by the sample mobile banking application. It's now time
to discuss how the mobile banking MIDlet uses the functionality of the
Kerberos client and communicates with the e-bank's server.
Listing 25 shows a
simple MIDlet, which simulates the sample mobile banking application.
Listing 25. A sample mobile banking MIDlet
import java.io.*;
import java.util.*;
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
import javax.microedition.io.*;
public class J2MEClientMIDlet extends MIDlet implements CommandListener, Runnable {
private Command OKCommand = null;
private Command exitCommand = null;
private Command sendMoneyCommand = null;
private Display display = null;
private Form transForm;
private Form transResForm;
private Form progressForm;
private TextField txt_userName;
private TextField txt_password;
private TextField txt_amount;
private TextField txt_sendTo;
private StringItem si_message;
private TextField txt_label;
private SocketConnection sc;
private DataInputStream is;
private DataOutputStream os;
private DatagramConnection dc;
private KerberosClient kc;
private TicketAndKey tk;
private String realmName = "EBANK.LOCAL";
private String kdcServerName = "krbtgt";
private String kdcAddress = "localhost";
private int kdcPort = 8080;
private String e_bankName = "ebankserver";
private String e_bankAddress = "localhost";
private int e_bankPort = 8000;
private int i =0;
private byte[] response;
public J2MEClientMIDlet() {
exitCommand = new Command("Exit", Command.EXIT, 0);
sendMoneyCommand = new Command("Pay", Command.SCREEN, 1);
OKCommand = new Command("Back", Command.EXIT, 2);
display = Display.getDisplay(this);
transactionForm();
}
public void startApp() {
Thread t = new Thread(this);
t.start();
}//startApp()
public void pauseApp() {
}//pauseApp()
public void destroyApp(boolean unconditional) {
}//destroyApp
public void commandAction(Command c, Displayable s) {
if (c == exitCommand) {
destroyApp(false);
notifyDestroyed();
} else if(c == sendMoneyCommand) {
sendMoney();
} else if (c == OKCommand) {
transactionForm();
} else if (c == exitCommand) {
destroyApp(true);
}
}//commandaction
public void sendMoney() {
System.out.println("MIDlet... SendMoney() Starts");
String userName = txt_userName.getString();
String password = txt_password.getString();
kc.setParameters(userName, password, realmName);
System.out.println("MIDlet... Getting TGT Ticket");
response = kc.getTicketResponse (
userName,
kdcServerName,
realmName,
null,
null
);
System.out.println ("MIDLet...Getting Session Key from TGT Response");
tk = new TicketAndKey();
tk = kc.getTicketAndKey(response, kc.getSecretKey());
System.out.println ("MIDLet...Getting Service Ticket (TGS)");
response = kc.getTicketResponse (
userName,
e_bankName,
realmName,
tk.getTicket(),
tk.getKey()
);
System.out.println ("MIDLet...Getting Sub-Session Key from TGS Response");
tk = kc.getTicketAndKey( response, tk.getKey());
i++;
System.out.println ("MIDLet...Establishing Secure context with E-Bank");
boolean isEstablished = kc.createKerberosSession (
tk.getTicket(),
realmName,
userName,
i,
tk.getKey(),
is,
os
);
if (isEstablished) {
System.out.println ("MIDLet...Sending transactoin message over secure context");
byte[] rspMessage = kc.sendSecureMessage(
"Transaction of Amount:"+txt_amount.getString()
+ " From: "+userName
+" To: "+txt_sendTo.getString(),
tk.getKey(),
i,
is,
os
);
String decodedMessage = kc.decodeSecureMessage(rspMessage, tk.getKey());
if (decodedMessage!=null)
showTransResult(" OK", decodedMessage);
else
showTransResult(" Error!", "Transaction failed..");
} else
System.out.println ("MIDlet...Context establishment failed..");
}//sendMoney()
public synchronized void run() {
try {
dc = (DatagramConnection)Connector.open("datagram://"+kdcAddress+":"+kdcPort);
kc = new KerberosClient(dc);
sc = (SocketConnection)Connector.open("socket://"+e_bankAddress+":"+e_bankPort);
sc.setSocketOption(SocketConnection.KEEPALIVE, 1);
is = sc.openDataInputStream();
os = sc.openDataOutputStream();
} catch (ConnectionNotFoundException ce) {
System.out.println("Socket connection to server not found....");
} catch (IOException ie) {
ie.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}//run
public void transactionForm(){
transForm = new Form("EBANK Transaction Form");
txt_userName = new TextField("Username", "", 10, TextField.ANY);
txt_password = new TextField("Password", "", 10, TextField.PASSWORD);
txt_amount = new TextField("Amount", "", 4, TextField.NUMERIC);
txt_sendTo = new TextField("Pay to", "", 10, TextField.ANY);
transForm.append(txt_userName);
transForm.append(txt_password);
transForm.append(txt_amount);
transForm.append(txt_sendTo);
transForm.addCommand(sendMoneyCommand);
transForm.addCommand(exitCommand);
transForm.setCommandListener(this);
display.setCurrent(transForm);
}
public void showTransResult(String info, String message) {
transResForm = new Form("Transaction Result");
si_message = new StringItem("Status:" , info);
txt_label = new TextField("Result:", message, 150, TextField.ANY);
transResForm.append(si_message);
transResForm.append(txt_label);
transResForm.addCommand(exitCommand);
transResForm.addCommand(OKCommand);
transResForm.setCommandListener(this);
display.setCurrent(transResForm);
}
}//J2MEClientMIDlet
|
When you run the MIDlet, you get a screen as shown in Figure 2.
Figure 2.
Figure 2 shows that you
have developed a very simple GUI for the sample mobile banking
application. Figure 2 also shows four
data entry fields:
- The
"Username" field takes the user name of the person
wanting to use the financial services of the mobile banking MIDlet.
- The
"Password" field takes the user's password.
- The
"Amount" field allows the entry of the amount of
money that you want to pay to a beneficiary.
- The
"Pay to" field contains the user name of the
beneficiary.
After entering your data, press the Pay button. The event handler for
the Pay button (sendMoney() method in Listing 25) performs all
seven phases of Kerberos messaging:
- Author a
TGT request, send the request to the server,
and receive the TGT response.
- Extract the
TGT and the session key from the
TGT response.
- Author a service ticket request, send the request to the
KDC , and receive the service ticket response.
- Extract the service ticket and the sub-session key from the service
ticket response.
- Author and send a context establishment request to the e-bank's
business logic server, receive the response, and parse it to make sure
that the server agrees to establish a new secure context.
- Author a secure message, send the message to the server, and receive
the response from the server.
- Decode the message from the server.
The MIDlet code of Listing 25 is quite
simple and does not need much explanation. Just make note of a few points:
- I have used a separate thread (the
run() method in Listing
25) to create a Datagram connection (dc )
and data input and output streams on a Socket connection
(sc ). That's because MIDP 2.0 does not allow creating
Datagram and Socket connections in the main
execution thread of a J2ME MIDlet.
- I have hard-coded the realm, server name, address, and port of the
KDC server and the name and address of the e-bank's server in the J2ME
MIDlet of Listing
25. Note that this hard-coding is only in the MIDlet for
demonstration. The
KerberosClient , on the other hand, is
fully reusable.
- In order to try this application, you need a GSS server running as
an e-bank's server. The source
code download of this article contains a server-side application and
a readme.txt file that describes how to run the server.
- Finally, notice that I have not designed the e-bank's messaging
framework; I have just designed a Kerberos-based security framework. You
can design your own messaging and use the KerberosClient for security
support. For example, you might want to use XML formatting to define the
different types of messages as money transfer instructions.
Summary In this three-part
series of articles, I have demonstrated secure Kerberos messaging in a
J2ME application. You have studied the details of Kerberos messages that
result in an exchange of cryptographic keys. You have also learned how a
J2ME application uses the keys to establish a communication context and
securely exchange messages with a remote e-bank server. I have also
provided the J2ME code that demonstrates all the concepts that the
articles have discussed.
Resources
About the
author Faheem Khan is an independent software consultant
specializing in Enterprise Application Integration (EAI) and B2B
solutions. Readers can reach Faheem at fkhan872@yahoo.com. |
|
|