當數據在網絡上傳播的時候,通過使用 SSL 對其進行加密和保護,JSSE 為 Java 應用程序提供了安全的通信。在本篇有關該技術的高級研究中,Java 中間件開發人員 Ian Parkinson 深入研究了 JSSE API 較不為人知的方面,為您演示了如何圍繞 SSL 的一些限制進行編程。您將學習如何動態地選擇 KeyStore 和 TrustStore、放寬 JSSE 的密碼匹配要求,以及構建您自己定制的 KeyManager 實現。
JSSE(Java 安全套接字擴展,Java Secure Socket Extension)使 Java 應用程序能夠在因特網上使用 SSL 安全地進行通信。由于 developerWorks 已經提供了一篇涵蓋 JSSE 基本用法的教程,所以本文將集中闡述該技術的更高級用法。本文將演示如何使用 JSSE 接口定制 SSL 連接的屬性。
首先,我們將開發一個非常簡單的安全客戶機/服務器聊天應用程序。在我們構建該程序的客戶機端時,我將演示如何定制 KeyStore 和 TrustStore 文件,以便從客戶機的文件系統裝入它們。接著,我們將著重說明證書和標識。通過從 KeyStore 選擇不同的證書,可以將客戶機以不同的形式提供給不同的服務器。如果您的客戶機應用程序需要連接到多個對等方,或者甚至它需要冒充不同的用戶,這項高級的功能都特別有用。
由于本文著重講述更高級的主題,因此假定您已經具備了 JSSE 的使用經驗。要運行示例,需要一個帶有正確安裝和配置 JSSE 提供程序的 Java SDK。J2SE 1.4 SDK 提供了已安裝和配置的 JSSE。如果您正在使用 J2SE 1.2 或 1.3,則需要獲取一個 JSSE 實現并安裝它。
JSSE API 只是 J2SE 1.4 的一項標準,并且早期的 JSSE 實現之間存在略有不同的變體。本文的示例基于 1.4 API。必要的時候,我會強調使示例與 J2SE 1.2 和 1.3 的 Sun JSSE 實現協同工作所必需的更改。
入門:設置
在我們深入研究 JSSE 之前,先讓我們熟悉一下將要使用的客戶機/服務器應用程序。SimpleSSLServer 和 SimpleSSLClient 是我們的演示應用程序的兩個組件。為了運行示例,需要在應用程序的每一端上設置好幾個 KeyStore 和 TrustStore 文件。特別是您將需要:
接下來,下載本文隨附的 jar 文件。這些文件包含客戶機/服務器應用程序的源代碼和已編譯的版本,因此,只要把它們放到 CLASSPATH 中,就可以使用了。
建立一個安全連接
要運行 SimpleSSLServer,我們輸入如下(稍微冗長的)命令:
|
可以看到,我們已指定了 KeyStore,用它來標識服務器,還指定了在 KeyStore 中設置的密碼。由于服務器將需要客戶機認證,因此我們也為它提供了 TrustStore。通過指定 TrustStore,我們確保 SSLSimpleServer 將信任由 SSLSimpleClient 提供的證書。服務器初始化它自己后,您將得到下述報告:
|
之后,服務器將等待來自客戶機的連接。如果希望在另一個端口上運行服務器,在命令的結尾處指定 ,用選定的端口代替變量
。
接下來,設置應用程序的客戶機組件。從另一個控制臺窗口輸入如下命令:
|
缺省情況下,客戶機將嘗試連接到運行在本地主機端口 49152 上的服務器。可以在命令行上使用 和
參數更改主機。當客戶機已連接到服務器時,您會得到消息:
|
與此同時,服務器將報告連接請求,并顯示由客戶機提供的區別名,如下所示:
|
為了測試新連接,試著向客戶機輸入一些文本,按 Return 鍵,并觀察服務器是否回顯文本。要殺死客戶機,在客戶機控制臺上按 Ctrl-C 鍵。服務器將如下所示記錄斷開連接:
|
無需殺死服務器;在各種練習過程中我們只需保持服務器運行。
SimpleSSLServer 內幕
盡管本文余下部分主要都是講述客戶機應用程序的,但是,查看一下服務器代碼仍然是很值得的。除了可以了解服務器應用程序是如何工作外,您還可以學會如何使用 接口檢索有關 SSL 連接的信息。
從三條 import 語句開始,如下所示:
|
接下來, 方法檢查命令行中可選的
參數。然后它獲取缺省的
,構造一個
對象,把工廠(factory)傳遞給構造器,并且啟動服務器,如下所示:
|
由于服務器是在單獨的線程上運行的,因此只要啟動并運行, 就退出。新的線程調用
方法,這樣會創建一個
,并且設置服務器以要求客戶機認證,如下所示:
|
HandshakeCompletedListener
將它激活之后, 方法進行無限循環,等待來自客戶機的請求。每個套接字都與
實現相關聯,后者用來顯示來自客戶機證書的區別名(DN)。套接字的
封裝在一個
中,它作為另一個線程運行,并且將來自套接字的數據回顯到
。SimpleSSLServer 的主循環如清單 1 所示:
|
我們的 ―
提供了一個
方法的實現。當 SSL 握手階段完成時,該方法由 JSSE 基礎設施調用,并且傳遞(在
對象中)有關連接的信息。我們使用這個方法獲取并顯示客戶機的 DN,如清單 2 所示:
|
在 J2SE 1.2 或 1.3 上運行服務器應用程序 |
用紅色突出顯示的行是非常重要的兩行: 返回作為
對象數組的證書鏈。這些證書對象建立對等方的(即客戶機的)標識。數組中的第一個是客戶機本身的證書;最后一個通常是 CA 證書。一旦我們擁有了對等方的證書,我們可以獲取 DN 并將其顯示到
。
是在包
中定義的。
SimpleSSLClient 內幕
我們將研究的第一個客戶機應用程序根本不能做什么。但是,在后面的示例中我們會擴展它來闡述更高級的功能。設置 SimpleSSLClient 的目的是為了方便地添加子類。打算覆蓋下面四個方法:
目前,SimpleSSLClient 僅能理解 和
參數,這允許用戶把客戶機指向服務器。在這第一個基本示例中,
返回(JVM 范圍的)缺省工廠,如下所示:
|
從子類的 方法調用的
方法,負責處理命令行參數,然后從子類獲取
來使用。然后它使用
方法連接到服務器,并且使用
方法在安全通道上開始傳輸數據。
方法相當簡單。在使用
連接到服務器之后,它調用安全套接字上的
。這迫使 JSSE 完成 SSL 握手階段,并因而觸發服務器端上的
。盡管 JSSE 確實會自動啟動握手,但是僅當數據首次通過套接字發送時它才這樣做。因為用戶在鍵盤上輸入消息之前我們不會發送任何數據,但是我們希望服務器立即報告連接,所以我們需要使用
強制進行握手。
方法同樣相當簡單。它的首要任務把輸入源包裝到適當的
,如下所示:
|
我們使用 ,因為它將幫我們把輸入分割成單獨的行。
接下來, 方法把輸出流 ― 在本案例中,由安全套接字提供
― 包裝到適當的
中。服務器希望文本是以 UTF-8 編碼的,因此我們可以讓
使用下列編碼:
|
主循環很簡單;正如您在清單 3 中看到的,它看起來更象 SimpleSSLServer 中 的主循環:
|
基本的 JSSE 服務器和客戶機代碼就只有這些。現在,我們可以繼續擴展 SimpleSSLClient,并且看看一些其它 實現。
自制的 KeyStore
還記得我們是如何運行 SimpleSSLClient 的嗎?命令如下:
|
命令簡直太長了!幸運的是,該示例及接下來的示例將為您演示如何設置一個帶有到 KeyStore 和 TrustStore 的硬編碼路徑的 。除了減少上述命令的長度之外,您將學習的技術將允許您設置多個
對象,每個對象都帶有不同的 KeyStore 和 TrustStore 設置。如果沒有這種配置,JVM 中的每個安全連接必須使用相同的 KeyStore 和 TrustStore。盡管對于較小的應用程序而言這是可以接受的,但是較大的應用程序可能需要連接到多個代表許多不同用戶的對等方。
介紹 CustomKeyStoreClient
對于第一個示例,我們將使用示例應用程序 CustomKeyStoreClient(可在本文的源代碼中找到)來動態定義一個 KeyStore。在研究源代碼之前,讓我們看看正在使用的 CustomKeyStoreClient。對于這個練習,我們將指定 TrustStore 而不是 KeyStore。在 CustomKeyStoreClient 命令行上輸入下列參數,我們將看到出現的結果:
|
假定客戶機連接良好,服務器將報告說提供的證書是有效的。連接成功,因為 已經硬編碼了 KeyStore 的名稱(
)和密碼(
)。如果您為客戶機 KeyStore 選擇了另外的文件名或密碼,那么可以使用新的命令行選項
和
來指定它們。
研究一下 的源代碼,
做的第一件事是調用助手方法
。稍后我們將考慮這是如何工作的;目前只是注明它返回
對象數組,已經利用必需的 KeyStore 文件和密碼對其進行了設置。
|
獲得 數組之后,
執行一些對所有 JSSE 定制通常都很重要的設置工作。為了構造
,應用程序獲取一個
實例,對其進行初始化,然后使用
生成一個
。
當得到 時,我們指定
的協議;我們也可以在這放入特定的 SSL(或 TLS)協議版本,并且強制通信在特定的級別發生。通過指定
,我們允許 JSSE 缺省至它能支持的最高級別。
的第一個參數是要使用的
數組。第二個參數(這里保留為 null)類似于
對象數組,稍后我們將使用它們。通過讓第二個參數為 null,我們告訴 JSSE 使用缺省的 TrustStore,它從
和
系統屬性挑選設置。第三個參數允許我們覆蓋 JSSE 的隨機數生成器(RNG)。RNG 是 SSL 的一個敏感領域,誤用該參數會致使連接變得不安全。我們讓該參數為 null,這樣允許 JSSE 使用缺省的 ― 并且安全的!―
對象。
裝入 KeyStore
接下來,我們將研究 如何裝入和初始化
數組。先從清單 5 中的代碼開始,然后我們將討論正在發生什么。
|
首要工作是獲取 ,但是要這樣做,我們需要知道將使用哪種算法。幸運的是,JSSE 使缺省的
算法可用。可以使用
安全性屬性配置缺省算法。
接下來, 方法裝入 KeyStore 文件。這其中包括從文件建立一個
、獲取一個
實例,以及從
裝入
。除了
,
需要知道流的格式(我們使用缺省的
)和存儲密碼。存儲密碼必須作為字符數組提供。
CustomKeyStoreClient 包導入 |
要說明的一個可能很有用的竅門是, 會獲取任何
。您的應用程序可以從任何地方構建這些流;除了文件,您可以通過網絡、從移動設備獲取流,或者甚至直接生成流。
裝入 之后,我們使用它來初始化以前創建的
。我們需要再次指定一個密碼,這次是單獨的證書密碼。通常,對于 JSSE 而言,KeyStore 中的每個證書都需要具備與 KeyStore 本身相同的密碼。自己構造
可以克服這個限制。
初始化之后,它通常使用
方法獲取相應的
對象的數組。
對于 CustomKeyStoreClient 而言,我們已經研究了如何從任意的位置(本文使用本地文件系統)裝入 KeyStore,以及如何讓證書和 KeyStore 本身使用不同的密碼。稍后我們將研究如何允許 KeyStore 中的每個證書擁有不同的密碼。盡管在本示例中我們著重于客戶機端,但是,我們可以在服務器端使用相同的技術來構建適當的 對象。
CustomTrustStoreClient 包導入 |
使用您自己的 TrustStore
覆蓋 JSSE 的缺省 TrustStore 非常類似于我們剛才用 KeyStore 所做的工作,這并不令人驚奇。我們已經設置了 CustomTrustStoreClient(可在本文的源代碼中找到)來使用硬編碼的 KeyStore 和硬編碼的 TrustStore。開始運行它所需做的全部工作就是輸入命令: 。
CustomTrustStoreClient 希望 KeyStore 將是一個名為 并且密碼為
的文件。希望 TrustStore 將是一個名為
,密碼為
的文件。就象使用 CustomKeyStoreClient 一樣,可以使用
、
、
和
參數覆蓋這些缺省值。
在許多方面與 CustomKeyStoreClient 中相同方法是一樣的。我們甚至從 CustomKeyStoreClient 調用
方法來獲取與前面示例中相同的定制的
對象數組。但是這時
還必須獲取一個定制的
對象數組。在清單 6 中,我們可以看到
如何使用助手方法
獲取定制的
對象:
|
這時,當初始化上下文時,我們覆蓋了 KeyStore 和 TrustStore。但是,我們仍然讓 JSSE 通過傳遞 作為第三個參數來使用它缺省的
。
也非常類似于 CustomKeyStoreClient 的等價物同樣不足為奇,如清單 7 所示:
|
就象以前一樣, 方法首先根據缺省算法實例化一個
。然后將 TrustStore 文件裝入
對象 ― 是的,命名不大恰當 ― 并且初始化
。
跟 KeyStore 等價物不同,請注意,當初始化 時,無需提供密碼。不象私鑰,可信的證書無需利用單獨的密碼進行保護。
到目前為止,我們已經研究了如何動態地覆蓋 KeyStore 和 TrustStore。到這兩個示例都完成時,您應該非常清楚如何設置 和
,并使用這些來“播種”一個
。最后的示例有點煩瑣:我們將構建自己的
實現。
定制 KeyManager 設置:選擇別名
當運行客戶機應用程序的以前版本時,您是否注意到了服務器顯示的是哪個證書 DN?我們故意設置客戶機 KeyStore 以獲得兩個可接受的證書,一個用于 Alice,另一個用于 Bob。在這個案例中,JSSE 將選擇任何一個它認為合適的證書。在我的安裝中,似乎始終選取 Bob 的證書,但是您的 JSSE 的行為可能有所不同。
我們的示例應用程序 ― SelectAliasClient 允許您選擇提供哪個證書。因為我們在 Keystore 中按照別名 或
命名了每個證書,所以要選擇 Alice 的證書可輸入命令:
。
當客戶機連接并且 SSL 握手完成時,服務器將用如下所示進行響應:
|
(或者創建 Alice 的證書時所選的任何值)。類似地,如果選擇 Bob 的證書,請輸入: ,服務器將報告下述信息:
|
定制 KeyManager 實現
為了強制選擇一個特殊的別名,我們將編寫一個 實現,
通常由 JSSE 使用來進行 SSL 通信。我們的實現將包含一個真正的
,并且簡單地通過它傳遞大多數的調用。它攔截的唯一方法是
;我們的實現檢查以便了解所需的別名有效還是無效,如果有效,則返回它。
在 SSL 握手階段, 接口使用許多方法來檢索密鑰,然后使用它來標識對等方。在 參考資料部分可以找到所有方法的參考。下列方法對于本練習很重要:
定制 KeyManager 中的控制流
控制流的工作如下所示:
- JSSE 調用
以發現要使用的別名。
在真實的
上調用
來發現一個有效的別名列表,以便于它能檢查所需的別名是否有效。
- JSSE 通過指定正確的別名調用
的
和
,X509KeyManager 讓調用可以訪問被包裝的 KeyManager。
KeyManager 的
方法實際上需要多次調用
,一次對應一個 JSSE 安裝支持的密鑰類型,如清單 8 所示:
|
需要
的其它五種方法的實現;這些只是調用它們在
上的對應部分。
目前,它仍然使用 ,而不是通常的
。這發生在
中,它首先從其它示例中調用
和
,但是接著將每個從
返回的
封裝進一個
實例,如清單 9 所示:
|
KeyManager 重新打包 |
可以使用本文探討的技術覆蓋 的任何方面。類似地,可以使用它們代替
,更改 JSSE 的機制以決定是否信任從遠程對等方流出的證書。
結束語
本文已經討論了相當多的技巧和技術,因此讓我們以快速回顧來結束本文。現在您應當基本了解如何:
在適當的地方,我還建議擴展這些技術以用于各種應用程序案例。在您自己的實現中封裝 的技巧可用于 JSSE 中的許多其它類,當然,利用
和
可以做更有趣的事情,而不只是裝入硬編碼的文件。
不管您如何選擇實現本文演示的高級 JSSE 定制,任何一個都不是隨便就可以實現的。在調整 SSL 內部機理的時候,請牢記:一個錯誤就會致使您連接變得不安全,這很重要。