IBM Skip to main content
Search for:   within 
      Search help  
     IBM home  |  Products & services  |  Support & downloads   |  My account

developerWorks > Wireless
developerWorks
Lock down J2ME applications with Kerberos, Part 3: Establish secure communication with an e-bank
code221 KBe-mail it!
Contents:
Sending the TGT request to the KDC server
Processing a TGT response
Extracting the ticket and key from the ticket response
Getting a service ticket
Authoring a service ticket request
Extracting the service ticket and sub-session key from the service ticket response
Creating a secure communication context
Sending a secure message to the e-bank's business logic server
Decoding the server message
The sample mobile banking application
Summary
Resources
About the author
Rate this article
Related content:
Simplify enterprise Java authentication with single sign-on
Subscriptions:
dW newsletters
dW Subscription
(CDs and downloads)
Set up the server, request a ticket, retrieve the response

Level: Intermediate

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:

  1. isSequence()
  2. getIntegerValue()
  3. isASN1Structure()
  4. getNumberOfLengthBytes()
  5. getLength()
  6. getASN1Structure()
  7. 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:

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

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

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

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

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

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

  1. encrypt()
  2. decrypt()
  3. getMD5DigestValue()
  4. 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:

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

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

  1. First, you instantiate an MD5Digest object.
  2. Then, you call the update() method of the MD5Digest object, passing the data to be digested along with the method call.
  3. Next, instantiate an output byte array to hold the MD5 digest value.
  4. 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 EncryptedDataSEQUENCE 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:

  1. getMD5DigestValue()
  2. getChceksumBytes()
  3. authorDigestAndEncrypt()
  4. 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:

  1. First, you generate eight random bytes, which form the confounder.
  2. 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.
  3. 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.
  4. The fourth step is to calculate the MD5 digest value over the concatenated byte array of step 3.
  5. 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.
  6. Now you call the encrypt() method to encrypt step 5's byte array.
  7. Next, you author the etype field (context-specific tag number 0).
  8. 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).
  9. 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:

  1. The byte array named ticketContent contains the Kerberos ticket (the TGT) that the getAuthenticationHeader() method wraps inside an authentication header.
  2. The string type parameter named clientRealm specifies the name of the realm in which the Kerberos client (who is authoring this request) is registered.
  3. The string type parameter named clientName specifies the name of the Kerberos client that is authoring this request.
  4. The checksumBytes byte array carries a Checksum structure along with the getChecksumBytes() method.
  5. The encryptionKey byte array carries the encryption key that you use to produce the encrypted portion of the authentication header.
  6. 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:

  1. The authenticator structure contains the cksum field.
  2. The cksum field contains the cryptographic hash value of some application data.
  3. 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:

  1. 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).
  2. Call the getChecksumBytes() method and pass on the data for the cksumtype and checksum fields. The getChecksumBytes() method authors the complete Checksum structure.
  3. 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:

  1. Wrap the authentication header in an OCTET STRING by calling the getOctetStringBytes() method that I described in Listing 5 of the second article.
  2. Wrap the OCTET STRING in the padata-value field (context-specific tag number 2); a getTagAndLengthBytes() method call can do this job.
  3. Another getTagAndLengthBytes() method call authors the padata-type field that accompanies the padata-value you authored in step 2.
  4. Now, link the padata-type and padata-value fields.
  5. 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.
  6. 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.
  7. 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:

  1. The ticketContent byte array caries the service ticket that you intend to use in order to establish a secure context.
  2. The clientRealm string wraps the name of the realm to which the requesting client belongs.
  3. The clientName string specifies the name of the requesting client.
  4. The sequenceNumber parameter is an integer that represents the sequence number of this message.
  5. encryptionKey: the sub-session key.
  6. 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:

  1. A GSS header similar to the header that I talked about while discussing the createKerberosSession() method.
  2. 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.
  3. 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).
  4. An encrypted digest value of the message.
  5. 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:

  1. 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.
  2. The second step is to read the message checksum (the fourth field of Figure 1)
  3. Now you decrypt the encrypted message using the decryption key.
  4. 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.
  5. At this point you encrypt the MD5 digest value.
  6. 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.
  7. 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:

  1. The "Username" field takes the user name of the person wanting to use the financial services of the mobile banking MIDlet.
  2. The "Password" field takes the user's password.
  3. The "Amount" field allows the entry of the amount of money that you want to pay to a beneficiary.
  4. 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:

  1. Author a TGT request, send the request to the server, and receive the TGT response.
  2. Extract the TGT and the session key from the TGT response.
  3. Author a service ticket request, send the request to the KDC, and receive the service ticket response.
  4. Extract the service ticket and the sub-session key from the service ticket response.
  5. 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.
  6. Author a secure message, send the message to the server, and receive the response from the server.
  7. 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.


code221 KBe-mail it!

What do you think of this document?
Killer! (5) Good stuff (4) So-so; not bad (3) Needs work (2) Lame! (1)

Comments?



developerWorks > Wireless
developerWorks
  About IBM  |  Privacy  |  Terms of use  |  Contact