用 Kerberos 为 J2ME 应用程序上锁

来源:互联网 发布:哪里看opta数据 编辑:程序博客网 时间:2024/05/17 03:55

(源自:http://www.ibm.com/developerworks/cn/java/wi-kerberos/)

用 Kerberos 为 J2ME 应用程序上锁,第 1 部分: Kerberos 数据格式介绍

理解 Kerberos 如何保证无线安全性

文档选项

将此页作为电子邮件发送

未显示需要 JavaScript 的文档选项


级别: 初级

Faheem Khan (fkhan872@yahoo.com), 自由顾问

2003 年 12 月 18 日

用户需要确保所使用的无线应用程序不会损害他们的敏感信息。其中一种方法就是使用行业标准协议如 Kerberos 来提供安全性。在本系列中,Faheem Khan 将创建一个示例 J2ME MIDlet,它使用 Kerberos 来保护财务数据。本文是该系列的第一篇文章,他通过解释为他的应用程序的安全性提供骨架的 Kerberos 数据格式,介绍了一些基本知识。

许多用户不愿意使用通过无线连接发送敏感数据的应用程序,因为他们不信任无线安全性。但是使传统有线网络上的电子商务的安全成为可能的这些协议,同样也可以使无线交易成为安全的。在这个由三篇文章组成的系列中,我将展示 J2ME 客户机和服务器端 Java 应用程序之间使用 Kerberos 协议(参阅 参考资料中的链接)进行的安全消息传递。我将开发一个移动银行 MIDlet 应用程序,它可以通过 Internet 安全地发送和接收付款信息。MIDlet 应用程序将使用一个基于 J2ME 的 Kerberos 客户机来进行实际的安全消息传递。在本文中,我首先解释移动银行应用程序的使用模型,然后我将解释 Kerberos 消息交换的顺序,从而为后续的 J2ME 客户机和服务器端 Java 应用程序之间进行的安全消息传递建立安全的上下文。紧接着描述了 Kerberos 消息传递中使用的数据格式。本文的最后一部分简单介绍了 Kerberos 客户机的体系结构,它最终创建并处理 Kerberos 消息和数据格式。

一个用于移动银行的安全 MIDlet

我将首先考虑这样一个用例场景:其中有两个拥有电子银行帐户的移动电话用户 Alice 和 Bob。

如果 Alice 想要付款给 Bob,那么她就向 MIDlet 应用程序提供 Bob 的手机号码(或者他的银行帐户号码)并让 MIDlet 向他付款。MIDlet 安全地与电子银行联系并请求电子银行向 Bob 付款。电子银行完成这笔交易并将确认发送回 Alice。Bob 在他的手机上也安装了 MIDlet,可以用于查看 Alice 的付款是否到达他的帐户。MIDlet 安全地与电子银行进行通信以验证付款状态。

我在本系列文章中将设计并实现的 MIDlet 处理所有与电子银行进行的特定于应用程序的消息传递。当然,MIDlet 必须保证通信是安全的。下面的功能构成了 MIDlet 的安全性需求:

  • 电子银行应该可以确认请求付款和更新帐户状态的用户的身份。这种安全性需求通常称为 身份验证,其中服务器需要验证发出请求的客户的身份。
  • 像 Alice 和 Bob 这样的用户应该可以确保他们是真的与电子银行而不是一些恶意的黑客进行通信。这也是一种身份验证的情况,在这里客户希望对服务器进行身份验证。客户和服务器彼此验证的情况称为 双向身份验证
  • 所有通信都应该是加密的以维护消息的机密性。即使黑客可以得到在网络上传送的消息字节,他也不能理解这些加密的数据字节的意义。
  • 通信的双方(用户和电子银行)都应该能够验证收到的消息的完整性。如果恶意黑客改变了传输中的消息,那么接收一方应该可以发现这种改变。

我将使用 Kerberos 协议来满足这些安全性需求(有关 Kerveros 的更多信息的链接请参阅 参考资料)。我将所有与 Kerberos 相关的功能封装到一个小型的、适合于 J2ME 设备的客户端 Kerberos 实现中。我们的移动银行 MIDlet 只需要负责特定于应用程序的消息传递功能,而 Kerberos 客户机将处理所有安全问题。

这种技术对于读者来说有一个好处。Kerberos 客户机包装了安全功能(身份验证、机密性和消息完整性)。因此,如果要开发自己的 MIDlet,以用于发出和接收付款以外的目的,您也可以使用这里描述的 Kerberos 客户机来使之具有安全功能。

本文的大部分用于讨论 Kerberos 是如何与应用程序一同工作的,以及描述为了建立安全通信而在客户机与服务器之间交换的不同消息。我将在本系列的后续文章中详细描述客户端应用程序本身。




回页首

Kerberos 消息交换

本节我将描述以下三个参与者之间的 Kerberos 消息交换:

  • 支持 J2ME 的无线设备
  • 无线设备用户
  • 电子银行

图 1. 为建立安全通信上下文而进行的 Kerberos 消息交换 
 

图 1 给出了三个参与者之间出现的消息交换的概括视图。本节我将讨论这一视图并解释图中显示的消息交换的最终结果。下一节解释每一消息的具体细节(即结构和格式)。

注意,图 1 中支持 J2ME 的无线设备包含两个参与者:MIDlet 和 Kerberos 客户机。与此类似,电子银行系统也包含两个参与者:业务逻辑服务器和 Kerberos 分发中心 (Kerberos Distribution Center,KDC)。

业务逻辑服务器是实现了电子银行业务逻辑的服务器端 Java 应用程序。KDC 是一个管理和分发 Kerberos 票据的 Kerberos 服务器。KDC 又由两个服务器组成:一个 身份验证服务器(AS) 和一个 票据授予服务器(TGS)。AS 和 TGS 都接收客户机请求并发出 Kerberos 票据以响应这些请求。当 AS 接收客户机的票据请求时,它发出一个初始票据。然后客户机向 TGS 展示这个初始票据。TGS 根据这个初始票据发出服务票据。

获得初始票据的主要目的是在以后用它得到一个或者多个服务票据。这就是为什么初始票据也称为 票据授予票据(TGT)。

注意,服务票据是用于客户机与特定服务器之间的安全通信的一种手段。另一方面, TGT 并不是针对任何特定的服务器的。因此,TGT 逻辑上等于一个开放连接,它的一端是客户机,而另一端是未知的。当对一个 TGT 生成一个服务票据时,另一端也就确定了。同一个 TGT 可以用于获得任意数量的服务票据。

下面描述的消息交换步骤揭开了 MIDlet 如何以及为什么获得并使用 Kerberos 票据的神秘面纱。图 1 中的每一个数字都对应于下面讨论的一个步骤。

  1. 手机用户向 MIDlet 应用程序提供他或者她的用户名和密码(只由用户和电子银行共享的一个秘密)。这个密码只用于在 J2ME 应用程序内部的处理,它永远也不会进入网络。通过网络传输的只有用户名。
  2. MIDlet 将用户名和密码交给 Kerberos 客户机。由 Kerberos 客户机负责建立与电子银行进行安全通信的上下文。
  3. Kerberos 客户机请求 AS 发出一个 TGT。一个 TGT 请求表示一个安全会话。一个客户机可以在一个安全会话中建立多个子会话。 TGT 请求包含了发出请求的客户的用户名,但是不包括密码(共享的秘密)。
  4. Kerberos 客户机向 AS 发送请求。
  5. 当 AS 收到 TGT 请求时,它从请求中提出用户名并从内部数据库中取出相应的密码(共享的秘密)。然后 AS 发布一个 TGT 并将 TGT 包装在回复消息中。这个 TGT 包含一个纯文本部分和一个密码文本(加密的)部分。为了加密 TGT 的密码部分,AS 使用了由用户的密码生成的加密密钥。因此,只有知道密码的用户才能解开加密的部分。TGT 的加密部分还包含一个加密密钥,称为 会话密钥
  6. AS 向发出请求的 Kerberos 客户机发送回复消息(包括 TGT)。
  7. 收到 AS 的回复后,Kerberos 客户机从回复中取出 TGT 并解密 TGT 中加密的部分。 然后 Kerberos 客户机发出一个服务票据请求。这个请求包含了 TGT 和一个称为 鉴别码 (authenticator)的加密结构。客户机用从 TGT 提取出的会话密钥加密鉴别码。鉴别码证明客户机掌握会话密钥。服务票据请求还指定了电子银行业务逻辑服务器的名字。
  8. 客户机向 TGS(它是电子银行的 KDC 的一部分)发送服务票据请求。
  9. 收到服务票据请求后,TGS 提取客户机向其请求服务票据的服务器的名称,然后授予一个服务票据。服务票据与 TGT 没有很大差别。与 TGT 一样,服务票据包含一个纯文本部分和一个密码文本(加密的)部分。TGS 用服务器的密钥(一个用服务器的共享秘密生成的密钥)加密服务票据的密码文本部分,这样只有那个服务器可以解密这个服务票据的密码文本部分。TGS 在服务票据的密码文本部分中还加入了一个新的加密密钥。这个密钥称为 子会话密钥。注意现在有两个密钥:会话密钥和子会话密钥。
  10. 授予了服务票据之后,TGS 将服务票据包装在一个响应消息中。这个响应消息也包含一个密码文本部分,它是用会话密钥加密的。响应消息的密码文本部分包含子会话密钥。
  11. TGS 向客户机发送响应消息。
  12. 收到 TGS 响应后,客户机用会话密钥解密密码文本部分,从而提取出子会话密钥。客户机还提取服务票据。
  13. 然后客户机向电子银行业务逻辑服务器发出消息,并在消息中包装了服务票据。这个消息请求服务器与客户机建立新的安全会话。
  14. 客户机向电子银行的业务逻辑服务器发送消息。
  15. 电子银行的业务逻辑服务器从请求中提取服务票据,解密它的密码文本部分,并提取子会话密钥。这样客户机和服务器就都掌握这个密钥了。
  16. 电子银行的服务器向客户机发送一个肯定应答。

客户机和电子银行服务器现在可以用子会话密钥进行安全通信了。




回页首

Kerberos 消息

那么这些加密是如何工作的呢?在本文的其余部分,我将详细探讨图 1 中步骤 3 到步骤 16 中交换的 Kerberos 消息的结构。

TGT 请求

图 2 图示了在图 1 的步骤 3 中讨论的 TGT 请求消息的表示。
图2. TGT 请求消息的结构 
 

Kerberos 协议定义了在 Kerberos 消息传递中使用的所有数据结构和消息的标题 (title)。注意,图 2 中消息的标题是AS-REQ --这是一个 AS 请求。

图 2 显示了一种嵌套框的结构。每一个框表示一个数据字段。一些字段又包含了不同的字段,从而构成了一种嵌套的层次结构。

最外面的框标记为 AS-REQ ,包含一个标记为 KDC-REQ 的更小的框。这个 KDC-REQ 框包含四个字段:

  • pvno :这个数据字段表示 Kerberos 协议版本号。本系列文章的讨论是针对 Kerveros 版本 5 的,它相当稳定,并且在目前是最新的。
  • msg-type :可以通过消息类型号来识别不同的 Kerberos 消息。TGS 请求消息的类型号是 10。
  • padata :这是一个可选的字段,在大多数 TGT 请求消息中都没有它。这个字段的目的是加入身份验证信息。在本节后面描述服务票据请求时我将描述 padata 字段的结构。
  • 第四个字段标记为 req-body ,它是 TGT 请求的正文。它又进一步分为几个字段:
    • kdc-options : 这个字段表示 KDC 选项。Kerveros 定义了客户机可能希望请求的几个选项。例如,客户机可能需要一个 forwardable 票据(可以转发给不同 KDC 服务器的票据)。与此类似,客户机可能会请求一个 renewable 票据(可以在失效后更新的票据)。在本系列文章的后面,我将讨论一些可用的选项,特别是那些与我的移动银行应用程序有关的选项。
    • cname :客户的用户名。
    • realm :KDC 领域(realm)。注意,使用 Kerberos 的每一个机构都可以建立自己的领域。领域就像信任域,比如我们的电子银行。一个领域可能跨越或者不跨越企业边界。这意味着一个领域可能需要或者不需要与属于其他企业的用户通信。在移动银行应用程序中,我将尽量保持简单,假定所有用户(如 Alice 和 Bob)是在电子银行自己的企业中注册的。
    • sname :这是一个标识客户机将向其出示所请求的票据的服务器的名称。对于 TGT 请求, sname 字段指定了 TGS 的名称(因为客户机最终要向 TGS 展示 TGT)。另一方面,对于服务票据请求(您将在本节的后面看到它), sname 字段将指定电子银行服务器的名称(因为客户机最终是要向电子银行的业务逻辑服务器出示服务票据)。
    • from :这是一个可选的字段,它表明客户机需要一个填迟日期的票据,即其有效性将在将来的某一时刻开始。在移动银行应用程序中我不需要这个功能。
    • till :这个字段表明 TGT 失效的时间。客户机指定 TGT 在什么时候失效。所有 Kerberos 时间字段都遵循YearMonthDateHourMinSecZ 格式。例如,1947 年 8 月 14 日早上 3:30 将表示为 19470814033000Z 。
    • rtime :这个字段指定在什么时刻后票据就不能更新了。这是一个可选的字段,只有当客户机在 kdc-options 字段中选择了 renewable 选项后才会使用。
    • nonce :这是一个随机生成的整数。尽管这个整数本身没有意义,但是它有助于检测回复攻击。
    • etype :这个字段指定客户机要使用的加密算法。Kerberos 为常用的加密算法定义了不同的整数,客户机将使用对应的整数值。
    • addresses :这是一个可选的字段,它包含一组地址,只有从这些地址来的票据才是有效的。客户可以在这里指定从什么网络地址上使用所请求的票据。
    • enc-authorization-data :这是一个可选字段,它包装了身份验证数据,服务器可以根据这些数据来实施其身份验证策略。我不准备在移动银行应用程序中展示这种功能的使用。
    • additional-tickets :这是一个可选字段,它使 Kerberos 客户机可以根据客户机已经获得的多个票据请求一个安全会话。我不准备在移动银行应用程序中使用这个功能。
用 ASN.1 定义数据结构

Kerberos 用 Abstract Syntax Notation One (ASN.1) 定义在 Kerberos 通信中使用的各种数据结构和字节格式。ASN.1 是一个 ITU-T (International Telecommunication Union-Telecommunication standardization sector) 标准。它由参考号为 X.680 到 X.699 的不同文档组成(参阅 参考资料中的链接)。

ASN.1 语法和编码细节不是本文的重点。不过,我需要对 ASN.1 的概念做一些讨论以解释 Kerberos 结构和字节格式。我将只讨论那些解释 Kerberos 消息格式所需要的 ASN.1 概念。

Kerberos 消息的字节编码 在讨论 Kerberos 消息的其他内容之前,我将首先描述如何将图 2 中的 TGT 请求消息编码为字节值序列。

清单 1 是图 2 中 TGT 请求消息的 ASN.1 类定义(有关 ASN.1 的更多内容见侧栏)。稍后的表 2 显示了 TGT 请求消息编码的每一字节的格式。为了理解 TGT 请求消息的字节编码,必须将图 2、清单 1 和表 2 联系在一起。

注意,清单 1 中的主要结构标记为 AS-REQ。相同的 AS-REQ 标记出现在图 2 中的外围框中。

现在看一下清单 1 中 AS-REQ 的后面是什么。 AS-REQ 后面的两个冒号和等号 ( ::= ) 表明这一行定义了 AS-REQ 结构。接下来是方括号中的一个字符串 APPLICATION 10 。A APPLICATION 10 表明这个 AS-REQ 结构在这个 APPLICATION 的各种结构中是用编号 10 识别的。我们可以说编号 10 是一个 应用程序级标签号。这个编号在应用程序中是惟一的── 换句话说,其他 Kerberos 结构将不会使用这个编号。

现在注意在 [APPLICATION 10] 字符串后的 KDC-REQ 字符串。这表明在 AS-REQ 结构的格式后面是另一个名为 KDC-REQ 的结构的定义。这是 ASN.1 中的一种重用机制。 KDC-REQ 结构用在两个地方。因此,Kerberos 定义 KDC-REQ 一次并使用它两次。

总而言之, AS-REQ ::= [APPLICATION 10] KDC-REQ 表明在该应用程序中的不同结构中 AS-REQ 标记为 10,后面是另一个名为 KDC-REQ 的结构的定义。 
清单 1. TGT 请求消息的 ASN.1 类定义

AS-REQ ::=         [APPLICATION 10] KDC-REQKDC-REQ ::=        SEQUENCE {           pvno[1]               INTEGER,           msg-type[2]           INTEGER,           padata[3]             SEQUENCE OF PA-DATA OPTIONAL,           req-body[4]           KDC-REQ-BODY}PA-DATA ::=        SEQUENCE {           padata-type[1]        INTEGER,           padata-value[2]       OCTET STRING,                         -- might be encoded AP-REQ}KDC-REQ-BODY ::=   SEQUENCE {            kdc-options[0]       KDCOptions,            cname[1]             PrincipalName OPTIONAL,                         -- Used only in AS-REQ            realm[2]             Realm, -- Server's realm                         -- Also client's in AS-REQ            sname[3]             PrincipalName OPTIONAL,            from[4]              KerberosTime OPTIONAL,            till[5]              KerberosTime,            rtime[6]             KerberosTime OPTIONAL,            nonce[7]             INTEGER,            etype[8]             SEQUENCE OF INTEGER, -- EncryptionType,                         -- in preference order            addresses[9]         HostAddresses OPTIONAL,            enc-authorization-data[10]   EncryptedData OPTIONAL,                         -- Encrypted AuthorizationData encoding            additional-tickets[11]       SEQUENCE OF Ticket OPTIONAL}EncryptedData ::=   SEQUENCE {            etype[0]     INTEGER, -- EncryptionType            kvno[1]      INTEGER OPTIONAL,            cipher[2]    OCTET STRING -- ciphertext}KDCOptions ::=   BIT STRING {                    reserved(0),                    forwardable(1),                    forwarded(2),                    proxiable(3),                    proxy(4),                    allow-postdate(5),                    postdated(6),                    unused7(7),                    renewable(8),                    unused9(9),                    unused10(10),                    unused11(11),                    renewable-ok(27),                    enc-tkt-in-skey(28),                    renew(30),                    validate(31)}PrincipalName ::=   SEQUENCE {              name-type[0]     INTEGER,              name-string[1]   SEQUENCE OF GeneralString}KerberosTime ::=   GeneralizedTime            -- Specifying UTC time zone (Z)HostAddresses ::=   SEQUENCE OF SEQUENCE {                    addr-type[0]             INTEGER,                    address[1]               OCTET STRING}

现在让我们来看一下 KDC-REQ 结构。

清单 1 中的 KDC-REQ ::= SEQUENCE 这一行表明 KDC-REQ 结构是不同数据结构的序列。在 SEQUENCE 关键词后的一组花括号描述了共同构成 KDC-REQ 结构的数据结构。

花括号中有四行代码。第一行 ( pvno[1] INTEGER ) 定义了 KDC-REQ 结构的第一个元素,它是图 2 中的 pvno 字段。

第二行 ( msg-type[2] INTEGER ) 对应于图 2 中的 msg-type 字段。注意 pvno 和 msg-type 字段的类型为 INTEGER,这意味着它们是用 INTEGER 数据类型构建的。

还要注意在清单 1 中 pvno 和 msg-type 后面的方括号中的数字 ( pvno[1] and msg-type[2] )。与在前面 AS-REQ ::= [APPLICATION 10] KDC-REQ 一行中见到的应用程序级标签号相反,它们是 上下文特定的标签号。

应用程序级和上下文特定的标签号有什么区别呢?应用程序级标签号在整个应用程序中是惟一的和有效的。例如,在整个 Kerberos 应用程序中编号 10 都是指 AS-REQ 结构。而上下文特定的标签号只在定义它们的上下文中有意义。例如,在 KDC-REQ 的结构内部,上下文特定的标签号 1 表示 pvno 。但是在查看其他结构的内部时,同样的上下文特定的标签号 1 表示的则是一些其他的字段。

在后面讨论将清单 1 编码为表 2 中的字节序列时,我将解释这些应用程序级和上下文特定的标签号的使用。

现在看一看清单 1 中 KDC-REQ 结构中第三行 ( padata[3] SEQUENCE OF PA-DATA ) 和第四行 ( req-body[4] KDC-REQ-BODY )。第三行定义了图 2 中的 padata 字段,它是 PA-DATA 结构的一个 SEQUENCE 。第四行表示图 2 中的 req-body框。

padata 和 req-body 字段又是由不同字段组成的 Kerberos 结构。例如, req-body 的数据类型是 KDC-REQ-BODY ,而该数据类型自己又是一个带有几个字段(像前面讨论的那样带有 kdc-options、 cname 、 realm 、 sname 、 till 、 nonce 和 etype 等字段)的 Kerberos 结构。

回想一下 pvno 字段是用一个 INTEGER 构建的。另一方面, req-body 字段的数据类型为 KDC-REQ-BODY ,该数据类型本身又是由几个字段构建的一种结构。

还要注意 INTEGER 是基本 ASN.1 数据类型的一个例子,而 KDC-REQ-BODY 是由其他字段构建的派生数据类型。

ASN.1 定义了一些可以被应用程序使用的数据类型,称为 通用数据类型。大多数通用数据类型是基本类型,只有少数是构建的。ASN.1 为通用数据类型定义了 标签号,如表 1 所示。

表 1. 一些通用数据类型和标签号

 

通用数据类型通用标签号构建类型还是基本类型?BOOLEAN1基本类型INTEGER2基本类型BIT STRING3基本类型OCTET STRING4基本类型SEQUENCE16构建类型GeneralizedTime24基本类型GeneralString27基本类型

表 1 显示 INTEGER 数据类型的通用标签号是 2,并且 INTEGER 数据类型是基本类型。 SEQUENCE 是表 1 中惟一的构建类型的通用标签。这是因为 SEQUENCE 数据类型是使用其他字段普遍定义的, SEQUENCE 总是用其他字段构建的。

在 ASN.1 定义中没有提到通用数据类型的标签号,这是因为这些标签号已经得到普遍定义和理解。不过,在试图将 Kerberos 结构编码为字节序列时需要用到通用标签号。在稍后您会看到这一点。

现在,让我解释图 2 与清单 1 中的 AS-REQ 消息是如何编码为字节序列的。为了展示这个编码过程,我提供了表 2,它显示了客户机发送给 KDC 服务器以请求 TGT 的 TGT 请求(一个 AS-REQ 结构)中实际的字节序列。表 2 中显示的字节序列是我们要分析的 AS-REQ 编码过程的最终结果。

因为它比较长,所以我用一个 单独的文件来提供表 2。在阅读进行下面的讨论时您应该在一个单独的浏览器窗口中打开它。

看一下表 2 中的第一个字节 ( 01101010 )。这是消息的第一个字节,表示清单 1 的 AS-REQ 结构的开始(图 2 中的外围框)。位 8 和位 7 ( 01 ) 表明这是一个应用程序级标签。位 6 ( 1 ) 表明这是一个构建的结构。位 5 到位 1 ( 01010 ) 表示 AS-REQ 的标签号(我在前面的讨论中说过 AS-REQ 结构的应用级标签号为 10,其二进制表示为 01010 )。

可以将标签字节分为三部分:

  • 位 8 和位 7 指定标签的 类型或者类。对于应用程序级标签,位 8 和位 7 应该是 01 ,对于上下文特定的标签,它们应该是 10 ,对于通用标签,它们是 00 。
  • 位 6 指定标签是基本的 ( 0 ) 还是构建的 ( 1 )。
  • 位 5 到位 1 编码标签号。

因为用于表示标签号的位数限制了可以您可以拥有的标签的个数,所以您可能奇怪为什么只有 5 位用于标签号。ASN.1 定义了编码标签号的一个完整机制,与标签号的大小无关。不过,Kerberos 没有定义任何不能用 5 位表达的标签号。因此,为了将讨论集中于 Kerberos,我在这里将不讨论如何处理大的标签号。

现在,看一下表 2 中的第二个字节 ( 10000001 )。在标签字节后,有一个或者多个 长度字节。这些长度字节指定了构成完整标签的总字节数。

有两种定义长度字节的方法。第一种是 单字节长度表示,第二种是 多字节长度表示。对于单字节长度表示,要使第一个长度字节的位 8 为 0 ,并用其余的位指定长度字节的个数。例如,如果您想说这个结构中有 65 个字节,可以将长度值编码为 01000001 (位 8 设置为 0 ,位 7 到位 1 ──1000001 ──表示 65)。在这种方法中,总是有一个长度字节,下一个字节标记结构内容的开始。用这种方法可编码的最大值是 127。

对于多字节表示法,设置第一个长度字节的位 8 为 1 ,并用位 7 到位 1 指定随后的长度字节的个数。例如,如果要编码值 210,第一个长度字节将是 10000001 (位 8 设置为 1 ,位 7 到位 1 设置为 1 ,即 0000001 表明还有一个长度字节),后面再跟一个字节,其值为 11010010 (表示十进制的 210 )。

看一下表 2 中的字节 2 和字节 3 ,它们分别是 10000001 和 11010010 。这意味着我用多字节长度表示法来指定 AS-REQ 结构的长度,即 210 。

注意在表 2 中共有 213 字节,其中 210 是在第 3 个字节后。在字节 3 后的所有 210 个字节都属于 AS-REQ 结构。因此,第 4 个字节和后面的所有字节构成了 AS-REQ 结构的内容。

注意,清单 1 中 AS-REQ 结构后面是 KDC-REQ 结构的定义,这个结构是一个 SEQUENCE 。回想一下,在表 1 中 SEQUENCE 是 通用标签,它是 构建的数据类型,并且其标签号为 16。这就是为什么表 2 中字节 4 的值为 00110000 。位 8和位 7 ( 00 ) 指定这是一个通用标签。位 6 ( 1 ) 表明这个结构是构建的。位 5 到位 1 指定标签号,对于 SEQUENCE来说这是 16(二进制为 10000 )。

字节 5 和字节 6 指定 KDC-REQ 结构中的字节数。字节 5 ( 10000001 ) 指定有一个长度字节。字节 6 ( 11001111 ) 指定 KDC-REQ 序列的实际长度 (207)。我已经解释过长度字节的工作方式。

清单 1 中 KDC-REQ 序列的第一个字段是 pvno ,它是一个 上下文特定的,构建 的字段,其标签号为 1 。表 2 中的字节 7 ( 10100001 ) 表示这个字段。位 8 和位 7 ( 10 ) 表示这是一个上下文特定的标签。位 6 ( 1 ) 表示这是一个构建的字段,位 5 到位 1 ( 00001 ) 指定 pvno 字段的标签号。

字节 8 ( 00000011 ) 指定 pvno 字段的长度。这个长度字节的位 8 是 0 ,这表明我使用单字节长度表示法。位 7 到位 1 ( 0000011 ) 指定长度为 3。这里要注意,正如前面所解释的,当结构的长度小于 127 时,我就使用单字节长度表示法。

pvno 字段的内容从表 2 中的第 9 个字节开始。 pvno 字段是用 INTEGER 基本数据类型构建的,所以我可以期望 pvno 的内容为一个整数值。

字节 9 ( 00000010 ) 表示 INTEGER 标签。位 8 和位 7 ( 00 ) 标识这是一个通用标签,位 6 (0) 说明它是基本类型,位 5 到位 1 ( 00010 ) 表明标签号为 2(见表 1)。

位 10 ( 00000001 ) 提供了这个 INTEGER 数据类型在单字节长度表示法中的长度。 INTEGER 值只由一个字节组成,即下一个字节(第 11 个字节)。

您将注意到字节编码过程遵循一个清晰的模式。我以对应于清单 1 中一个字段的标签值开始。标签值后面是长度字节,长度字节后面是标签的内容。我不断使用嵌套的、分层次的构建结构,直到达到一个基本数据类型。

您可以根据这种模式完成表 2。表 2 的说明栏可以帮助您理解每一字节的作用。对于本文中的要讨论的其他消息,我不会提供逐字节的分析,不过对这个消息的分析可以使您了解它们的工作方式。

包装 TGT 的响应

图 3 是一个 AS 发出的、包装了 TGT 的响应消息的图形表示。图 3 同样使用了图 2 中展示的嵌套框结构。
图 3. AS 的 TGT 响应的结构 
 

图 3 中的主框(外围框)标记为 AS-REP ,它包含一个标记为 KDC-REP 的更小的框。 KDC-REP 是由几个字段组成的一个序列。

  • pvno :在讨论图 2 时我解释了这个字段。
  • msg-type :消息的类型。它是一个整数,对于 TGT 响应消息它的值应该为 11。
  • padata :在讨论图 2 时我对这个字段做过说明。这是一个可选字段,在大多数 TGT 响应消息中没使用它。
  • crealm 和 cname :在讨论图 2 时对这些字段做过说明。
  • ticket :实际的 TGT。我将在本文的后面一节中讨论 Kerberos 票据自身的格式。
  • enc-part :这是一个加密数据的包装器。Kerveros 消息中所有加密的部分都包含三个字段:
    • etype :指定用于进行密码加密的加密算法的标识符。
    • kvno :用于加密的加密密钥的版本。AS 使用客户机的密钥进行加密。这个字段指定用于进行加密的密钥的版本。
    • cipher :一系列字节。这是实际的加密数据。数据解密后,我就有了另一个结构,如图 4 所示。

图 4. TGT 响应消息的加密部分的结构 
 

图 4 显示了对 TGT 响应消息中加密部分进行解密后得到的结构。它包含以下字段:

  • key :这是会话密钥。当前会话的后续通信将使用这个密钥而不是密码密钥。
  • last-req :这个字段指定客户机的最后一次票据请求的时间。这个字段有助于通知客户机,它的请求已经收到了。
  • nonce :在讨论图 2 时我对这个字段做过说明。AS 将包含它在请求中收到的随机数的一个副本。这有助于检测回复攻击。如果黑客取得了 TGT 响应并想反复回复它,那么客户机就可以将响应的 nonce 字段与其请求进行比较以检测回复。
  • key-expiration :这是一个可选字段,它指定客户机的密钥失效的时间。
  • flags :这个字段对应于图 2 的 TGT 请求的 kdc-options 字段。这些 flags 表示 Kerberos 客户机可能请求的不同可选功能。AS 将标志发送回客户机,从而允许客户机比较 AS 是否可以提供所请求的可选功能。
  • authtime :AS 发出票据的时间。
  • starttime :票据生效的时间。
  • endtime :票据失效的时间。
  • renew-till :可更新的票据的最终失效时间。
  • srealm :服务器的领域。
  • sname :服务器名,其所带票据是有效的。
  • caddr :这个字段指定一个地址列表,这些地址给出的相应票据是可以使用的。这个字段的目的是使黑客使用偷来的票据更困难。

清单 2 提供了 TGT 响应消息的 ASN.1 类定义。读者可以将清单 2 中的类定义与图 3 和图 4 中显示的不同字段相对照。
清单 2. TGT 响应消息的 ASN.1 类定义

   AS-REP ::=    [APPLICATION 11] KDC-REP   KDC-REP ::=   SEQUENCE {                 pvno[0]                    INTEGER,                 msg-type[1]                INTEGER,                 padata[2]                  SEQUENCE OF PA-DATA OPTIONAL,                 crealm[3]                  Realm,                 cname[4]                   PrincipalName,                 ticket[5]                  Ticket,                 enc-part[6]                EncryptedData   }   EncryptedData ::=   SEQUENCE {               etype[0]     INTEGER, -- EncryptionType               kvno[1]      INTEGER OPTIONAL,               cipher[2]    OCTET STRING -- ciphertext   }   EncASRepPart ::=    [APPLICATION 25] EncKDCRepPart   EncKDCRepPart ::=   SEQUENCE {               key[0]                       EncryptionKey,               last-req[1]                  LastReq,               nonce[2]                     INTEGER,               key-expiration[3]            KerberosTime OPTIONAL,               flags[4]                     TicketFlags,               authtime[5]                  KerberosTime,               starttime[6]                 KerberosTime OPTIONAL,               endtime[7]                   KerberosTime,               renew-till[8]                KerberosTime OPTIONAL,               srealm[9]                    Realm,               sname[10]                    PrincipalName,               caddr[11]                    HostAddresses OPTIONAL   }

服务票据请求

收到 TGT 后,客户机发出服务票据请求,如图 5 所示。服务票据请求与我在图 2 中讨论的 TGT 请求非常相似。可以将图 5 中的所有字段与图 2 中的相应字段进行对照。我只需要解释专门针对服务票据请求的 padata 字段。 
图 5. 服务票据请求消息的结构
 

padata 字段是 PA-DATA 结构的序列,它包含 身份验证数据。服务票据请求需要向票据授予服务器发送 TGT。 padata 字段将 TGT 包装到它的一个 PA-DATA 结构中。

Kerberos 用 padata 字段实现不同的目的,包装 TGT 只是其中一项。因此,Kerberos 定义了不同的整数值以指定 PA-DATA 结构包装的是什么类型的数据。

图 5 显示 PA-DATA 序列只包含一个 PA-DATA 结构,它由两个子字段组成,即 padata-type 和 padata-value 。 padata 序列中的每一个 PA-DATA 结构都包含这两个字段。

padata-type 是一个整数值,用于指定所带 padata-value 字段中的数据类型。当在服务票据请求中 padata-value 字段包装了一个 TGT 时, padata-type 字段的值就是 1。

padata-value 字段是一串字节,其中包含 TGT。 padata-value 字段中的字节串实际上是另一个名为 KRB_AP_REQ (或者简称为 AP-REQ )的 Kerberos 结构,也称为 身份验证头

身份验证头包含 TGT 以及一些其他字段,如下所示:

  • pvno :在对图 2 的讨论中我已经解释过这个字段。
  • message-type :这个字段包含一个整数值 (12),用于识别 KRB_TGS_REQ 消息。
  • ap-options :这是一组选项。第一个选项保留为以后使用。第二个选项指定相应 TGT 是用包装在相应 TGT 中的会话密钥加密的。因为 TGS 已经知道会话密钥,所以它可以使用这把密钥进行解密。如果选择了第三个选项,那么它就指定客户机请求双向身份验证。
  • ticket :TGT 本身。
  • authenticator :这是一个加密的结构,其中包含几个字段,允许客户机证明它拥有会话密钥并帮助 TGS 检测回复攻击。在身份验证头中 鉴别码是以加密的(密文)形式出现的。客户机使用会话密钥加密鉴别码。鉴别码结构的字段如下:
    • authenticator-vno :鉴别码格式的版本号。对于 Kerberos 版本 5,这个字段应该指定 5。
    • crealm 和 cname: 我在对图 2 的讨论中解释了这些字段。
    • cksum :一个校验和或者散列值,由图 5 中显示的 req-body 字段的字节编码计算而来。这个字段让 TGS 可以检查请求消息的完整性。因为该校验和位于一个用会话密钥加密的结构中,所以这个字段也证明客户机拥有会话密钥。
    • cusec 和 ctime :这两个字段共同指定发出 KRB_AP_REQ 消息的客户机时间。 cusec 字段指定时间的微秒部分,而 ctime 字段指定日期和以毫秒计的时间。
    • subkey :这是一个可选的字段,客户机可以用它指定随后与服务器之间的通信所要使用的密钥。对于移动银行应用程序,我将尽量减少在 J2ME 客户机上的处理负荷,因此,让服务器决定会话密钥和子会话密钥。
    • seq-number :这是一个可选字段,可以包含消息的一个序列号以检测回复攻击。
    • authorization-data :这是一个可选字段,带有特定于应用程序的身份验证数据。在移动银行应用程序中我没有使用这个字段。

清单 3 提供了服务票据请求消息的 ASN.1 类定义。读者可以将图 5 中显示的不同字段与清单 3 中的类定义相对照。
清单 3. 服务票据请求消息的 ASN.1 类定义

TGS-REQ ::=        [APPLICATION 12] KDC-REQKDC-REQ ::=        SEQUENCE {           pvno[1]               INTEGER,           msg-type[2]           INTEGER,           padata[3]             SEQUENCE OF PA-DATA OPTIONAL,           req-body[4]           KDC-REQ-BODY}PA-DATA ::=        SEQUENCE {           padata-type[1]        INTEGER,           padata-value[2]       OCTET STRING,                         -- might be encoded AP-REQ}KDC-REQ-BODY ::=   SEQUENCE {            kdc-options[0]       KDCOptions,            realm[2]             Realm, -- Server's realm                         -- Also client's in AS-REQ            sname[3]             PrincipalName OPTIONAL,            from[4]              KerberosTime OPTIONAL,            till[5]              KerberosTime,            rtime[6]             KerberosTime OPTIONAL,            nonce[7]             INTEGER,            etype[8]             SEQUENCE OF INTEGER, -- EncryptionType,                         -- in preference order            addresses[9]         HostAddresses OPTIONAL,            enc-authorization-data[10]   EncryptedData OPTIONAL,                         -- Encrypted AuthorizationData encoding            additional-tickets[11]       SEQUENCE OF Ticket OPTIONAL}AP-REQ ::= [APPLICATION 14] SEQUENCE {            pvno [0]        INTEGER,        -- indicates Version 5            msg-type [1]    INTEGER,        -- indicates KRB_AP_REQ            ap-options[2]   APOptions,            ticket[3]       Ticket,            authenticator[4]        EncryptedData}APOptions ::= BIT STRING {            reserved (0),            use-session-key (1),            mutual-required (2)}Ticket ::= [APPLICATION 1] SEQUENCE {            tkt-vno [0]     INTEGER,        -- indicates Version 5            realm [1]       Realm,            sname [2]       PrincipalName,            enc-part [3]    EncryptedData}-- Encrypted part of ticketEncTicketPart ::= [APPLICATION 3] SEQUENCE {            flags[0]        TicketFlags,            key[1]          EncryptionKey,            crealm[2]       Realm,            cname[3]        PrincipalName,            transited[4]    TransitedEncoding,            authtime[5]     KerberosTime,            starttime[6]    KerberosTime OPTIONAL,            endtime[7]      KerberosTime,            renew-till[8]   KerberosTime OPTIONAL,            caddr[9]        HostAddresses OPTIONAL,            authorization-data[10]  AuthorizationData OPTIONAL}-- Unencrypted authenticatorAuthenticator ::= [APPLICATION 2] SEQUENCE  {            authenticator-vno[0]    INTEGER,            crealm[1]               Realm,            cname[2]                PrincipalName,            cksum[3]                Checksum OPTIONAL,            cusec[4]                INTEGER,            ctime[5]                KerberosTime,            subkey[6]               EncryptionKey OPTIONAL,            seq-number[7]           INTEGER OPTIONAL,            authorization-data[8]   AuthorizationData OPTIONAL}

响应包含服务票据

当 TGS 收到服务票据的请求时,它就在响应中发出一个服务票据。图 6 显示了包装有服务票据的 TGS 响应。您可以对照图 6 与图 3。您会发现图 3 中显示的字段与在图 6 中显示的一样,只不过图 3 中的 ticket 字段是 TGT,而图 6 的 ticket 字段是服务票据。

还要注意,在产生图 6 的加密部分时,KDC 使用了在以前的消息中与客户机交换的会话密钥。服务票据包装了客户机与电子银行服务器进行安全通信时将会使用的子会话密钥。
图 6. TGS 的服务票据响应的结构 
 

清单 4 提供了服务票据响应消息的 ASN.1 类定义。读者可以将图 6 中不同字段与清单 4 中的类定义进行对照。
清单 4. 服务票据响应消息的 ASN.1 类定义

   TGS-REP ::=   [APPLICATION 13] KDC-REP   KDC-REP ::=   SEQUENCE {                 pvno[0]                    INTEGER,                 msg-type[1]                INTEGER,                 padata[2]                  SEQUENCE OF PA-DATA OPTIONAL,                 crealm[3]                  Realm,                 cname[4]                   PrincipalName,                 ticket[5]                  Ticket,                 enc-part[6]                EncryptedData   }   EncryptedData ::=   SEQUENCE {               etype[0]     INTEGER, -- EncryptionType               kvno[1]      INTEGER OPTIONAL,               cipher[2]    OCTET STRING -- ciphertext   }   EncTGSRepPart ::=   [APPLICATION 26] EncKDCRepPart   EncKDCRepPart ::=   SEQUENCE {               key[0]                       EncryptionKey,               last-req[1]                  LastReq,               nonce[2]                     INTEGER,               key-expiration[3]            KerberosTime OPTIONAL,               flags[4]                     TicketFlags,               authtime[5]                  KerberosTime,               starttime[6]                 KerberosTime OPTIONAL,               endtime[7]                   KerberosTime,               renew-till[8]                KerberosTime OPTIONAL,               srealm[9]                    Realm,               sname[10]                    PrincipalName,               caddr[11]                    HostAddresses OPTIONAL   }

从客户机到电子银行业务逻辑服务器的消息

现在客户机有了服务票据,可以将它发送到电子银行业务逻辑服务器。客户机发送图 7 中的消息到电子银行服务器。这个消息的目的是请求服务器建立与客户机的新的安全上下文。
图 7. 从 Kerberos 客户机到电子银行服务器的消息的结构 
 

我说过本文的目的是展示基于 J2ME 的安全移动银行应用程序。我准备在服务器端使用 Generic Security Services API(GSS API,或者简称为 GSS)来为电子银行业务逻辑服务器提供安全功能。GSS 是一种一般性的高层安全 API ,它可以在像 Kerberos 这样的不同安全技术上面工作(有关 GSS 的更多内容请参阅 参考资料)。

我将在本系列的第三篇文章中讨论 GSS API 在电子银行业务逻辑服务器中的使用。现在,只要知道与 Kerberos 一样,GSS 也是一种 IETF 标准。IETF 为客户机与服务器之间相互传递的 Kerveros 消息定义了 GSS 包装器。为了在服务器端使用 GSS,我必须保证 GSS 客户机可以发出并处理 GSS 包装器。

图 7 中的外围框标记为 InitialContextToken ,它实际上是包装了从 GSS 客户机到 GSS 服务器的消息的 GSS 包装器的名字。在 InitialContextToken 包装器中,第一个字段名为 thisMech ,它定义了 GSS 作为低层安全技术使用的安全机制(在这里是 Kerveros)。

InitialContextToken 框中的第二个字段标记为 KRB_AP_REQ ,我在讨论图 5 时分析过它。回想一下前面的讨论中说过 KRB_AP_REQ 结构包装了票据。这就是为什么我可以使用这个结构包装一个服务票据并发送给电子银行服务器。我说过服务票据已经包含了子会话密钥。

清单 5 提供了 Kerberos 客户机发给电子银行业务逻辑服务器的消息的 ASN.1 类定义。您可以将图 7 中的不同字段与清单 5 中的类定义进行对照。
清单 5. 从客户机到服务器的安全上下文请求消息的 ASN.1 类定义

   InitialContextToken ::=   [APPLICATION 0] IMPLICIT SEQUENCE {           thisMech        MechType                   -- MechType is OBJECT IDENTIFIER                   -- representing "Kerberos V5"           innerContextToken ANY DEFINED BY thisMech                   -- contents mechanism-specific;                   -- ASN.1 usage within innerContextToken                   -- is not required   }   AP-REQ ::= [APPLICATION 14] SEQUENCE {           pvno [0]        INTEGER,        -- indicates Version 5           msg-type [1]    INTEGER,        -- indicates KRB_AP_REQ           ap-options[2]   APOptions,           ticket[3]       Ticket,           authenticator[4]        EncryptedData   }   APOptions ::= BIT STRING {           reserved (0),           use-session-key (1),           mutual-required (2)   }   Ticket ::= [APPLICATION 1] SEQUENCE {           tkt-vno [0]     INTEGER,        -- indicates Version 5           realm [1]       Realm,           sname [2]       PrincipalName,           enc-part [3]    EncryptedData   }   -- Encrypted part of ticket   EncTicketPart ::= [APPLICATION 3] SEQUENCE {           flags[0]        TicketFlags,           key[1]          EncryptionKey,           crealm[2]       Realm,           cname[3]        PrincipalName,           transited[4]    TransitedEncoding,           authtime[5]     KerberosTime,           starttime[6]    KerberosTime OPTIONAL,           endtime[7]      KerberosTime,           renew-till[8]   KerberosTime OPTIONAL,           caddr[9]        HostAddresses OPTIONAL,           authorization-data[10]  AuthorizationData OPTIONAL   }   -- Unencrypted authenticator   Authenticator ::= [APPLICATION 2] SEQUENCE  {           authenticator-vno[0]    INTEGER,           crealm[1]               Realm,           cname[2]                PrincipalName,           cksum[3]                Checksum OPTIONAL,           cusec[4]                INTEGER,           ctime[5]                KerberosTime,           subkey[6]               EncryptionKey OPTIONAL,           seq-number[7]           INTEGER OPTIONAL,           authorization-data[8]   AuthorizationData OPTIONAL   }

电子银行的响应

当电子银行的业务逻辑服务器收到图 7 中的消息时,它提取出服务票据并解密票据中的加密部分以得到子会话密钥。客户机已经有了同样的子会话密钥。因此,服务器和客户机可以使用这个子会话密钥彼此进行安全通信。
图 8. 电子银行对 Kerberos 客户机的响应的结构 
 

电子银行业务逻辑服务器向客户机发回一个确认消息,如图 8 所示。下面是构成图 8 的消息的字段:

  • pvno :我在对图 2 的讨论中解释了这个字段。
  • msg-type :这是具有整数值 14 的消息类型标识符。
  • enc-part :消息的加密部分。客户机将用子会话密钥解密这个加密部分。在解密时,它将展现另一个带有以下字段的结构:
    • ctime 和 cusec :我在对图 6 的讨论中解释了这些字段。
    • subkey :这是一个服务器可能发送给客户机的可选密钥。如果服务器将这个密钥发送给客户机,那么这个密钥将被用于随后客户机与服务器之间的安全通信(取代子会话密钥)。
    • seq-number :我在对图 5 的讨论中解释了这个字段。

清单 6 提供了电子银行对 Kerveros 客户机的响应的 ASN.1 类定义。读者可以将图 8 中显示的不同字段与清单 6 中的类定义相对照。
清单 6. 从服务器到客户机的安全上下文响应的 ASN.1 类定义

   AP-REP ::= [APPLICATION 15] SEQUENCE {           pvno [0]        INTEGER,        -- represents Kerberos V5           msg-type [1]    INTEGER,        -- represents KRB_AP_REP           enc-part [2]    EncryptedData   }   EncAPRepPart ::= [APPLICATION 27] SEQUENCE {           ctime [0]       KerberosTime,           cusec [1]       INTEGER,           subkey [2]      EncryptionKey OPTIONAL,           seq-number [3]  INTEGER OPTIONAL   }





回页首

Kerberos 票据

在结束这篇文章之前,我想要展示 Kerberos 票据本身的结构。图 9 显示了 Kerberos 票据的结构。
图 9. Kerberos 票据的结构 
 

它包含 11 个字段:

  • tkt-vno :票据格式的版本。当前它是 5。
  • realm 和 sname :我在对图 2 的讨论中解释了这些字段。这两个字段共同指定可以给出有效票据的服务器的完整标识符。对于 TGT,这两个字段标识了 TGS。另一方面,对于服务票据,它们指定电子银行业务逻辑服务器。
  • enc-part :这是票据的机密部分。这个加密的部分解密后是另一个 Kerberos 结构,它包含如下所描述的一些字段:
    • flags :这是我在讨论图 2 中的 kdc-options 字段时提到的一组标志。其中一个标志用于说明这是 TGT 还是服务票据。
    • key :这是会话密钥(对于 TGT)或者子会话密钥(对于服务票据)。
    • creal 和 cname :我在对图 2 的讨论中解释了这些字段。
    • transited :正如前面提到的,在不同领域中工作的不同 Kerberos 服务器可以将票据从一个领域转发到另一个领域。这个字段指定在发布这种票据时所涉及的不同领域的名字。在移动银行应用程序中我不需要这种功能。
    • authtime :这是 KDC 验证请求客户身份的时间。
    • starttime 和 endtime :票据从 starttime 到 endtime 是有效的。
    • renew-till :正如前面提到的,Kerberos 票据是可以更新的。这种票据可以包含这个字段,它指定票据的最终失效时间。在这个时间之后,票据将不再是 renewable 的。
    • caddr :我在对图 4 的讨论中解释了这个字段。




回页首

展望:设计 Kerberos 客户机

在本系列的其余部分,我将构建一个 Kerberos 客户机,它为移动银行应用程序提供安全功能。Kerberos 客户机的主要目的是发布并处理这里详细说明的 Kerberos 消息。客户机将可以从客户机向票据或者电子银行服务器发布所有消息(图 2、5 和 7 所示的消息)并处理从服务器发回的消息(图 2、4、6、8 和 9 所示的消息)。

我所开发的 Kerberos 客户机将在资源有限的无线设备上运行。因此,客户机只有很少的资源。我的重点放在高效地使用可用的设备资源上。

安全应用程序通常使设备资源承担繁重的处理负荷。为了提高程序的效率,我必须对良好的面向对象的设计做法做出一些妥协。这对于 重大的 J2ME 应用程序来说是很常见的。本系列的后两篇文章中将展示在移动银行应用程序中是如何做的。




回页首

结束语

在本文中,我解释了移动银行应用程序的使用模型和安全性需求。我还描述了在 Kerberos 客户机和一个电子银行服务器之间交换加密密钥以进行安全通信的 Kerberos 消息的序列(以及 Kerberos 数据格式)。然后我简要展望了要在本系列后两篇文章中构建的 J2ME Kerberos 客户机。

我希望本文的内容对于所有希望了解 Kerberos 消息的工作细节的读者可以提供有用的信息。在写作本系列的其余部分时我需要用到所有这些信息。

参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文
  • 下载本文中讨论的全部 清单以及 表 2。 

  • 阅读 IETF.org 上的 Kerberos(版本 5)的官方 RFC 1510以及 GSS 的 RFC 1964。 

  • 访问 IETF 网站上的 Kerberos working group页。 

  • 在“ Simplify enterprise Java authentication with single sign-on”一文中( developerWorks,2003 年 9 月),Faheem Khan 使用 Kerberos 和 Java GSS API 论证了单点登录。 

  • MIT 上的这一页包含关于 Kerberos 的一组很好的链接。 

  • 下载完整的 ASN.1 文档和编码规则。 

  • 阅读 Jason Garman 的 Kerberos: The Definitive Guide 一书(O'Reilly & Associates,2003 年),以学习 Kerberos 的使用。 

  • 看看 IBM 产品是如何使用 Kerberos 的。 

  • IBM alphaWorks 提供了 Web Services Toolkit for Mobile Devices以将您的 J2ME 设备与 Web 服务世界相连接。 


关于作者

 

Faheem Khan 是一个独立软件顾问,专长是企业应用集成 (EAI) 和 B2B 解决方案。读者可以通过 fkhan872@yahoo.com与 Faheem 联系。


用 Kerberos 为 J2ME 应用程序上锁,第 2 部分: 生成一个 Kerberos 票据请求

J2ME 有足够的能力进行复杂的加密

文档选项

将此页作为电子邮件发送

未显示需要 JavaScript 的文档选项


级别: 初级

Faheem Khan (fkhan872@yahoo.com), 自由顾问

2004 年 1 月 16 日

在本系列的上一篇文章中,您看到了对可以安全地连接到支持 Kerveros 的服务器的 J2ME 应用程序的描述,还可了解在字节水平上 Kerberos 加密的细节问题。本文则深入到应用程序自身内部。您将看到如何使用 J2ME 的工具程序以及一些开放源代码库完成异常强大的加密任务。

在本系列的 上一篇文章 中,我介绍了一个使用 Kerberos 与电子银行服务器进行安全通信的移动银行 MIDlet 应用程序。我还解释了基于 J2ME 的 Kerveros 客户机应用程序与远程服务器交换 Kerberos 票据和密钥时所交换的数据格式和消息序列。

在本文中,我将开始实现生成并处理这些消息的 J2ME 类。我将首先简单描述构成这个基于 J2ME 的 Kerveros 客户机的主要类的作用,然后我将解释并展示这些类如何生成在第一篇文章中讨论过的基本 ASN.1 数据类型。在第三节中,我将展示如何生成一个用于在 Kerveros 通信中进行加密和解密的密钥。最后一节将展示 J2ME 客户机如何生成对 Kerveros 票据的请求。

基于 J2ME 的 Kerveros 客户机中的类

在本文中,将要讨论三个 J2ME 类的操作:

  • ASN1DataTypes
  • KerberosClient
  • KerberosKey

ASN1DataTypes 类将包装所有一般性的 ASN.1 功能,如发布像 INTEGER 和 STRING 这样的通用数据类型。 KerberosClient 类扩展 ASN1DataTypes 类,使用它的底层功能,并提供所有特定于 Kerveros 的功能。因此,可以说我将所需要的功能简单地分为两组:所有一般性的 ASN.1 功能都在 ASN1DataTypes 类中,而所有特定于 Kerveros 的功能都在 KerberosClient 类中。这提高了代码的重用性。如果您希望构建自己的、使用 ASN.1 功能的非 Kerveros 应用程序,那么您可以使用 ASN1DataTypes 类。

Kerberos 定义了一种利用用户的密码生成密钥的算法。 KerberosKey 类实现了这种算法 。在 Kerveros 通信中您将需要这个密钥。

我将在本文分别展示这些类中的每个方法。我还在一个单独的 源代码下载中加入了这些类。这个包将所有东西放到一组类中,可以将它们编译为一个 J2ME 项目。这个下载包含以下文件:

  • ReadMe.txt ,它包含描述如何根据本文的需要练习这些代码的指导。
  • ASN1DataTypes.java ,它实现了 ASN1DataTypes 类。
  • KerberosClient.java ,它实现了 KerberosClient 类。
  • KerberosKey.java ,它实现了 KerberosKey 类。
  • J2MEClientMIDlet.java ,它提供了可以用来测试这些代码的一个非常简单的 MIDlet 包装器。

现在,我将进一步探讨这些类的细节。




回页首

生成基本 ASN.1 数据类型

清单 1 中显示的 ASN1DataTypes 类处理生成和处理 ASN.1 数据结构所需要的所有底层功能。这个类包含两种方法: 生成(authoring) 方法负责生成 ASN.1 数据结构,而 处理(processing) 方法负责处理已生成的或者从远程应用程序收到的消息。我将在本文中解释并实现生成方法,在本系列的下一篇文章中讨论处理方法。

清单 1 只包含 ASN.1 类中不同方法的声明。我将在后面的几节中用单独的清单展示每一个方法的实现。
清单 1. ASN1DataTypes 类

public class ASN1DataTypes {    public byte[] getLengthBytes(int length){}    public byte[] getIntegerBytes (int integerContents){}    public byte[] getGeneralStringBytes (String generalStringContent){}    public byte[] getOctetStringBytes (byte[] octetStringContents){}    public byte[] getBitStringBytes (byte[] content){}    public byte[] getGeneralizedTimeBytes (byte[] generalizedTimeContent){}    public byte[] concatenateBytes (byte[] array1, byte[] array2){}    public byte[] getSequenceBytes (byte[] sequenceContents){}    public byte[] getTagAndLengthBytes (int tagType, int tagNumber, byte[] tagContents){}}//ASN1DataTypes

getLengthBytes()

(在清单 2 中显示的)这个方法将一个整数值( length )作为参数。它生成一个该长度的 ASN.1 表示,并返回一个符合 ASN.1 长度格式的字节数组。 
清单 2. getLengthBytes() 方法

    public byte[] getLengthBytes(int length)    {        if (length < 0)            return null;        byte lengthBytes[];        if (length <= 127)        {            lengthBytes = new byte[1];            lengthBytes[0] = (byte)(length & 0xff);        }        else        {            int tempLength = length;            int bytesRequired = 2;            do             {                tempLength = tempLength / 256;                if (tempLength > 0)                    bytesRequired ++;            }while (tempLength > 0);               lengthBytes = new byte[bytesRequired];            byte firstLengthByte = (byte) (bytesRequired -1);            firstLengthByte |= 0x80;            lengthBytes[0] = firstLengthByte;            int j = bytesRequired - 1;            for (int i=1; i < bytesRequired; i++) {                j--;                lengthBytes[i] = (byte)(length >>> (j*8) & 0xff);            }//for        }//else                return lengthBytes;    }//getLengthBytes

回想一下在本系列的 第一篇文章 中对表 2 的讨论,有两种表示字节长度的方法:单字节表示法和多字节表示法。单字节长度表示法用于表示小于或者等于 127 的长度值,而当长度值大于 127 时使用多字节长度表示法。

getLengthBytes() 方法首先检查长度值是否为负。如果为负,则只是返回 null,因为我不能处理负值。

然后这个方法检查长度值是否小于或者等于 127。如果是,就需要使用单字节长度表示法。

注意在 J2ME 中一个整数是 4 字节数据,而单字节长度表示法只需要 1 个字节。如果长度参数是 0 到 127 之间(包括这个两数)的一个值,那么其字节表达就在 0x00000000 与 0x0000007f 之间(意味着只有最低有效位字节包含有用的数据)。将这个整数造型为一个单字节时,只有最低有效位字节( 0x00 到 0x7f )会作为十六进制值拷贝到单字节数组。因此,如果长度值在 0 到 127 之间,那么我可以只执行该长度与 0xff 之间的一个按位 AND 操作。这个操作会得到一个整数,它有效的最高 3 个字节都将填入零。因此,我可以将按位操作的结果造型为一个字节,将这个字节放入一个单字节数组,并将这个数组返回给调用应用程序。

如果长度值大于 127,那么我必须使用多字节长度表示法,它至少使用 2 字节数据。第一个字节表明长度字节的字节数,后面是实际的长度字节(有关这种格式的详细解释请参阅 第一篇文章)。

如果长度值小于 256,那么就需要总共 2 个长度字节 ── 1 个字节表明还有一个长度字节,1 个字节包含实际的长度值。如果长度值至少为 256 并小于 65536(256 乘 256),那么就需要总共 3 个长度字节 ── 1 个字节表明还有 2 个长度字节,两个字节包含实际的长度值。

因此,在多字节格式中所需要的字节数取决于长度值。这就是为什么在 getLengthBytes() 的 else 块的 do - while 循环中要计算长度字节所需要的字节数。

确定所需要字节数的方法很简单。我声明了一个名为 bytesRequired 的字节计数器,从 2 开始计数(所需要的最少字节数),将长度值除以 256,并检查商是否大于或者等于 1。如果是,那么就表明原始长度值大于 256,因而需要至少 3 个字节,所以我增加计数器( bytesRequired )。

我继续将长度值除以 256 并增加字节计数器,直到除得的值小于 1。这时,我就知道找到了在多字节整数格式中需要的字节数。

知道了所需要的字节数后,我就实例化一个具有适当大小的字节数组。自然,长度字节中的第一个字节将表明还有多少个长度字节。因此,我只是将所需要的字节数减 1( bytesRequired-1 ),并拷贝到一个名为 firstLengthByte 的字节中。

看一下清单 2 中 getLengthBytes() 方法中的 firstLengthByte |= 0x80 这一行代码。这一行代码对 firstLengthByte和 0x80 ( 1000 0000 )进行按拉 OR 操作,并将结果储存到 firstLengthByte 中。这种逻辑 OR 操作会将 firstLengthByte 的最左边(最高有效)位设置为 1。回想在本系列 第一篇文章 中的讨论,在希望使用多字节整数格式的时候,必须将第一个长度字节的最左边一位设置为 1。

下一行( lengthBytes[0]=firstLengthByte )只是拷贝在包含长度字节的数组的开始位置上的 firstLengthByte 。然后,有一个 for 循环,它将长度字节从长度参数中拷贝到在 lengthBytes 数组中它们的正确位置上。当 for 循环退出时,就得到了符合 ASN.1 格式的这个 lengthBytes 数组。清单 2 中 getLengthBytes() 方法的最后一行返回这个数组。

getIntegerBytes()

这个方法取一个整数( value )作为参数并返回以 ASN.1 INTEGER 表达的这个整数值。回想一下在本系列 第一篇文章的表 1 中曾提到,在ASN.1 中 INTEGER 是一种通用数据类型。

清单 3 中显示了 getIntegerBytes() 方法的实现。 
清单 3. getIntegerBytes() 方法

    public byte[] getIntegerBytes (int integerContents)    {        //1. Declare a byte array named finalBytes, which will         //   hold all the bytes of the ASN.1 byte array representation.        byte finalBytes[];        //2. Calculate the number of bytes required to hold the         //   contents part of the ASN.1 byte array representation.        int tempValue = integerContents;        int contentBytesCount  = 1;        do {            tempValue = tempValue / 256;            if (tempValue >0)                contentBytesCount  ++;        } while (tempValue > 0);        //3. Use the getLengthBytes() method of Listing 3 to author         //   the length bytes. Store the length bytes in an array named lengthBytes.        byte lengthBytes[] = getLengthBytes(contentBytesCount );        //4. Get the number of bytes in the lengthBytes array.        int lengthBytesCount = lengthBytes.length;        //5. Calculate the number of bytes required to hold the         //   complete ASN.1 byte array representation         //   (the sum total of the number of tag bytes, length bytes, and content bytes).        //   Store the number of bytes in a variable named totalBytesCount.        int totalBytesCount = 1 + lengthBytesCount + contentBytesCount  ;        //6. Instantiate the finalBytes array to totalBytesCount size.        finalBytes = new byte[totalBytesCount];        //7. Copy the tag byte at the start of the finalBytes array.        finalBytes[0] = (byte)0x02;        //8. Copy the length bytes from the lengthBytes array         //   to the finalBytes array just after the tag byte.        for (int i=0; i < lengthBytes.length; i++)            finalBytes[i+1] = lengthBytes[i];        //9. Copy the content bytes to the finalBytes array         //   just after the length bytes.        int k = totalBytesCount - lengthBytesCount - 1;        for (int j=lengthBytesCount+1; j<totalBytesCount; j++){            k--;                        finalBytes[j] = (byte) (integerContents >>> (k*8) & 255);         }//for        //10. Return the finalBytes array.        return finalBytes;    }//getIntegerBytes

这个方法首先声明一个名为 finalBytes 的字节数组。这个字节数组包含 INTEGER 数据类型结构的所有字节。不过,我还不知道 finalBytes 数组的大小。我首先需要计算 INTEGER 结构中的字节数,这种计算由几步组成:

第一步是计算容纳这个整数值( INTEGER 结构的内容部分)所需要的字节数。为此,我使用了一个 do - while 循环,它不断地将 value 整数除以 256,直到得到的值小于1。当这个循环退出时,容纳内容部分所需要的字节数就储存在一个名为 contentBytesCount 的变量中。

这个方法再将所需要的长度作为一个整数传递给 getLengthBytes() 方法,这个方法返回以 ASN.1 表达的长度字节。我将长度字节数储存到一个名为 lengthBytesCount 的变量中。

回想一下在 本系列第一篇文章中讨论过,所有 ASN.1 数据类型表达的字节数组都包含三个部分:标签字节、长度字节和内容字节。因此,ASN.1 字节数组表达需要包含所有这三部分的足够空间。

下一步是计算将要包含 INTEGER 结构的所有字节的数组的大小。我是通过将标签字节长度(对于 INTEGER 和所有其他在 Kerberos 中使用的标签来说是 1)、长度字节数和内容字节数相加进行这种计算的。 int totalBytesCount = 1 + lengthBytesCount + contentBytesCount; 一行进行的就是这种计算,并将所需要的字节数储存到一个名为 totalBytesCount的变量中。

下面,我实例化一个大小为 totalBytesCount 的字节数组 finalBytes 。过程的其余部分很简单,我将标签字节(对于 INTEGER 来说是 0x02 )储存到 finalBytes 数组的开始处。然后,将长度字节拷贝到 finalBytes 数组中标签字节后面。最后,我将内容字节拷贝到长度字节后并返回 finalBytes 数组。

getGeneralStringBytes()、getOctetStringBytes()、getBitStringBytes() 和 getGeneralizedTimeBytes()

像 getIntegerBytes() 一样,每一个方法返回一种 ASN.1 通用数据类型结构。

清单 4 中的 getGeneralStringBytes() 方法生成一个 ASN.1 GeneralString 的字节数组表达。类似地,清单 5 中的 getOctetStringBytes() 方法返回 ASN.1 OctetString 的字节数组表达。清单 6 中的 getBitStringBytes() 方法返回 BitString 的 ASN.1 表达。最后,清单 7 中的 getGeneralizedTimeBytes() 方法返回 ASN.1 GeneralizedTime 值的字节数组表达。

所有这些方法遵循在前面对 getIntegerBytes() 方法的讨论中见过的同样实现逻辑:

  1. 声明一个名为 finalBytes 的字节数组,它将包含 ASN.1 字节数组表达的所有字节。
  2. 计算容纳 ASN.1 字节数组表达的内容所需要的字节数。
  3. 用清单 3 中的 getLengthBytes() 方法生成长度字节。将长度字节储存到一个名为 lengthBytes 的数组中。
  4. 得到 lengthBytes 数组中的字节数。
  5. 计算容纳完整的 ASN.1 字节数组表达所需要的字节数(标签字节、长度字节和内容字节的总和)。将这个字节数储存到一个名为 totalBytesCount 的变量中。
  6. 实例化一个具有 totalBytesCount 的值大小的 finalBytes 数组。
  7. 将标签字节拷贝到 finalBytes 数组的开始处。
  8. 将 lengthBytes 数组中的长度字节拷贝到 finalBytes 数组中紧随标签字节的位置。
  9. 将内容字节拷贝到 finalBytes 数组中紧随长度字节的位置。
  10. 返回 finalBytes 数组。

清单 4、清单 5、清单 6 和清单 7 带有帮助您跟踪和对照上述 10 步中每一步与 J2ME 代码中相应行的注释。
清单 4. getGeneralStringBytes() 方法

    public byte[] getGeneralStringBytes (String generalStringContent)    {        //1. Declare a byte array named finalBytes, which will         //   hold all the bytes of the ASN.1 byte array representation.        byte finalBytes[];        //2. Calculate the number of bytes required to hold the         //   contents part of the ASN.1 byte array representation.        int contentBytesCount  = generalStringContent.length();        //3. Use the getLengthBytes() method of Listing 3 to author         //   the length bytes. Store the length bytes in         //   an array named lengthBytes.        byte lengthBytes[] = getLengthBytes(contentBytesCount );        //4. Get the number of bytes in the lengthBytes array.        int lengthBytesCount = lengthBytes.length;        //5. Calculate the number of bytes required to hold the complete         //   ASN.1 byte array representation (the sum total of the number         //   of tag bytes, length bytes, and content bytes).         //   Store the number of bytes in a variable named totalBytesCount.        int totalBytesCount = 1 + lengthBytesCount + contentBytesCount ;        //6. Instantiate the finalBytes array to totalBytesCount size.        finalBytes = new byte[totalBytesCount];        //7.Copy the tag byte at the start of the finalBytes array.        finalBytes[0] = (byte)0x1B;        //8. Copy the length bytes from the lengthBytes array        //   to the finalBytes array just after the tag byte.        for (int i=0; i < lengthBytes.length; i++)            finalBytes[i+1] = lengthBytes[i];        //9. Copy the content bytes to the finalBytes array just after the length bytes.        byte tempString[] = generalStringContent.getBytes();        for (int j=lengthBytesCount+1; j<totalBytesCount; j++)            finalBytes[j] = tempString[j-(lengthBytesCount+1)];         //10. Return the finalBytes array.        return finalBytes;    }//getGeneralStringBytes



清单 5. getOctetStringBytes() 方法
    public byte[] getOctetStringBytes (byte[] octetStringContents)    {        //1. Declare a byte array named finalBytes, which will         //   hold all the bytes of the ASN.1 byte array representation.        byte finalBytes[];        //2. Calculate the number of bytes required to hold the         // contents part of the ASN.1 byte array representation.        int contentBytesCount  = octetStringContents.length;        //3. Use the getLengthBytes() method of Listing 3 to author         //   the length bytes. Store the length bytes in         //   an array named lengthBytes.        byte lengthBytes[] = getLengthBytes(contentBytesCount );        //4. Get the number of bytes in the lengthBytes array.        int lengthBytesCount = lengthBytes.length;        //5. Calculate the number of bytes required to hold the complete         //   ASN.1 byte array representation (the sum total of the number         //   of tag bytes, length bytes, and content bytes).         //   Store the number of bytes in a variable named totalBytesCount.        int totalBytesCount = 1 + lengthBytesCount + contentBytesCount ;        //6. Instantiate the finalBytes array to totalBytesCount size.        finalBytes = new byte[totalBytesCount];        //7. Copy the tag byte at the start of the finalBytes array.        finalBytes[0] = (byte)0x04;        //8. Copy the length bytes from the lengthBytes array to the         //   finalBytes array just after the tag byte.        for (int i=0; i < lengthBytes.length; i++)            finalBytes[i+1] = lengthBytes[i];        //9. Copy the content bytes to the finalBytes array         //   just after the length bytes.        for (int j=lengthBytesCount+1; j<totalBytesCount; j++)            finalBytes[j] = octetStringContents[j-(lengthBytesCount+1)];         //10. Return the finalBytes array.        return finalBytes;    }//getOctetStringBytes



清单 6. getBitStringBytes() 方法
    public byte[] getBitStringBytes (byte[] content)    {        //1. Declare a byte array named finalBytes, which will         //   hold all the bytes of the ASN.1 byte array representation.        byte finalBytes[];  //2. Calculate the number of bytes required to hold the         //   contents part of the ASN.1 byte array representation.        int contentBytesCount  = content.length;        //3. Use the getLengthBytes() method of Listing 3 to author         //   the length bytes. Store the length bytes in         //   an array named lengthBytes.        byte lengthBytes[] = getLengthBytes(contentBytesCount );        //4. Get the number of bytes in the lengthBytes array.        int lengthBytesCount = lengthBytes.length;        //5. Calculate the number of bytes required to hold the complete         //   ASN.1 byte array representation (the sum total of the number         //   of tag bytes, length bytes, and content bytes).         //   Store the number of bytes in a variable named totalBytesCount.        int totalBytesCount = 1 + lengthBytesCount + contentBytesCount ;        //6. Instantiate the finalBytes array to totalBytesCount size.        finalBytes = new byte[totalBytesCount];        //7. Copy the tag byte at the start of the finalBytes array.        finalBytes[0] = (byte)0x03;        //8. Copy the length bytes from the lengthBytes array to the         //   finalBytes array just after the tag byte.        for (int i=0; i < lengthBytes.length; i++)            finalBytes[i+1] = lengthBytes[i];        //9. Copy the content bytes to the finalBytes array         //   just after the length bytes.        for (int j=lengthBytesCount+1; j<totalBytesCount; j++)            finalBytes[j] = content[j-(lengthBytesCount+1)];         //10. Return the finalBytes array.        return finalBytes;    }//getBitStringBytes



清单 7. getGeneralizedTimeBytes() 方法
    public byte[] getGeneralizedTimeBytes (byte[] generalizedTimeContent)    {        //1. Declare a byte array named finalBytes, which will         //   hold all the bytes of the ASN.1 byte array representation.        byte finalBytes[];        //2. Calculate the number of bytes required to hold the         //   contents part of the ASN.1 byte array representation.        int contentBytesCount  = generalizedTimeContent.length;        //3. Use the getLengthBytes() method of Listing 3 to author         //   the length bytes. Store the length bytes in         //   an array named lengthBytes.        byte lengthBytes[] = getLengthBytes(contentBytesCount );        //4. Get the number of bytes in the lengthBytes array.        int lengthBytesCount = lengthBytes.length;        //5. Calculate the number of bytes required to hold the complete         //   ASN.1 byte array representation (the sum total of the number         //   of tag bytes, length bytes, and content bytes).         //   Store the number of bytes in a variable named totalBytesCount.        int totalBytesCount = 1 + lengthBytesCount + contentBytesCount ;        //6. Instantiate the finalBytes array to totalBytesCount size.        finalBytes = new byte[totalBytesCount];        //7. Copy the tag byte at the start of the finalBytes array.        finalBytes[0] = (byte)0x18;        //8. Copy the length bytes from the lengthBytes array to the         //   finalBytes array just after the tag byte.        for (int i=0; i < lengthBytes.length; i++)            finalBytes[i+1] = lengthBytes[i];        //9. Copy the content bytes to the finalBytes array         //   just after the length bytes.        for (int j=lengthBytesCount+1; j<totalBytesCount; j++)            finalBytes[j] = generalizedTimeContent[j-(lengthBytesCount+1)];         //10. Return the finalBytes array.        return finalBytes;    }//getGeneralizedTimeBytes

concatenateBytes()

这个方法(见清单 8)取两个字节数组,将第二个数组串接到第一个之后,并返回串接的数组。

因为这个方法取两个字节数组并返回另一个字节数组,所以它可以自身串联任意次以串接任意数量的字节数组。例如, concatenateBytes(byteArray1, concatenateBytes(byteArray2, byteArray3)) 会将 byteArray3 加在 byteArray2 后,再将结果加到 byteArray1 后。 
清单 8. concatenateBytes() 方法

    public byte[] concatenateBytes (byte[] array1, byte[] array2)    {        byte concatenatedBytes[] = new byte[array1.length + array2.length];        for (int i=0; i<array1.length; i++)             concatenatedBytes[i] = array1[i];        for (int j=array1.length; j<concatenatedBytes.length; j++)             concatenatedBytes[j] = array2[j-array1.length];        return concatenatedBytes;    }//concatenateBytes

getSequenceBytes()

这个方法(见清单 9)生成一个 ASN.1 SEQUENCE 的字节数组表达。它取一个字节数组作为输入参数,将这个字节数组作为 SEQUENCE 的内容,在内容前面加上 SEQUENCE 标签字节( 0x30 )和长度字节,并返回完整的 SEQUENCE 结构。

通常, getSequenceBytes() 方法会与 concatenateBytes() 配合使用。一个应用程序将生成 SEQUENCE 中单独的结构,将各个结构的字节数组表达串接在一起以构成一个数组,并将串接后的数组传递给 getSequenceBytes() 方法,这个方法将返回 SEQUENCE 的完整字节数组表达。 
清单 9. getSequenceBytes() 方法

    public byte[] getSequenceBytes (byte[] sequenceContents)    {        //1. Declare a byte array named finalBytes, which will         //   hold all the bytes of the ASN.1 byte array representation.        byte finalBytes[];        //2. Calculate the number of bytes required to hold the         //   contents part of the ASN.1 byte array representation.        int contentBytesCount  = sequenceContents.length;        //3. Use the getLengthBytes() method of Listing 3 to author         //   the length bytes. Store the length bytes in         //   an array named lengthBytes.        byte lengthBytes[] = getLengthBytes(contentBytesCount );        //4. Get the number of bytes in the lengthBytes array.        int lengthBytesCount = lengthBytes.length;        //5. Calculate the number of bytes required to hold the complete         //   ASN.1 byte array representation (the sum total of the number         //   of tag bytes, length bytes, and content bytes).         //   Store the number of bytes in a variable named totalBytesCount.        int totalBytesCount = lengthBytesCount + 1;        //6. Instantiate the finalBytes array to totalBytesCount size.        finalBytes = new byte[totalBytesCount];        //7. Copy the tag byte at the start of the finalBytes array.        finalBytes[0] = (byte)0x30;        //8. Copy the length bytes from the lengthBytes array to the         //   finalBytes array just after the tag byte.        for (int i=0; i < lengthBytes.length; i++)            finalBytes[i+1] = lengthBytes[i];        //9. Copy the content bytes to the finalBytes array         //   just after the length bytes.        finalBytes = concatenateBytes (finalBytes, sequenceContents);        //10. Return the finalBytes array.        return finalBytes;    }//getsequenceBytes

getTagAndLengthBytes()

这个方法与所讨论过的各种 getXXXBytes() 方法非常相象。不过,虽然其中每一个方法生成一个特定的 ASN.1 通用数据类型,但是 getTagAndLengthBytes() 方法(见清单 10)生成应用程序级和上下文特定的数据类型。

这个方法取三个参数。第一个参数( tagType )指定标签类型。如果它的值等于静态整数 ASN1DataTypes.Context_Specific ,那么它指定的是一个上下文特定标签,如果它的值等于 ASN1DataTypes.Application_Type ,那么它指定的是一个应用程序级标签。

第二个参数( tagNumber )指定标签数,而第三个( tagContents )包含了内容字节数组。

getTagAndLengthBytes() 根据输入参数计算标签和长度字节的值,将标签和长度字节加到内容字节前面,并返回应用程序级或者上下文特定的 ASN.1 结构的完整字节数组表达。 
清单 10. getTagAndLengthBytes() 方法

    public byte[] getTagAndLengthBytes (int tagType, int tagNumber, byte[] tagContents)    {        //1. Declare a byte array named finalBytes,         //   which will hold all the bytes of the ASN.1 byte array representation.        byte finalBytes[];        //2. Declare a byte array named tagAndLengthBytes,        //   which will hold the tag and length bytes.        byte tagAndLengthBytes[];        //3. Now calculate the value of the tag byte.        int tag = tagType + tagNumber;        //4. Calculate the number of bytes required to hold        //   the contents part of the ASN.1 byte array representation.        int contentBytesCount  = tagContents.length;        //5. Use the getLengthBytes() method of Listing 3         //   to author the length bytes.        //   Store the length bytes in an array named lengthBytes.        byte lengthBytes[] = getLengthBytes (contentBytesCount);        //6. Get the number of bytes in the lengthBytes array.        int lengthBytesCount = lengthBytes.length;        //7. Calculate the number of bytes required to hold         //   the tag byte and length bytes         //   (the sum total of the number of tag bytes and length bytes).        //   Store the number of bytes in a variable named tagBytesCount.        int tagAndLengthBytesCount =  1 + lengthBytesCount;        //8. Instantiate the finalBytes array to tagAndLengthBytesCount size.        tagAndLengthBytes = new byte[tagAndLengthBytesCount];        //9. Copy the tag byte at the start of the tagAndLengthBytes array.        tagAndLengthBytes[0] = (byte)tag;        //10. Copy the length bytes from the lengthBytes array         //    to the tagAndLengthBytes array just after the tag byte.        for (int i=0; i < lengthBytes.length; i++)            tagAndLengthBytes[i+1] = lengthBytes[i];        //11. Now instansiate the finalBytes array of size equal to         //    the sum total of the number of tag bytes,         //    length bytes and content bytes.        finalBytes = new byte [1 + tagAndLengthBytesCount + contentBytesCount ];        //12. Copy the content bytes to the finalBytes array         // just after the length bytes.        finalBytes = concatenateBytes(tagAndLengthBytes, tagContents);        //13. Return the finalBytes array.        return finalBytes;    }//getTagAndLengthBytes

至此就完成了对 ASN1DataTypes 类的生成方法的讨论。不过,在开始讨论 KerberosClient 如何使用 ASN1DataTypes 方法生成一个 TGT 请求之前,我需要讨论如何利用用户的密码生成密钥。在与 Kerberos 服务器进行通信时,会在几个地方需要这个密钥。 




回页首

利用用户密码生成密钥

Kerberos 定义了一种对用户密码进行处理以生成一个 密钥的算法。在获得 TGT 的过程中 Kerberos 客户机将用这个密钥进行解密

对这个基于 J2ME 的 Kerberos 客户机,我将只支持一种加密算法,即 CBC(密码分组链接 cipher block chaining)模式下的 DES(数据加密标准)。DES 是一个 FIPS(联邦信息处理标准 Federal Information Processing Standards)发表,它描述了一种将要加密的数据(纯文本)和密钥作为输入传递给加密过程的加密算法。根据 DES 算法对密钥和纯文本统一处理以生成一个加密的(密文)形式的纯文本数据。(有关 DES 的更多信息请参阅 参考资料)。

CBC 是一种加密操作模式,其中纯文本数据分为同样大小的数据块。例如,在 64 位 DES-CBC 加密中,数据会分为 8 字节的块。如果纯文数据中的字节数不是您希望每一个块所具有的字节数的整数倍,就要在最后一块中加上适当的数量的字节以使它的大小与其他的块相同。

然后创建一个与您的块具有同样大小的字节数组。这个字节数组称为 初始矢量(IV)。Kerveros 规范定义了所有基于 Kerberos 的应用程序的初始矢量(类似地,其他使用 DES-CBC 的规范定义了它们使用的 IV 值)。之后,取这个 IV、纯文数据的第一块以及密钥并根据 DES 算法对它们共同进行处理,以构成对应于纯文本数据第一个数据块的密文。然后取第一个数据块的密文形式作为第二个块的初始矢量并进行同样的 DES 加密过程以生成第二个纯文本数据块的密文形式。以这种方式继续一块接一块地生成每一个块的密文形式。最后,串接所有密文块以得到全部纯文本数据的密文形式。

因为我只打算在这个 Kerberos 客户机中支持 DES-CBC,所以我将只讨论 DES-CBC 所使用的密钥的生成过程,如下所示:

  1. 将用户密码、KDC 域名和用户的用户名串接到一起以构成一个字符串。Kerberos 利用这个串接的字符串而不仅仅是密码生成密钥。为什么要在密钥生成中加入域名和用户名呢?许多用户会在不同的服务器上使用同样的密码。如果我只使用密码生成密钥,那么一个给定的密码在所有 Kerberos 服务器上总是会生成同样的密钥。因而,如果一个黑客可以取得用户在一台 Kerberos 服务器上的密钥,那么,他就可以在所有 Kerberos 服务器上使用同一个密钥。另一方面,如果我加入了域名和用户名,那么一个受到这种攻击的密钥将只会侵害特定的域。
  2. 得到第 1 步中串接的字符串的字节数组表达。
  3. 统计第 2 步中字节数组中的字节数。在这个字节串的后面附加适当数量的零字节以使它成为 8 的整数倍。例如,如果这个字节数组包含 53 个字节,那么就在这个字节数组的最后附加三个字节使它具有 56 个字节。
  4. 将第 3 步中附加了字节后的字节数组分为大小相同的块,每一块有 8 个字节。
  5. 每隔一个块倒转块的位顺序。换句话说,第一块保持不变,第二块的位顺序应该倒转,第三块应保持不变,第中块的位顺序应倒转,以此类推。
  6. 取第一个(未改变的)块并与第二个(倒转的)块进行每一位的 exclusive OR 。然后将第一次 exclusive OR 操作得到的结果与第三个(未改变的)块进行另一次 exclusive OR 操作。继续 exclusive OR 操作直到完成了所有块。所有 exclusive OR 操作的最后结果是一个 8 字节长的块。 
  7. 修正在第 6 步中得到的 8 字节块的奇偶性。每一块的最低有效位保留为奇偶位。统计 8 字节块中每字节中的 1 的个数,如果 1 的个数为偶数,那么就设置最低位为 1 使它成为奇数。例如,如果一个字节的值为 00000000 ,那么就要将它改为 00000001 。如果一个字节中 1 的个数已经为奇数,那么就将它的最低位设置为零。例如,如果一个字节为 00000010 ,那么就不需要为修正其奇偶性做任何改变。 
  8. DES 定义了一些弱的、因而不适合用于加密的密钥。我们的密钥生成过程的第八步是要检查奇偶修正后的字节数组是否是一个弱的密钥。如果是的话,就要用 0xf0 ( 11110000 )与奇偶修正过的 8 字节块进行 exclusive OR。如果奇偶修正得到的不是弱密钥,那么就不需要进行这种 exclusive OR 操作。经过这种弱密钥处理的字节数组是一个临时密钥。 
  9. 现在我要使用这个临时密钥以 DES-CBC 算法加密第 3 步中得到的附加后的字节数组。这个临时密钥同时作为密钥的值和 DES-CBC 加密的初始矢量的值。回想在前面的讨论中说过,CBC 要求密文块链接。第 9 步的结果是最后 8 字节块的加密结果(放弃所以以前的密文块)。因此,这一步的结果是另一个 8 字节块。
  10. 现在我修正第 9 步产生的 8 字节块中的每一个字节的奇偶性。在上面第 7 步中我解释了奇偶性修正。
  11. 现在再次检查第 10 步得到的经过奇偶修正的 8 字节块是不是弱密钥(就像在第 8 步中所做的那样)。

第 11 步的结果是一个 Kerveros 客户机可以用来与 Kerberos 服务器进行通信的密钥。

现在看一下清单 11 中的 KerberosKey 类。这个类的 generateKey() 方法实现了上面描述的 11 步密钥生成算法。 
清单 11. KerberosKey 类

import org.bouncycastle.crypto.params.ParametersWithRandom;import org.bouncycastle.crypto.modes.CBCBlockCipher;import org.bouncycastle.crypto.generators.DESKeyGenerator;import org.bouncycastle.crypto.params.DESParameters;import org.bouncycastle.crypto.engines.DESEngine;import org.bouncycastle.crypto.params.KeyParameter;import org.bouncycastle.crypto.params.ParametersWithIV;public class KerberosKey{    private CBCBlockCipher cipher;    private KeyParameter kp;    private ParametersWithIV iv;    private byte kerberosKey[];    private ASN1DataTypes asn1;    private String principalID;    public KerberosKey(String userName, String password, String realmName)    {        kerberosKey = new byte[8];        kerberosKey = generateKey (password, realmName, userName);    }//KerberosKey    public byte[] generateKey (String password, String realmName, String userName)    {        //Step 1:        String str = new String (password + realmName + userName);        byte secretKey [] = new byte[8];        //Step 2:        byte encodedByteArray[] = encodeString(str);        //Step 3:        byte paddedByteArray[] = padString(encodedByteArray);        //Step 4:        int i = paddedByteArray.length / 8;        //Step 5:        for(int x=0; x<i; x++)        {            byte blockValue1[] = new byte [8];            System.arraycopy (paddedByteArray, x*8, blockValue1, 0, 8);            if(x % 2 == 1)            {                byte tempbyte1 = 0;                byte tempbyte2 = 0;                byte blockValue2[] = new byte [8];                for (int y=0; y<8; y++)                {                    tempbyte2 = 0;                    for (int z=0; z<4; z++)                    {                        tempbyte2 = (byte) ((1<<(7-z)) & 0xff);                        tempbyte1 |= (blockValue1[y] & tempbyte2) >>> (7-2*z);                        tempbyte2 = 0;                    }                    for (int z=4; z<8; z++)                    {                        tempbyte2 = (byte) ((1<<(7-z)) & 0xff);                        tempbyte1 |= (blockValue1[y] & tempbyte2) << (2*z-7);                        tempbyte2 = 0;                    }                    blockValue2 [7-y] =tempbyte1;                    tempbyte1 = 0;                }//outer for                for (int a = 0; a <8; a ++)                    blockValue2[a] = (byte) ((((byte)blockValue2[a] & 0xff) >>> 1) & 0xff);                System.arraycopy(blockValue2, 0, blockValue1, 0, blockValue2.length);            }//if(x % 2 == 1)            for (int a = 0; a <8; a ++)                blockValue1[a] = (byte) ((((byte)blockValue1[a] & 0xff) << 1) & 0xff);            //Step 6:            for (int b = 0; b <8; b ++)                secretKey[b] ^= blockValue1[b];        }// for            //Step 7:        secretKey= setParity(secretKey);        //Step 8:         if (isWeakKey(secretKey))            secretKey = getStrongKey(secretKey);          //Step 9:        secretKey = getFinalKey(paddedByteArray, secretKey);        //Step 10:        secretKey = setParity(secretKey);        if (isWeakKey(secretKey))            secretKey = getStrongKey(secretKey);          return secretKey;    }//generateKey    public byte[] getFinalKey (byte data[], byte key[])    {        //The cipher instance with DES algo and CBC mode.        cipher = new CBCBlockCipher( new DESEngine());        kp = new KeyParameter(key);        iv = new ParametersWithIV (kp, key);        cipher.init(true, iv);        byte encKey[] = new byte[data.length];        byte ivBytes[] = new byte[8];        for(int x = 0; x < data.length / 8; x ++)        {            cipher.processBlock(data, x*8, encKey, x*8);            System.arraycopy(encKey, x*8, ivBytes, 0, 8);            iv = new ParametersWithIV (kp, ivBytes);            cipher.init (true, iv);        }                return ivBytes;    }//getFinalKey    public byte[] setParity (byte byteValue[])    {        for(int x=0; x<8; x++)            byteValue[x] = parityValues[byteValue[x] & 0xff];        return byteValue;    }    // Checks weak key    public boolean isWeakKey (byte keyValue[])    {        byte weakKeyValue[];            for(int x = 0; x < weakKeyByteValues.length; x++)            {                weakKeyValue = weakKeyByteValues[x];                if(weakKeyValue.equals(keyValue))                    return true;            }          return false;    }//isWeakKey// Corrects the weak key by exclusive OR with 0xf0 constant.    public byte[] getStrongKey(byte keyValue[])    {        keyValue[7] ^= 0xf0;        return keyValue;    }//checkWeakKey    // Encodes string with ISO-Lation encodings    public byte[] encodeString (String str)    {        byte encodedByteArray[] = new byte[str.length()];        try        {            encodedByteArray = str.getBytes("8859_1");        }        catch(java.io.UnsupportedEncodingException ue)        {         }        return encodedByteArray;    }//encodeString    //This method pads the byte[] with ASCII nulls to an 8 byte boundary.    public byte[] padString (byte encodedString[])    {         int x;         if(encodedString.length < 8)             x = encodedString.length;         else             x = encodedString.length % 8;         if(x == 0)             return encodedString;          byte paddedByteArray[] = new byte[(8 - x) + encodedString.length];         for(int y = paddedByteArray.length - 1; y > encodedString.length - 1; y--)             paddedByteArray[y] = 0;          System.arraycopy(encodedString, 0, paddedByteArray, 0, encodedString.length);         return paddedByteArray;     }//padString    //returns the secret key bytes.    public byte[] getKey()    {        return this.kerberosKey;    }//getKey()    private byte weakKeyByteValues[][] = {        {(byte)0x10, (byte)0x10, (byte)0x10, (byte)0x10,         (byte)0x10, (byte)0x10, (byte)0x10, (byte)0x1},        {(byte)0xfe, (byte)0xfe, (byte)0xfe, (byte)0xfe,         (byte)0xfe, (byte)0xfe, (byte)0xfe, (byte)0xfe},        {(byte)0x1f, (byte)0x1f, (byte)0x1f, (byte)0x1f,         (byte)0x1f, (byte)0x1f, (byte)0x1f, (byte)0x1f},        {(byte)0xe0, (byte)0xe0, (byte)0xe0, (byte)0xe0,         (byte)0xe0, (byte)0xe0, (byte)0xe0, (byte)0xe0},        {(byte)0x1f, (byte)0xe0, (byte)0x1f, (byte)0xe0,         (byte)0x1f, (byte)0xe, (byte)0x01, (byte)0xfe},        {(byte)0xfe, (byte)0x01, (byte)0xfe, (byte)0x01,         (byte)0xfe, (byte)0x01, (byte)0xfe, (byte)0x01},        {(byte)0x1f, (byte)0xe0, (byte)0x1f, (byte)0xe0,         (byte)0x0e, (byte)0xf1, (byte)0x0e, (byte)0xf1},        {(byte)0xe0, (byte)0x1f, (byte)0xe0, (byte)0x1f,         (byte)0xf1, (byte)0x0e, (byte)0xf1, (byte)0x0e},        {(byte)0x1e, (byte)0x00, (byte)0x1e, (byte)0x00,         (byte)0x1f, (byte)0x10, (byte)0x1f, (byte)0x1},        {(byte)0xe0, (byte)0x01, (byte)0xe0, (byte)0x01,         (byte)0xf1, (byte)0x01, (byte)0xf1, (byte)0x01},         {(byte)0x1f, (byte)0xfe, (byte)0x1f, (byte)0xfe,         (byte)0x0e, (byte)0xfe, (byte)0x0e, (byte)0xfe},        {(byte)0xfe, (byte)0x1f, (byte)0xfe, (byte)0x1f,         (byte)0xfe, (byte)0x0e, (byte)0xfe, (byte)0x0e},        {(byte)0x11, (byte)0xf0, (byte)0x11, (byte)0xf0,         (byte)0x10, (byte)0xe0, (byte)0x10, (byte)0xe},        {(byte)0x1f, (byte)0x01, (byte)0x1f, (byte)0x01,         (byte)0x0e, (byte)0x01, (byte)0x0e, (byte)0x01},        {(byte)0xe0, (byte)0xfe, (byte)0xe0, (byte)0xfe,         (byte)0xf1, (byte)0xfe, (byte)0xf1, (byte)0xfe},        {(byte)0xfe, (byte)0xe0, (byte)0xfe, (byte)0xe0,         (byte)0xfe, (byte)0xf1, (byte)0xfe, (byte)0xf1}    };    //Parity values for all possible combinations    //256 entries    private byte parityValues[] = {        1, 1, 2, 2, 4, 4, 7, 7, 8, 8,         11, 11, 13, 13, 14, 14, 16, 16, 19, 19,         21, 21, 22, 22, 25, 25, 26, 26, 28, 28,         31, 31, 32, 32, 35, 35, 37, 37, 38, 38,         41, 41, 42, 42, 44, 44, 47, 47, 49, 49,         50, 50, 52, 52, 55, 55, 56, 56, 59, 59,         61, 61, 62, 62, 64, 64, 67, 67, 69, 69,         70, 70, 73, 73, 74, 74, 76, 76, 79, 79,         81, 81, 82, 82, 84, 84, 87, 87, 88, 88,         91, 91, 93, 93, 94, 94, 97, 97, 98, 98,         100, 100, 103, 103, 104, 104, 107, 107, 109, 109,         110, 110, 112, 112, 115, 115, 117, 117, 118, 118,         121, 121, 122, 122, 124, 124, 127, 127, -128, -128,         -125, -125, -123, -123, -122, -122, -119, -119, -118, -118,         -116, -116, -113, -113, -111, -111, -110, -110, -108, -108,         -105, -105, -104, -104, -101, -101, -99, -99, -98, -98,         -95, -95, -94, -94, -92, -92, -89, -89, -88, -88,         -85, -85, -83, -83, -82, -82, -80, -80, -77, -77,         -75, -75, -74, -74, -71, -71, -70, -70, -68, -68,         -65, -65, -63, -63, -62, -62, -60, -60, -57, -57,         -56, -56, -53, -53, -51, -51, -50, -50, -48, -48,         -45, -45, -43, -43, -42, -42, -39, -39, -38, -38,         -36, -36, -33, -33, -32, -32, -29, -29, -27, -27,         -26, -26, -23, -23, -22, -22, -20, -20, -17, -17,         -15, -15, -14, -14, -12, -12, -9, -9, -8, -8,         -5, -5, -3, -3, -2, -2    };}//KerberosKey class

我已经用注释标记了清单 11 中 generateKey() 方法中那些代码行,以帮助您将算法的各个步骤与 J2ME 代码中的相应行对应起来。编码细节中真正需要解释的一点是第 9 步,在这里我实际执行了 DES-CBC 加密。

看一下清单 11 中 generateKey() 方法中的那些行代码,它们用注释标记为第 9 步。它是一个对名为 getFinalKey()的方法的调用,这个方法实现了第九步并取两个参数。第一个参数( data )是第 3 步的附加操作得到的字节数组,而第二个参数( key )是作为第 8 步的结果得到的临时密钥。

DESEngine 和 CBCBlockCipher 类进行实际的加密操作。这些类是 Bouncy Castle 组的 J2ME 平台开放源代码加密实现的一部分。Bouncy Castle 的实现可以免费得到,并可用于任何目的,只要您在发布时加入许可证信息。您将需要下载 Bouncy Castle 类(链接请参阅 参考资料)并遵照它所附带的设置指示才能使用本文的示例代码。清单 11 中的 KerberosKey 类包含在 Kerberos 类中使用 Bouncy Castle 类时需要的所有 import 语句。

现在看一下在清单 11 中的 getFinalKey() 方法中发生了什么事情。我首先实例化了 DESEngine 类,这个类实现了 DES 加密算法。然后,我将这个 DESEngine 对象传递给构造函数 CBCBlockCipher 以创建一个名为 cipher 的 CBCBlockCipher 对象。这个 cipher 对象将执行实际的 DES-CBC 操作。

然后我通过向名为 KeyParameter 的类的构造函数传递一个 key 参数创建一个名为 kp 的对象。这个 KeyParameter 类也是 Bouncy Castle 的加密库的一部分。 kp 对象现在包装了密钥,所以在需要指定密钥时我将传递这个对象。

下一步是创建另一个名为 iv 的对象。这个对象是另一个名为 ParameterWithIV 的 Bouncy Castle 类的实例。 ParameterWithIV 构造函数取两个参数。第一个是包装了密钥的 kp 对象。第二个是初始矢量字节数组。因为我必须用密钥作为初始矢量,所以将密钥作为初始矢量字节数组传递。

iv 对象现在包装了密钥以及初始矢量,所以我在需要指定密钥和初始矢量时传递这个对象。

下一步是调用 cipher 对象的 init() 方法初始化这个对象。这个方法取两个参数。第一个是布尔类型,在需要初始化一个密码进行加密时传递 true ,在希望进行解码时传递 false 。第二个是包装了密钥和初始矢量的 iv 对象,

现在可以进行密文块链接了。我声明了一个名为 ivBytes 的字节数组,它将包含密码块链接每一步的初始矢量字节。一个 for 循环将连续调用 cipher 对象的 processBlock() 方法。 processBlock() 方法一次处理一个数据块。

processBlock() 方法取四个参数。第一个是输入数组( data ),第二个是字节数组中的偏移。 processBlock() 方法从这个偏移值开始处理块输入。第三个参数是输出数组的名字,第四个是输出数组中的偏移。

for 循环调用 processBlock() 方法一次处理一个块。这个方法一次处理一块并将输出(加密的结果)储存在 ivBytes 数组中。之后,我通过向 ParametersWithIV 构造函数传递 ivBytes 数组创建一个新的 iv 对象( ParametersWithIV 类的一个实例)。然后我用新的 iv 对象重新初始化这个密码。于是循环可以用与第一块的结果相等的初始矢量处理下一块。

循环退出时,我只是返回最后一个数据块的加密结果,这就是密钥生成过程第 9 步的结果。




回页首

生成 TGT 请求

到目前为止,我讨论了 ASN1DataTypes 类的底层方法并实现了利用用户的密码生成密钥的算法。现在可以展示KerberosClient 类如何利用这些底层细节了。

看一下清单 12,它显示了 getTicketResponse() 方法的实现。这个方法属于 KerberosClient 类。

getTicketResponse() 方法的基本目的是生成一个对 Kerberos 票据(一个 TGT 或者服务票据)的请求、向 Kerberos 服务器发送票据请求、从服务器得到响应、并将响应返回给调用应用程序。在本文中,我将只描述生成 TGT 请求的过程。本系列的下一篇文章将展示设置 KDC 服务器、向 KDC 发送请求、得到响应并对它进行处理的步骤。 
清单 12. getTicketResponse() 方法

import org.bouncycastle.crypto.digests.MD5Digest;public class KerberosClient extends ASN1DataTypes{    static long seed = System.currentTimeMillis();    private String kdcServiceName = "krbtgt";    private KerberosKey krbKey;    private String userName;    private String password;    private String realmName;    public KerberosClient(String userName, String password, String realmName)    {        krbKey = new KerberosKey(userName, password, realmName);         this.userName = userName;        this.password = password;        this.realmName = realmName;    }//KerberosClient    public byte[] getTicketResponse ()    {        byte pvno[] = getTagAndLengthBytes(ASN1DataTypes.Context_Specific,                                 1, getIntegerBytes(5));        byte msg_type[] = getTagAndLengthBytes(ASN1DataTypes.Context_Specific,                                 2, getIntegerBytes(10));        byte noOptions[] = new byte [5];        byte kdc_options[] = getTagAndLengthBytes(ASN1DataTypes.Context_Specific,                                 0, getBitStringBytes(noOptions));        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)                                                                )                                                             )                                                          )                                                    )                                                 )                                               )                                             );           byte ticketRequest[] = getTagAndLengthBytes(                                        ASN1DataTypes.Application_Type,                                        10,                                        getSequenceBytes(                                           concatenateBytes(                                               pvno,                                               concatenateBytes                                                   (msg_type,req_body)                                                           )                                                        )                                                    );        return ticketRequest;    }    public byte[] getRandomNumber ()    {        String userData = userName + password;        byte secretKey[] = getByteArray(System.currentTimeMillis() * 6 + seed);        seed = seed / 5;        int userDataHash = userData.hashCode() * 5;        byte numData[] = new String(String.valueOf(userDataHash)).getBytes();        byte numBytes[]= krbKey.getFinalKey(numData, secretKey);        byte randomNum []= new byte[4];        int j=1;        for (int i=0; i<4; i++)        {            randomNum[i]= numBytes[i+j];            j++;        }        return randomNum;    }//getRandomNumber    //It is a helper method used to generate the random number bytes structure.    public byte[] getIntegerBytes (byte[] byteContent)    {        byte finalBytes[];        int contentBytesCount  = byteContent.length;        byte lengthBytes[] = getLengthBytes(contentBytesCount );        int lengthBytesCount = lengthBytes.length;        int integerBytesCount = lengthBytesCount + contentBytesCount  + 1;        finalBytes = new byte[integerBytesCount];        finalBytes[0] = (byte)0x02;        for (int i=0; i < lengthBytes.length; i++)            finalBytes[i+1] = lengthBytes[i];        for (int j=lengthBytesCount+1; j<integerBytesCount; j++)            finalBytes[j] = byteContent[j-(lengthBytesCount+1)];         return finalBytes;    }//getIntegerBytes    // Converts a long into a byte array.    public byte[] getByteArray (long l)    {        byte byteValue[] = new byte[8];        for(int x=0; x<8; x++)            byteValue[x] = (byte)(int)(l >>> (7 - x) * 8 & 255L);        return byteValue;    }}//KerberosClient class

在本系列的 第一篇文章 对图 2、清单 1 和表 2 的讨论中我讨论过 TGT 请求的结构。回想在那里的讨论中,TGT 请求包含四个数据字段: pvno 、 msg-type 、 padata 和 req-body 。生成 pvno 和 msg-type 字段非常简单,因为这两个字段分别只包含一个整数(如在 第一篇文章 中“请求 TGT”一节中提到的, pvno 为 5, msg-type 为 10)。

您只需要调用 getIntegerBytes() 方法,向这个方法传递这个整数值。 getIntegerBytes() 方法返回以 ASN.1 字节数组表达的 INTEGER 结构,您将它传递给 getTagAndLengthBytes() 方法。这个方法将返回 pvno 或者 msg-type 字段的完整 ASN.1 表达。这就是我在清单 12 中的 getTicketResponse() 方法的开始时生成 pvno 和 msg-type 字段的方法。

在生成 pvno 和 msg-type 字段后,下一步就是生成 padata 字段。这个字段是可选的。大多数 KDC 服务器有一个设置选项,可以对单独的客户机进行配置。系统管理员可以将 Kerberos 服务器设置为特定客户可以发送不包括 padata 字段的 TGT 请求。

为了减轻在资源有限的 J2ME 设备上的处理负担,我假定电子银行有一个允许无线移动用户发送不带 padata 字段的 TGT 请求的 Kerberos 服务器(并且我将在本系列的下一篇文章中展示如何设置 Keberos 服务器使它具有这种行为)。因此我将在要生成的 TGT 请求中略去 padata 字段。所以,在生成 pvno 和 msg-type 字段后,我就直接开始生成 req-body结构,这需要几步。

生成请求正文

在清单 12 的 getTicketResponse() 方法中,我的请求正文( req-body 结构)生成策略是生成结构的所有单独的子字段,然后将它们串接到一起并包装到一个 SEQUENCE 中以构成请求正文。

回想在 第一篇文章 图 2 的讨论中, req-body 的子字段有(去掉了一些可选字段):

  • kdc-options
  • cname
  • realm
  • sname
  • till
  • nonce
  • etype

我将按它们在上面列表中的顺序生成这些字段。因此,第一项任务是生成 kdc-options 字段。

因为我不想使用任何 KDC 选项,所以我不需要对生成 kdc-options 字段进行任何逻辑处理。我只是使用一个全为零的 5 字节数组作为其内容。看一下清单 12 的 getTicketResponse() 方法中 byte noOptions[] = new byte [5]; 这一行。这个方法实例化一个名为 noOptions 的 5 字节数组,它初始化为五个零。

下一行( byte kdc_options[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 0, getBitStringBytes(noOptions)) )执行两项任务:

  1. 它首先向 getBitStringBytes() 方法传递 noOptions 字节数组,它返回用 ASN.1 的位字符串表达的 5 个零。
  2. 然后它将位字符串传递给 getTagAndLengthBytes() 方法,这个方法返回 kdc-options 字段的完整 ASN.1 字节数组表达。

下一步是生成 cname 结构。在 第一篇文章 清单 1 的讨论中说过, cname 字段的类型为 type cname 。这种数据类型是两个字段 - 即 name-type 和 name-string ── 的 SEQUENCE 。 name-type 字段是用一个 INTEGER 构造的。 name-string 字段是 GeneralString s 的一个 SEQUENCE 。

因此,为了生成 cname 结构,我必须遵循清单 12 的 getTicketResponse() 方法中的几个步骤:

  1. 调用 getGeneralStringBytes() 方法,同时传递客户的用户名。 getGeneralStringBytes() 方法将返回客户的用户名的 GeneralString 表达。

  2. 向 getSequenceBytes() 方法传递 GeneralString ,这个方法会在 GeneralString 前面附加 SEQUENCE 字节并返回包含客户的用户名字符串的 SEQUENCE 的 ASN.1 表达。

    byte generalStringSequence[] = getSequenceBytes (getGeneralStringBytes (userName)); 这一行执行这前两步。

  3. 调用 getTagAndLengthBytes() 方法,传递 SEQUENCE 字节作为其内容。 getTagAndLengthBytes() 方法会在 SEQUENCE 前面附加 name-string 标签字节(上下文特定的标签数字 0)以及长度字节,并返回完整的 name-string 结构。

    byte name_string[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 1, generalStringSequence); 这一行执行这一步。

  4. 生成 PrincipalName 的 name-type 部分。 name-type 部分只包含一个 INTEGER ,它标识了用户名的类型。Kerbros 允许几种类型的名字(用户名、惟一标识等等)。对于这个基于 J2ME 的 Kerberos 客户机,我感兴趣的惟一名称类型是用户名,它的名称类型标识是 1。因此,我将首先构造一个 INTEGER ,然后向 getTagAndLengthBytes() 方法传递这个 INTEGER 字节。这个方法生成 PrincipalName 的完整 name-type 部分。清单 12 中 byte name_type[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 0, getIntegerBytes (ASN1DataTypes.NT_PRINCIPAL)); 这一行执行这项任务。

  5. 将 PrincipalName 的 name-type 和 name-string 部分串接到一起,然后在串接字节数组前面附加 SEQUENCE 字节。 byte principalNameSequence [] = getSequenceBytes (concatenateBytes (name_type, name_string)); 一行执行这项任务。

  6. 在上面第 5 步的 SEQUENCE 前面附加 cname 标签字节(上下文特定的标签数 1)和长度字节。这样就得到了完整的 cname 结构。 byte cname[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 1, principalNameSequence); 一行执行这项任务。

上述 6 步策略就可以生成完整的 cname 结构。

我的下一步是生成 realm 字段,它的类型为 GeneralString 。生成 realm 字段的策略如下:

  1. 用 getGeneralStringBytes() 方法调用生成 GeneralString 。
  2. 连同 getTagAndLengthBytes() 方法一起传递 GeneralString 字节,它会返回 realm 字段的完整字节字符串表达。

清单 12 中 byte realm[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 2, getGeneralStringBytes (realmName)); 这一行进行这两个方法调用。

下一项任务是生成 sname 字段,它是 PrincipalName 数据类型。我已经在上面讨论 cname 字段时描述过了生成 PrincipalName 数据结构的策略。

在 sname 字段后,我需要生成 till 字段,它指定我所请求的票据的失效时间。对于这个基于 J2ME 的 Kerberos 客户机,我不想指定票据的任何特定失效时间,我只希望由 KDC 服务器根据服务器的策略发布具有标准失效时间的票据。因此,我总是发送硬编码的日期(1970 年 1 月 1 日)作为 till 字段的值。我所选择的日期是过去日期,这表明我不希望为请求的票据指定一个失效时间。

till 字段为 KerberosTime 类型,它遵循 GeneralizedTime 通用数据类型。生成 KerberosTime 结构的过程是首先调用getGeneralizedTimeBytes() 方法并与方法调用同时传递时间字符串。例如, etGeneralizedTimeBytes(new String("19700101000000Z") 方法调用会返回 1970 年 1 月 1 日的 GeneralizedTime 结构。

有了 GeneralizedTime 字节数组后,我可以将它传递给 getTagAndLengthbytes() 方法调用,它会生成 till 参数的完整字节数组。清单 12 中 getTicketResponse() 方法的 byte till[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 5, getGeneralizedTimeBytes (new String("19700101000000Z").getBytes())); 这一行生成完整的 till 结构。

下面,需要生成 nonce 字段,它包装了一个随机数作为一个整数。我首先生成一个随机数,然后生成这个随机数的字节数组表达,最后调用 getTagAndLengthBytes() 方法,它生成 nonce 字段的完整结构。

在 req-body 字段中,还必须生成的最后一个结构是 etype 字段,这是一个 INTEGER 序列。 SEQUENCE 中的每个 INTEGER 指定客户机支持的一种加密算法。我只希望支持一种加密算法(CBC 模式下的 DES),根据客户机所选择的消息摘要算法,它的 INTEGER 标识号是 1、2 或者 3。我将在本系列的下一篇文章中解释消息摘要算法的使用,但是现在只要知道我要在 Kerberos 客户机中使用 MD5 消息摘要算法。

DES-CBC-MD5 组合的标识号是 3。因此,我将首先生成 3 的 INTEGER 字节,然后在 INTEGER 字节前附加 SEQUENCE 字节,最后调用 getTagAndLengthBytes() 方法,获得 etype 字段的完整字节数组表达。

现在我已经生成了 req-body 字段的所有字段。因此,我可以多次调用 concatenateBytes() 方法以将所有单独的字段串接为一个字节数组。下一步是调用 getSequenceBytes() 方法以将串接的字节数组放到一个 SEQUENCE 中。一个 getTagAndLengthBytes() 方法将取 SEQUENCE 字节并生成完整的 req-body 结构。

生成 TGT 请求的最后一步是将在本节前面生成的 pvno 和 msg-fields 与 req-body 字节串接在一起。然后将这些字段放入一个 SEQUENCE ,最后调用 getTagAndLengthBytes() 方法,得到一个完整的、可以发送给 Kerberos 服务器的票据请求。 




回页首

结束语

在本文中我讨论了几个基本概念。我开发了一个 J2ME 类,它包含几个用于生成 ASN.1 数据结构的方法,我还展示了如何利用用户的密码生成一个 Kerberos 密钥。最后,我演示了 Kerberos 客户机如何生成 TGT 请求。

下一次,我将搭建一个 KDC 服务器、从该服务器中获取 Kerberos 票据、并用这些票据与电子银行的业务逻辑服务器交换密钥。

参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文
  • 下载本文附带的 源代码。 

  • 阅读本系列的 第一篇文章。 

  • 阅读 IETF.org 上的 Kerberos(第 5 版)官方 RFC 1510。 

  • 下载 Bouncy Castle 的加密库。我用 Bouncy Castle 的 1.19 版测试了本文的代码。如果您要发布包括这些库的代码,一定要阅读 Bouncy Castle 的 许可条款。 

  • 阅读官方 DES 和 DES Modes of Operation(包括 CBC 模式)规范。 

  • 访问 IETF 网站上的 Kerberos working group页。 

  • 在“ Simplify enterprise Java authentication with single sign-on”一文中( developerWorks,2003 年 9 月),Faheem Khan 使用 Kerberos 和 Java GSS API 论证了单点登录。 

  • MIT 的这一页包含有关 Kerberos 的很好的一组链接。 

  • 下载 完整的 ASN.1 文档和编码规则。 

  • 阅读 Jason Garman 的 Kerberos: The Definitive Guide (O'Reilly & Associates,2003),以学习 Kerberos 的使用。 


  • 看看 IBM 产品是如何使用 Kerberos的。 

  • IBM alphaWorks 提供了 Web Services Toolkit for Mobile Devices,可以将您的 J2ME 设备与 Web 服务世界相连接。 


关于作者

 

Faheem Khan 是一个独立软件顾问,专长是企业应用集成 (EAI) 和 B2B 解决方案。读者可以通过 fkhan872@yahoo.com与 Faheem 联系。


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

设置服务器、请求票据、获取响应

文档选项

将此页作为电子邮件发送

未显示需要 JavaScript 的文档选项


级别: 初级

Faheem Khan (fkhan872@yahoo.com), 自由顾问

2004 年 3 月 27 日

如果您已经学习了本系列的前两部分,那么现在可以开始第三部分,也就是最后一部分,您将设置一个 KDC 服务器,向它发送 Kerberos 票据请求并取得其响应。然后,您将学习处理 KDC 服务器的响应所需的低层 ASN1 处理方法,以便取得票据和会话密钥。取得了服务票据后,将向电子银行的业务逻辑服务器发送一个建立安全上下文的请求。最后,您将学会与电子银行业务逻辑服务器进行实际的安全通信。

回顾本系列的 第一篇文章,它介绍了移动银行 MIDlet 应用程序,并解释了 Kerberos 是如何满足这种应用程序的安全要求的。文章还描述了 Kerberos 用来提供安全性的数据格式。

本系列的 第二篇 文章展示了如何在 J2ME 中生成 ASN.1 数据类型。介绍了如何用 Bouncy Castle 加密库进行 DES 加密,并用用户的密码生成 Kerberos 密钥。最后将这些内容放到一起并生成一个 Kerberos 票据请求。

在本系列文章中开发的 Kerberos 客户不要求某个特定的 Kerberos 服务器,它可以使用所有 KDC 实现。参考资料部分包含了一些可以被 Kerberos 客户机所使用的 KDC 服务器的链接。

不管所选的是什么 KDC 服务器,必须告诉服务器移动银行 MIDlet 的用户在对 TGT 的请求中不需要发送预认证数据( padata , 本系列第一篇文章的图 2 中显示的 KDC-REQ 结构的第三个字段)。

根据 Kerberos 规范,发送 padata 字段是可以选择的。因此,KDC 服务器通常允许配置特定的用户,使得对于所配置的用户不需要 padata 字段就可以接受 TGT 请求。为了尽量减少 Kerberos 客户机上的负荷,必须告诉 KDC 服务器接受电子银行移动用户的不带 padata 的 TGT 请求。

在这个例子中,我使用了 Microsoft 的 KDC 服务器以试验基于 J2ME 的移动银行应用程序。在本文 源代码下载 中的 readme.txt 文件包含了如何设置 KDC 服务器、以及如何告诉它接受不带 padata 字段的 TGT 请求的指导。(在我的“用单点登录简化企业 Java 认证”一文中,我使用了同一个 KDC 服务器展示单点登录。有关链接请参阅 参考资料。)

向 KDC 服务器发送 TGT 请求

设置了 KDC 服务器后,就向它发送 TGT 请求。看一下 清单 1 中的 getTicketResponse() 方法。它与 本系列第二篇文章中的清单 12 中的 getTicketResponse() 方法是相同的,只有一处不同:这个方法现在包括向 KDC 服务器发送 TGT 请求的 J2ME 代码。在 清单 1中标出了新的代码,所以您可以观察在 清单 12中没有的新增代码。

在 清单 1 的 NEW CODE 部分中,我以一个现有的 DatagramConnection 对象( dc )为基础创建了一个新的 Datagram 对象( dg )。注意在本文的最后一节中,移动银行 MIDlet 创建了我在这里用来创建 Datagram 对象的 dc 对象。

创建了 dg 对象后, getTicketResponse() 方法调用了它的 send() 方法,向 KDC 服务器发送票据请求。

在向服务器发送了 TGT 请求之后, 清单 1 的 getTicketResponse() 方法接收服务器的 TGT 响应。收到响应后,它将响应返回给调用应用程序。 
清单 1. getTicketResponse() 方法

    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





回页首

处理 TGT 响应

既然已经收到了来自 KDC 的 TGT 响应,现在该对响应进行处理以便从响应中提取 票据 和 会话密钥 。

自然,响应处理包括一些低层 ASN.1 处理(就像在本系列第二篇文章中生成票据请求时遇到的低层 ASN.1 生成方法一样)。所以在展示如何使用低层处理方法从票据响应中提取 票据 和 会话密钥 之前,我将实现并解释一些低层 ASN.1 处理方法以及一些低层加密支持方法。

像以前一样,低层 ASN1 处理方法放在 ASN1DataTypes 类中。下面的方法在本文的 源代码下载 中的 ASN1DataTypes.java 文件中:

  1. isSequence()
  2. getIntegerValue()
  3. isASN1Structure()
  4. getNumberOfLengthBytes()
  5. getLength()
  6. getASN1Structure()
  7. getContents()

 

下面是上面列出的每一个低层 ASN.1 处理方法的说明。

isSequence()

清单 2 中显示的 isSequence() 方法取单个 字节 作为参数,并检查这个 字节 是否是一个 ASN.1 SEQUENCE 字节。如果 字节 值表示一个 SEQUENCE ,那么它就返回 true,否则它返回 false。 
清单 2. isSequence() 方法

   public boolean isSequence(byte tagByte)   {      if (tagByte == (byte)0x30)         return true;      else         return false;      }//isSequence

getIntegerValue()

清单 3 中显示的 getIntegerValue() 方法只取一个输入参数,它是表示一个 ASN.1 INTEGER 数据类型的内容的 字节 数组。它将输入 字节 数组转换为 J2ME int 数据类型,并返回 J2ME int 。在从 ASN.1 INTEGER 中提取了内容字节,并且希望知道它所表示的是什么 integer 值时就需要这个方法。还要用这个方法将长度字节转换为 J2ME int 。

注意, getIntegerValue() 方法设计为只处理正的 integer 值。

ASN.1 以最高有效位优先(most-significant-byte-first)的序列存储一个正的 INTEGER 。例如,用 ASN.1 表示的十进制 511 就是 0x01 0xFF 。可以写出十进制值的完整位表示(对于 511 ,它是 1 11111111 ),然后对每一个字节写出十六进制 值(对于 511,它是 0x01, 0xFF ),最后以最高有效位优先的顺序写出 十六进制 值。

另一方面,在 J2ME 中一个 int 总是四字节长,并且最低有效 字节 占据了最右边的位置。在正 integer 值中空出的位置上填入零。例如,对于 511 ,J2ME int 的写法是 0x00 0x00 0x01 0xFF 。

这意味着在将 ASN.1 INTEGER 转换为一个 J2ME int 时,必须将输入数组的每一个 字节 正确地放到输出 J2ME int 中的相应位置上。

例如,如果输入字节数组包含两个字节的数据 (0x01, 0xFF) ,那么必须像下面这样将这些字节放到输出 int 中:

  • 必须在输出 int 的最左边或者最高有效位置写入 0x00 。
  • 类似地,必须在与输出 int 的最高有效 字节 相邻的位置上写入 0x00 。
  • 输入数组的第一个字节 (0x01) 放入输出 int 中与最低有效位置相邻的位置。
  • 输出数组的第二个字节 (0xFF) 放到输出 int 的最低有效或者最右边的位置。

 

getIntegerValue() 方法中的 for 循环计算每一个 字节 的正确位置,再将这个 字节 拷贝到其相应的位置上。

还要注意因为 J2ME int 总是有四个字节, getIntegerValue() 方法只能处理最多四 字节 integer 值。能力有限的、基于 J2ME 的 Kerberos 客户不需要处理更大的值。 
清单 3. getIntegerValue() 方法

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

清单 4 中显示的 isASN1Structure() 方法分析一个输入字节是否表示具有特定标签号的特定类型的 ASN.1 结构(即, 特定于上下文的 (context specific)、应用程序级 (application level) 或者通用类型 (universal type ))的标签字节(第一个字节)。

这个方法取三个参数。第一个参数( tagByte )是要分析的输入 字节 。第二和第三个参数( tagType 和 tagNumber )分别表示所要查找的标签类型和标签号。

为了检查 tagByte 是否具有所需要的标签号的标签类型, isASN1Structure() 方法首先用 tagType 和 tagNumber 参数构建一个新的临时标签字节( tempTagByte )。然后比较 tempTagByte 与 tagByte 。如果它们是相同的,那么方法就返回 true,如果不相同它就返回 false。 
清单 4. isASN1Structure() 方法

   public boolean isASN1Structure (byte tagByte, int tagType, int tagNumber)   {      byte tempTagByte = (byte) (tagType + tagNumber);      if (tagByte == tempTagByte)         return true;      else         return false;   }//isASN1Structure

getNumberOfLengthBytes()

清单 5 显示的 getNumberOfLengthBytes() 方法取一个参数( firstLengthByte )。 firstLengthByte 参数是 ASN.1 结构的第一个长度字节。 getNumberOfLengthBytes() 方法处理第一个长度字节,以计算 ASN.1 结构中长度字节的字节数。这是一个工具方法, ASN1DataTypes 类中的其他方法在需要知道一个 ASN.1 结构的长度字节的字节数时就使用它。

清单 5 中的 getNumberOfLengthBytes() 方法的实现策略如下:

  1. 检查 firstLengthByte 的最高有效位(第 8 位)是否为零。 清单 5 中的 if ( (firstLengthByte)& (1<<8)==0) 这一行完成这一任务。 
  2. 如果最高有效位为零,那么长度字节就遵循 单字节 长度表示法。在 本系列的第 1 部分 我们说过有两种长度表示法 ── 单字节 和 多字节 。在 单字节 长度表示法中总是有一个长度字节。因此,如果最高有效位为零,那么只需返回 1 作为长度字节的字节数。 
  3. 如果 firstLengthByte 的最高有效位是 1,这意味着长度字节遵循 多字节 长度表示法。在这时, 清单 5 中的 else 块取得控制。

 

在 多字节 长度格式中, firstLengthByte 的最高有效位指定后面有多少长度字节。例如,如果 firstLengthByte 的值是 1000 0010 ,那么最左边的 1(最高有效位)说明后面的长度字节使用 多字节 长度表示法。其他 7 位( 000 0010 )说明还有两个长度字节。因此,在这里 getNumberOfLengthBytes() 方法应当返回 3( firstLengthBytes 加上另外两个长度字节)。

清单 5 中 else 块的第一行( firstLengthByte &= (byte)0x7f; )删除 firstLengthByte 的最高有效位。

else 块中的第二行( return (int)firstLengthByte + 1; )将 firstLengthByte 强制转换为 integer ,在得到的 integer 值中加 1,并返回这个 integer 。 
清单 5. getNumberOfLengthBytes() 方法

   public int getNumberOfLengthBytes (byte firstLengthByte) {      if ( (firstLengthByte & 1<<8) == 0 )         return 1;      else {         firstLengthByte &= (byte)0x7f;         return (int)firstLengthByte + 1;      }   }//getNumberOfLengthBytes

getLength()

这个方法的目的是检查一个特定的 AS1 结构有多少个字节。处理应用程序通常有一个由多个 ASN.1 结构构成的嵌入层次所组成的字节数组。 getLength() 方法计算特定结构中的字节数。

这个方法取两个参数。第一个参数( ASN1Structure )是一个字节数组,它应当包含至少一个完整的 ASN.1 结构,这个结构本身包含标签字节、长度字节和内容字节。第二个参数( offset )是一个在 ASN1Structure字节数组中的偏移值。这个参数指定在 ASN1Structure 字节数组中包含的 ASN.1 结构的开始位置。

getLength() 方法返回一个等于从 offset 字节处开始的 ASN.1 结构中的字节总数。

看一下 清单 6,它显示了 getLength() 方法的一个实现:

  1. 第一步是向 getNumberOfLengthBytes() 方法传 ASN.1 结构的第二个字节。这个 ASN.1 结构从 offset 字节开始,所以可以预计 offset 字节实际上就是标签字节。因为所有 Kerberos 结构只包含一个标签字节,所以第二个字节(在 offset 字节后面的那个字节)是第一个长度字节。第一个长度字节说明长度字节的总字节数, getNumberOfLengthBytes() 方法返回长度字节数。 int numberOfLengthBytes = getNumberOfLengthBytes(ASN1Structure [offset+1]); 这一行执行这项任务 。 
  2. 如果 getNumberOfLengthBytes() 方法返回一个大于 1 的值,那么必须处理 多字节 长度表示法。在这种情况下,将从 offset + 2 (让过标签字节和第一个长度字节) 开始的长度字节读到一个名为 lengthValueAsBytes 的变量中。然后用 getIntegerValue() 方法将长度值从 ASN.1 字节转换为 J2ME int 。最后,将结果加 1(以补偿不包含在长度值中的标签字节),再将长度值返回给调用应用程序。 
  3. 如果 getNumberOfLengthBytes() 方法返回 1,则要处理 单字节 长度表示法。在这种情况下,只要将第一个(也是惟一的一个)长度字节转换为 J2ME int ,对它加 1(以补偿不包含在长度值中的标签字节),并将得到的值返回给调用应用程序。


清单 6 getLength() 方法

   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

清单 7 中的 getASN1Structure() 方法从一个包含一系列 ASN.1 结构的字节数组中找出并提取特定 ASN.1 结构。这个方法有三个参数。第一个参数( inputByteArray )是输入字节数组,需要从这个字节数组中找到所需要的 ASN.1 结构。第二个参数是一个 int ,它指定要查找的标签的类型。第三个参数指定标签号。

看一下 清单 7 中的 getASN1Strucute() 方法实现。它将 offset 值初始化为零并进入 do-while 循环。

在 do-while 循环中,将字节数组中第一个字节读入名为 tagByte 的字节中。然后用 isASN1Structure() 方法检查输入数组的第一个字节是否是所需要的 ASN.1 结构。

如果第一个字节代表所需要的结构,那么就用 getLength() 方法找到要返回的所需数量的字节。然后将所需要的字节拷贝到名为 outputBytes 的字节数组中、并将这些字节返回到调用应用程序。

如果第一个字节不代表所需要的结构,那么就要跳到下一个结构。为此,将 offset 值设置为下一个结构的开始位置。

do-while 循环在下一个循环中检查下一个结构,并以此方式检查整个输入数组。如果没有找到所需要的结构,那么 do-while 循环就会退出并返回 null。 
清单 7. getASN1Structure() 方法

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

清单 8 中显示的 getContents() 方法取 ASN1Structure 字节数组并返回一个包含 ASN1Structure 内容的字节数组。

getContents() 方法假定所提供的字节数组是一个有效的 ASN1 结构,所以它忽略结构中表示标签字节的第一个字节。它将第二个字节(即第一个长度字节)传递给 getNumberOfLengthBytes() 方法,这个方法返回 ASN1Structure 输入字节数组中的长度字节数。

然后它构建一个名为 contentBytes 的新字节数组,并将 ASN1Structure 的内容拷贝到 contentBytes 数组中(去掉标签和长度字节)。 
清单 8. getContents() 方法

   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

一些低层加密支持方法

除了前面描述的低层处理方法,还需要一些低层加密支持方法以处理一个票据响应。这就是为什么在解释票据响应的处理之前,我要讨论以下这些为 Kerberos 客户机提供加密支持的方法:

  1. encrypt()
  2. decrypt()
  3. getMD5DigestValue()
  4. decryptAndVerifyDigest()

 

这些方法是 KerberosClient 类的组成部分,可以在 KerberosClient.java 文件中找到它们,本文的 源代码下载中可以找到这个文件。下面是对这几个方法的说明:

encrypt()

清单 9 中显示的 encrypt() 方法处理低层加密并加密一个输入字节数组。

这个方法取三个字节数组参数,即一个用于加密的密码( keyBytes )、要加密的纯文本数据( plainData )和一个初始向量或者 IV( ivBytes )。它用密钥和 IV 加密纯文本数据,并返回加密后的纯文本数据。

注意在 清单 9 中的 encrypt() 方法中,我使用了 DESEngine 、 CBCBlockCipher 、 KeyParameter 和 ParametersWithIV 类以加密这个纯文本数据。这些类属于在讨论 第二篇文章中的清单 11 中的 getFinalKey()方法时介绍的 Bouncy Castle 加密库。回头看一下并比较 清单 9 中的 encrypt() 方法与第二篇文章中 清单 11 中的 getFinalKey() 方法。注意以下几点:

  1. getFinalKey() 方法使用一个包装了初始向量的 ParametersWithIV 类。Kerberos 规范要求在生成加密密钥时,用加密密钥作为 IV。因此,方法中的加密算法用加密密钥作为 IV。因此, getFinalKey() 方法中的算法使用这个加密密钥作为一个 IV。 

    另一方面, encrypt() 方法设计为可以使用或者不使用 IV 值。更高级别的应用程序逻辑使用 encrypt() 方法时可以提供一个 IV 值或者忽略它。如果应用程序要求一个没有 IV 值的数据加密,那么它将传递 null 作为第三个参数。 
    如果有 IV,那么 encrypt() 方法用一个 ParametersWithIV 实例初始化 CBCBlockCipher。注意在 清单 9 的 if (ivBytes != null) 块中,我传递了一个 ParametersWithIV 实例作为给 cbcCipher.init() 方法调用的第二个参数。 
    如果第三个参数为 null,那么 encrypt() 方法就用一个 KeyParameter 对象实始化 CBCBlockCipher 对象。注意在 清单 9 中的 else 块中,我传递了一个 KeyParameter 实例作为 cbcCipher.init() 方法调用的第二个参数。

  2. 第二篇文章的清单 11 中的 getFinalKey() 方法返回输入数据最后一块的处理结果。另一方面, encrypt() 方法将纯文本处理的每一步的结果串接在一起、并返回串接在一起的所有处理过的(加密的)字节。


清单 9. encrypt() 方法

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

( 清单 10 显示的) decrypt() 方法与 encrypt() 方法的工作方式完全相同,只不过解密时, cbcCipher.init() 方法的第一个参数是 false (加密时它是 true )。 
清单 10. decrypt() 方法

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

清单 11 中显示的 getMD5DigestValue() 方法取一个输入数据字节数组,并返回一个用输入数据计算的 MD5 摘要值。

Bouncy Castle 加密库在一个名为 MD5Digest 的类中包含 MD5 摘要支持。使用 MD5Digest 类进行摘要计算需要四步:

  1. 首先,实例化一个 MD5Digest 对象。
  2. 然后,调用 MD5Digest 对象的 update() 方法,在调用同时传递要摘要的数据。
  3. 然后,实例化一个用来包含 MD5 摘要值输出字节数组。
  4. 最后,调用 MD5Digest 对象的 doFinal() 方法,同时传递输出字节数组。 doFinal() 方法计算摘要值并将它放到输出字节数组中。


清单 11. getMD5DigestValue() 方法

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

回想一下在 第一篇文章图 3 和清单 2 中,KDC 服务器的票据响应包含一个名为 enc-part 的字段,它包装了一个名为 EncryptedData 的加密的数据结构。就像在第一篇文章的 图 3 的说明中描述的那样, EncryptedData 结构由三个字段组成。

清单 12 中显示的 decryptAndVerifyDigest() 方法取一个 EncryptedData 结构(实质上就是 enc-part 字段的内容)和一个解密密钥作为参数,并返回 EncryptedData 结构的纯文本表示。加密过程步骤如下:

第 1 步:注意在 第一篇文章的清单 2 中, EncryptedData 结构实际上是 etype、kvno 和 cipher 字段的一个 SEQUENCE 。因此,第一步是检查输入字节数组是否是一个 SEQUENCE 。为此调用 isSequence() 方法。

第 2 步:如果输入字节数组是一个 SEQUENCE ,那么需要解析这个 SEQUENCE 并提取出其内容。调用 getContents() 方法以提取出 SEQUENCE 内容。

在 SEQUENCE 内容中,感兴趣的是第一个字段( etype ,特定于上下文的标签号 0),它表明了加密类型。使用了 getASN1Structure() 方法调用以从 SEQUENCE 内容中提取 etype 字段。

第 3 步:调用 getContents() 方法以提取 etype 字段的内容,这是一个 ASN.1 INTEGER 。再次调用 getContents() 方法以提取 INTEGER 的内容。然后将 INTEGER 内容传递给 getIntegerValue() 方法,这个方法返回 J2ME int 格式的 INETGER 内容。将 J2ME int 值存储为一个名为 eTypeValue 的变量。 eTypeValue int 指定在生成 EncryptedData 结构时使用的加密类型。

第 4 步:回想一下 Kerberos 客户机只支持一种加密类型 ── DES-CBC ── 它的标识号为 3。因此,我检查 eTypeValue 是否为 3。如果它不是 3(即服务器使用了非 DES-CBC 的加密算法), 那么 Kerberos 客户机就不能处理这一过程。

第 5 步:下一步是从 EncryptedData SEQUENCE 内容中提取第三个字段( cipher ,特定于上下文的标签号 2)。调用 getASN1Structure() 方法以完成这项任务。

第 6 步:下一步,调用 getContents() 方法提取 cipher 字段的内容。cipher 字段的内容是一个 ASN.1 OCTET STRING 。还需要再调用 getContents() 方法,以提取 OCTET STRING 的内容 。

第 7 步: OCTET STRING 内容是加密的,因此需要用前面讨论的 decrypt() 方法解密。

第 8 步:解密的数据字节数组由三部分组成。第一部分由前八位组成,它包含一个称为 confounder 的随机数。confounder 字节没有意义,它们只是帮助增加黑客的攻击的难度。

解密的数据的第 9 到第 24 个字节构成了第二部分,它包含一个 16 字节的 MD5 摘要值。这个摘要值是对整个解密的数据 ── 其中16 个摘要字节(第二部分)是用零填充的 ── 计算的。

第三部分是要得到实际纯文本数据。

因为第八步进行完整性检查,所以必须将解密的数据的第 9 到第 24 个字节用零填充,对整个数据计算一个 MD5 摘要值,并将摘要值与第二部分(第 9 到第 24 个字节)进行匹配。如果两个摘要值匹配,那么消息的完整性就得到验证。

第 9 步:如果通过了完整性检查,那么就返回解密的数据的第三部分(第 25 个字节到结束)。 
清单 12. decryptAndVerifyDigest() 方法

   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





回页首

从票据响应中提取票据和密钥

我们已经讨论了低层 ASN.1 处理以及低层加密支持方法,现在可以讨论如何用这些方法处理在前面用 清单 1 中的 getTicketResponse() 方法提取的票据响应了。

看一下 清单 13 中显示的 getTicketAndKey() 方法(它属于 KerberosClient 类)。这个方法取票据响应字节数组和一个解密密钥字节数组作为参数。这个方法从票据响应中提取票据和密钥。

getTicketAndKey() 方法返回一个名为 TicketAndKey 的类的实例(这是一个要从票据响应中提取的密钥和票据的包装器)。我在 清单 14 中已经展示了 TicketAndKey 类。这个类只有四个方法:两个子 setter 方法和两个 getter 方法。 setKey() 和 getKey() 方法分别设置和获得密钥字节。 setTicket() 和 getTicket()方法分别设置和获得票据字节。

现在看一看在 清单 13 的 getTicketAndKey() 方法中所发生的过程。回想在对 第一篇文章的图 4 和清单 2的讨论中,介绍了 Kerberos 密钥和票据是如何存储在票据响应中的。从票据响应中提取密钥是一个漫长的过程,包括以下步骤:

1. 首先,检查 ticketResponse 字节数组是否真的包含了票据响应。为此,我使用了 isASN1Structure() 方法。如果 isASN1Structure() 方法返回 false,那么它表明输入 ticketResponse 字节数组不是有效的票据响应。在这种情况下,不进行任何进行一步的处理并返回 null。

注意在 清单 13 中,我调用了两次 isASN1Structure() 方法。第一调用 isASN1Structure() 方法时用“11”作为第三个参数的值,而第二次调用 isASN1Structure() 方法时,用“13”作为第三个参数的值。这是因为“11”是 TGT 响应的特定于应用程序的标签号(本系列的 第一篇文章的清单 2),而“13”是服务票据响应的特定于应用程序的标签号(本系列的 第一篇文章的清单 4)。如果 ticketResponse 字节数组是一个 TGT 响应或者服务票据响应,那么这两次方法调用之一会返回 true,就可以进行进一步的处理。如果这两个方法调用都不返回 true,那么表明 ticketResponse 字节数组不是一个票据响应,就要返回 null 并且不做任何进一步的处理。

2. 第二步是提取票据响应结构的内容。为此,我使用了 getContents() 方法调用。

3. 票据响应的内容应当是一个 ASN.1 SEQUENCE ,可以调用 isSequence() 方法对此进行检查。

4. 接下来,我调用 getContents() 方法提取 SEQUENCE 的内容。

5. SEQUENCE 的内容是票据响应的七个结构(如图 3 和 第一篇文章的清单 2所示)。在这七个结构之外,只需要两个:ticket 和 enc-part。

因此,第五步是从 SEQUENCE 内容中提取 ticket 字段(调用 getASN1Structure() 方法),提取 ticket 字段(调用 getContents() 方法)的内容,并将内容存储到在前面创建的 TicketAndKey 对象中。注意 ticket 字段是特定于上下文的标签号 5,而这个字段的内容是实际的票据,它以一个应用程序级别的标签号 1 开始,如 第一篇文章的清单 3 和图 9所示。

6. 下面,必须从在第 4 步中得到的 SEQUENCE 内容中提取密钥。这个键在在 SEQUENCE 内容的 enc-part 字段中。因此,在第 6 步,我调用 getASN1Structure() 方法从 SEQUENCE 内容中捕捉 enc-part 字段。

7. 得到了 enc-part 字段后,就要调用 getContents() 方法得到其内容。 enc-part 字段的内容构成了一个EncryptedData 结构。

8. 可以向 decryptAndVerifyDigest() 方法传递 EncryptedData 结构,这个方法解密 EncryptedData 结构并对 EncryptedData 进行一个摘要验证检查。

9. 如果成功进行了解密和摘要验证过程,那么 decryptAndVerifyDigest() 方法就从已解密的密文数据中提取了 ASN.1 数据。ASN.1 数据应当符合我在 第一篇文章的图 4中展示的结构。注意所需要的密钥是 第一篇文章的图 4中显示的结构中的第一个字段。一个应用程序级别的标签号“25”或者“26”包装纯文本数据。这个结构称为 EncKDCRepPart (加密的 KDC 回复部分)。

这样,下一步就是检查由 decryptAndVerifyDigest() 方法返回的数据是否是一个应用程序级别的标签号 25 或者 26。

10. 下一步是提取 EncKDCRepPart 结构的内容 。调用 getContents() 方法提取所需要的内容。

EncKDCRepPart 内容是一个 SEQUENCE ,所以还必须提取 SEQUENCE 内容 。再一次调用 getContents() 方法以提取 SEQUENCE 内容。

11. SEQUENCE 内容的第一个字段(称为 key,具有上下文特定的标签号 0)包含 key 字段。可以调用 getASN1Structure() 方法以从 SEQUENCE 内容中提取第一个字段。

12. 下面,提取 key 字段的内容。调用 getConents() 方法可以返回这些内容。

key 字段的内容构成另一个名为 EncryptionKey 的 ASN.1 结构,它是一个两字段 ── 即 keytype 和 keyvalue ── 的 SEQUENCE 。再一次调用 getContents() 方法提取 SEQUENCE 的内容。

13. 所需要的会话密钥在 SEQUENCE 内容的第二个字段中( keyvalue )。因此,必须调用 getASN1Structure() 方法以从 SEQUENCE 内容中提取 keyvalue 字段(特定于上下文的标签号 1)。

14. 现在已经有了 keyvalue 字段。必须调用 getContents() 方法提取它的内容。 keyvalue 内容是一个 OCTET STRING ,所以必须再次调用 getContents() 方法以提取 OCTET STRING 的内容,它就是所要找的那个密钥。

所以只要将这个密钥字节包装在 KeyAndTicket 对象中(通过调用其 setKey() 方法)并返回 KeyAndTicket 对象。 
清单 13. getTicketAndKey() 方法

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


清单 14. TicketAndKey 类

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}





回页首

得到一个服务票据

已经处理了 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 类中加入了以下方法以生成一个认证头:

  1. getMD5DigestValue()
  2. getChceksumBytes()
  3. authorDigestAndEncrypt()
  4. getAuthenticationHeader()

 

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

authorDigestAndEncrypt()

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

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

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

  1. 首先,生成八个随机字节,它们构成了 confounder。
  2. 然后,声明一个名为 zeroedChecksum 的字节数组,它有十六个字节并初始化为零。这个有十六个零的数组作为一个全为零的摘要值。
  3. 第三,用其他的字节填入输入数据字节数组,以使数组中的字节数成为八的倍感数。编写了一个名为 getPaddedData() 的方法(如 清单 16所示),它取一个字节数组并在填充后返回这个数组。下面,链接(第 1 步得到的)confounder、(第 2 步得到的)全为零的摘要以及填充后的纯文本字节数组。
  4. 第四步是对第 3 步串接的字节数组计算 MD5 摘要值。
  5. 第五步是将摘要字节放到它们相应的位置上。第 5 的结果与第 3 步一样,只不过全为零的摘要现在换成了真正的摘要值。
  6. 现在调用 encrypt() 方法以加密第 5 步得到的字节数组。
  7. 然后,生成 etype 字段(特定于上下文的标签号 0)。
  8. 然后,调用 getOctetStringBytes() 方法将第 6 步得到的加密字节数组包装到 OCTET STRING 中。然后将 OCTET STRING 包装到 cipher 字段中(一个特定于上下文的标签号 2)。
  9. 最后,链接 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() 方法有几个输入参数:

  1. 名为 ticketContent 的字节数组,它包含由 getAuthenticationHeader() 方法包装到认证头的 Kerberos 票据( TGT )。
  2. 名为 clientRealm 的字符串类型参数,它指定(生成这个请求的)Kerberos 客户机所注册的域(realm )的名字。
  3. 名为 clientName 的字符串类型参数指定生成这个请求的 Kerberos 客户机的名字。
  4. checksumBytes 字节数组携带一个 Checksum 结构以及 getChecksumBytes() 方法。
  5. encryptionKey 字节数组携带用于生成认证头的加密部分的加密密钥。
  6. 名为 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 与一些应用程序数据。注意以下三点:

  1. authenticator 结构包含 cksum 字段。
  2. cksum 字段包含一些应用程序数据的加密哈希值。
  3. 整个 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 字段的数据。

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

  1. 生成 cksumtype 和 checksum 字段的数据。如果是服务票据请求,那么 checksum 字段的数据只是对包含服务票据请求的 req-body 字段的所有子字段的 SEQUENCE 计算的 MD5 摘要值(注意在 第一篇文章的图 5req-body 是服务票据请求的第四个字段,就在服务票据请求的第三个字段 padata 字段后面)。 cksumtype 字段的数据是 integer 7 的 ASN1 表示。这个值指定 checksum 的类型。
  2. 调用 getChecksumBytes() 方法并传递 cksumtype 和 checksum 字段的数据。 getChecksumBytes() 方法生成完整的 Checksum 结构。
  3. 调用 getAuthenticationHeader() 方法,同时传递 Checksum 结构。 getAuthenticationHeader() 返回认证头。

 

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

  1. 调用我在 第二篇文章的清单 5 中描述的 getOctetStringBytes() 方法将认证头包装到一个 OCTET STRING 中。
  2. 将这个 OCTET STRING 包装到 padata-value 字段中(特定于上下文的标签号 2),调用 getTagAndLengthBytes() 方法以完成这项任务。
  3. 再次调用 getTagAndLengthBytes() 方法生成对应于第 2 步生成的 padata-value 的 padata-type 字段。
  4. 现在,链接 padata-type 和 padata-value 字段。
  5. 将第 4 步链接的字节数组放入一个 SEQUENCE 中。这个 SEQUENCE 表示一个 PADATA 结构(如 第一篇文章的图 5 和清单 3所示)。
  6. 第一篇文章的图 5 和清单 3 中显示的 padata 字段是 PADATA 结构的一个 SEQUENCE 。这意味着一个 padata 字段可以包含几个 PADATA 结构。不过,只有一个 PADATA 结构要包装到 padata 字段中,这意味着只要将第 5 步得到的 SEQUENCE 包装到另一个外部或者更高层的 SEQUENCE 中。
  7. 第 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() 方法,它有以下参数:

 

  1. ticketContent 字节数组带有准备用于建立安全上下文的服务票据。
  2. clientRealm 字符串包装了请求客户机所属的域 realm 的名字。
  3. clientName 字符串指定了请求客户机的名字。
  4. sequenceNumber 参数是一个表示这个消息序号(sequence number)的 integer 。
  5. encryptionKey:子会话密钥。
  6. 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 格式由以下部分组成:

  1. 一个 GSS 头,类似于我在讨论 createKerberosSession() 方法时介绍的头。
  2. 一个八字节 token 头。在 GSS-Kerveros 规范中有几个不同类型的 token,每一种 token 类型都由一个惟一的头所标识。其中只有要在 sendSecureMessage() 方法中生成的安全消息头是我们感兴趣的。一个安全消息 token 是由具有值 0x02、0x01、0x00、0x00、0x00、0x00、0xff 和 0xff 的头所标识的。
  3. 一个加密的序号,它有助于检测回复攻击。例如,如果有恶意的黑客想要重现(即重复)一个转账指令,他是无法生成加密形式的正确序号的(当然,除非他知道 子会话 密钥)。
  4. 消息的加密摘要值。
  5. 加密后的消息。

 

将上面列出的五个字段以正确的顺序链接在一起,然后包装到一个 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 所示),它以一个安全消息和解密密钥为参数,解密这个消息并返回纯文本格式的消息。解码算法如下:

  1. 第一步是将消息的加密部分(图 24 所示的第五个字段)与 token 头分离。token 头的长度是固定的,所以只有长度字节的数目是随消息的总长度而变化的。因此,只要读取长度字节数并相应地将消息的加密部分拷贝到一个单独的字节数组中。
  2. 第二步是读取消息 checksum( 图 1的第四个字段)。
  3. 现在用解密密钥解密加密的消息。
  4. 然后,取 token 头( 图 1 的第二个字段),将它与解密的消息链接,然后取链接的字节数组的 MD5 摘要值。
  5. 现在加密 MD5 摘要值。
  6. 需要比较第 2 步的八字节消息 checksum 与第 5 步的 MD5 摘要值的后八个字节。如果它们相匹配,那么完整性检查就得到验证。
  7. 经过验证后,删除 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 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

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

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

  1. “ Username ”字段取要使用移动银行 MIDlet 的金融服务的人的用户名。
  2. “ Password ”字段取用户的密码。
  3. “ Amount ”字段允许输入要支付给一个收款人的金额。
  4. “ Pay to ”字段包含收款人的用户名。

 

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

  1. 生成一个 TGT 请求、将请求发送给出服务器、并接收 TGT 响应。
  2. 从 TGT 响应中提取 TGT 和会话密钥。
  3. 生成一个服务票据请求、将请求发送给 KDC 、并接收服务票据响应。
  4. 从服务票据响应中提取服务票据和子会话密钥。
  5. 生成上下文建立请求并发送给电子银行的业务逻辑服务器、接收响应、并解析它以确定服务器同意建立一个新的安全上下文。
  6. 生成一个安全消息、将这个消息发送给服务器、并接收服务器的响应。
  7. 解码来自服务器的消息。

 

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

  1. 我使用了不同的线程( 清单 25 中的 run() 方法)创建 Datagram 连接 ( dc ) 和 Socket 连接上的数据输入和输出流。这是因为 MIDP 2.0 不允许在 J2ME MIDlet 的主执行线程中创建 Datagram 和 Socket 连接。
  2. 在 清单 25 的 J2ME MIDlet 中,我硬编码了 KDC 服务器的域、服务器名、地址和端口号以及电子银行服务器的名字和地址。注意 MIDlet 中的硬编码只是出于展示目的。另一方面, KerberosClient 是完全可重用的。
  3. 为了试验这个应用程序,需要一个作为电子银行服务器运行的 GSS 服务器。本文的 源代码下载 包含一个服务器端应用程序和一个 readme.txt 文件,它描述了如何运行这个服务器。
  4. 最后,注意我没有设计电子银行通信框架,我只是设计了基于 Kerberos 的安全框架。可以设计自己的通信框架,并用 KerberosClient 提供安全支持。例如,可以使用 XML 格式定义不同的消息作为转账指令。

 




回页首

结束语

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

参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文
  • 阅读本系列的 第一篇 和 第二篇文章。 

  • 下载下载本文附带的源代码。 

  • 阅读 Kerberos(第 5 版)的官方 RFC 1510。 

  • 下载 Bouncy Castle 的加密库。本文的代码是用 Bouncy Castle 的 1.19 版测试的。 

  • 阅读官方 DES 和 DES Modes of Operation(包括 CBC 模式)规范。 

  • 访问 IETF 网站上的 Kerberos 工作组页面。 

  • 在“ Simplify enterprise Java authentication with single sign-on”一文中( developerWorks,2003 年 9 月),展示了使用 Kerberos 和 Java GSS API 的单点登录(SSO)。 

  • 在这里可以找到 有关 Kerberos 的很好的一组链接。 

  • 下载 完整的 ASN.1 文档和编码规则。 

  • 阅读 Jason Garman 的 Kerberos: The Definitive Guide (O'Reilly Associates,2003年),以学习 Kerberos 的使用。 

  • 访问 GSS 页。 

  • 看看 MIT 的这个 KDC 实现。 

  • CSG 组和 Heimdal 都提供了免费的 Kerberos 实现。 

  • 在 开发者书店 上寻找有关 Kerberos 所有内容的更多信息。 


关于作者

 

Faheem Khan 是一个独立软件顾问,专长是企业应用集成 (EAI) 和 B2B 解决方案。读者可以通过 fkhan872@yahoo.com与 Faheem 联系

原创粉丝点击