用Kerberos为J2ME应用程序上锁,第3部分-建立与电子银行的安全通

得到一个服务票据

已经处理了 TGT 响应并提取了 TGT 和会话密钥。现在可以使用这个 TGT 和会话密钥向 KDC 服务器 请求一个服务票据。对服务票据的请求类似于对我在清单 1 中生成的对 TGT 的请求。我在 TGT 请求中 省略的可选 padata 字段在服务票据请求中不再是可选的了。因此,需要在服务票据请求中加上 padata 字段。

padata 字段是包含两个字段 —— padata-type 和 padata-value —— 的 SEQUENCE。padata-value 字段带有几种类型的数据,因此相应的 padata-type 字段指定了 padata-value 字段所带的数据的类型 。

在 本系列的第一篇文章的图 5 中我介绍了服务票据中的 padata 字段的结构。在那里说过服务票据 请求中的 padata 字段包装了一个认证头(一个 KRB_AP_REQ 结构),它又包装了 TGT 以及其他数据。

所以,在可以开始生成票据请求之前,必须生成一个认证头。下面是分析了生成认证头的过程。

生成一个认证头

我在 KerberosClient 类中加入了以下方法以生成一个认证头:

getMD5DigestValue()

getChceksumBytes()

authorDigestAndEncrypt()

getAuthenticationHeader()

这四个方法都是 helper 方法。第五个方法( getAuthenticationHeader() )使用 helper 方法并生 成认证头。

authorDigestAndEncrypt()

清单 15 显示的 authorDigestAndEncrypt() 方法取一个纯文本数据字节数组和一个加密密钥。这个 方法对纯文本数据计算一个摘要值、加密纯文本数据、并返回一个 EncryptedData 结构,这个结构与我 作为输入传递给 清单 12 的 decryptAndVerifyDigest() 方法的结构完全匹配。

可以说 清单 15 的 authorDigestAndEncrypt() 方法与前面讨论的 decryptAndVerifyDigest() 方法 正好相反。authorDigestAndEncrypt() 方法取 decryptAndVerifyDigest() 方法返回的纯文本数据作为 输入。与此类似, authorDigestAndEncrypt() 方法返回的 EncryptedData 结构就是我作为输入传递给 decryptAndVerifyDigest() 方法的结构。

authorDigestAndEncrypt() 方法实现了以下策略:

首先,生成八个随机字节,它们构成了 confounder。

然后,声明一个名为 zeroedChecksum 的字节数组,它有十六个字节并初始化为零。这个有十六个零 的数组作为一个全为零的摘要值。

第三,用其他的字节填入输入数据字节数组,以使数组中的字节数成为八的倍感数。编写了一个名为 getPaddedData() 的方法(如 清单 16所示),它取一个字节数组并在填充后返回这个数组。下面,链接 (第 1 步得到的)confounder、(第 2 步得到的)全为零的摘要以及填充后的纯文本字节数组。

第四步是对第 3 步串接的字节数组计算 MD5 摘要值。

第五步是将摘要字节放到它们相应的位置上。第 5 的结果与第 3 步一样,只不过全为零的摘要现在 换成了真正的摘要值。

现在调用 encrypt() 方法以加密第 5 步得到的字节数组。

然后,生成 etype 字段(特定于上下文的标签号 0)。

然后,调用 getOctetStringBytes() 方法将第 6 步得到的加密字节数组包装到 OCTET STRING 中。 然后将 OCTET STRING 包装到 cipher 字段中(一个特定于上下文的标签号 2)。

最后,链接 etype 和 cipher 字段,将这个字符串包装到一个 SEQUENCE 中,并返回这个 SEQUENCE 。

清单 15. authorDigestAndEncrypt() 方法

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

清单 16. getPaddedData() 方法

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()

getChecksumBytes() 方法生成一个称为 Checksum 的结构,如 清单 17 所示。Checksum 结构包含两 个字段: cksumtype 和 checksum。

清单 17. Checksum 结构

Checksum ::= SEQUENCE {         cksumtype[0]  INTEGER,         checksum[1]  OCTET STRING   }+

有两个地方需要 Checksum 结构 —— 第一个是生成服务票据响应时,然后是生成安全上下文建立请 求时。Checksum 结构的作用在这两种情况下是不同的,需要在生成服务票据和上下文建立请求时说明 (elaborate)。

清单 18 所示的 getChecksumBytes() 方法取两个字节数组参数。第一个参数带有 checksum 字段, 而第二个参数带有 cksumtype 字段。

getChecksumBytes() 方法将 cksumtype 字段包装到一个特定于上下文的标签号 0(它表示 cksumtype 字段,如 清单 17 所示),而将 checksum 字段包装到一个特定于上下文的标签号 1(它表 示 checksum 字段,同样如 清单 17 所示)。然后它链接这两个字段,将这个数组包装到一个 SEQUENCE 中,并返回这个 SEQUENCE。

清单 18. getChecksumBytes() 方法

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()

在 本系列的第一篇文章 中的“服务票据请求”一节中,介绍过 KRB-AP-REQ 结构(也称为认证头) 包装了 Kerberos 票据。此外,认证头还包装了 authenticaTor 字段,它表明客户机是否掌握了 会话 或者 子会话 密钥。

如 第一篇文章的图 5 所示,认证头由五个字段组成,即 pvno、msg-type、ap-options、ticket 和 authenticaTor。

清单 19 的 getAuthenticationHeader() 方法逐一生成这五个字段,然后以正确的顺序将各个字段串 接起来以形成一个完整的认证头。

清单 19. getAuthenticationHeader() 方法

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

getAuthenticationHeader() 方法有几个输入参数:

名为 ticketContent 的字节数组,它包含由 getAuthenticationHeader() 方法包装到认证头的 Kerberos 票据( TGT )。

名为 clientRealm 的字符串类型参数,它指定(生成这个请求的)Kerberos 客户机所注册的域 (realm )的名字。

名为 clientName 的字符串类型参数指定生成这个请求的 Kerberos 客户机的名字。

checksumBytes 字节数组携带一个 Checksum 结构以及 getChecksumBytes() 方法。

encryptionKey 字节数组携带用于生成认证头的加密部分的加密密钥。

名为 sequenceNumber 的参数是一个 integer 值,它标识发送者的请求号。

在 第一篇文章的图 5 介绍过,认证头包含以下字段:

pvno

msg-type

ap-options

ticket

authenticaTor

现在让我们看看 清单 19 中的 getAuthenticationHeader() 方法实现是如何生成认证头的各个字段 的( KRB-AP-REQ 结构):

首先要生成 pvno 字段,它有特定于上下文的标签号 0,并包装一个值为 5 的 ASN1 INTEGER 。调用 getTagAndLengthBytes() 方法执行这项任务。我将 pvno 字段存储 在一个名为 vno 的字节数组中。

类似地,两次调用 getTagAndLengthBytes() 方法生成 msg-type (特定于上下文的标签号 1)和 ap-options 字段(特定于上下文的标签号 2)。

下一行( byte[] ticket = getTagAndLengthBytes(ASN1DataTypes.Context_Specific, 3, ticketContent) )将票据结构包装到特定于上下文的标签号 3 中,这是认证头的第四个字段。

然后,必须生成认证头的第五个字段(名为 authenticaTor ,它有特定于上下文的标签号 4)。 authenticaTor 字段是一个 EncryptedData 结构。authenticaTor 字段的纯文本格式是一个 AuthenticaTor 结构。因此,首先生成纯文本格式的完整 AuthenticaTor 结构,将这个纯文本 AuthenticaTor 传递给 authorDigestAndEncrypt() 方法,这个方法返回 AuthenticaTor 的完整 EncryptedData 表示。

注意在 第一篇文章中的清单 3 和图 5 中,纯文本格式的 AuthenticaTor 结构由以下字段组成(省 略最后一个字段,它是不需要的):

authenticaTor-vno

creal

cname

cksum

cusec

ctime

subkey

seq-number

在解释 第一篇文章的图 5时,我已经介绍了每一个字段的意义。

authenticaTor-vno 字段与 pvno 字段完全相同(本节前面讨论了 vno 字节数组,它包含特定于上下 文的标签号 0 且带值为 5 的 INTEGER )。所以我重用了在 authenticaTor_vno 字段中使用的同一个字 节数组。

现在该生成 crealm 字段了,它类似于我在 第二篇 文章“生成请求正文”一节中介绍的 realm 字段 。同样,在那一节也介绍了 PrincipalName 类型的 cname 字段。在这里我就不介绍 crealm 和 cname 字段的生成细节了。

下一项任务是生成 cksum 字段,它是 Checksum 类型。服务票据请求中的 cksum 字段的作用是加密 结合 authenticaTor 与一些应用程序数据。注意以下三点:

authenticaTor 结构包含 cksum 字段。

cksum 字段包含一些应用程序数据的加密哈希值。

整个 authenticaTor 结构(包括 cksum 字段)是用一个密钥加密的。

只要在 authenticaTor 中的 cksum 字段与对应用程序数据的加密 checksum 相匹配,就证明生成 authenticaTor 和应用程序数据的客户机拥有密钥。

调用 getAuthenticationHeader() 方法的应用程序(通过调用 getChecksumBytes() 方法)生成 Checksum 结构,并将 Checksum 字节数组作为 checksumBytes 参数的值传递给 getAuthenticationHeader() 方法。

结果, checksumBytes 参数中就有了 Checksum 结构。只需要将 checksumBytes 包装到特定于上下 文的标签号 3 中(这是 authenticaTor 结构中 cksum 字段的标签号)。

现在生成 cusec 字段,它表示客户机时间的微秒部分。这个字段的取值范围为 0 到 999999。这意味 着可以在这个字段提供的最大值为 999999 微秒。不过,MIDP 不包含任何可以提供比一毫秒更精确的时 间值的方法。因此,不能指定客户机的微秒部分。只是对这个字段传递一个零值。

在 AuthenticaTor 结构中,还要生成两个字段 —— subkey 和 seq-number 。在为票据请求而生成 的 AuthenticaTor 中不一定要包含这两个字段,但是后面在用同一个 getAuthenticationHeader() 方法 生成上下文建立请求时需要它们。

现在,只需知道只要检查 sequenceNumber 参数是否为零。对于服务票据请求它为零,对于上下文建 立请求它为非零。

如果 sequenceNumber 参数为非零,那么就生成 subkey 和 seq-number 字段,然后链接 authenticaTor-vno、 realm、cname、cksum、cusec、ctime、subkey 和 seq-number 字段以构成一个字 节数组,将这个字节数组包装到一个 SEQUENCE 中,然后将 SEQUENCE 包装到 AuthenticaTor 中(应用 程序级别标签号 2)。

如果 sequenceNumber 参数为零,那么可以忽略 subkey 和 seq-number 字段,链接 authenticaTor -vno、crealm、cname、cksum、cusec 和 ctime 字段以构成串接的字节数组,将这个字节数组包装到一 个 SEQUENCE 中,然后将这个 SEQUENCE 包装到 AuthenticaTor 中(应用程序级别标签号 2)。

下面,需要取完整的 AuthenticaTor 结构并将它传递 authorDigestAndEncrypt() 方法,这个方法返 回纯文本 AuthenticaTor 的完整 EncryptedData 表示。

下一个任务是串接认证头或者 KRB-AP-REQ 结构的五个字段( pvno、msg-type、ap-options、ticket 、authenticaTor )为一个字节数组,将这个字节数组包装为一个 SEQUECNE ,最后将这个 SEQUENCE 包 装到应用程序级别的标签号 14。

现在已经完成认证头,可以将它返回给给调用应用程序了。

生成服务票据请求

我讨论了生成服务票据请求需要的所有低层方法。将使用 清单 1 中请求 TGT 时所使用的同一个 getTicketResponse() 方法生成服务票据请求,只需要对 清单 1 稍加修改以使它可以同时用于 TGT 和 服务票据请求。让我们看一下这个过程。

看一下 清单 20,其中可以看到修改过的清单 1 中的 getTicketRespone() 方法。与 清单 1相比, 修改过的版本增加了一些代码:

清单 20. getTicketResponse() 方法

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

清单 20 中显示的新的 getTicketResponse() 方法有五个参数: userName、serverName、realmName 、kerberosTicket 和 key 。要请求一个服务票据,需要传递 kerberosTicket 字节数组的 TGT 。另一 方面,在请求 TGT 时,不必传递一个票据,因此对于 kerberosTicket 字节数组传递“null”。

TGT 请求与服务票据请求的主要区别是 padata 字段。在本系列 第一篇文章 中的“请求服务票据” 一节中讨论服务票据请求的 padata 字段时已经介绍过。

在 getTicketResponse() 的最后,我加入了一个 if (kerberosTicket!=null) 块。只有在 kerberosTicket 参数不为 null 时才进入这个块(在所有 TGT 请求中它都是 null)。

在 if (kerberosTicket!=null) 块中,我生成了 padata 字段。正如 第一篇文章的图 5 中描述的, 这个 padata 字段包装一个可由 getAuthenticationHeader() 方法生成的认证头。

在 getAuthenticationHeader() 方法中还可了解到,为了生成一个认证头,需要一个可由 getChecksumBytes() 方法生成的 Checksum 结构。

现在,回想一下在讨论 getChecksumBytes() 方法时说过,为了生成一个 Checksum 结构,需要用于 cksumtype 和 checksum 字段的数据。

因此,生成一个认证头需要三步:

生成 cksumtype 和 checksum 字段的数据。如果是服务票据请求,那么 checksum 字段的数据只是对 包含服务票据请求的 req-body 字段的所有子字段的 SEQUENCE 计算的 MD5 摘要值(注意在 第一篇文章 的图 5, req-body 是服务票据请求的第四个字段,就在服务票据请求的第三个字段 padata 字段后面) 。cksumtype 字段的数据是 integer 7 的 ASN1 表示。这个值指定 checksum 的类型。

调用 getChecksumBytes() 方法并传递 cksumtype 和 checksum 字段的数据。getChecksumBytes() 方法生成完整的 Checksum 结构。

调用 getAuthenticationHeader() 方法,同时传递 Checksum 结构。getAuthenticationHeader() 返 回认证头。

生成了认证头后,必须将它包装到一个 padata 字段中。为此,有几件事要做:

调用我在 第二篇文章的清单 5 中描述的 getOctetStringBytes() 方法将认证头包装到一个 OCTET STRING 中。

将这个 OCTET STRING 包装到 padata-value 字段中(特定于上下文的标签号 2),调用 getTagAndLengthBytes() 方法以完成这项任务。

再次调用 getTagAndLengthBytes() 方法生成对应于第 2 步生成的 padata-value 的 padata-type 字段。

现在,链接 padata-type 和 padata-value 字段。

将第 4 步链接的字节数组放入一个 SEQUENCE 中。这个 SEQUENCE 表示一个 PADATA 结构(如 第一 篇文章的图 5 和清单 3所示)。

第一篇文章的图 5 和清单 3 中显示的 padata 字段是 PADATA 结构的一个 SEQUENCE 。这意味着一 个 padata 字段可以包含几个 PADATA 结构。不过,只有一个 PADATA 结构要包装到 padata 字段中,这 意味着只要将第 5 步得到的 SEQUENCE 包装到另一个外部或者更高层的 SEQUENCE 中。

第 6 步的更高层 SEQUENCE 表示 PADATA 结构的 SEQUENCE ,现在可以将它包装到 padata 字段中( 一个特定于上下文的标签号 3)。

在 清单 20 的结尾处的 if (kerberosTicket!=null) 块中可以找到 getTicketResponse() 方法中增 加的所有新代码。

到此就结束了对于修改现有的 getTicketResponse() 方法以使它可同时用于 TGT 和服务票据请求的 讨论。getTicketResponse() 方法生成一个服务票据请求、将这个请求发送给 KDC 、接收服务票据响应 、并将响应返回给调用应用程序。

从服务票据响应中提取服务票据和子会话密钥

服务票据响应类似于 TGT 响应。在 清单 13 中的 getTicketAndKey() 方法解析一个 TGT 响应以提 取 TGT 和会话密钥。同一个方法也解析服务票据响应以从服务票据响应中提取服务票据和子会话密钥。 所以,不必编写任何提取服务票据和子会话密钥的代码。

创建一个安全通信上下文

现在有了与电子银行的业务逻辑服务器建立安全通信上下文所需要的两项内容:子会话密钥和服务票 据。这时 Kerberos 客户机必须生成针对电子银行的业务逻辑服务器的上下文建立请求。

参见 第一篇文章的图 7 和清单 5,它们描述了客户机为建立安全上下文而发送给电子银行服务器的 消息。清单 21 中显示的 createKerberosSession() 方法处理与电子银行的业务逻辑服务器建立安全通 信上下文的所有方面(包括生成上下文建立请求、向服务器发送请求、从服务器中获得响应、解析响应以 检查远程服务器是否同意上下文建立请求,并将这些工作的结果返回给调用应用程序)。

看一下 清单 21 中的 createKerberosSession() 方法,它有以下参数:

ticketContent 字节数组带有准备用于建立安全上下文的服务票据。

clientRealm 字符串包装了请求客户机所属的域 realm 的名字。

clientName 字符串指定了请求客户机的名字。

sequenceNumber 参数是一个表示这个消息序号(sequence number)的 integer。

encryptionKey:子会话密钥。

inStream 和 outStream 是 createKerberosSession() 方法用来与电子银行的服务器通信的输入输出 流。

正如在第一篇文章中介绍的,要使用 Java-GSS 实现电子银行的服务器端逻辑。GSS-Kerberos 机制规 定服务票据包装在一个认证头中,而这个认证头本身又包装在 第一篇文章的图 7 和清单 5 中显示的 InitialContextToken 包装器中。

可以使用 清单 19 的 getAuthenticationHeader() 方法包装服务票据。回想一下在 清单 20 的 getTicketResponse() 方法中我使用了 getAuthenticationHeader() 方法包装一个 TGT。

为了生成认证头,需要一个 Checksum 。回想在讨论 清单 19 的 getAuthenticationHeader() 方法 时说过, Checksum 的目的是加密绑定认证头与一些应用程序数据。但是,与票据请求认证头不一样,上 下文建立认证头不带有应用程序数据。

GSS-Kerberos 机制出于不同的目的使用 Checksum 结构。除了将认证头绑定到一些应用程序数据外, GSS-Kerberos 机制使用一个 Checksum 结构用物理网络地址(即客户机可以用来与服务器进行安全通信 的网络地址)绑定安全上下文。如果使用这种功能,那么只能从它所绑定的网络地址上使用安全上下文。

不过,我不作准备在这个示例移动银行应用程序中使用这种功能。这就是为什么我在 Checksum 结构 中指定安全上下文没有任何绑定的缘故。为此,我编写了一个名为 getNoNetworkBindings() 的方法,如 清单 22 所示。getNoNetworkBindings() 方法非常简单。它只是生成一个硬编码的字节数组,表明不需 要任何网络绑定。然后它调用 getChecksumBytes() 方法以将硬编码的数组包装到 cksum 字段中。

得到了无网络绑定的 Checksum 的字节数组后,可以将这个数组传递给 getAuthenticationHeader() 方法,这个方法返回完整的认证头。

生成了认证头后, 清单 21 的 createKerberosSession() 方法将认证头字节数组与一个名为 gssHeaderComponents 的硬编码的字节数组相链接。gssHeaderComponents 字节数组包含一个 GSS 头的 字节表示,这个 GSS 头在上下文建立请求中将伴随一个认证头。

最后,将串接的 GSS 头和认证头包装到一个应用程序级别的标签号 0 中。GSS 要求所有上下文建立 请求都包装到应用程序级别的标签号 0 中。

现在完成了上下文建立请求。下一项任务就是通过一个输出流( outStream 对象)发送这个请求。发 送了请求后,监听并接收 inStream 对象上的响应。

当 createKerberosSession() 方法收到响应后,它就检查响应是确认创建一个新的上下文还是显示一 个错误消息。要进行这种检查,必须知道消息开始标签字节后面的长度字节的字节数。GSS 头字节(紧接 着长度字节)提供了答案。

不用解析响应以进行任何进一步的处理。只是要知道电子银行的服务器是创建了一个新会话还是拒绝 会话。如果电子银行的服务器确认创建新会话,那么 createKerberosSession() 方法就返回 true ,如 果不是,它就返回 false。

清单 21. createKerberosSession() 方法

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

清单 22. getNoNetworkBindings() 方法

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()

向电子银行的业务逻辑服务器发送安全消息

如果 createKerberosSession() 方法返回 true ,就知道成功地与远程 Kerberos 服务器建立了一个 安全会话。就可以开始与 Kerveros 服务器交换消息了。

看一下 清单 23 的 sendSecureMessage() 方法。这个方法取一个纯文本消息、一个加密密钥、一个 序号(它惟一地标识了发送的消息)和与服务器交换数据所用的输入输出流对象作为参数。 endSecureMessage() 方法生成一个安全消息、通过输出流将这个消息发送给服务器、监听服务器的响应 ,并返回服务器的响应。

发送给服务器的消息是用子会话密钥保护的。这意味着只有特定的接收者(拥有子会话密钥的电子银 行业务逻辑服务器)可以解密并理解这个消息。而且,安全消息包含消息完整性数据,所以电子银行的服 务器可以验证来自客户机的消息的完整性。

让我们看一下 sendSecureMessage() 方法是如何用一个纯文本消息生成一个安全 GSS 消息的。

一个 GSS 安全消息采用 token(token 格式的字节数组)的形式。token 格式由以下部分组成:

一个 GSS 头,类似于我在讨论 createKerberosSession() 方法时介绍的头。

一个八字节 token 头。在 GSS-Kerveros 规范中有几个不同类型的 token,每一种 token 类型都由 一个惟一的头所标识。其中只有要在 sendSecureMessage() 方法中生成的安全消息头是我们感兴趣的。 一个安全消息 token 是由具有值 0x02、0x01、0x00、0x00、0x00、0x00、0xff 和 0xff 的头所标识的 。

一个加密的序号,它有助于检测回复攻击。例如,如果有恶意的黑客想要重现(即重复)一个转账指 令,他是无法生成加密形式的正确序号的(当然,除非他知道 子会话 密钥)。

消息的加密摘要值。

加密后的消息。

将上面列出的五个字段以正确的顺序链接在一起,然后包装到一个 ASN.1 应用程序级别的标签号 0 中。这就构成了完整的 GSS-Kerberos 安全消息 token,如 图 1所示。

图 1.

为了生成如 图 1所示的完整安全 token,必须生成所有五个字段。

前两个字段没有动态内容,它们在所有安全消息中都是相同的,所以我在 清单 23中硬编码了它们的 值。另外三个字段必须根据以下算法动态计算:

1. 在纯文本消息中添加额外的字节以使消息中的字节数是八的倍数。

2. 生成一个名为 confounder 的八字节随机数。链接 confounder 与第 1 步中填充的消息。

3. 串接 token 头( 图 1中的第二个字段)和第 2 步的结果。然后对链接的结果计算 16 字节 MD5 摘要值。

4. 用 子会话 密钥加密第 3 步得到的 16 字节摘要值。加密算法是使用零 IV 的 DES-CBC。加密的 数据的最后八个字节(放弃前八个字节)构成了 图 1第四个字段(加密的摘要值)。

5. 现在必须生成一个加密的 8 字节序号( 图 1 的第三个字段)。这个序号是用 子会话 密钥和第 4 步使用 IV 的加密摘要值的后八个字节加密的。

6. 现在取第 2 步的结果(链接在一起的 confounder 和填充的消息)并用 DES-CBC 加密它。要进行 这种加密,使用一个用 0xF0 对 子会话 密钥的所有字节执行 OR 操作生成的密钥。这种加密得到的结果 构成了 图 1的第五个字段,也就是加密的消息。

生成了各个字段后,将它们链接为一个字节数组,最后,调用 getTagAndLengthBytes() 方法以在链 接的字节数组前面附加一个应用程序级别的标签号 0。

可以观察 清单 23 的 sendSecureMessage() 方法中的这些步骤。生成了安全消息后,通过输出流将 消息发送给服务器、监听服务器的响应并将响应返回给接收者。

清单 23. sendSecureMessage() 方法

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

解码服务器消息

就像生成并发送给服务器的消息一样, sendSecureMessage() 方法返回的服务器消息是安全的。它遵 循 图 1 所示的同样的 token 格式,这意味着只有拥有 子会话 密钥的客户机才能解密这个消息。

我编写了一个名为 decodeSecureMessage() 的方法(如 清单 24 所示),它以一个安全消息和解密 密钥为参数,解密这个消息并返回纯文本格式的消息。解码算法如下:

第一步是将消息的加密部分(图 24 所示的第五个字段)与 token 头分离。token 头的长度是固定的 ,所以只有长度字节的数目是随消息的总长度而变化的。因此,只要读取长度字节数并相应地将消息的加 密部分拷贝到一个单独的字节数组中。

第二步是读取消息 checksum( 图 1的第四个字段)。

现在用解密密钥解密加密的消息。

然后,取 token 头( 图 1 的第二个字段),将它与解密的消息链接,然后取链接的字节数组的 MD5 摘要值。

现在加密 MD5 摘要值。

需要比较第 2 步的八字节消息 checksum 与第 5 步的 MD5 摘要值的后八个字节。如果它们相匹配, 那么完整性检查就得到验证。

经过验证后,删除 cofounder(解密的消息的前八个字节)并返回消息的其余部分(它就是所需要的 纯文本消息)。

清单 24. decodeSecureMessage() 方法

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

示例移动银行应用程序

已经完成了示例移动银行应用程序所需要的安全 Kerberos 通信的所有阶段。现在可以讨论移动银行 MIDlet 如何使用 Kerberos 客户机功能并与电子银行的服务器通信了。

清单 25显示了一个简单的 MIDlet,它模拟了示例移动银行应用程序。

清单 25. 一个示例移动银行 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 KCommand = 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);    KCommand = 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();      s = 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

运行这个 MIDlet 会得到如 图 2所示的屏幕。

图 2.

图 2显示为这个示例移动银行应用程序开发了一个非常简单的 GUI。图 2 还显示了四个数据输入字段 :

“ Username ”字段取要使用移动银行 MIDlet 的金融服务的人的用户名。

“ Password ”字段取用户的密码。

“ Amount ”字段允许输入要支付给一个收款人的金额。

“ Pay to ”字段包含收款人的用户名。

输入完数据后,按 Pay 按钮。Pay 按钮的事件处理器( 清单 25 中的 sendMoney() 方法)执行 Kerveros 通信的所有七个阶段:

生成一个 TGT 请求、将请求发送给出服务器、并接收 TGT 响应。

从 TGT 响应中提取 TGT 和会话密钥。

生成一个服务票据请求、将请求发送给 KDC 、并接收服务票据响应。

从服务票据响应中提取服务票据和子会话密钥。

生成上下文建立请求并发送给电子银行的业务逻辑服务器、接收响应、并解析它以确定服务器同意建 立一个新的安全上下文。

生成一个安全消息、将这个消息发送给服务器、并接收服务器的响应。

解码来自服务器的消息。

清单 25 的 MIDlet 代码相当简单,不需要很多解释。只要注意以下几点:

我使用了不同的线程( 清单 25 中的 run() 方法)创建 Datagram 连接 ( dc ) 和 Socket 连接上 的数据输入和输出流。这是因为 MIDP 2.0 不允许在 J2ME MIDlet 的主执行线程中创建 Datagram 和 Socket 连接。

在 清单 25 的 J2ME MIDlet 中,我硬编码了 KDC 服务器的域、服务器名、地址和端口号以及电子银 行服务器的名字和地址。注意 MIDlet 中的硬编码只是出于展示目的。另一方面, KerberosClient 是完 全可重用的。

为了试验这个应用程序,需要一个作为电子银行服务器运行的 GSS 服务器。本文的 源代码下载 包含 一个服务器端应用程序和一个 readme.txt 文件,它描述了如何运行这个服务器。

最后,注意我没有设计电子银行通信框架,我只是设计了基于 Kerberos 的安全框架。可以设计自己 的通信框架,并用 KerberosClient 提供安全支持。例如,可以使用 XML 格式定义不同的消息作为转账 指令。

结束语

在这个三部分的系列文章中,我展示了 J2ME 应用程序中的安全 Kerberos 通信。介绍了进行一系列 加密密钥交换的 Kerveros 通信。还介绍了 J2ME 应用程序是如何使用密钥与远程电子银行服务器建立通 信上下文并安全地交换消息的。我还提供了展示文章中讨论的所有概念的 J2ME 代码。

所有的胜利,与征服自己的胜利比起来,都是微不足道

用Kerberos为J2ME应用程序上锁,第3部分-建立与电子银行的安全通

相关文章:

你感兴趣的文章:

标签云: