| | | | 將此頁作為電子郵件發送 | | | 未顯示需要 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 消息交換: 圖 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 中的每一個數字都對應于下面討論的一個步驟。 手機用戶向 MIDlet 應用程序提供他或者她的用戶名和密碼(只由用戶和電子銀行共享的一個秘密)。這個密碼只用于在 J2ME 應用程序內部的處理,它永遠也不會進入網絡。通過網絡傳輸的只有用戶名。MIDlet 將用戶名和密碼交給 Kerberos 客戶機。由 Kerberos 客戶機負責建立與電子銀行進行安全通信的上下文。Kerberos 客戶機請求 AS 發出一個 TGT。一個 TGT 請求表示一個安全會話。一個客戶機可以在一個安全會話中建立多個子會話。 TGT 請求包含了發出請求的客戶的用戶名,但是不包括密碼(共享的秘密)。Kerberos 客戶機向 AS 發送請求。當 AS 收到 TGT 請求時,它從請求中提出用戶名并從內部數據庫中取出相應的密碼(共享的秘密)。然后 AS 發布一個 TGT 并將 TGT 包裝在回復消息中。這個 TGT 包含一個純文本部分和一個密碼文本(加密的)部分。為了加密 TGT 的密碼部分,AS 使用了由用戶的密碼生成的加密密鑰。因此,只有知道密碼的用戶才能解開加密的部分。TGT 的加密部分還包含一個加密密鑰,稱為?會話密鑰。AS 向發出請求的 Kerberos 客戶機發送回復消息(包括 TGT)。收到 AS 的回復后,Kerberos 客戶機從回復中取出 TGT 并解密 TGT 中加密的部分。 然后 Kerberos 客戶機發出一個服務票據請求。這個請求包含了 TGT 和一個稱為?鑒別碼 (authenticator)的加密結構。客戶機用從 TGT 提取出的會話密鑰加密鑒別碼。鑒別碼證明客戶機掌握會話密鑰。服務票據請求還指定了電子銀行業務邏輯服務器的名字。客戶機向 TGS(它是電子銀行的 KDC 的一部分)發送服務票據請求。收到服務票據請求后,TGS 提取客戶機向其請求服務票據的服務器的名稱,然后授予一個服務票據。服務票據與 TGT 沒有很大差別。與 TGT 一樣,服務票據包含一個純文本部分和一個密碼文本(加密的)部分。TGS 用服務器的密鑰(一個用服務器的共享秘密生成的密鑰)加密服務票據的密碼文本部分,這樣只有那個服務器可以解密這個服務票據的密碼文本部分。TGS 在服務票據的密碼文本部分中還加入了一個新的加密密鑰。這個密鑰稱為?子會話密鑰。注意現在有兩個密鑰:會話密鑰和子會話密鑰。授予了服務票據之后,TGS 將服務票據包裝在一個響應消息中。這個響應消息也包含一個密碼文本部分,它是用會話密鑰加密的。響應消息的密碼文本部分包含子會話密鑰。TGS 向客戶機發送響應消息。收到 TGS 響應后,客戶機用會話密鑰解密密碼文本部分,從而提取出子會話密鑰。客戶機還提取服務票據。然后客戶機向電子銀行業務邏輯服務器發出消息,并在消息中包裝了服務票據。這個消息請求服務器與客戶機建立新的安全會話。客戶機向電子銀行的業務邏輯服務器發送消息。電子銀行的業務邏輯服務器從請求中提取服務票據,解密它的密碼文本部分,并提取子會話密鑰。這樣客戶機和服務器就都掌握這個密鑰了。電子銀行的服務器向客戶機發送一個肯定應答。 客戶機和電子銀行服務器現在可以用子會話密鑰進行安全通信了。
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-REQ
KDC-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-REQrealm[2] Realm, -- Server's realm-- Also client's in AS-REQsname[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 orderaddresses[9] HostAddresses OPTIONAL,enc-authorization-data[10] EncryptedData OPTIONAL,-- Encrypted AuthorizationData encodingadditional-tickets[11] SEQUENCE OF Ticket OPTIONAL
}
EncryptedData ::= SEQUENCE {etype[0] INTEGER, -- EncryptionTypekvno[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. 一些通用數據類型和標簽號 ? | 通用數據類型 | 通用標簽號 | 構建類型還是基本類型? | | BOOLEAN | 1 | 基本類型 | | INTEGER | 2 | 基本類型 | | BIT STRING | 3 | 基本類型 | | OCTET STRING | 4 | 基本類型 | | SEQUENCE | 16 | 構建類型 | | GeneralizedTime | 24 | 基本類型 | | GeneralString | 27 | 基本類型 | 表 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-REPKDC-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, -- EncryptionTypekvno[1] INTEGER OPTIONAL,cipher[2] OCTET STRING -- ciphertext}EncASRepPart ::= [APPLICATION 25] EncKDCRepPartEncKDCRepPart ::= 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-REQ
KDC-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-REQsname[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 orderaddresses[9] HostAddresses OPTIONAL,enc-authorization-data[10] EncryptedData OPTIONAL,-- Encrypted AuthorizationData encodingadditional-tickets[11] SEQUENCE OF Ticket OPTIONAL
}
AP-REQ ::= [APPLICATION 14] SEQUENCE {pvno [0] INTEGER, -- indicates Version 5msg-type [1] INTEGER, -- indicates KRB_AP_REQap-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 5realm [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
}
| 響應包含服務票據 當 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-REPKDC-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, -- EncryptionTypekvno[1] INTEGER OPTIONAL,cipher[2] OCTET STRING -- ciphertext}EncTGSRepPart ::= [APPLICATION 26] EncKDCRepPartEncKDCRepPart ::= 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 5msg-type [1] INTEGER, -- indicates KRB_AP_REQap-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 5realm [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}
| 電子銀行的響應 當電子銀行的業務邏輯服務器收到圖 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 V5msg-type [1] INTEGER, -- represents KRB_AP_REPenc-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}//elsereturn 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()?方法的討論中見過的同樣實現邏輯: 聲明一個名為?finalBytes?的字節數組,它將包含 ASN.1 字節數組表達的所有字節。計算容納 ASN.1 字節數組表達的內容所需要的字節數。用清單 3 中的?getLengthBytes()?方法生成長度字節。將長度字節儲存到一個名為?lengthBytes?的數組中。得到?lengthBytes?數組中的字節數。計算容納完整的 ASN.1 字節數組表達所需要的字節數(標簽字節、長度字節和內容字節的總和)。將這個字節數儲存到一個名為?totalBytesCount?的變量中。實例化一個具有?totalBytesCount?的值大小的?finalBytes?數組。將標簽字節拷貝到?finalBytes?數組的開始處。將?lengthBytes?數組中的長度字節拷貝到?finalBytes?數組中緊隨標簽字節的位置。將內容字節拷貝到?finalBytes?數組中緊隨長度字節的位置。返回?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 所使用的密鑰的生成過程,如下所示: 將用戶密碼、KDC 域名和用戶的用戶名串接到一起以構成一個字符串。Kerberos 利用這個串接的字符串而不僅僅是密碼生成密鑰。為什么要在密鑰生成中加入域名和用戶名呢?許多用戶會在不同的服務器上使用同樣的密碼。如果我只使用密碼生成密鑰,那么一個給定的密碼在所有 Kerberos 服務器上總是會生成同樣的密鑰。因而,如果一個黑客可以取得用戶在一臺 Kerberos 服務器上的密鑰,那么,他就可以在所有 Kerberos 服務器上使用同一個密鑰。另一方面,如果我加入了域名和用戶名,那么一個受到這種攻擊的密鑰將只會侵害特定的域。得到第 1 步中串接的字符串的字節數組表達。統計第 2 步中字節數組中的字節數。在這個字節串的后面附加適當數量的零字節以使它成為 8 的整數倍。例如,如果這個字節數組包含 53 個字節,那么就在這個字節數組的最后附加三個字節使它具有 56 個字節。將第 3 步中附加了字節后的字節數組分為大小相同的塊,每一塊有 8 個字節。每隔一個塊倒轉塊的位順序。換句話說,第一塊保持不變,第二塊的位順序應該倒轉,第三塊應保持不變,第中塊的位順序應倒轉,以此類推。取第一個(未改變的)塊并與第二個(倒轉的)塊進行每一位的 exclusive?OR?。然后將第一次 exclusive?OR?操作得到的結果與第三個(未改變的)塊進行另一次 exclusive?OR?操作。繼續 exclusive?OR?操作直到完成了所有塊。所有 exclusive?OR?操作的最后結果是一個 8 字節長的塊。?修正在第 6 步中得到的 8 字節塊的奇偶性。每一塊的最低有效位保留為奇偶位。統計 8 字節塊中每字節中的 1 的個數,如果 1 的個數為偶數,那么就設置最低位為 1 使它成為奇數。例如,如果一個字節的值為?00000000?,那么就要將它改為?00000001?。如果一個字節中 1 的個數已經為奇數,那么就將它的最低位設置為零。例如,如果一個字節為?00000010?,那么就不需要為修正其奇偶性做任何改變。?DES 定義了一些弱的、因而不適合用于加密的密鑰。我們的密鑰生成過程的第八步是要檢查奇偶修正后的字節數組是否是一個弱的密鑰。如果是的話,就要用?0xf0?(?11110000?)與奇偶修正過的 8 字節塊進行 exclusive?OR。如果奇偶修正得到的不是弱密鑰,那么就不需要進行這種 exclusive?OR?操作。經過這種弱密鑰處理的字節數組是一個臨時密鑰。?現在我要使用這個臨時密鑰以 DES-CBC 算法加密第 3 步中得到的附加后的字節數組。這個臨時密鑰同時作為密鑰的值和 DES-CBC 加密的初始矢量的值。回想在前面的討論中說過,CBC 要求密文塊鏈接。第 9 步的結果是最后 8 字節塊的加密結果(放棄所以以前的密文塊)。因此,這一步的結果是另一個 8 字節塊。現在我修正第 9 步產生的 8 字節塊中的每一個字節的奇偶性。在上面第 7 步中我解釋了奇偶性修正。現在再次檢查第 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);}//KerberosKeypublic 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 forfor (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;}//generateKeypublic 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;}//getFinalKeypublic byte[] setParity (byte byteValue[]){for(int x=0; x<8; x++)byteValue[x] = parityValues[byteValue[x] & 0xff];return byteValue;}// Checks weak keypublic 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 encodingspublic 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;elsex = 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 entriesprivate 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;}//KerberosClientpublic 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))?)執行兩項任務: 它首先向?getBitStringBytes()?方法傳遞?noOptions?字節數組,它返回用 ASN.1 的位字符串表達的 5 個零。然后它將位字符串傳遞給?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()?方法中的幾個步驟: 調用?getGeneralStringBytes()?方法,同時傳遞客戶的用戶名。?getGeneralStringBytes()?方法將返回客戶的用戶名的?GeneralString?表達。 向?getSequenceBytes()?方法傳遞?GeneralString?,這個方法會在?GeneralString?前面附加?SEQUENCE?字節并返回包含客戶的用戶名字符串的?SEQUENCE?的 ASN.1 表達。 byte generalStringSequence[] = getSequenceBytes (getGeneralStringBytes (userName));?這一行執行這前兩步。 調用?getTagAndLengthBytes()?方法,傳遞?SEQUENCE?字節作為其內容。?getTagAndLengthBytes()?方法會在?SEQUENCE?前面附加?name-string?標簽字節(上下文特定的標簽數字 0)以及長度字節,并返回完整的?name-string?結構。 byte name_string[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 1, generalStringSequence);?這一行執行這一步。 生成?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));?這一行執行這項任務。 將?PrincipalName?的?name-type?和?name-string?部分串接到一起,然后在串接字節數組前面附加?SEQUENCE?字節。?byte principalNameSequence [] = getSequenceBytes (concatenateBytes (name_type, name_string));?一行執行這項任務。 在上面第 5 步的?SEQUENCE?前面附加?cname?標簽字節(上下文特定的標簽數 1)和長度字節。這樣就得到了完整的?cname?結構。?byte cname[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 1, principalNameSequence);?一行執行這項任務。 上述 6 步策略就可以生成完整的?cname?結構。 我的下一步是生成?realm?字段,它的類型為?GeneralString?。生成?realm?字段的策略如下: 用?getGeneralStringBytes()?方法調用生成?GeneralString?。連同?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 文件中: isSequence()getIntegerValue()isASN1Structure()getNumberOfLengthBytes()getLength()getASN1Structure()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;elsereturn 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;elsereturn false;}//isASN1Structure
| getNumberOfLengthBytes() 清單 5?顯示的?getNumberOfLengthBytes()?方法取一個參數(?firstLengthByte?)。?firstLengthByte?參數是 ASN.1 結構的第一個長度字節。?getNumberOfLengthBytes()?方法處理第一個長度字節,以計算 ASN.1 結構中長度字節的字節數。這是一個工具方法,?ASN1DataTypes?類中的其他方法在需要知道一個 ASN.1 結構的長度字節的字節數時就使用它。 清單 5?中的?getNumberOfLengthBytes()?方法的實現策略如下: 檢查?firstLengthByte?的最高有效位(第 8 位)是否為零。?清單 5?中的?if ( (firstLengthByte)& (1<<8)==0)?這一行完成這一任務。?如果最高有效位為零,那么長度字節就遵循?單字節?長度表示法。在?本系列的第 1 部分?我們說過有兩種長度表示法 ──?單字節?和?多字節?。在?單字節?長度表示法中總是有一個長度字節。因此,如果最高有效位為零,那么只需返回 1 作為長度字節的字節數。?如果?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()?方法的一個實現: 第一步是向?getNumberOfLengthBytes()?方法傳 ASN.1 結構的第二個字節。這個 ASN.1 結構從?offset?字節開始,所以可以預計 offset 字節實際上就是標簽字節。因為所有 Kerberos 結構只包含一個標簽字節,所以第二個字節(在 offset 字節后面的那個字節)是第一個長度字節。第一個長度字節說明長度字節的總字節數,?getNumberOfLengthBytes()?方法返回長度字節數。?int numberOfLengthBytes = getNumberOfLengthBytes(ASN1Structure [offset+1]);?這一行執行這項任務 。?如果?getNumberOfLengthBytes()?方法返回一個大于 1 的值,那么必須處理?多字節?長度表示法。在這種情況下,將從?offset + 2?(讓過標簽字節和第一個長度字節) 開始的長度字節讀到一個名為?lengthValueAsBytes?的變量中。然后用?getIntegerValue()?方法將長度值從 ASN.1 字節轉換為 J2ME?int?。最后,將結果加 1(以補償不包含在長度值中的標簽字節),再將長度值返回給調用應用程序。?如果?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;}elseoffset += 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 客戶機提供加密支持的方法: encrypt()decrypt()getMD5DigestValue()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()?方法。注意以下幾點: 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()?方法調用的第二個參數。 第二篇文章的清單 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);} elsecbcCipher.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);} elsecbcCipher.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?類進行摘要計算需要四步: 首先,實例化一個?MD5Digest?對象。然后,調用?MD5Digest?對象的?update()?方法,在調用同時傳遞要摘要的數據。然后,實例化一個用來包含 MD5 摘要值輸出字節數組。最后,調用?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;} elsereturn null;} elsereturn null;} elsereturn null;} elsereturn 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;} elsereturn null;} else return null;} else return null;} else return null; } catch (Exception e) {e.printStackTrace();}return null;} elsereturn 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;}//getKeypublic void setTicket(byte[] ticket) {this.ticket = ticket;}//setTicketpublic 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?類中加入了以下方法以生成一個認證頭: getMD5DigestValue()getChceksumBytes()authorDigestAndEncrypt()getAuthenticationHeader() ? 這四個方法都是 helper 方法。第五個方法(?getAuthenticationHeader()?)使用 helper 方法并生成認證頭。 authorDigestAndEncrypt() 清單 15?顯示的?authorDigestAndEncrypt()?方法取一個純文本數據字節數組和一個加密密鑰。這個方法對純文本數據計算一個摘要值、加密純文本數據、并返回一個?EncryptedData?結構,這個結構與我作為輸入傳遞給?清單 12?的?decryptAndVerifyDigest()?方法的結構完全匹配。 可以說?清單 15?的?authorDigestAndEncrypt()?方法與前面討論的?decryptAndVerifyDigest()?方法正好相反。?authorDigestAndEncrypt()?方法取?decryptAndVerifyDigest()?方法返回的純文本數據作為輸入。與此類似,?authorDigestAndEncrypt()?方法返回的?EncryptedData?結構就是我作為輸入傳遞給?decryptAndVerifyDigest()?方法的結構。 authorDigestAndEncrypt() 方法實現了以下策略: 首先,生成八個隨機字節,它們構成了 confounder。然后,聲明一個名為?zeroedChecksum?的字節數組,它有十六個字節并初始化為零。這個有十六個零的數組作為一個全為零的摘要值。第三,用其他的字節填入輸入數據字節數組,以使數組中的字節數成為八的倍感數。編寫了一個名為?getPaddedData()?的方法(如?清單 16所示),它取一個字節數組并在填充后返回這個數組。下面,鏈接(第 1 步得到的)confounder、(第 2 步得到的)全為零的摘要以及填充后的純文本字節數組。第四步是對第 3 步串接的字節數組計算 MD5 摘要值。第五步是將摘要字節放到它們相應的位置上。第 5 的結果與第 3 步一樣,只不過全為零的摘要現在換成了真正的摘要值。現在調用?encrypt()?方法以加密第 5 步得到的字節數組。然后,生成?etype?字段(特定于上下文的標簽號 0)。然后,調用?getOctetStringBytes()?方法將第 6 步得到的加密字節數組包裝到?OCTET STRING?中。然后將?OCTET STRING?包裝到?cipher?字段中(一個特定于上下文的標簽號 2)。最后,鏈接?etype?和?cipher?字段,將這個字符串包裝到一個?SEQUENCE?中,并返回這個?SEQUENCE?。 清單 15. authorDigestAndEncrypt() 方法 | public byte[] authorDigestAndEncrypt(byte[] key, byte[] data){/****** Step 1: ******/byte[] conFounder = concatenateBytes (getRandomNumber(), getRandomNumber()); /****** Step 2: ******/byte[] zeroedChecksum = new byte[16];/****** Step 3: ******/byte[] paddedDataBytes = concatenateBytes (conFounder, concatenateBytes(zeroedChecksum, getPaddedData(data)));/****** Step 4: ******/byte[] checksumBytes = getMD5DigestValue(paddedDataBytes);/****** Step 5: ******/for (int i=8; i < 24; i++)paddedDataBytes[i] = checksumBytes[i-8];/****** Step 6: ******/byte[] encryptedData = encrypt(key, paddedDataBytes, null);/****** Step 7: ******/byte[] etype = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,0, getIntegerBytes(3));/****** Step 8: ******/byte[] cipher = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,2, getOctetStringBytes(encryptedData));/****** Step 9: ******/byte[] ASN1_encryptedData = getSequenceBytes (concatenateBytes(etype,cipher));return ASN1_encryptedData; }//authorDigestAndEncrypt
| 清單 16. getPaddedData() 方法 | public byte[] getPaddedData(byte[] data) {int numberToPad = 8 - ( data.length % 8 );if (numberToPad > 0 && numberToPad != 8){byte[] bytesPad = new byte[numberToPad];for (int x = 0; x < numberToPad; x++)bytesPad [x] = (byte)numberToPad;return concatenateBytes(data, bytesPad);}elsereturn data;}//getPaddedData()
| getChecksumBytes() getChecksumBytes()?方法生成一個稱為?Checksum?的結構,如?清單 17?所示。Checksum 結構包含兩個字段:?cksumtype?和?checksum?。? 清單 17. Checksum 結構 | Checksum ::= SEQUENCE {cksumtype[0] INTEGER,checksum[1] OCTET STRING}+
| 有兩個地方需要 Checksum 結構 ── 第一個是生成服務票據響應時,然后是生成安全上下文建立請求時。Checksum 結構的作用在這兩種情況下是不同的,需要在生成服務票據和上下文建立請求時說明(elaborate)。 清單 18?所示的?getChecksumBytes()?方法取兩個字節數組參數。第一個參數帶有?checksum?字段,而第二個參數帶有?cksumtype?字段。 getChecksumBytes()?方法將?cksumtype?字段包裝到一個特定于上下文的標簽號 0(它表示?cksumtype?字段,如?清單 17?所示),而將?checksum?字段包裝到一個特定于上下文的標簽號 1(它表示 checksum 字段,同樣如?清單 17?所示)。然后它鏈接這兩個字段,將這個數組包裝到一個?SEQUENCE?中,并返回這個?SEQUENCE?。? 清單 18. getChecksumBytes() 方法 | public byte[] getChecksumBytes(byte[] cksumData, byte[] cksumType){byte[] cksumBytes = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC, 3,getSequenceBytes (concatenateBytes (getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,0,cksumType),getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC, 1,getOctetStringBytes(cksumData)))));return cksumBytes;}//getChecksumBytes()
| getAuthenticationHeader() 在?本系列的第一篇文章?中的“服務票據請求”一節中,介紹過?KRB-AP-REQ?結構(也稱為認證頭)包裝了 Kerberos 票據。此外,認證頭還包裝了 authenticator 字段,它表明客戶機是否掌握了?會話?或者?子會話 密鑰。 如?第一篇文章的圖 5?所示,認證頭由五個字段組成,即?pvno、msg-type、ap-options、ticket?和?authenticator?。 清單 19?的?getAuthenticationHeader()?方法逐一生成這五個字段,然后以正確的順序將各個字段串接起來以形成一個完整的認證頭。? 清單 19. getAuthenticationHeader() 方法 | public byte[] getAuthenticationHeader( byte[] ticketContent,String clientRealm,String clientName,byte[] checksumBytes,byte[] encryptionKey,int sequenceNumber){byte[] authenticator = null;byte[] vno = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,0, getIntegerBytes(5));byte[] ap_req_msg_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,1, getIntegerBytes(14));byte[] ap_options = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,2, getBitStringBytes(new byte[5]));byte[] ticket = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,3, ticketContent);byte[] realmName = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,1, getGeneralStringBytes(clientRealm));byte[] generalStringSequence = getSequenceBytes(getGeneralStringBytes (clientName));byte[] name_string = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,1, generalStringSequence);byte[] name_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,0, getIntegerBytes(ASN1DataTypes.NT_PRINCIPAL));byte[] clientNameSequence = getSequenceBytes(concatenateBytes (name_type, name_string));byte[] cName = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,2, clientNameSequence);byte[] cusec = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,4, getIntegerBytes(0));byte[] ctime = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,5, getGeneralizedTimeBytes (getUTCTimeString(System.currentTimeMillis()).getBytes()));if (sequenceNumber !=0 ) {byte[] etype = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,0, getIntegerBytes(3));byte[] eKey = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,1, getOctetStringBytes(encryptionKey));byte[] subKey_sequence = getSequenceBytes (concatenateBytes(etype, eKey));byte[] subKey = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,6, subKey_sequence);byte[] sequenceNumberBytes = {(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff};sequenceNumberBytes[3] = (byte)sequenceNumber;byte[] seqNumber = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,7, getIntegerBytes(sequenceNumberBytes));authenticator = getTagAndLengthBytes(ASN1DataTypes.APPLICATION_TYPE,2, getSequenceBytes(concatenateBytes(vno,concatenateBytes(realmName,concatenateBytes(cName,concatenateBytes(checksumBytes,concatenateBytes(cusec,concatenateBytes(ctime,concatenateBytes(subKey,seqNumber)))))))));} else {authenticator = getTagAndLengthBytes(ASN1DataTypes.APPLICATION_TYPE,2, getSequenceBytes(concatenateBytes(vno,concatenateBytes(realmName,concatenateBytes(cName,concatenateBytes(checksumBytes,concatenateBytes(cusec,ctime)))))));}//if (sequenceNumber !=null)byte[] enc_authenticator = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,4, authorDigestAndEncrypt(encryptionKey, authenticator));byte[] ap_req = getTagAndLengthBytes (ASN1DataTypes.APPLICATION_TYPE,14, getSequenceBytes(concatenateBytes (vno,concatenateBytes(ap_req_msg_type,concatenateBytes(ap_options,concatenateBytes(ticket, enc_authenticator))))));return ap_req;}//getAuthenticationHeader
| getAuthenticationHeader()?方法有幾個輸入參數: 名為?ticketContent?的字節數組,它包含由?getAuthenticationHeader()?方法包裝到認證頭的 Kerberos 票據(?TGT?)。名為?clientRealm?的字符串類型參數,它指定(生成這個請求的)Kerberos 客戶機所注冊的域(realm )的名字。名為?clientName?的字符串類型參數指定生成這個請求的 Kerberos 客戶機的名字。checksumBytes?字節數組攜帶一個 Checksum 結構以及?getChecksumBytes()?方法。encryptionKey?字節數組攜帶用于生成認證頭的加密部分的加密密鑰。名為?sequenceNumber?的參數是一個?integer?值,它標識發送者的請求號。 ? 在?第一篇文章的圖 5?介紹過,認證頭包含以下字段: - pvno
- msg-type
- ap-options
- ticket
- authenticator
? 現在讓我們看看?清單 19?中的?getAuthenticationHeader()?方法實現是如何生成認證頭的各個字段的(?KRB-AP-REQ?結構): 首先要生成?pvno?字段,它有特定于上下文的標簽號 0,并包裝一個值為 5 的 ASN1?INTEGER?。調用?getTagAndLengthBytes()?方法執行這項任務。我將?pvno?字段存儲 在一個名為?vno?的字節數組中。 類似地,兩次調用?getTagAndLengthBytes()?方法生成?msg-type?(特定于上下文的標簽號 1)和?ap-options?字段(特定于上下文的標簽號 2)。 下一行(?byte[] ticket = getTagAndLengthBytes(ASN1DataTypes.Context_Specific, 3, ticketContent)?)將票據結構包裝到特定于上下文的標簽號 3 中,這是認證頭的第四個字段。 然后,必須生成認證頭的第五個字段(名為?authenticator?,它有特定于上下文的標簽號 4)。authenticator 字段是一個?EncryptedData?結構。authenticator 字段的純文本格式是一個?Authenticator?結構。因此,首先生成純文本格式的完整?Authenticator?結構,將這個純文本?Authenticator?傳遞給?authorDigestAndEncrypt()?方法,這個方法返回?Authenticator?的完整?EncryptedData?表示。 注意在?第一篇文章中的清單 3 和圖 5?中,純文本格式的?Authenticator?結構由以下字段組成(省略最后一個字段,它是不需要的): - authenticator-vno
- creal
- cname
- cksum
- cusec
- ctime
- subkey
- seq-number
? 在解釋?第一篇文章的圖 5時,我已經介紹了每一個字段的意義。 authenticator-vno?字段與?pvno?字段完全相同(本節前面討論了?vno?字節數組,它包含特定于上下文的標簽號 0 且帶值為 5 的?INTEGER?)。所以我重用了在?authenticator_vno?字段中使用的同一個字節數組。 現在該生成?crealm?字段了,它類似于我在?第二篇?文章“生成請求正文”一節中介紹的 realm 字段。同樣,在那一節也介紹了?PrincipalName?類型的?cname?字段。在這里我就不介紹?crealm?和?cname?字段的生成細節了。 下一項任務是生成?cksum?字段,它是?Checksum?類型。服務票據請求中的 cksum 字段的作用是加密結合 authenticator 與一些應用程序數據。注意以下三點: authenticator 結構包含?cksum?字段。cksum?字段包含一些應用程序數據的加密哈希值。整個 authenticator 結構(包括?cksum?字段)是用一個密鑰加密的。 ? 只要在 authenticator 中的?cksum?字段與對應用程序數據的加密 checksum 相匹配,就證明生成 authenticator 和應用程序數據的客戶機擁有密鑰。 調用?getAuthenticationHeader()?方法的應用程序(通過調用?getChecksumBytes()?方法)生成?Checksum?結構,并將?Checksum?字節數組作為?checksumBytes?參數的值傳遞給?getAuthenticationHeader()?方法。 結果,?checksumBytes?參數中就有了?Checksum?結構。只需要將?checksumBytes?包裝到特定于上下文的標簽號 3 中(這是 authenticator 結構中?cksum?字段的標簽號)。 現在生成?cusec?字段,它表示客戶機時間的微秒部分。這個字段的取值范圍為 0 到 999999。這意味著可以在這個字段提供的最大值為 999999 微秒。不過,MIDP 不包含任何可以提供比一毫秒更精確的時間值的方法。因此,不能指定客戶機的微秒部分。只是對這個字段傳遞一個零值。 在 Authenticator 結構中,還要生成兩個字段 ──?subkey?和?seq-number?。在為票據請求而生成的?Authenticator?中不一定要包含這兩個字段,但是后面在用同一個?getAuthenticationHeader()?方法生成上下文建立請求時需要它們。 現在,只需知道只要檢查?sequenceNumber?參數是否為零。對于服務票據請求它為零,對于上下文建立請求它為非零。 如果?sequenceNumber?參數為非零,那么就生成?subkey?和?seq-number?字段,然后鏈接?authenticator-vno、 realm、cname、cksum、cusec、ctime、subkey?和?seq-number?字段以構成一個字節數組,將這個字節數組包裝到一個?SEQUENCE?中,然后將?SEQUENCE?包裝到?Authenticator?中(應用程序級別標簽號 2)。 如果?sequenceNumber?參數為零,那么可以忽略?subkey?和?seq-number?字段,鏈接?authenticator-vno、crealm、cname、cksum、cusec?和?ctime?字段以構成串接的字節數組,將這個字節數組包裝到一個?SEQUENCE?中,然后將這個?SEQUENCE?包裝到?Authenticator?中(應用程序級別標簽號 2)。 下面,需要取完整的?Authenticator?結構并將它傳遞?authorDigestAndEncrypt()?方法,這個方法返回純文本 Authenticator 的完整?EncryptedData?表示。 下一個任務是串接認證頭或者?KRB-AP-REQ?結構的五個字段(?pvno、msg-type、ap-options、ticket、authenticator?)為一個字節數組,將這個字節數組包裝為一個?SEQUECNE?,最后將這個?SEQUENCE?包裝到應用程序級別的標簽號 14。 現在已經完成認證頭,可以將它返回給給調用應用程序了。
生成服務票據請求 我討論了生成服務票據請求需要的所有低層方法。將使用?清單 1?中請求?TGT?時所使用的同一個?getTicketResponse()?方法生成服務票據請求,只需要對?清單 1?稍加修改以使它可以同時用于?TGT?和服務票據請求。讓我們看一下這個過程。 看一下?清單 20,其中可以看到修改過的清單 1 中的?getTicketRespone()?方法。與?清單 1相比,修改過的版本增加了一些代碼:? 清單 20. getTicketResponse() 方法 | public byte[] getTicketResponse( String userName,String serverName,String realmName,byte[] kerberosTicket,byte[] key){byte ticketRequest[];byte msg_type[];byte pvno[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,1, getIntegerBytes(5));msg_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,2, getIntegerBytes(10));byte kdc_options[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,0, getBitStringBytes(new byte[5]));byte generalStringSequence[] = getSequenceBytes (getGeneralStringBytes (userName));byte name_string[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,1, generalStringSequence);byte name_type[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,0, getIntegerBytes(ASN1DataTypes.NT_PRINCIPAL));byte principalNameSequence [] = getSequenceBytes(concatenateBytes (name_type, name_string));byte cname[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,1, principalNameSequence);byte realm[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,2, getGeneralStringBytes (realmName));byte sgeneralStringSequence[] = concatenateBytes(getGeneralStringBytes(serverName),getGeneralStringBytes (realmName));byte sname_string[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,1, getSequenceBytes(sgeneralStringSequence));byte sname_type[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,0, getIntegerBytes(ASN1DataTypes.NT_UNKNOWN));byte sprincipalNameSequence [] = getSequenceBytes(concatenateBytes (sname_type, sname_string));byte sname[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,3, sprincipalNameSequence);byte till[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,5,getGeneralizedTimeBytes (new String("19700101000000Z").getBytes()));byte nonce[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,7,getIntegerBytes (getRandomNumber()));byte etype[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,8,getSequenceBytes(getIntegerBytes(3)));byte req_body[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,4,getSequenceBytes(concatenateBytes(kdc_options,concatenateBytes(cname,concatenateBytes(realm,concatenateBytes(sname,concatenateBytes(till,concatenateBytes(nonce, etype))))))));if (kerberosTicket != null) {msg_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,2, getIntegerBytes(12));sname_string = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,1, getSequenceBytes(getGeneralStringBytes(serverName)));sname_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,0, getIntegerBytes(ASN1DataTypes.NT_UNKNOWN));sprincipalNameSequence = getSequenceBytes(concatenateBytes (sname_type, sname_string));sname = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,3, sprincipalNameSequence);byte[] req_body_sequence = getSequenceBytes(concatenateBytes(kdc_options,concatenateBytes(realm,concatenateBytes(sname,concatenateBytes(till,concatenateBytes(nonce, etype))))));req_body = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,4, req_body_sequence);byte[] cksum = getChecksumBytes(getMD5DigestValue(req_body_sequence), getIntegerBytes(7));byte[] authenticationHeader = getAuthenticationHeader(kerberosTicket,realmName,userName,cksum,key,0);byte[] padata_sequence = getSequenceBytes(concatenateBytes(getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,1,getIntegerBytes(1)),getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,2, getOctetStringBytes(authenticationHeader))));byte[] padata_sequences = getSequenceBytes(padata_sequence);byte[] padata = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,3, padata_sequences);ticketRequest = getTagAndLengthBytes(ASN1DataTypes.APPLICATION_TYPE,12, getSequenceBytes(concatenateBytes(pvno,concatenateBytes(msg_type,concatenateBytes(padata, req_body)))));} else {ticketRequest = getTagAndLengthBytes(ASN1DataTypes.APPLICATION_TYPE,10, getSequenceBytes(concatenateBytes(pvno,concatenateBytes(msg_type, req_body))));}try {Datagram dg = dc.newDatagram(ticketRequest, ticketRequest.length);dc.send(dg);} catch (IllegalArgumentException il) {il.printStackTrace();} catch (Exception io) {io.printStackTrace();} byte ticketResponse[] = null;try {Datagram dg = dc.newDatagram(700);dc.receive(dg);if (dg.getLength() > 0) {ticketResponse = new byte[dg.getLength()];System.arraycopy(dg.getData(), 0, ticketResponse, 0, dg.getLength());} else return null;} catch (IOException ie){ie.printStackTrace();}return ticketResponse;}//getTicketResponse
| 清單 20?中顯示的新的?getTicketResponse()?方法有五個參數:?userName、serverName、realmName、kerberosTicket?和?key?。要請求一個服務票據,需要傳遞?kerberosTicket?字節數組的?TGT?。另一方面,在請求?TGT?時,不必傳遞一個票據,因此對于?kerberosTicket?字節數組傳遞“null”。 TGT?請求與服務票據請求的主要區別是?padata?字段。在本系列?第一篇文章?中的“請求服務票據”一節中討論服務票據請求的?padata?字段時已經介紹過。 在?getTicketResponse()?的最后,我加入了一個?if (kerberosTicket!=null)?塊。只有在?kerberosTicket參數不為 null 時才進入這個塊(在所有?TGT?請求中它都是 null)。 在?if (kerberosTicket!=null)?塊中,我生成了?padata?字段。正如?第一篇文章的圖 5?中描述的,這個?padata?字段包裝一個可由?getAuthenticationHeader()?方法生成的認證頭。 在?getAuthenticationHeader()?方法中還可了解到,為了生成一個認證頭,需要一個可由?getChecksumBytes()?方法生成的?Checksum?結構。 現在,回想一下在討論?getChecksumBytes()?方法時說過,為了生成一個?Checksum?結構,需要用于?cksumtype?和?checksum?字段的數據。 因此,生成一個認證頭需要三步: 生成?cksumtype?和?checksum?字段的數據。如果是服務票據請求,那么?checksum?字段的數據只是對包含服務票據請求的?req-body?字段的所有子字段的?SEQUENCE?計算的 MD5 摘要值(注意在?第一篇文章的圖 5,?req-body?是服務票據請求的第四個字段,就在服務票據請求的第三個字段?padata?字段后面)。?cksumtype?字段的數據是?integer?7 的 ASN1 表示。這個值指定 checksum 的類型。調用?getChecksumBytes()?方法并傳遞?cksumtype?和?checksum?字段的數據。?getChecksumBytes()?方法生成完整的?Checksum?結構。調用?getAuthenticationHeader()?方法,同時傳遞?Checksum?結構。?getAuthenticationHeader()?返回認證頭。 ? 生成了認證頭后,必須將它包裝到一個?padata?字段中。為此,有幾件事要做: 調用我在?第二篇文章的清單 5?中描述的?getOctetStringBytes()?方法將認證頭包裝到一個?OCTET STRING?中。將這個?OCTET STRING?包裝到?padata-value?字段中(特定于上下文的標簽號 2),調用?getTagAndLengthBytes()?方法以完成這項任務。再次調用?getTagAndLengthBytes()?方法生成對應于第 2 步生成的?padata-value?的?padata-type?字段。現在,鏈接?padata-type?和?padata-value?字段。將第 4 步鏈接的字節數組放入一個?SEQUENCE?中。這個?SEQUENCE?表示一個?PADATA?結構(如?第一篇文章的圖 5 和清單 3所示)。第一篇文章的圖 5 和清單 3?中顯示的?padata?字段是?PADATA?結構的一個?SEQUENCE?。這意味著一個?padata?字段可以包含幾個?PADATA?結構。不過,只有一個?PADATA?結構要包裝到?padata?字段中,這意味著只要將第 5 步得到的?SEQUENCE?包裝到另一個外部或者更高層的?SEQUENCE?中。第 6 步的更高層?SEQUENCE?表示?PADATA?結構的?SEQUENCE?,現在可以將它包裝到?padata?字段中(一個特定于上下文的標簽號 3)。 ? 在?清單 20?的結尾處的?if (kerberosTicket!=null)?塊中可以找到?getTicketResponse()?方法中增加的所有新代碼。 到此就結束了對于修改現有的?getTicketResponse()?方法以使它可同時用于?TGT?和服務票據請求的討論。?getTicketResponse()?方法生成一個服務票據請求、將這個請求發送給?KDC?、接收服務票據響應、并將響應返回給調用應用程序。?
從服務票據響應中提取服務票據和子會話密鑰 服務票據響應類似于?TGT?響應。在?清單 13?中的?getTicketAndKey()?方法解析一個?TGT?響應以提取?TGT?和會話密鑰。同一個方法也解析服務票據響應以從服務票據響應中提取服務票據和子會話密鑰。所以,不必編寫任何提取服務票據和子會話密鑰的代碼。?
創建一個安全通信上下文 現在有了與電子銀行的業務邏輯服務器建立安全通信上下文所需要的兩項內容:子會話密鑰和服務票據。這時 Kerberos 客戶機必須生成針對電子銀行的業務邏輯服務器的上下文建立請求。 參見?第一篇文章的圖 7 和清單 5,它們描述了客戶機為建立安全上下文而發送給電子銀行服務器的消息。?清單 21?中顯示的?createKerberosSession()?方法處理與電子銀行的業務邏輯服務器建立安全通信上下文的所有方面(包括生成上下文建立請求、向服務器發送請求、從服務器中獲得響應、解析響應以檢查遠程服務器是否同意上下文建立請求,并將這些工作的結果返回給調用應用程序)。 看一下?清單 21?中的?createKerberosSession()?方法,它有以下參數: ? ticketContent?字節數組帶有準備用于建立安全上下文的服務票據。clientRealm?字符串包裝了請求客戶機所屬的域?realm?的名字。clientName?字符串指定了請求客戶機的名字。sequenceNumber?參數是一個表示這個消息序號(sequence number)的?integer?。encryptionKey:子會話密鑰。inStream?和?outStream?是?createKerberosSession()?方法用來與電子銀行的服務器通信的輸入輸出流。 ? 正如在第一篇文章中介紹的,要使用 Java-GSS 實現電子銀行的服務器端邏輯。GSS-Kerberos 機制規定服務票據包裝在一個認證頭中,而這個認證頭本身又包裝在?第一篇文章的圖 7 和清單 5?中顯示的?InitialContextToken?包裝器中。 可以使用?清單 19?的?getAuthenticationHeader()?方法包裝服務票據。回想一下在?清單 20?的?getTicketResponse()?方法中我使用了?getAuthenticationHeader()?方法包裝一個?TGT?。 為了生成認證頭,需要一個?Checksum?。回想在討論?清單 19?的?getAuthenticationHeader()?方法時說過,Checksum?的目的是加密綁定認證頭與一些應用程序數據。但是,與票據請求認證頭不一樣,上下文建立認證頭不帶有應用程序數據。 GSS-Kerberos 機制出于不同的目的使用?Checksum?結構。除了將認證頭綁定到一些應用程序數據外,GSS-Kerberos 機制使用一個?Checksum?結構用物理網絡地址(即客戶機可以用來與服務器進行安全通信的網絡地址)綁定安全上下文。如果使用這種功能,那么只能從它所綁定的網絡地址上使用安全上下文。 不過,我不作準備在這個示例移動銀行應用程序中使用這種功能。這就是為什么我在?Checksum?結構中指定安全上下文沒有任何綁定的緣故。為此,我編寫了一個名為?getNoNetworkBindings()?的方法,如?清單 22?所示。getNoNetworkBindings()?方法非常簡單。它只是生成一個硬編碼的字節數組,表明不需要任何網絡綁定。然后它調用?getChecksumBytes()?方法以將硬編碼的數組包裝到?cksum?字段中。 得到了無網絡綁定的?Checksum?的字節數組后,可以將這個數組傳遞給?getAuthenticationHeader()?方法,這個方法返回完整的認證頭。 生成了認證頭后,?清單 21?的?createKerberosSession()?方法將認證頭字節數組與一個名為?gssHeaderComponents?的硬編碼的字節數組相鏈接。?gssHeaderComponents?字節數組包含一個 GSS 頭的字節表示,這個 GSS 頭在上下文建立請求中將伴隨一個認證頭。 最后,將串接的 GSS 頭和認證頭包裝到一個應用程序級別的標簽號 0 中。GSS 要求所有上下文建立請求都包裝到應用程序級別的標簽號 0 中。 現在完成了上下文建立請求。下一項任務就是通過一個輸出流(?outStream?對象)發送這個請求。發送了請求后,監聽并接收?inStream?對象上的響應。 當?createKerberosSession()?方法收到響應后,它就檢查響應是確認創建一個新的上下文還是顯示一個錯誤消息。要進行這種檢查,必須知道消息開始標簽字節后面的長度字節的字節數。?GSS?頭字節(緊接著長度字節)提供了答案。 不用解析響應以進行任何進一步的處理。只是要知道電子銀行的服務器是創建了一個新會話還是拒絕會話。如果電子銀行的服務器確認創建新會話,那么?createKerberosSession()?方法就返回?true?,如果不是,它就返回false?。? 清單 21. createKerberosSession() 方法 | public boolean createKerberosSession (byte[] ticketContent, String clientRealm, String clientName,int sequenceNumber, byte[] encryptionKey,DataInputStream inStream,DataOutputStream outStream){byte[] cksum = getNoNetworkBindings();if (sequenceNumber == 0)sequenceNumber++;byte[] authenticationHeader = getAuthenticationHeader(ticketContent,clientRealm,clientName, cksum, encryptionKey,sequenceNumber);byte[] gssHeaderComponents = {(byte)0x6, (byte)0x9, (byte)0x2a,(byte)0xffffff86,(byte)0x48,(byte)0xffffff86,(byte)0xfffffff7,(byte)0x12,(byte)0x1,(byte)0x2,(byte)0x2,(byte)0x1, (byte)0x0};byte[] contextRequest = getTagAndLengthBytes(ASN1DataTypes.APPLICATION_TYPE,0, concatenateBytes (gssHeaderComponents, authenticationHeader));try { outStream.writeInt(contextRequest.length);outStream.write(contextRequest );outStream.flush();byte[] ebankMessage = new byte[inStream.readInt()];inStream.readFully(ebankMessage);int respTokenNumber = getNumberOfLengthBytes (ebankMessage[1]);respTokenNumber += 12;byte KRB_AP_REP = (byte)0x02;if (ebankMessage[respTokenNumber] == KRB_AP_REP){return true;} elsereturn false;} catch (Exception io) {io.printStackTrace();}return false;}//createKerberosSession
| 清單 22. getNoNetworkBindings() 方法 | public byte[] getNoNetworkBindings() {byte[] bindingLength = { (byte)0x10, (byte)0x0, (byte)0x0, (byte)0x0};byte[] bindingContent = new byte[16];byte[] contextFlags_bytes = {(byte)0x3e,(byte)0x00,(byte)0x00,(byte)0x00 };byte[] cksumBytes = concatenateBytes (concatenateBytes(bindingLength,bindingContent), contextFlags_bytes);byte[] cksumType = {(byte)0x2,(byte)0x3,(byte)0x0,(byte)0x80,(byte)0x3};byte[] cksum = getChecksumBytes(cksumBytes, cksumType);return cksum;}//getNoNetWorkBindings()
|
向電子銀行的業務邏輯服務器發送安全消息 如果?createKerberosSession()?方法返回?true?,就知道成功地與遠程 Kerberos 服務器建立了一個安全會話。就可以開始與 Kerveros 服務器交換消息了。 看一下?清單 23?的?sendSecureMessage()?方法。這個方法取一個純文本消息、一個加密密鑰、一個序號(它惟一地標識了發送的消息)和與服務器交換數據所用的輸入輸出流對象作為參數。?endSecureMessage()?方法生成一個安全消息、通過輸出流將這個消息發送給服務器、監聽服務器的響應,并返回服務器的響應。 發送給服務器的消息是用子會話密鑰保護的。這意味著只有特定的接收者(擁有子會話密鑰的電子銀行業務邏輯服務器)可以解密并理解這個消息。而且,安全消息包含消息完整性數據,所以電子銀行的服務器可以驗證來自客戶機的消息的完整性。 讓我們看一下?sendSecureMessage()?方法是如何用一個純文本消息生成一個安全 GSS 消息的。 一個 GSS 安全消息采用 token(token 格式的字節數組)的形式。token 格式由以下部分組成: 一個 GSS 頭,類似于我在討論?createKerberosSession()?方法時介紹的頭。一個八字節 token 頭。在 GSS-Kerveros 規范中有幾個不同類型的 token,每一種 token 類型都由一個惟一的頭所標識。其中只有要在?sendSecureMessage()?方法中生成的安全消息頭是我們感興趣的。一個安全消息 token 是由具有值?0x02、0x01、0x00、0x00、0x00、0x00、0xff?和?0xff?的頭所標識的。一個加密的序號,它有助于檢測回復攻擊。例如,如果有惡意的黑客想要重現(即重復)一個轉賬指令,他是無法生成加密形式的正確序號的(當然,除非他知道?子會話?密鑰)。消息的加密摘要值。加密后的消息。 ? 將上面列出的五個字段以正確的順序鏈接在一起,然后包裝到一個 ASN.1 應用程序級別的標簽號 0 中。這就構成了完整的 GSS-Kerberos 安全消息 token,如?圖 1所示。? 圖 1.? ? 為了生成如?圖 1所示的完整安全 token,必須生成所有五個字段。 前兩個字段沒有動態內容,它們在所有安全消息中都是相同的,所以我在?清單 23中硬編碼了它們的值。另外三個字段必須根據以下算法動態計算:? 1.?在純文本消息中添加額外的字節以使消息中的字節數是八的倍數。? 2.?生成一個名為?confounder?的八字節隨機數。鏈接 confounder 與第 1 步中填充的消息。? 3.?串接 token 頭(?圖 1中的第二個字段)和第 2 步的結果。然后對鏈接的結果計算 16 字節 MD5 摘要值。 4.?用?子會話?密鑰加密第 3 步得到的 16 字節摘要值。加密算法是使用零 IV 的 DES-CBC。加密的數據的最后八個字節(放棄前八個字節)構成了?圖 1第四個字段(加密的摘要值)。? 5.?現在必須生成一個加密的 8 字節序號(?圖 1?的第三個字段)。這個序號是用?子會話?密鑰和第 4 步使用 IV 的加密摘要值的后八個字節加密的。? 6.?現在取第 2 步的結果(鏈接在一起的 confounder 和填充的消息)并用 DES-CBC 加密它。要進行這種加密,使用一個用?0xF0?對?子會話?密鑰的所有字節執行 OR 操作生成的密鑰。這種加密得到的結果構成了?圖 1的第五個字段,也就是加密的消息。 生成了各個字段后,將它們鏈接為一個字節數組,最后,調用?getTagAndLengthBytes()?方法以在鏈接的字節數組前面附加一個應用程序級別的標簽號 0。 可以觀察?清單 23?的?sendSecureMessage()?方法中的這些步驟。生成了安全消息后,通過輸出流將消息發送給服務器、監聽服務器的響應并將響應返回給接收者。? 清單 23. sendSecureMessage() 方法 | public byte[] sendSecureMessage( String message, byte[] sub_sessionKey, int seqNumber,DataInputStream inStream,DataOutputStream outStream){byte[] gssHeaderComponents = {(byte)0x6,(byte)0x9,(byte)0x2a,(byte)0x86,(byte)0x48,(byte)0x86,(byte)0xf7,(byte)0x12,(byte)0x01,(byte)0x02,(byte)0x02 };byte[] tokenHeader = {(byte)0x02,(byte)0x01,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0xff,(byte)0xff}; try {/***** Step 1: *****/byte[] paddedDataBytes = getPaddedData (message.getBytes());/***** Step 2: *****/byte[] confounder = concatenateBytes (getRandomNumber(), getRandomNumber());/***** Step 3: *****/byte[] messageBytes = concatenateBytes(confounder, paddedDataBytes);byte[] digestBytes = getMD5DigestValue(concatenateBytes (tokenHeader,messageBytes));CBCBlockCipher cipher = new CBCBlockCipher(new DESEngine());KeyParameter kp = new KeyParameter(sub_sessionKey);ParametersWithIV iv = new ParametersWithIV (kp, new byte[8]);cipher.init(true, iv);byte processedBlock[] = new byte[digestBytes.length];byte message_cksum[] = new byte[8];for(int x = 0; x < digestBytes.length/8; x ++) {cipher.processBlock(digestBytes, x*8, processedBlock, x*8);System.arraycopy(processedBlock, x*8, message_cksum, 0, 8);iv = new ParametersWithIV (kp, message_cksum);cipher.init (true, iv);}/***** Step 4: *****/ byte[] sequenceNumber = {(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00};sequenceNumber[0] = (byte)seqNumber;/***** Step 5: *****/byte[] encryptedSeqNumber = encrypt(sub_sessionKey, sequenceNumber, message_cksum);/***** Step 6: *****/byte[] encryptedMessage = encrypt(getContextKey(sub_sessionKey), messageBytes, new byte[8]); byte[] messageToken = getTagAndLengthBytes (ASN1DataTypes.APPLICATION_TYPE,0, concatenateBytes (gssHeaderComponents, concatenateBytes (tokenHeader, concatenateBytes (encryptedSeqNumber, concatenateBytes (message_cksum, encryptedMessage)))));/***** Step 7: *****/ outStream.writeInt(messageToken.length);outStream.write(messageToken);outStream.flush();/***** Step 8: *****/byte[] responseToken = new byte[inStream.readInt()];inStream.readFully(responseToken);return responseToken;} catch(IOException ie){ie.printStackTrace();} catch(Exception e){e.printStackTrace();}return null;}//sendSecureMessagepublic byte[] getContextKey(byte keyValue[]){for (int i =0; i < keyValue.length; i++)keyValue[i] ^= 0xf0;return keyValue;}//getContextKey
|
解碼服務器消息 就像生成并發送給服務器的消息一樣,?sendSecureMessage()?方法返回的服務器消息是安全的。它遵循?圖 1?所示的同樣的 token 格式,這意味著只有擁有?子會話?密鑰的客戶機才能解密這個消息。 我編寫了一個名為?decodeSecureMessage()?的方法(如?清單 24?所示),它以一個安全消息和解密密鑰為參數,解密這個消息并返回純文本格式的消息。解碼算法如下: 第一步是將消息的加密部分(圖 24 所示的第五個字段)與 token 頭分離。token 頭的長度是固定的,所以只有長度字節的數目是隨消息的總長度而變化的。因此,只要讀取長度字節數并相應地將消息的加密部分拷貝到一個單獨的字節數組中。第二步是讀取消息 checksum(?圖 1的第四個字段)。現在用解密密鑰解密加密的消息。然后,取 token 頭(?圖 1?的第二個字段),將它與解密的消息鏈接,然后取鏈接的字節數組的 MD5 摘要值。現在加密 MD5 摘要值。需要比較第 2 步的八字節消息 checksum 與第 5 步的 MD5 摘要值的后八個字節。如果它們相匹配,那么完整性檢查就得到驗證。經過驗證后,刪除 cofounder(解密的消息的前八個字節)并返回消息的其余部分(它就是所需要的純文本消息)。 清單 24. decodeSecureMessage() 方法 | public String decodeSecureMessage (byte[] message, byte[] decryptionKey){int msg_tagAndHeaderLength = 36;int msg_lengthBytes = getNumberOfLengthBytes (message[1]);int encryptedMsg_offset = msg_tagAndHeaderLength + msg_lengthBytes;byte[] encryptedMessage = new byte[message.length - encryptedMsg_offset];System.arraycopy(message, encryptedMsg_offset, encryptedMessage, 0,encryptedMessage.length);byte[] msg_checksum = new byte[8];System.arraycopy(message, (encryptedMsg_offset-8), msg_checksum, 0, msg_checksum.length);byte[] decryptedMsg = decrypt (decryptionKey, encryptedMessage, new byte[8]);byte[] tokenHeader = {(byte)0x2,(byte)0x1,(byte)0x0,(byte)0x0,(byte)0x0,(byte)0x0,(byte)0xff,(byte)0xff};byte[] msg_digest = getMD5DigestValue (concatenateBytes(tokenHeader,decryptedMsg));byte[] decMsg_checksum = new byte[8];try {CBCBlockCipher cipher = new CBCBlockCipher(new DESEngine());KeyParameter kp = new KeyParameter(getContextKey(decryptionKey));ParametersWithIV iv = new ParametersWithIV (kp, decMsg_checksum);cipher.init(true, iv);byte[] processedBlock = new byte[msg_digest.length];for(int x = 0; x < msg_digest.length/8; x ++) {cipher.processBlock(msg_digest, x*8, processedBlock, x*8);System.arraycopy(processedBlock, x*8, decMsg_checksum, 0, 8);iv = new ParametersWithIV (kp, decMsg_checksum);cipher.init (true, iv);}} catch(java.lang.IllegalArgumentException il){il.printStackTrace();}for (int x = 0; x < msg_checksum.length; x++) {if (!(msg_checksum[x] == decMsg_checksum[x])) return null;}return new String (decryptedMsg, msg_checksum.length, decryptedMsg.length - msg_checksum.length);}//decodeSecureMessage()public byte[] getContextKey(byte keyValue[]){for (int i =0; i < keyValue.length; i++)keyValue[i] ^= 0xf0;return keyValue;}//getContextKey
|
示例移動銀行應用程序 已經完成了示例移動銀行應用程序所需要的安全 Kerberos 通信的所有階段。現在可以討論移動銀行 MIDlet 如何使用 Kerberos 客戶機功能并與電子銀行的服務器通信了。 清單 25顯示了一個簡單的 MIDlet,它模擬了示例移動銀行應用程序。? 清單 25. 一個示例移動銀行 MIDlet | import java.io.*;
import java.util.*;
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
import javax.microedition.io.*;
public class J2MEClientMIDlet extends MIDlet implements CommandListener, Runnable {private Command 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) { }//destroyApppublic 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);}}//commandactionpublic 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);elseshowTransResult(" Error!", "Transaction failed..");} elseSystem.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();}}//runpublic void transactionForm(){transForm = new Form("EBANK Transaction Form");txt_userName = new TextField("Username", "", 10, TextField.ANY);txt_password = new TextField("Password", "", 10, TextField.PASSWORD);txt_amount = new TextField("Amount", "", 4, TextField.NUMERIC);txt_sendTo = new TextField("Pay to", "", 10, TextField.ANY);transForm.append(txt_userName);transForm.append(txt_password);transForm.append(txt_amount);transForm.append(txt_sendTo);transForm.addCommand(sendMoneyCommand);transForm.addCommand(exitCommand);transForm.setCommandListener(this);display.setCurrent(transForm); }public void showTransResult(String info, String message) {transResForm = new Form("Transaction Result");si_message = new StringItem("Status:" , info);txt_label = new TextField("Result:", message, 150, TextField.ANY);transResForm.append(si_message);transResForm.append(txt_label);transResForm.addCommand(exitCommand); transResForm.addCommand(OKCommand);transResForm.setCommandListener(this);display.setCurrent(transResForm);}
}//J2MEClientMIDlet
| 運行這個 MIDlet 會得到如?圖 2所示的屏幕。? 圖 2.? ? 圖 2顯示為這個示例移動銀行應用程序開發了一個非常簡單的 GUI。?圖 2?還顯示了四個數據輸入字段: “?Username?”字段取要使用移動銀行 MIDlet 的金融服務的人的用戶名。“?Password?”字段取用戶的密碼。“?Amount?”字段允許輸入要支付給一個收款人的金額。“?Pay to?”字段包含收款人的用戶名。 ? 輸入完數據后,按 Pay 按鈕。Pay 按鈕的事件處理器(?清單 25?中的?sendMoney()?方法)執行 Kerveros 通信的所有七個階段: 生成一個?TGT?請求、將請求發送給出服務器、并接收?TGT?響應。從?TGT?響應中提取?TGT?和會話密鑰。生成一個服務票據請求、將請求發送給?KDC?、并接收服務票據響應。從服務票據響應中提取服務票據和子會話密鑰。生成上下文建立請求并發送給電子銀行的業務邏輯服務器、接收響應、并解析它以確定服務器同意建立一個新的安全上下文。生成一個安全消息、將這個消息發送給服務器、并接收服務器的響應。解碼來自服務器的消息。 ? 清單 25?的 MIDlet 代碼相當簡單,不需要很多解釋。只要注意以下幾點: 我使用了不同的線程(?清單 25?中的?run()?方法)創建?Datagram?連接 (?dc?) 和?Socket?連接上的數據輸入和輸出流。這是因為 MIDP 2.0 不允許在 J2ME MIDlet 的主執行線程中創建?Datagram?和?Socket?連接。在?清單 25?的 J2ME MIDlet 中,我硬編碼了 KDC 服務器的域、服務器名、地址和端口號以及電子銀行服務器的名字和地址。注意 MIDlet 中的硬編碼只是出于展示目的。另一方面,?KerberosClient?是完全可重用的。為了試驗這個應用程序,需要一個作為電子銀行服務器運行的 GSS 服務器。本文的?源代碼下載?包含一個服務器端應用程序和一個 readme.txt 文件,它描述了如何運行這個服務器。最后,注意我沒有設計電子銀行通信框架,我只是設計了基于 Kerberos 的安全框架。可以設計自己的通信框架,并用 KerberosClient 提供安全支持。例如,可以使用 XML 格式定義不同的消息作為轉賬指令。 ?
結束語 在這個三部分的系列文章中,我展示了 J2ME 應用程序中的安全 Kerberos 通信。介紹了進行一系列加密密鑰交換的 Kerveros 通信。還介紹了 J2ME 應用程序是如何使用密鑰與遠程電子銀行服務器建立通信上下文并安全地交換消息的。我還提供了展示文章中討論的所有概念的 J2ME 代碼。 參考資料 - 您可以參閱本文在 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 聯系 | | |