2016-03-31 75 views
7

我有一個Apache Web服務器,運行多個TLS虛擬主機與不同的證書和SNI。SNI客戶端神祕使用Java8

我可以使用curl很好地訪問各種虛擬主機(據推測SNI使它工作)。我也可以通過一個基本上只有openConnection()的命令行Java程序來訪問它們。

在我的Tomcat應用程序中,基本相同的客戶端代碼訪問與客戶端相同的Apache服務器,但始終以缺省證書(defaulthost.defaultdomain)結束,而不是在指定的虛擬主機證書它嘗試訪問的URL。 (這產生了一個SunCertPathBuilderException - 基本上它不能驗證證書的證書路徑,當然這是真的,因爲它是非官方的證書,但是不應該使用默認證書。)

就好像SNI已經在我的應用/ Tomcat中停用客戶端一樣。我不知道爲什麼它在我的應用程序和命令行之間表現不一樣;相同的JDK,相同的主機等。

我發現財產jsse.enableSNIExtension,但我確認它在兩種情況下均設置爲true。問題:

  1. 任何想法,甚至野生的,爲什麼這兩個程序行爲不同?

  2. 任何想法我將如何調試?

這是在86_64,JDK 8u77,Tomcat 8.0.32上的Arch Linux。

+0

你可以嘗試捕獲網絡跟蹤,看看在你的tomcat中,應用程序是否發送SNI指示?你的代碼是做什麼https請求? –

+0

SNI不會奇蹟般地添加到SSL連接,但負責建立SSL連接的代碼必須使用它。並非所有的HTTP庫都這樣做。我的猜測是,您在tomcat中使用不同的庫或代碼來訪問URL,而不是在您的小測試程序中,以便在一種情況下使用SNI,而在另一種情況下不使用。 –

+1

您可以在命令行中使用'-Djavax.net.debug = ssl'啓用Java的SSL消息調試_e.g._,並查看Java客戶端正在發送/接收的內容...... – Castaglia

回答

5

經過幾個小時的JDK調試之後,這是不幸的結果。這工作:

URLConnection c = new URL("https://example.com/").openConnection(); 
InputStream i = c.getInputStream(); 
... 

這種失敗:

URLConnection c = new URL("https://example.com/").openConnection(); 
((HttpsURLConnection)c).setHostnameVerifier(new HostnameVerifier() { 
     public boolean verify(String s, SSLSession sess) { 
      return false; // or true, won't matter for this 
     } 
}); 
InputStream i = c.getInputStream(); // Exception thrown here 
... 

添加setHostnameVerifier調用有禁用SNI的結果,雖然定製HostnameVerifier永遠不會調用

的罪魁禍首似乎是這樣的代碼sun.net.www.protocol.https.HttpsClient

  if (hv != null) { 
       String canonicalName = hv.getClass().getCanonicalName(); 
       if (canonicalName != null && 
       canonicalName.equalsIgnoreCase(defaultHVCanonicalName)) { 
        isDefaultHostnameVerifier = true; 
       } 
      } else { 
       // Unlikely to happen! As the behavior is the same as the 
       // default hostname verifier, so we prefer to let the 
       // SSLSocket do the spoof checks. 
       isDefaultHostnameVerifier = true; 
      } 
      if (isDefaultHostnameVerifier) { 
       // If the HNV is the default from HttpsURLConnection, we 
       // will do the spoof checks in SSLSocket. 
       SSLParameters paramaters = s.getSSLParameters(); 
       paramaters.setEndpointIdentificationAlgorithm("HTTPS"); 
       s.setSSLParameters(paramaters); 

       needToCheckSpoofing = false; 
      } 

其中一些聰慧的頭腦檢查配置HostnameVerifier的類是否是默認的JDK類(被調用時,就返回false,像我上面的代碼),並基於此,更改SSL連接的參數 - 作爲副作用,關閉SNI。

如何檢查一個類的名稱並制定一些邏輯取決於它是一個好主意逃脫我。 (「媽媽!我們不需要虛擬方法,我們可以檢查課程名稱和派送信息!」)但更糟的是,SNI與HostnameVerifier首先有什麼關係?

也許解決方法是使用具有相同名稱但大小寫不同的自定義HostnameVerifier,因爲同樣明亮的頭腦也決定做不區分大小寫的名稱比較。

'nuff說。

+2

您可以在'HttpsURLConnection'上設置自己的'SSLSocketFactory'子類,這樣您的工廠_before_返回'SSLSocket',就可以對其執行'socket.getSSLParameters()。setEndpointIdentificationAlgorithm(「HTTPS」)'上面的代碼? – Castaglia

+0

你似乎是正確的。偉大的取證! –

7

這個答案來得晚,但我們只是碰到了問題(我不敢相信它,它似乎是一個非常大的錯誤)。

所有的說法似乎都是真的,但它並不是默認的HostnameVerifier罪魁禍首,而是故障排除者。當HttpsClient做afterConnect首先嚐試建立setHost(僅當插座SSLSocketImpl):

SSLSocketFactory factory = sslSocketFactory; 
try { 
    if (!(serverSocket instanceof SSLSocket)) { 
     s = (SSLSocket)factory.createSocket(serverSocket, 
              host, port, true); 
    } else { 
     s = (SSLSocket)serverSocket; 
     if (s instanceof SSLSocketImpl) { 
      ((SSLSocketImpl)s).setHost(host); 
     } 
    } 
} catch (IOException ex) { 
    // If we fail to connect through the tunnel, try it 
    // locally, as a last resort. If this doesn't work, 
    // throw the original exception. 
    try { 
     s = (SSLSocket)factory.createSocket(host, port); 
    } catch (IOException ignored) { 
     throw ex; 
    } 
} 

如果您使用自定義SSLSocketFactory的無覆蓋的createSocket()(不帶參數的方法),以及參數化的的createSocket被使用,而且所有按預期工作(使用客戶端sni擴展)。但是,當第二個方法它的使用(儘量setHost EN SSLSocketImpl)執行的代碼是:

// ONLY used by HttpsClient to setup the URI specified hostname 
// 
// Please NOTE that this method MUST be called before calling to 
// SSLSocket.setSSLParameters(). Otherwise, the {@code host} parameter 
// may override SNIHostName in the customized server name indication. 
synchronized public void setHost(String host) { 
    this.host = host; 
    this.serverNames = 
     Utilities.addToSNIServerNameList(this.serverNames, this.host); 
} 

的評論說所有。您需要在客戶端握手之前調用setSSLParameters。如果使用默認的HostnameVerifier,HttpsClient將調用setSSLParameters。但是,相反,沒有setSSLParameters執行。 Oracle的修復應該非常簡單:

SSLParameters paramaters = s.getSSLParameters(); 
if (isDefaultHostnameVerifier) { 
    // If the HNV is the default from HttpsURLConnection, we 
    // will do the spoof checks in SSLSocket. 
    paramaters.setEndpointIdentificationAlgorithm("HTTPS"); 

    needToCheckSpoofing = false; 
} 
s.setSSLParameters(paramaters); 

Java 9在SNI中按預期工作。但是他們(甲骨文)似乎並不想解決這個問題:

+0

謝謝,非常有幫助。 – Cheeso

+1

好消息,終於在java 1.8.0_141上修正了SNI:http://www.oracle.com/technetwork/java/javase/8u141-relnotes-3720385.html – Dawuid

0

不能確定這是否是同樣的問題。我的JVM配置爲信任專用簽名證書以連接到SNI服務器。該證書工作了一段時間(幾個小時),然後開始失敗,出現SunCertPathBuilderException: unable to find valid certification path to requested target錯誤。一旦錯誤開始,我的Java應用程序就無法再連接到HTTPS服務器了,除非我使用完全相同的配置重新啓動JVM - 然後所有事情都會重新開始。這一直重複着。

這是上面描述的同樣的問題嗎?

謝謝