天天看點

Java Secure Socket Extension (JSSE) -類和接口篇SocketFactory and ServerSocketFactory ClassesSSLSocket and SSLServerSocket ClassesSSLEngine ClassSSLSession and ExtendedSSLSessionHttpsURLConnection ClassSupport Classes and Interfaces

Java Secure Socket Extension (JSSE) -類和接口篇

  • SocketFactory and ServerSocketFactory Classes
    • Obtaining an SSLSocketFactory
  • SSLSocket and SSLServerSocket Classes
    • Obtaining an SSLSocket
    • Cipher Suite Choice and Remote Entity Verification
  • SSLEngine Class
    • Understanding SSLEngine Operation Statuses
    • SSLEngine for TLS Protocols
      • Creating an SSLEngine Object
      • Generating and Processing TLS Data
    • Dealing With Blocking Tasks
    • Shutting Down a TLS/DTLS Connection
  • SSLSession and ExtendedSSLSession
  • HttpsURLConnection Class
    • Setting the Assigned SSLSocketFactory
    • Setting the Assigned HostnameVerifier
  • Support Classes and Interfaces
    • SSLContext Class
      • Obtaining and Initializing the SSLContext Class
      • Creating an SSLContext Object
    • TrustManager Interface
    • TrustManagerFactory Class
      • Creating a TrustManagerFactory
      • PKIX TrustManager Support
    • X509TrustManager Interface
      • Creating an X509TrustManager
      • Creating Your Own X509TrustManager
      • Updating the Keystore Dynamically
    • X509ExtendedTrustManager Class
      • Creating an X509ExtendedTrustManager
      • Creating Your Own X509ExtendedTrustManager
    • KeyManager Interface
    • KeyManagerFactory Class
    • X509KeyManager Interface
      • Relationship Between a TrustManager and a KeyManager

介紹篇

為了 通信安全,兩側的連接配接必須是SSL-enabled。JSSE API裡,連接配接的endpoint類是SSLSocket和SSLEngine。

Java Secure Socket Extension (JSSE) -類和接口篇SocketFactory and ServerSocketFactory ClassesSSLSocket and SSLServerSocket ClassesSSLEngine ClassSSLSession and ExtendedSSLSessionHttpsURLConnection ClassSupport Classes and Interfaces

SSLSocketFactory或者SSLServerSocket生成的SSLSocket,接收入站連接配接。

SSLServerSocket由SSLServerSocketFactory增加。SSLSocketFactory和SSLServerSocketFactory對象由SSLContext生成。

SSLEngine由SSLContext直接生成,依靠這些處理全部I/O。

注意:

使用原生的SSLSocket和SSLEngine類的時候,在發送資料之前,你總是應該檢查對方的證書。從JDK 7開始,endpoint識别/驗證過程可以在SSL/TLS握手期間處理。見方法SSLParameters.setEndpointIdentificationAlgorithm。

例如,URL裡的主機名應該和對方證書裡的主機名比對。如果不校驗主機名,程式可以進行URL欺騙攻擊。

SocketFactory and ServerSocketFactory Classes

抽象的javax.net.SocketFactory類用來增加sockets。它的子類是增加特定sockets的工廠,這樣提供了一個添加公共socket級功能的通用架構。

抽象的javax.net.ServerSocketFactory類類似SocketFactory類,但是,隻能用來增加伺服器sockets。

Socket工廠是捕獲與正在構造的sockets相關的各種政策簡單的辦法,這樣生成sockets,不需要特定的配置代碼:

  • 通過工廠和sockets的多态性,不同類型的sockets可以使用相同的程式代碼,僅僅傳給不同類型的工廠
  • 工廠使用構造socket時的參數,定制自己。比如,工廠傳回的sockets可以有不同的網絡逾時或者其他安全參數
  • 程式傳回的sockets可以是java.net.Socket(或者javax.net.ssl.SSLSocket)的子類,這樣可以直接暴露新的API,比如壓縮、安全、打标簽、統計或者防火牆隧道。

javax.net.ssl.SSLSocketFactory是增加安全sockets的工廠,是抽象類javax.net.SocketFactory的子類。安全socket工廠封裝了增加和初始化配置安全sockets的細節。包括認證keys,對端證書驕傲眼、确定密碼套件等。

javax.net.ssl.SSLServerSocketFactory類類似SSLSocketFactory類,但是用來增加伺服器sockets。

Obtaining an SSLSocketFactory

使用下列辦法獲得SSLSocketFactory:

  • 使用SSLSocketFactory.getDefault()靜态方法獲得預設的工廠
  • 把工廠作為一個API參數。這樣做,生成sockets的代碼,不用關心配置sockets的細節,可以包括一個帶SSLSocketFactory參數的方法。可以由用戶端在增加sockets的時候調用(比如 javax.net.ssl.HttpsURLConnection)。
  • 使用特定的配置行為構造一個新工廠

預設工廠支援伺服器認證的典型配置,是以,預設工廠增加的sockets不會比普通的TCP socket洩漏更多的用戶端資訊。

增加和使用sockets的很多類不需要知道socket生成行為的細節。

你可以或者實作自己的socket工廠子類,或者使用另一個類作為socket工廠的工廠來增加新的socket工廠執行個體。比如SSLContext就是這樣一個類。

SSLSocket and SSLServerSocket Classes

javax.net.ssl.SSLSocket類是标準的java.net.Socket類的子類。它支援全部的标準socket的方法,增加了安全sockets的特定方法。該類的執行個體封裝了SSLContext。它有為socket執行個體增加安全的sessions的API,但是trust和key管理沒有被直接暴露。

javax.net.ssl.SSLServerSocket用來增加伺服器sockets。

為了防止對方欺騙,應該總是驗證SSLSocket的證書。

注意:由于SSL和TLS協定的複雜性,很難預測從連接配接接收的資訊是握手資訊還是程式資料,資料的不同可能影響目前的連接配接狀态(甚至導緻過程的結束)。Oracle的JSSE實作裡,通過SSLSocket.getInputStream()獲得的對象的available()方法傳回從SSL連接配接解密的還沒有讀取的程式資料的位元組數。

Obtaining an SSLSocket

有兩種辦法獲得SSLSocket執行個體:

  • SSLSocketFactory執行個體的幾個createSocket方法
  • SSLServerSocket類的accept方法

Cipher Suite Choice and Remote Entity Verification

SSL/TLS協定定義一系列步驟確定受保護的連接配接。cipher suite的選擇直接影響連接配接的安全類型。例如,如果選擇了匿名的cipher suite,程式就沒辦法校驗對方的身份。如果選擇了不加密的suite,就不能保護資料的隐私。另外,SSL/TLS協定沒規定接收的證書必須比對對方可能期望發送的。如果連接配接不知何故重定向到一個流氓端,而流氓的證書基于目前的trust資料被接受了,會認為連接配接是有效的。

當使用原生的SSLSocket和SSLEngine類的時候,你應該總是在發送資料之前檢查對方的證書。SSLSocket和SSLEngine類不自動校驗URL裡的主機名是否和對方證書裡的主機名比對。如果不校驗主機名,程式可能被URL欺騙所利用。從JDK 7開始,endpoint識别/驗證過程能在SSL/TLS期間被處理。

比如HTTPS協定不需要主機名校驗。從JDK 7開始,預設地,HTTPS endpoint識别在握手期間由HttpsURLConnection強制執行。見SSLParameters.getEndpointIdentificationAlgorithm方法。另外,程式可以使用HostnameVerifier接口覆寫預設的HTTPS主機名規則。見HostnameVerifier接口和HttpsURLConnection類。

SSLEngine Class

TLS/DTLS越來越受歡迎。用于很多計算平台和裝置。這要求TLS/DTLS使用不同的I/O和線程模型,以滿足性能、擴充性、跟蹤(footprint)和其他需求。這要求TLS/DTLS既能使用阻塞channels,也能使用非阻塞channels、支援異步、任意的輸入/輸出流和byte buffers。要求有高的可擴充性,能滿足性能需求,能管理數以千計的網絡連接配接。在Java SE中,使用SSLEngine類抽象I/O傳輸機制,允許程式以與傳輸無關的方式使用TLS/DTLS協定,開發者可以自由選擇最符合需要的傳輸和計算模型。這些抽象使程式既可以使用非阻塞I/O channels也可以使用其他I/O模型,還包括不同的線程模型。因為這些靈活性,開發者必須管理I/O和線程,以及對TLS / DTLS協定的一些了解。初學者應該使用SSLSocket。

使用其他API,比如Java Generic Security Services(Java GSS-API)和Java Simple Authentication Security Layer (Java SASL)的開發者要注意相似之處在于應用程式還負責傳輸資料。

核心類是javax.net.ssl.SSLEngine。它包含TLS/DTLS狀态機,由SSLEngine的使用者操作入站出站byte buffers。

下圖表明了程式的資料流向。

Java Secure Socket Extension (JSSE) -類和接口篇SocketFactory and ServerSocketFactory ClassesSSLSocket and SSLServerSocket ClassesSSLEngine ClassSSLSession and ExtendedSSLSessionHttpsURLConnection ClassSupport Classes and Interfaces

程式,顯示在左邊,提供資料,放入buffer,傳給SSLEngine。SSLEngine對象處理buffer裡的資料(包括握手資料),産生TLS/DTLS編碼的資料,放到程式的網絡buffer。程式然後使用恰當的傳輸(顯示在右邊),把内容發送給對端的網絡buffer。從對端接收到TLS/DTLS編碼的資料,程式把資料放到網絡buffer,傳給SSLEngine。SSLEngine對象處理網絡buffer的内容,生成握手資料或者程式資料。

SSLEngine的執行個體可以有下列狀态:

  • Creation:SSLEngine已經生成,且已經初始化完成,但是還沒有使用。期間,程式可以配置SSLEngine(啟用的密碼套件,SSLEngine工作在用戶端方式還是伺服器方式等)一旦開始握手,任何新的設定(除了用戶端/伺服器方式)将在下次握手時生效
  • Initial handshaking:初始握手是一個過程,雙方交換通信參數,直到SSLSession建立。此階段,不能發送程式資料
  • Application data:确定了通信參數,握手完成以後,可以傳輸程式資料了。出站消息是加密的,收到了完整性保護,入站資料做相反的處理
  • Rehandshaking:在Application Data階段的任何時間,session的任何一側都可以請求重新談判。新的握手資料可以和程式資料混合。在重新握手階段開始前,程式可以複位TLS/DTLS通信參數,比如可以啟用的加密套件清單,比如是否需要用戶端認證,但是不能改變用戶端/伺服器方式。握手開始以後,新的SSLEngine配置就得等下次握手再生效了
  • Closure:當不再需要連接配接了,程式應該關閉SSLEngine,在關閉底層傳輸機制之前,應該發送/接收剩餘消息。一旦引擎被關閉了,将不再可用,必須增加新的SSLEngine

Understanding SSLEngine Operation Statuses

SSLEngine的狀态由SSLEngineResult.Status表示。

想知道引擎的狀态,想知道程式應該做什麼操作,可以使用SSLEngine.wrap()和SSLEngine.unwrap()方法傳回一個SSLEngineResult執行個體。

SSLEngineResult對象包含兩條狀态資訊:引擎的整體狀态和握手狀态。

SSLEngineResult.Status枚舉的值如下:

  • OK:沒有錯誤
  • CLOSED:操作關閉了SSLEngine,或者操作完成不了,因為引擎已經關閉
  • BUFFER_UNDERFLOW:輸入buffer資料不足,程式應該擷取更多對端資料(比如從網絡讀資料)
  • BUFFER_OVERFLOW:輸出buffer沒有足夠的空間儲存結果,程式應該清理或者擴充buffer
SSLEngineResult res = engine.unwrap(peerNetData, peerAppData);
    switch (res.getStatus()) {

    case BUFFER_OVERFLOW:
            // 可能需要放大peer application data buffer
        if (engine.getSession().getApplicationBufferSize() > peerAppData.capacity()) {
            // 放大buffer
        } else {
            // compact or clear the buffer
        }
        // retry 
    break;
        
    case BUFFER_UNDERFLOW:
        // 可能需要放大peer network packet buffer
        if (engine.getSession().getPacketBufferSize() > peerNetData.capacity()) {
        // 放大
        } else {
        // compact or clear the buffer
        }
        // 擷取更多入站資料,然後重試操作
       break;

       // 處理其他狀态: CLOSED, OK
       // ...
    }        
           

握手狀态由SSLEngineResult.HandshakeStatus表示:

  • FINISHED:SSLEngine已經完成了握手
  • NEED_TASK:握手能繼續之前,SSLEngine需要一個或者更多的授權任務結果
  • NEED_UNWRAP:握手能繼續之前,SSLEngine需要從對端接收資料
  • NEED_UNWRAP_AGAIN:握手能繼續之前,SSLEngine需要unwrap。表示先前接收的資料還未解釋,不用重新接收。資料進入JSSE架構,但是還沒處理
  • NEED_WRAP:握手能繼續之前,SSLEngine需要發送資料,是以應該調用SSLEngine.wrap()了
  • NOT_HANDSHAKING:SSLEngine目前不在握手進行中
void doHandshake(SocketChannel socketChannel, SSLEngine engine,
    ByteBuffer myNetData, ByteBuffer peerNetData) throws Exception {

    // 增加byte buffers,用來儲存程式資料
    int appBufferSize = engine.getSession().getApplicationBufferSize();
    ByteBuffer myAppData = ByteBuffer.allocate(appBufferSize);
    ByteBuffer peerAppData = ByteBuffer.allocate(appBufferSize);

    // 開始握手
    engine.beginHandshake();
    SSLEngineResult.HandshakeStatus hs = engine.getHandshakeStatus();

    // 處理握手資訊
    while (hs != SSLEngineResult.HandshakeStatus.FINISHED &&
        hs != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) {

        switch (hs) {

        case NEED_UNWRAP:
            // 從對端接收握手資料
            if (socketChannel.read(peerNetData) < 0) {
                // channel到達流的結尾
            }

            // 處理入站握手資料
            peerNetData.flip();
            SSLEngineResult res = engine.unwrap(peerNetData, peerAppData);
            peerNetData.compact();
            hs = res.getHandshakeStatus();

            // 檢查狀态
            switch (res.getStatus()) {
            case OK :
                // 處理 OK 狀态
                break;

            // 處理其他狀态: BUFFER_UNDERFLOW, BUFFER_OVERFLOW, CLOSED
            // ...
            }
            break;

        case NEED_WRAP :
            // 清空 local network packet buffer.
            myNetData.clear();

            // 生成握手資料
            res = engine.wrap(myAppData, myNetData);
            hs = res.getHandshakeStatus();

            // 檢查狀态
            switch (res.getStatus()) {
            case OK :
                myNetData.flip();

                // 發送握手資料
                while (myNetData.hasRemaining()) {
                    socketChannel.write(myNetData);
                }
                break;

            // 處理其他狀态:  BUFFER_OVERFLOW, BUFFER_UNDERFLOW, CLOSED
            // ...
            }
            break;

        case NEED_TASK :
            // 處理阻塞的任務
            break;

            // 處理其他狀态:  // FINISHED or NOT_HANDSHAKING
            // ...
        }
    }

    // 處理其他握手
    // ...
}
           

每個結果有兩種狀态,表明程式必須執行兩個動作。比如,調用SSLEngine.unwrap()傳回SSLEngineResult.Status.OK表明輸入資料處理完畢,

SSLEngineResult.HandshakeStatus.NEED_UNWRAP表明程式應該擷取更多的TLS/DTLS編碼的資料,提供給SSLEngine.unwrap(),這樣可以繼續握手。

SSLEngine for TLS Protocols

下來看看如何增加一個SSLEngine對象,如何用它生成和處理TLS資料。

Creating an SSLEngine Object

使用SSLContext.createSSLEngine() 方法增加javax.net.ssl.SSLEngine對象。

增加SSLEngine對象之前,你必須配置引擎,充當用戶端還是伺服器,設定其他參數,比如加密套件和是否需要用戶端認證。

import javax.net.ssl.*;
    import java.security.*;

    // 使用key,增加和初始化SSLContext
    char[] passphrase = "passphrase".toCharArray();

    // 首先初始化key和trust
    KeyStore ksKeys = KeyStore.getInstance("JKS");
    ksKeys.load(new FileInputStream("testKeys"), passphrase);
    KeyStore ksTrust = KeyStore.getInstance("JKS");
    ksTrust.load(new FileInputStream("testTrust"), passphrase);

    // KeyManagers決定使用什麼key
    KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
    kmf.init(ksKeys, passphrase);

    // TrustManagers決定是否允許連接配接
    TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
    tmf.init(ksTrust);

    // 擷取TLS協定的SSLContext執行個體
    sslContext = SSLContext.getInstance("TLS");
    sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

    // 增加引擎
    SSLEngine engine = sslContext.createSSLengine(hostname, port);

    // 當作用戶端
    engine.setUseClientMode(true);
           

Generating and Processing TLS Data

SSLEngine的兩個主要方法是wrap()和unwrap()。他們分别負責生成和使用網絡資料。依賴SSLEngine對象的狀态,資料可能是握手資料,也可能是程式資料。

每個SSLEngine對象,在生存期内可分為幾個階段。在程式資料能被發送或者接收之前,TLS協定需要通過握手建立加密參數。握手需要幾次往返步驟。

在初始化握手期間,wrap()和unwrap()方法生成或者消費握手資料。wrap()和unwrap()方法序列重複多次,直到握手完成。每個SSLEngine操作生成一個SSLEngineResult執行個體,其中的SSLEngineResult.HandshakeStatus屬性被用來決定接下來做什麼操作。

Java Secure Socket Extension (JSSE) -類和接口篇SocketFactory and ServerSocketFactory ClassesSSLSocket and SSLServerSocket ClassesSSLEngine ClassSSLSession and ExtendedSSLSessionHttpsURLConnection ClassSupport Classes and Interfaces

握手完成以後,再調用wrap()方法将嘗試消費資料資料,為傳輸打包資料。unwrap()跟它相反。

要把資料送給對端,程式首先提供想要發送的資料,資料由SSLEngine.wrap()生成TLS編碼資料。程式然後使用選擇的傳輸機制發給對端。

當程式接收到對端發來的TLS編碼資料,由SSLEngine.unwrap()生成純文字資料。

發送“hello”

// 增加非阻塞channel
    SocketChannel socketChannel = SocketChannel.open();
    socketChannel.configureBlocking(false);
    socketChannel.connect(new InetSocketAddress(hostname, port));

    // 完成連接配接
    while (!socketChannel.finishedConnect()) {
    // 做一些事情,直到連接配接建立
    }

    //增加byte buffers,儲存程式資料和編碼後的資料

    SSLSession session = engine.getSession();
    ByteBuffer myAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
    ByteBuffer myNetData = ByteBuffer.allocate(session.getPacketBufferSize());
    ByteBuffer peerAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
    ByteBuffer peerNetData = ByteBuffer.allocate(session.getPacketBufferSize());

    // 握手
    doHandshake(socketChannel, engine, myNetData, peerNetData);

    myAppData.put("hello".getBytes());
    myAppData.flip();

    while (myAppData.hasRemaining()) {
    // 生成TLS/DTLS編碼資料(握手資料或者程式資料)
    SSLEngineResult res = engine.wrap(myAppData, myNetData);

    // 處理狀态
    if (res.getStatus() == SSLEngineResult.Status.OK) {
        myAppData.compact();

        // 發送TLS/DTLS編碼資料
        while(myNetData.hasRemaining()) {
            int num = socketChannel.write(myNetData);
            if (num == 0) {
                // 沒有要寫的資料了,以後再試
            }
        }
    }

    // 處理其他狀态:  BUFFER_OVERFLOW, CLOSED
    ...
    }
           

接收資料

// 讀TLS/DTLS編碼資料
    int num = socketChannel.read(peerNetData);
    if (num == -1) {
        // 沒資料了
    } else if (num == 0) {
        // 沒有資料,再試一次
    } else {
        // 處理入站資料
        peerNetData.flip();
        res = engine.unwrap(peerNetData, peerAppData);

        if (res.getStatus() == SSLEngineResult.Status.OK) {
            peerNetData.compact();

        if (peerAppData.hasRemaining()) {
            // Use peerAppData
        }
    }
    // 處理其他狀态:  BUFFER_OVERFLOW, BUFFER_UNDERFLOW, CLOSED
    ...
    }
           

Dealing With Blocking Tasks

握手期間,SSLEngine可能遭遇阻塞的任務或者任務執行了很長時間。

比如,TrustManager需要連接配接到遠方的證書校驗服務,或者KeyManager需要提醒使用者決定用戶端認證使用的證書。

SSLEngine是非阻塞的,碰到這樣的任務,将傳回SSLEngineResult.HandshakeStatus.NEED_TASK。

收到這個狀态,程式應該調用SSLEngine.getDelegatedTask()擷取這個任務,然後,使用适當的線程模型,處理任務。

當主線程處理其他I/O的時候,程式可以從線程池擷取線程處理該任務。

下面的代碼使用新增加的線程處理每個任務:

if (res.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_TASK) {
    Runnable task;
    while ((task = engine.getDelegatedTask()) != null) {
        new Thread(task).start();
    }
}
           

SSLEngine将阻塞接下來的wrap()和unwrap()調用,直到完成了這些任務。

Shutting Down a TLS/DTLS Connection

為了有序關閉TLS/DTLS連接配接, TLS/DTLS協定需要傳遞關閉消息。是以,當一個程式完成了TLS/DTLS連接配接,它需要首先從SSLEngine獲得關閉消息,然後傳給對端,最後關閉傳輸機制。

除了明确關閉SSLEngine的程式之外,SSLEngine可能被對端關閉(當處理握手資料時,收到關閉消息),或者處理程式資料或者握手資料時,發生錯誤(SSLException)。此時,程式應該調用SSLEngine.wrap()擷取關閉消息,送給對端,直到SSLEngine.isOutboundDone()傳回true,或者直到SSLEngineResult.getStatus()傳回CLOSED。

除了有序關閉,當傳輸鍊路在交換關閉消息前被切斷,也可能意外關閉。程式可能得到-1或者在從非阻塞的SocketChannel讀資料時抛IOException異常。

當你讀完輸入資料,應該調用engine.closeInbound(),它會校驗對端是否已關閉。然後,程式仍然應該嘗試上面的關閉過程。

明顯地,不像SSLSocket,程式使用SSLEngine必須處理更多的狀态。

// Indicate that application is done with engine
engine.closeOutbound();

while (!engine.isOutboundDone()) {
    // 擷取關閉消息
    SSLEngineResult res = engine.wrap(empty, myNetData);

    // 檢查狀态

    // 發送關閉消息
    while(myNetData.hasRemaining()) {
        int num = socketChannel.write(myNetData);
        if (num == 0) {
            // 沒有要寫的資料了,等一會重試
        }
        myNetData().compact();
    }
}

// 關閉
socketChannel.close();
           

SSLSession and ExtendedSSLSession

javax.net.ssl.SSLSession接口代表協商好的安全上下文。session建立以後,被連接配接兩端的未來的SSLSocket或者SSLEngine對象共享。

在某些情況下,握手期間協商的參數将在握手後期進行,以便做出有關信任的決策。比如,有效的簽名算法可能限制用來做認證的證書類型。在握手期間,通過調用SSLSocket或者SSLEngine對象的getHandshakeSession()方法可以擷取SSLSession。TrustManager和KeyManager的實作可以使用getHandshakeSession()擷取session參數的相關資訊,幫助他們做決定。

完整的SSLSession初始化包括通信使用的加密套件,和增加時間、最後使用時間等管理資訊。session也包含共享的協商秘密,用來增加加密key和保護通信的完整性。

ExtendedSSLSession擴充了SSLSession接口,支援附加屬性。ExtendedSSLSession增加的方法描述雙方支援的簽名算法。在請求的Server Name Indication (SNI) Extension中,getRequestedServerNames()用來擷取SNIServerName對象清單。伺服器應該使用請求的伺服器名稱,指導自己選擇恰當的簽名證書,以及安全政策的其他資訊。用戶端應該使用請求的伺服器名稱指導它的端點識别,以及安全政策的其他資訊。

調用SSLSession的getPacketBufferSize()和getApplicationBufferSize()方法,SSLEngine可以決定buffer容量。

注意:TLS協定規定,packets最多包含16KB的純文字。但是,很多實作違反了規定,生成超過32KB的大記錄。如果SSLEngine.unwrap()檢測到入站大包,SSLSession傳回的buffer容量就變成動态的。程式應該總是檢查BUFFER_OVERFLOW和BUFFER_UNDERFLOW狀态,必要的時候擴大buffer的容量。SunJSSE總是發送标準的16KB記錄,允許入站的32KB記錄。

HttpsURLConnection Class

javax.net.ssl.HttpsURLConnection類擴充了java.net.HttpURLConnection,支援HTTPS相關特性。

HTTPS類似HTTP,但是,HTTPS首先通過TLS sockets建立安全channel,然後在請求和接收資料前驗證對端身份。

要擷取HttpsURLConnection執行個體,你可以實際上通過URLConnection.connect()方法初始化網絡連接配接之前,配置HTTP和HTTPS參數。

Setting the Assigned SSLSocketFactory

有時候,要定制HttpsURLConnection執行個體使用的SSLSocketFactory。

比如,你可能想要預設實作不支援的代理隧道(tunnel)。新的SSLSocketFactory可以傳回已經執行了所有必要隧道操作的的sockets,這樣HttpsURLConnection可以使用代理。

HttpsURLConnection類有預設的SSLSocketFactory,類被加載的時候就配置設定好了(通過SSLSocketFactory.getDefault()方法傳回)。

後來的HttpsURLConnection執行個體将繼承預設的SSLSocketFactory,直到新的預設SSLSocketFactory通過靜态方法HttpsURLConnection.setDefaultSSLSocketFactory()配置設定給類。

一旦生成了HttpsURLConnection的執行個體,執行個體的SSLSocketFactory可以通過調用setSSLSocketFactory()覆寫。

Setting the Assigned HostnameVerifier

如果URL的主機名不比對TLS握手時接收到的證書裡的主機名,就可能是發生了URL攻擊。如果實作不能确定主機比對的合理性,TLS實作将回調配置設定給實作的HostnameVerifier。主機名驗證将執行必要的步驟,作出決定,比如看主機名是否比對,或者打開互動式的對話框。驗證失敗會關閉連接配接。

Support Classes and Interfaces

算法或協定
KeyStore PKCS12
KeyManagerFactory PKIX, SunX509
TrustManagerFactory PKIX (X509 or SunPKIX), SunX509
SSLContext SSLv3, TLSv1, TLSv1.1, TLSv1.2, TLSv1.3, DTLSv1.0, DTLSv1.2

SSLContext Class

javax.net.ssl.SSLContext類是安全socket協定的引擎類。該類的執行個體實際上是SSLSocket、SSLServerSocket和SSLEngine的工廠類。

SSLContext對象儲存了全部狀态資訊,共享給由它生成的全部對象。比如,SSLContext關聯的session狀态。這些緩存的session能重用,共享給其他sockets。

每個執行個體通過它的init方法配置keys、證書鍊、需要認證的信任的根CA憑證。配置以key和trust管理器的方式提供。這些管理器提供認證支援和密碼套件的關鍵約定。

目前,隻支援基于X.509的管理器。

Obtaining and Initializing the SSLContext Class

使用SSLSocketFactory或者SSLServerSocketFactory生成SSLContext。

有兩種辦法生成SSLContext:

  • 最簡單的是SSLSocketFactory或者SSLServerSocketFactory靜态方法SSLContext.getDefault。該方法生成預設的SSLContext,包括預設的KeyManager、TrustManager和SecureRandom(安全随機數生成器)。預設的KeyManagerFactory和TrustManagerFactory可以生成KeyManager和TrustManager。使用的密鑰材料位于預設密鑰庫(keystore)和信任庫(truststore)中
  • 這種辦法給調用者更大的控制權,使用SSLContext類的SSLContext.getDefault方法,然後通過調用執行個體的init()方法初始化上下文。init()方法的一個變種需要三個參數:KeyManager數組、TrustManager數組和SecureRandom。KeyManager對象和TrustManager對象或者通過實作接口增加,或者使用KeyManagerFactory和TrustManagerFactory增加。KeyManagerFactory和TrustManagerFactory的init()方法使用的參數是儲存在KeyStore裡的密鑰材料。

一旦建立了TLS連接配接,就生成SSLSession,包含諸如身份和密碼套件之類的資訊。SSLSession被用來描述兩個實體間的關系和狀态資訊。

每個TLS連接配接一次隻涉及一個session,但是,session可以在這些實體之間,同時或者順序地被很多連接配接使用。

Creating an SSLContext Object

SSLContext類的getInstance()可以生成SSLContext對象。

這些靜态方法每一個都可以傳回一個執行個體,包含至少一個安全socket協定。傳回的執行個體也可以實作其他協定。例如,getInstance(“TLSv1”)傳回的執行個體實作了TLSv1, TLSv1.1和TLSv1.2。增加SSLSocket、SSLServerSocket或者SSLEngine的時候,getSupportedProtocols()方法傳回支援的協定清單。你能使用setEnabledProtocols(String[] protocols) 方法,控制使用哪個協定建立SSL連接配接。

public static SSLContext getInstance(String protocol);
public static SSLContext getInstance(String protocol, String provider);
public static SSLContext getInstance(String protocol, Provider provider);
           

如果隻傳協定名,系統将決定請求的協定的實作是否有效。如果有多個實作,将使用最合适的一個。

如果傳了協定名和供應商,系統将決定是否有協定的實作,如果找不到,抛出異常。

可以這樣擷取SSLContext:

也可以使用這個方法:

如果KeyManager[]是null,該上下文将有一個空的KeyManager。如果TrustManager[]是null,将搜尋安裝的安全供應商,擷取一個恰當的TrustManager。如果SecureRandom是null,将使用預設實作。

TrustManager Interface

TrustManager的主要任務是決定接收的認證證書是否應該被信任。如果證書不被信任,連接配接被斷開。

要認證對方的身份,你必須使用一個或者多個TrustManager對象初始化SSLContext。你必須給支援的每個認證機制傳遞一個TrustManager。如果SSLContext初始化時,傳的是null,将為你增加一個TrustManager。通常是支援基于X.509的公鑰證書做認證的TrustManager(比如X509TrustManager)一些安全socket實作也支援基于共享密鑰、Kerberos或者其他機制的認證。

可以使用TrustManagerFactory增加TrustManager,也可以直接實作該接口。

TrustManagerFactory Class

可以自己實作和配置工廠,以提供額外的TrustManager,以提供更複雜的服務或實作特定的身份驗證政策。

Creating a TrustManagerFactory

可以這樣生成

如果你使用X509TrustManager接口實作TrustManager,就不需要使用TrustManagerFactory。

也可以這樣增加工廠

public void init(KeyStore ks);
public void init(ManagerFactoryParameters spec);
           

很多工廠,比如SunJSSE的SunX509 TrustManagerFactory,隻需要一個參數就可以初始化TrustManagerFactory。

有時候,供應商需要除KeyStore之外的初始化參數。供應商要求開發者傳入供應商定義的ManagerFactoryParameters,供應商可以調用ManagerFactoryParameters的其他方法,擷取需要的資訊。比如,假設TrustManagerFactory供應商需要初始化參數B、R和S。供應商需要程式提供實作ManagerFactoryParameters接口的類的執行個體。本例中,假設供應商需要實作MyTrustManagerFactoryParams,傳給它的第二個init方法,MyTrustManagerFactoryParams看起來可能是這樣的:

public interface MyTrustManagerFactoryParams extends ManagerFactoryParameters {
  public boolean getBValue();
  public float getRValue();
  public String getSValue();
}
           

一些TrustManager作出信任決定,不需要明确地初始化KeyStore或者其他參數。比如,他們能通過LDAP通路信任材料,使用遠方的線上證書狀态檢查服務,或者在本地通路預設的信任材料。

PKIX TrustManager Support

預設的信任管理器算法是PKIX。編輯java.security檔案的ssl.TrustManagerFactory.algorithm屬性,可以修改。

PKIX信任管理器用的是CertPath PKIX實作。

下面的代碼讓信任管理器使用LDAP證書存儲和打開廢止檢查(revocation checking)。

import javax.net.ssl.*;
    import java.security.cert.*;
    import java.security.KeyStore;
    import java.io.FileInputStream;
    ...
    
    // 擷取 Keystore 密碼
    char[] pass = System.console().readPassword("Password: ");

    // 增加 PKIX 參數
    KeyStore anchors = KeyStore.getInstance("JKS");
    anchors.load(new FileInputStream(anchorsFile, pass));
    PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(anchors, new X509CertSelector());
    
    // 使用 LDAP 證書存儲
    LDAPCertStoreParameters lcsp = new LDAPCertStoreParameters("ldap.imc.org", 389);
    pkixParams.addCertStore(CertStore.getInstance("LDAP", lcsp));
    
    // 廢止檢查
    pkixParams.setRevocationEnabled(true);
    
    // 給信任管理器PKIX 參數 
    ManagerFactoryParameters trustParams = new CertPathTrustManagerParameters(pkixParams);
    
    // 增加與PKIX相容的TrustManagerFactory 
    TrustManagerFactory factory = TrustManagerFactory.getInstance("PKIX");
    
    // 傳給工廠的是CertPath 實作
    factory.init(trustParams);
    
    // 使用工廠
    SSLContext ctx = SSLContext.getInstance("TLS");
    ctx.init(null, factory.getTrustManagers(), null);
           

X509TrustManager Interface

javax.net.ssl.X509TrustManager擴充了TrustManager接口。如果使用基于X.509的認證,必須實作該接口。

想讓對端socket通過JSSE支援X.509認證,該接口的執行個體必須傳給一個SSLContext對象。

Creating an X509TrustManager

可以直接實作該接口,也可以使用供應商的TrustManagerFactory。你也可以實作自己的工廠類。

如果不給SunJSSE PKIX或者SunX509 TrustManagerFactory傳KeyStore參數,工廠使用下列步驟尋找信任材料:

  • 如果定義了javax.net.ssl.trustStore屬性,TrustManagerFactory就尋找該檔案,該檔案就是KeyStore參數。如果還定義了javax.net.ssl.trustStorePassword屬性,将在打開檔案前,檢查資料完整性。如果找不到檔案,使用空的keystore增加預設的TrustManager
  • 如果沒定義javax.net.ssl.trustStore屬性,然後
    • 如果有java-home/lib/security/jssecacerts,就使用它
    • 如果有java-home/lib/security/cacerts,就使用它
    • 如果這些檔案都不存在,TLS密碼套件就是匿名的,你執行認證,不需要truststore。

Creating Your Own X509TrustManager

假設MyX509TrustManager提高了預設的SunJSSE X509TrustManager 行為,在預設的X509TrustManager失敗後,使用其他認證邏輯。

可以這樣寫代碼,讓SSLContext生成的SocketFactories使用你自己的TrustManager:

TrustManager[] myTMs = new TrustManager[] { new MyX509TrustManager() };
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, myTMs, null);
           

MyX509TrustManager的代碼是這樣的:

class MyX509TrustManager implements X509TrustManager {

     X509TrustManager pkixTrustManager;

     MyX509TrustManager() throws Exception {
         // 增加預設的JSSE X509TrustManager.

         KeyStore ks = KeyStore.getInstance("JKS");
         ks.load(new FileInputStream("trustedCerts"), "passphrase".toCharArray());

         TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
         tmf.init(ks);

         TrustManager tms [] = tmf.getTrustManagers();

         /*
          * 周遊傳回的信任管理器,查找X509TrustManager執行個體
          * 找到以後,把它作為預設的信任管理器
          */
         for (int i = 0; i < tms.length; i++) {
             if (tms[i] instanceof X509TrustManager) {
                 pkixTrustManager = (X509TrustManager) tms[i];
                 return;
             }
         }

         /*
          * 找不到就失敗
          */
         throw new Exception("Couldn't initialize");
     }

     /*
      * 預設的信任管理器
      */
     public void checkClientTrusted(X509Certificate[] chain, String authType)
                 throws CertificateException {
         try {
             pkixTrustManager.checkClientTrusted(chain, authType);
         } catch (CertificateException excep) {
             // 其他處理,或者重新抛異常
         }
     }

     /*
      * 預設的信任管理器
      */
     public void checkServerTrusted(X509Certificate[] chain, String authType)
                 throws CertificateException {
         try {
             pkixTrustManager.checkServerTrusted(chain, authType);
         } catch (CertificateException excep) {
             /*
              * 也許彈出對話框,詢問是否信任該證書鍊
              */
         }
     }


     public X509Certificate[] getAcceptedIssuers() {
         return pkixTrustManager.getAcceptedIssuers();
     }
}
           

Updating the Keystore Dynamically

你可以增強MyX509TrustManager,處理動态keystore更新。當checkClientTrusted或者checkServerTrusted測試失敗時,不會建立受信任的證書鍊,

你可以給keystore增加一個需要的受信任的證書。你必須從TrustManagerFactory使用更新的keystore增加新的pkixTrustManager。

當建立新連接配接(使用以前初始化的SSLContext),将使用新證書做信任決定。

X509ExtendedTrustManager Class

X509ExtendedTrustManager是抽象實作。它增加了連接配接敏感(connection-sensitive)的信任管理方法。它能在TLS層做端點驗證。

TLS 1.2以及後來的版本,用戶端和伺服器都能指定他們能接受的hash和簽名算法。為了做對端認證,認證決定必須基于X509證書和本地接受的hash和簽名算法。可以使用ExtendedSSLSession.getLocalSupportedSignatureAlgorithms()擷取本地接受的hash和簽名算法。

調用SSLSocket.getHandshakeSession()方法或者SSLEngine.getHandshakeSession()方法可以擷取ExtendedSSLSession對象。

X509TrustManager接口不是連接配接敏感的。它不能通路SSLSocket和SSLEngine的session屬性。

除了TLS 1.2和以後版本的支援,X509ExtendedTrustManager也支援算法限制和SSL層主機名驗證。

Creating an X509ExtendedTrustManager

可以增加自己的X509ExtendedTrustManager子類,或者從供應商的TrustManagerFactory擷取一個。

Creating Your Own X509ExtendedTrustManager

import java.io.*;
import java.net.*;
import java.security.*;
import java.security.cert.*;
import javax.net.ssl.*;
    
public class MyX509ExtendedTrustManager extends X509ExtendedTrustManager {

  /*
   * 預設的PKIX X509ExtendedTrustManager.  
   * 如果預設的X509ExtendedTrustManager不信任,就執行回調
   */
  
  X509ExtendedTrustManager pkixTrustManager;
    
  MyX509ExtendedTrustManager() throws Exception {
    // 增加預設的JSSE X509ExtendedTrustManager.
    
    KeyStore ks = KeyStore.getInstance("JKS");
    ks.load(new FileInputStream("trustedCerts"), "passphrase".toCharArray());
    
    TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
    tmf.init(ks);
    
    TrustManager tms [] = tmf.getTrustManagers();
    
    /*
     * 周遊傳回的信任管理器,查找X509TrustManager執行個體
     * 找到以後,把它作為預設的信任管理器
     */
    for (int i = 0; i < tms.length; i++) {
      if (tms[i] instanceof X509ExtendedTrustManager) {
        pkixTrustManager = (X509ExtendedTrustManager) tms[i];
        return;
      }
    }
    
    /*
     * 想辦法初始化,或者傳回失敗
     */
    throw new Exception("Couldn't initialize");
  }
    
  /*
   * 預設的信任管理器
   */
  public void checkClientTrusted(X509Certificate[] chain, String authType)
    throws CertificateException {
    try {
      pkixTrustManager.checkClientTrusted(chain, authType);
    } catch (CertificateException excep) {
      // 其他處理,或者重新抛異常
    }
  }
    
  /*
   * 預設的信任管理器
   */
  public void checkServerTrusted(X509Certificate[] chain, String authType)
    throws CertificateException {
    try {
      pkixTrustManager.checkServerTrusted(chain, authType);
    } catch (CertificateException excep) {
      /*
       * 也許彈出對話框,詢問是否信任該證書鍊
       */
    }
  }
    
  /*
   * 連接配接敏感的驗證.
   */
  public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket)
    throws CertificateException {
    try {
      pkixTrustManager.checkClientTrusted(chain, authType, socket);
    } catch (CertificateException excep) {
      // 其他處理,或者重新抛異常
    }
  }
    
  public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
    throws CertificateException {
    try {
      pkixTrustManager.checkClientTrusted(chain, authType, engine);
    } catch (CertificateException excep) {
      // 其他處理,或者重新抛異常
    }
  }
    
  public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket)
    throws CertificateException {
    try {
      pkixTrustManager.checkServerTrusted(chain, authType, socket);
    } catch (CertificateException excep) {
      // 其他處理,或者重新抛異常
    }
  }
    
  public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
    throws CertificateException {
    try {
      pkixTrustManager.checkServerTrusted(chain, authType, engine);
    } catch (CertificateException excep) {
      // 其他處理,或者重新抛異常
    }
  }
         

  public X509Certificate[] getAcceptedIssuers() {
    return pkixTrustManager.getAcceptedIssuers();
  }
}
           

KeyManager Interface

KeyManager的主要責任是選擇将要送給對方的認證證書。

應該使用一個或者更多的KeyManager初始化SSLContext對象。要為每個認證機制傳遞一個KeyManager。

如果初始化SSLContext的時候,沒有傳KeyManager,将生成一個空的KeyManager。

KeyManagerFactory Class

X509KeyManager Interface

如果伺服器側支援 Server Name Indication (SNI) Extension,就可以檢查伺服器名稱并選擇适當的密鑰。

比如,假設keystore裡有三個key條目的證書:

cn=www.example.com

cn=www.example.org

cn=www.example.net

如果ClientHello請求連接配接www.example.net,伺服器應該能選擇www.example.net相應的證書。

Relationship Between a TrustManager and a KeyManager

曆史上,TrustManager和KeyManager的功能有混淆。

TrustManager決定遠方的認證證書應該被信任。

KeyManager決定哪個認證證書應該被送到遠方主機。