爲高級 JSSE 開發人員定製 SSL(二)

SimpleSSLClient 內幕

我們將研究的第一個客戶機應用程序根本不能做什麼。但是,在後面的示例中我們會擴展它來闡述更高級的功能。設置 SimpleSSLClient 的目的是爲了方便地添加子類。打算覆蓋下面四個方法:

  • main() 當然是在從命令行運行類時被調用。對於每個子類, main() 必須構造一個合適類的對象,並調用對象上的 runClient()close() 方法。這些方法是在超類 ― SimpleSSLClient 上提供的,並且不打算被覆蓋。
  • handleCommandLineOption()displayUsage() 允許每個子類在命令行上添加選項,而無需更新父類。它們都從 runClient() 方法調用。
  • getSSLSocketFactory() 是一個有趣的方法。JSSE 安全套接字始終是從 SSLSocketFactory 對象構造的。通過構造一個定製的套接字工廠,我們可以定製 JSSE 的行爲。爲了將來練習的目的,每個 SimpleSSLClient 子類都實現該方法,並相應定製 SSLSocketFactory

目前,SimpleSSLClient 僅能理解 -host-port 參數,這允許用戶把客戶機指向服務器。在這第一個基本示例中, getSSLSocketFactory 返回(JVM 範圍的)缺省工廠,如下所示:

protected SSLSocketFactory getSSLSocketFactory()
  throws IOException, GeneralSecurityException
{
  return (SSLSocketFactory)SSLSocketFactory.getDefault();
}

從子類的 main() 方法調用的 runClient() 方法,負責處理命令行參數,然後從子類獲取 SSLSocketFactory 來使用。然後它使用 connect() 方法連接到服務器,並且使用 transmit() 方法在安全通道上開始傳輸數據。

connect() 方法相當簡單。在使用 SSLSocketFactory 連接到服務器之後,它調用安全套接字上的 startHandshake 。這迫使 JSSE 完成 SSL 握手階段,並因而觸發服務器端上的 HandshakeCompletedListener 。儘管 JSSE 確實會自動啓動握手,但是僅當數據首次通過套接字發送時它才這樣做。因爲用戶在鍵盤上輸入消息之前我們不會發送任何數據,但是我們希望服務器立即報告連接,所以我們需要使用 startHandshake 強制進行握手。

transmit() 方法同樣相當簡單。它的首要任務把輸入源包裝到適當的 Reader ,如下所示:

 
BufferedReader reader=new BufferedReader(
  new InputStreamReader(in));

我們使用 BufferedReader ,因爲它將幫我們把輸入分割成單獨的行。

接下來, transmit() 方法把輸出流 ― 在本案例中,由安全套接字提供 OutputStream ― 包裝到適當的 Writer 中。服務器希望文本是以 UTF-8 編碼的,因此我們可以讓 OutputStreamWriter 使用下列編碼:

 
writer=new OutputStreamWriter(socket.getOutputStream(), "UTF-8");

主循環很簡單;正如您在清單 3 中看到的,它看起來更象 SimpleSSLServer 中 InputDisplayer 的主循環:

清單 3. SimpleSSLClient 主循環
boolean done=false;
while (!done) {
  String line=reader.readLine();
  if (line!=null) {
    writer.write(line);
    writer.write('/n');
    writer.flush();
  }
  else done=true;
}

基本的 JSSE 服務器和客戶機代碼就只有這些。現在,我們可以繼續擴展 SimpleSSLClient,並且看看一些其它 getSSLSocketFactory 實現。

自制的 KeyStore

還記得我們是如何運行 SimpleSSLClient 的嗎?命令如下:

 
java -Djavax.net.ssl.keyStore=clientKeys
   -Djavax.net.ssl.keyStorePassword=password 
   -Djavax.net.ssl.trustStore=clientTrust 
   -Djavax.net.ssl.trustStorePassword=password SimpleSSLClient

命令簡直太長了!幸運的是,該示例及接下來的示例將爲您演示如何設置一個帶有到 KeyStore 和 TrustStore 的硬編碼路徑的 SSLSocketFactory 。除了減少上述命令的長度之外,您將學習的技術將允許您設置多個 SSLSocketFactory 對象,每個對象都帶有不同的 KeyStore 和 TrustStore 設置。如果沒有這種配置,JVM 中的每個安全連接必須使用相同的 KeyStore 和 TrustStore。儘管對於較小的應用程序而言這是可以接受的,但是較大的應用程序可能需要連接到多個代表許多不同用戶的對等方。

介紹 CustomKeyStoreClient

對於第一個示例,我們將使用示例應用程序 CustomKeyStoreClient(可在本文的源代碼中找到)來動態定義一個 KeyStore。在研究源代碼之前,讓我們看看正在使用的 CustomKeyStoreClient。對於這個練習,我們將指定 TrustStore 而不是 KeyStore。在 CustomKeyStoreClient 命令行上輸入下列參數,我們將看到出現的結果:

 
java -Djavax.net.ssl.trustStore=clientTrust
   -Djavax.net.ssl.trustStorePassword=password CustomKeyStoreClient

假定客戶機連接良好,服務器將報告說提供的證書是有效的。連接成功,因爲 CustomKeyStoreClient.java 已經硬編碼了 KeyStore 的名稱( clientKeys )和密碼( password )。如果您爲客戶機 KeyStore 選擇了另外的文件名或密碼,那麼可以使用新的命令行選項 -ks-kspass 來指定它們。

研究一下 CustomKeystoreClient.java 的源代碼, getSSLSocketFactory 做的第一件事是調用助手方法 getKeyManagers() 。稍後我們將考慮這是如何工作的;目前只是註明它返回 KeyManager 對象數組,已經利用必需的 KeyStore 文件和密碼對其進行了設置。

清單 4. CustomKeyStoreClient.getSSLSocketFactory
protected SSLSocketFactory getSSLSocketFactory()
  throws IOException, GeneralSecurityException
{
  // Call getKeyManagers to get suitable key managers
  KeyManager[] kms=getKeyManagers();
  // Now construct a SSLContext using these KeyManagers. We
  // specify a null TrustManager and SecureRandom, indicating that the
  // defaults should be used.
  SSLContext context=SSLContext.getInstance("SSL");
  context.init(kms, null, null);
  // Finally, we get a SocketFactory, and pass it to SimpleSSLClient.
  SSLSocketFactory ssf=context.getSocketFactory();
  return ssf;
}

獲得 KeyManager 數組之後, getSSLSocketFactory 執行一些對所有 JSSE 定製通常都很重要的設置工作。爲了構造 SSLSocketFactory ,應用程序獲取一個 SSLContext 實例,對其進行初始化,然後使用 SSLContext 生成一個 SSLSocketFactory

當得到 SSLContext 時,我們指定 "SSL" 的協議;我們也可以在這放入特定的 SSL(或 TLS)協議版本,並且強制通信在特定的級別發生。通過指定 "SSL" ,我們允許 JSSE 缺省至它能支持的最高級別。

SSLContext.init 的第一個參數是要使用的 KeyManager 數組。第二個參數(這裏保留爲 null)類似於 TrustManager 對象數組,稍後我們將使用它們。通過讓第二個參數爲 null,我們告訴 JSSE 使用缺省的 TrustStore,它從 javax.net.ssl.trustStorejavax.net.ssl.trustStorePassword 系統屬性挑選設置。第三個參數允許我們覆蓋 JSSE 的隨機數生成器(RNG)。RNG 是 SSL 的一個敏感領域,誤用該參數會致使連接變得不安全。我們讓該參數爲 null,這樣允許 JSSE 使用缺省的 ― 並且安全的!― SecureRandom 對象。

裝入 KeyStore

接下來,我們將研究 getKeyManagers 如何裝入和初始化 KeyManagers 數組。先從清單 5 中的代碼開始,然後我們將討論正在發生什麼。

清單 5. 裝入和初始化 KeyManagers
protected KeyManager[] getKeyManagers()
  throws IOException, GeneralSecurityException
{
  // First, get the default KeyManagerFactory.
  String alg=KeyManagerFactory.getDefaultAlgorithm();
  KeyManagerFactory kmFact=KeyManagerFactory.getInstance(alg);
    
  // Next, set up the KeyStore to use. We need to load the file into
  // a KeyStore instance.
  FileInputStream fis=new FileInputStream(keyStore);
  KeyStore ks=KeyStore.getInstance("jks");
  ks.load(fis, keyStorePassword.toCharArray());
  fis.close();
  // Now we initialize the TrustManagerFactory with this KeyStore
  kmFact.init(ks, keyStorePassword.toCharArray());
  // And now get the TrustManagers
  KeyManager[] kms=kmFact.getKeyManagers();
  return kms;
}

首要工作是獲取 KeyManagerFactory ,但是要這樣做,我們需要知道將使用哪種算法。幸運的是,JSSE 使缺省的 KeyManagerFactory 算法可用。可以使用 ssl.KeyManagerFactory.algorithm 安全性屬性配置缺省算法。

接下來, getKeyManagers() 方法裝入 KeyStore 文件。這其中包括從文件建立一個 InputStream 、獲取一個 KeyStore 實例,以及從 InputStream 裝入 KeyStore 。除了 InputStreamKeyStore 需要知道流的格式(我們使用缺省的 "jks" )和存儲密碼。存儲密碼必須作爲字符數組提供。

CustomKeyStoreClient 包導入

爲了訪問 KeyStore 類,我們必須導入 javax.net.ssljava.security.cert 。其它類(如 SSLContextKeyManagerFactory )從 J2SE 1.4 起,是 javax.net.ssl 的成員。在 J2SE 1.2 或 1.3 中,這些類的位置不是標準的;例如,Sun JSSE 實現把它們放在 com.sun.net.ssl 中。

要說明的一個可能很有用的竅門是, KeyStore.load 會獲取任何 InputStream 。您的應用程序可以從任何地方構建這些流;除了文件,您可以通過網絡、從移動設備獲取流,或者甚至直接生成流。

裝入 KeyStore 之後,我們使用它來初始化以前創建的 KeyManagerFactory 。我們需要再次指定一個密碼,這次是單獨的證書密碼。通常,對於 JSSE 而言,KeyStore 中的每個證書都需要具備與 KeyStore 本身相同的密碼。自己構造 KeyManagerFactory 可以克服這個限制。

KeyManagerFactory 初始化之後,它通常使用 getKeyManagers() 方法獲取相應的 KeyManager 對象的數組。

對於 CustomKeyStoreClient 而言,我們已經研究瞭如何從任意的位置(本文使用本地文件系統)裝入 KeyStore,以及如何讓證書和 KeyStore 本身使用不同的密碼。稍後我們將研究如何允許 KeyStore 中的每個證書擁有不同的密碼。儘管在本示例中我們着重於客戶機端,但是,我們可以在服務器端使用相同的技術來構建適當的 SSLServerSocketFactory 對象。

CustomTrustStoreClient 包導入

同樣,本示例中使用的類會出現在不同 JSSE 供應商的不同包中。在 J2SE 1.4 中, TrustManagerFactory 位於 javax.net.ssl 中;在 J2SE 1.2 或 1.3 中,通常它位於 com.sun.net.ssl 中。

使用您自己的 TrustStore

覆蓋 JSSE 的缺省 TrustStore 非常類似於我們剛纔用 KeyStore 所做的工作,這並不令人驚奇。我們已經設置了 CustomTrustStoreClient(可在本文的源代碼中找到)來使用硬編碼的 KeyStore 和硬編碼的 TrustStore。開始運行它所需做的全部工作就是輸入命令: java CustomTrustStoreClient

CustomTrustStoreClient 希望 KeyStore 將是一個名爲 clientKeys 並且密碼爲 password 的文件。希望 TrustStore 將是一個名爲 clientTrust ,密碼爲 password 的文件。就象使用 CustomKeyStoreClient 一樣,可以使用 -ks-kspass-ts-tspass 參數覆蓋這些缺省值。

getSSLSocketFactory() 在許多方面與 CustomKeyStoreClient 中相同方法是一樣的。我們甚至從 CustomKeyStoreClient 調用 getKeyManagers() 方法來獲取與前面示例中相同的定製的 KeyManager 對象數組。但是這時 getSSLSocketFactory 還必須獲取一個定製的 TrustManager 對象數組。在清單 6 中,我們可以看到 getSSLSocketFactory 如何使用助手方法 getTrustManagers() 獲取定製的 TrustManager 對象:

清單 6. getSSLSocketFactory 如何使用 TrustManagers
protected SSLSocketFactory getSSLSocketFactory()
  throws IOException, GeneralSecurityException
{
  // Call getTrustManagers to get suitable trust managers
  TrustManager[] tms=getTrustManagers();
    
  // Call getKeyManagers (from CustomKeyStoreClient) to get suitable
  // key managers
  KeyManager[] kms=getKeyManagers();
  // Next construct and initialize a SSLContext with the KeyStore and
  // the TrustStore. We use the default SecureRandom.
  SSLContext context=SSLContext.getInstance("SSL");
  context.init(kms, tms, null);
  // Finally, we get a SocketFactory, and pass it to SimpleSSLClient.
  SSLSocketFactory ssf=context.getSocketFactory();
  return ssf;
}

這時,當初始化上下文時,我們覆蓋了 KeyStore 和 TrustStore。但是,我們仍然讓 JSSE 通過傳遞 null 作爲第三個參數來使用它缺省的 SecureRandom

getTrustManagers 也非常類似於 CustomKeyStoreClient 的等價物同樣不足爲奇,如清單 7 所示:

清單 7. 裝入和初始化 TrustManagers
protected TrustManager[] getTrustManagers()
  throws IOException, GeneralSecurityException
{
  // First, get the default TrustManagerFactory.
  String alg=TrustManagerFactory.getDefaultAlgorithm();
  TrustManagerFactory tmFact=TrustManagerFactory.getInstance(alg);
    
  // Next, set up the TrustStore to use. We need to load the file into
  // a KeyStore instance.
  FileInputStream fis=new FileInputStream(trustStore);
  KeyStore ks=KeyStore.getInstance("jks");
  ks.load(fis, trustStorePassword.toCharArray());
  fis.close();
  // Now we initialize the TrustManagerFactory with this KeyStore
  tmFact.init(ks);
  // And now get the TrustManagers
  TrustManager[] tms=tmFact.getTrustManagers();
  return tms;
}

就象以前一樣, getTrustManagers() 方法首先根據缺省算法實例化一個 TrustManagerFactory 。然後將 TrustStore 文件裝入 KeyStore 對象 ― 是的,命名不大恰當 ― 並且初始化 TrustManagerFactory

跟 KeyStore 等價物不同,請注意,當初始化 TrustManagerFactory 時,無需提供密碼。不象私鑰,可信的證書無需利用單獨的密碼進行保護。

到目前爲止,我們已經研究瞭如何動態地覆蓋 KeyStore 和 TrustStore。到這兩個示例都完成時,您應該非常清楚如何設置 KeyManagerFactoryTrustManagerFactory ,並使用這些來“播種”一個 SSLContext 。最後的示例有點煩瑣:我們將構建自己的 KeyManager 實現。

定製 KeyManager 設置:選擇別名

當運行客戶機應用程序的以前版本時,您是否注意到了服務器顯示的是哪個證書 DN?我們故意設置客戶機 KeyStore 以獲得兩個可接受的證書,一個用於 Alice,另一個用於 Bob。在這個案例中,JSSE 將選擇任何一個它認爲合適的證書。在我的安裝中,似乎始終選取 Bob 的證書,但是您的 JSSE 的行爲可能有所不同。

我們的示例應用程序 ― SelectAliasClient 允許您選擇提供哪個證書。因爲我們在 Keystore 中按照別名 alicebob 命名了每個證書,所以要選擇 Alice 的證書可輸入命令: java SelectAliasClient -alias alice

當客戶機連接並且 SSL 握手完成時,服務器將用如下所示進行響應:

1: New connection request
1: Request from CN=Alice, OU=developerWorks, O=IBM, L=Winchester, 
      ST=Hampshire, C=UK

(或者創建 Alice 的證書時所選的任何值)。類似地,如果選擇 Bob 的證書,請輸入: java SelectAliasClient -alias bob ,服務器將報告下述信息:

2: New connection request
2: Request from CN=Bob, OU=developerWorks, O=IBM, L=Winchester, 
      ST=Hampshire, C=UK

定製 KeyManager 實現

爲了強制選擇一個特殊的別名,我們將編寫一個 X509KeyManager 實現, KeyManager 通常由 JSSE 使用來進行 SSL 通信。我們的實現將包含一個真正的 X509KeyManager ,並且簡單地通過它傳遞大多數的調用。它攔截的唯一方法是 chooseClientAlias() ;我們的實現檢查以便了解所需的別名有效還是無效,如果有效,則返回它。

在 SSL 握手階段, X509KeyManager 接口使用許多方法來檢索密鑰,然後使用它來標識對等方。在 參考資料部分可以找到所有方法的參考。下列方法對於本練習很重要:

  • getClientAliases()getServerAliases() 分別爲使用 SSLSocketSSLServerSocket 提供了有效的別名數組。
  • chooseClientAlias()chooseServerAlias() 返回單個有效的別名。
  • getCertificateChain()getPrivateKey() 每個都把別名作爲參數,並返回有關已標識證書的信息。

定製 KeyManager 中的控制流

控制流的工作如下所示:

  1. JSSE 調用 chooseClientAlias 以發現要使用的別名。
  2. chooseClientAlias 在真實的 X509KeyManager 上調用 getClientAliases 來發現一個有效的別名列表,以便於它能檢查所需的別名是否有效。
  3. JSSE 通過指定正確的別名調用 X509KeyManagergetCertificateChaingetPrivateKey ,X509KeyManager 讓調用可以訪問被包裝的 KeyManager。

KeyManager AliasForcingKeyManager()chooseClientAlias() 方法實際上需要多次調用 getClientAliases() ,一次對應一個 JSSE 安裝支持的密鑰類型,如清單 8 所示:

清單 8. 強制別名的選擇
public String chooseClientAlias(String[] keyType, Principal[] issuers,
                                Socket socket)
{
  // For each keyType, call getClientAliases on the base KeyManager
  // to find valid aliases. If our requested alias is found, select it
  // for return.
  boolean aliasFound=false;
  for (int i=0; i<keyType.length && !aliasFound; i++) {
    String[] validAliases=baseKM.getClientAliases(keyType[i], issuers);
    if (validAliases!=null) {
      for (int j=0; j<validAliases.length && !aliasFound; j++) {
        if (validAliases[j].equals(alias)) aliasFound=true;
      }
    }
  }
  if (aliasFound) return alias;
  else return null;
}

AliasForcingKeyManager 需要 X509KeyManager 的其它五種方法的實現;這些只是調用它們在 baseKM 上的對應部分。

目前,它仍然使用 AliasForcingKeyManager ,而不是通常的 KeyManager 。這發生在 getSSLSocketFactory 中,它首先從其它示例中調用 getKeyManagersgetTrustManagers ,但是接着將每個從 getKeyManagers 返回的 KeyManager 封裝進一個 AliasForcingKeyManager 實例,如清單 9 所示:

清單 9. 封裝 X509KeyManagers
protected SSLSocketFactory getSSLSocketFactory() 
  throws IOException, GeneralSecurityException
{
  // Call the superclasses to get suitable trust and key managers
  KeyManager[] kms=getKeyManagers();
  TrustManager[] tms=getTrustManagers();
  // If the alias has been specified, wrap recognized KeyManagers
  // in AliasForcingKeyManager instances.
  if (alias!=null) {
    for (int i=0; i<kms.length; i++) {
      // We can only deal with instances of X509KeyManager
      if (kms[i] instanceof X509KeyManager)
        kms[i]=new AliasForcingKeyManager((X509KeyManager)kms[i], alias);
    }
  }
  // Now construct a SSLContext using these (possibly wrapped)
  // KeyManagers, and the TrustManagers. We still use a null
  // SecureRandom, indicating that the defaults should be used.
  SSLContext context=SSLContext.getInstance("SSL");
  context.init(kms, tms, null);
  // Finally, we get a SocketFactory, and pass it to SimpleSSLClient.
  SSLSocketFactory ssf=context.getSocketFactory();
  return ssf;
}
KeyManager 重新打包

J2SE 1.2 和 1.3 中的 KeyManagerX509KeyManager 在 J2SE 1.4 中都從供應商特定的包中移到了 javax.net.ssl 中;當接口移動時, X509KeyManager 方法說明會略微發生一點變化。

可以使用本文探討的技術覆蓋 KeyManager 的任何方面。類似地,可以使用它們代替 TrustManager ,更改 JSSE 的機制以決定是否信任從遠程對等方流出的證書。

 

本文已經討論了相當多的技巧和技術,因此讓我們以快速回顧來結束本文。現在您應當基本瞭解如何:

  • 使用 HandshakeCompletedListener 收集有關連接的信息
  • SSLContext 獲取 SSLSocketFactory
  • 使用定製、動態的 TrustStore 或 KeyStore
  • 放寬 KeyStore 密碼與單個證書密碼必須匹配的 JSSE 限制
  • 使用您自己的 KeyManager 強制選擇標識證書

在適當的地方,我還建議擴展這些技術以用於各種應用程序案例。在您自己的實現中封裝 X509KeyManager 的技巧可用於 JSSE 中的許多其它類,當然,利用 TrustStoreKeyStore 可以做更有趣的事情,而不只是裝入硬編碼的文件。

不管您如何選擇實現本文演示的高級 JSSE 定製,任何一個都不是隨便就可以實現的。在調整 SSL 內部機理的時候,請牢記:一個錯誤就會致使您連接變得不安全,這很重要。

本文源碼下載:本文源碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章