天天看點

SSL + Socket NIO 原理 與 SSLEngine + Naga SSLSocket 的使用什麼是SSLSSLEngine的使用Naga 庫中 SSL 使用小結

  • 什麼是SSL
    • SSL記錄協定
      • 工作流程
    • SSL握手協定
      • 原理
  • SSLEngine的使用
    • SSLEngine的握手流程
    • SSLEngine的狀态标志
  • Naga 庫中 SSL 使用
    • 自簽名認證證書的申請
    • Naga SSL 使用代碼
  • 小結

什麼是SSL

SSL(Secure Sockets Layer 安全套接層),及其繼任者傳輸層安全(Transport Layer Security,TLS)是為網絡通信提供安全及資料完整性的一種安全協定。TLS與SSL在傳輸層對網絡連接配接進行加密。

SSL協定位于TCP/IP協定與各種應用層協定之間,為資料通訊提供安全支援。SSL協定可分為兩層: SSL記錄協定(SSL Record Protocol):它建立在可靠的傳輸協定(如TCP)之上,為高層協定提供資料封裝、壓縮、加密等基本功能的支援。 SSL握手協定(SSL Handshake Protocol):它建立在SSL記錄協定之上,用于在實際的資料傳輸開始前,通訊雙方進行身份認證、協商加密算法、交換加密密鑰等。[百度百科]

SSL記錄協定

SSL紀錄協定(Record Protocol)為SSL連提供兩種服務。

  1. 保密性:利用握手協定所定義的共享密鑰對SSL淨荷(Payload)加密。
  2. 完整性:利用握手協定所定義的共享的MAC密鑰來生成封包的驗證碼(MAC)。

工作流程

  • 發送方
    1. 從應用層擷取要發送的資料(包括各種消息和資料)
    2. 對資訊進行分段,分成若幹紀錄
    3. 使用指定的壓縮算法進行資料壓縮(Optional)
    4. 使用指定的MAC算法生成MAC
    5. 使用指定的加密算法進行資料加密(RC4)
    6. 添加SSL紀錄協定的頭,發送資料
  • 接收方
    1. 接收資料并從SSL紀錄協定的頭中擷取相關資訊
    2. 使用指定的解密算法解密資料(RC4)
    3. 使用指定的MAC算法校驗MAC
    4. 使用壓縮算法對資料解壓縮(如指定)
    5. 将紀錄進行資料重組
    6. 将資料發送給應用層

SSL握手協定

SSL握手協定(Handshake Protocol)提供為SSL提供加密信道的建立服務。

  1. 身份認證:通過證書或者可信公鑰檔案進行身份認證
  2. 協商密鑰:通過資訊交換,互相協商一個流加密密鑰

原理

SSL在信道加密之前,首先要進行握手通信,協商出流加密密鑰,其握手過程如下圖:

SSL + Socket NIO 原理 與 SSLEngine + Naga SSLSocket 的使用什麼是SSLSSLEngine的使用Naga 庫中 SSL 使用小結

在網頁浏覽過程中,網站伺服器為Client端,浏覽器為Server端。這是因為此時網站為不可信源,是以需要對不可信源進行驗證。

在Android開發過程中,APP為不可信源,是以APP為Client端,伺服器為Server端。

從上圖可以看出,當建立連接配接時,用戶端主動發起請求,伺服器監聽後應答,并尋求身份驗證,身份驗證可通過證書或者伺服器所發送的公鑰進行識别。用戶端發送伺服器簽發的證書或者公鑰資訊、随機密碼,伺服器驗證後也會發送給用戶端一個随機密碼,當雙方都有一對随機密碼後,通過協商的算法将一對随機密碼合成為最終的流加密密鑰,到此握手協商完成。

SSLEngine的使用

SSLEngine的握手流程

在實際開發過程中,一般使用 SSLEngine進行SSL信道建立。SSLEngine可以和阻塞、非阻塞的Socket一起工作。

阻塞的Socket一般為SSLServerSocket和SSLSocke,雖然使用友善,但是擴充性和并發性較差。沒法滿足大并發的業務要求,是以暫且不在繼續分析。

非阻塞的Socket一般是與SocketChannel結合使用。SSLEngine在工作時,有以下幾個階段:

  1. 建立:在此階段,程式可以設定SSLEngine,其中包括:證書或者公鑰檔案的指定、通過SSLContext生成SSLEngine、設定SSLEngine在握手過程中的角色身份。
  2. 握手協商: SSLEngine根據上一節的握手過程進行握手操作。
  3. 資料傳輸:握手完成後,SSLEngine通過協商的流密鑰進行加解密資料并傳輸資料。
  4. 重新握手:程式可以在資料傳輸的任意時間,請求重新協商握手密鑰。SSLEngine會重置通信參數,但不能更改用戶端/伺服器角色模式,設定完成後,會在下一次通信中開始重新握手。
  5. 關閉引擎: 當關閉SSLEngine時,SSLEngine會完成剩餘資料的處理工作,并關閉連接配接。

SSLEngine的狀态标志

SSLEngine在握手協商過程中會根據握手資料互動,設定自己的不同狀态:NEED_UNWRAP、NEED_WRAP、NEED_TASK、FINISHED、NOT_HANDSHAKING。

  • NEED_UNWRAP: SSLEngine在等待握手資料時,狀态為NEED_UNWRAP,表明為解包資料的狀态。
  • NEED_TASK:由于SSLEngine為異步處理,是以當進行解包完畢後,采用回調方式觸發,這個中間過程SSLEngine狀态為NEED_TASK。
  • NEED_WRAP:當解包資料處理完畢後,SSLEngine的狀态為NEED_WRAP,這時候是将回告資料打包,等待下一步操作。
  • FINISHED:當握手協商結束時,SSLEngine的狀态為FINISHED,SSLEngine準備做随後的一些整理操作。
  • NOT_HANDSHAKING:此時的SSLEngine已經完成握手協商,準備對應用資料進行加解密處理。

Naga 庫中 SSL 使用

Naga提供了對SSL的支援,并且通過封裝NIOServerSocketSSL、SSLServerSocketChannelResponder、SSLSocketChannelResponder、SSLSocketObserver使SSL與channel完美結合,形成AIO Sokcet + SSL完整的通信加密方案。

自簽名認證證書的申請

SSLEngine認證過程中有時需要使用自簽名的認證證書及其認證檔案,SSLContext可以引入自簽名檔案并由SSLContext.createSSLEngine()所産生SSLEngine,由于SSLContext自帶的預設TrustManager無法驗證自簽名證書或檔案,是以我們需要自定義TrustManager,導入我們的可信證書檔案。

注:

1. SSL雙向認證時需要生成兩對認證證書,也就是執行兩次證書生成過程,但是在Android用戶端需要對伺服器證書進行一下加工才能正常讀取。

2. 在Android中公鑰檔案及證書資訊可使用BKS檔案存儲,BKS檔案可以由證書檔案*.cer通過bcprov-jdk15on.jar包導出,其下載下傳位址為:http://www.bouncycastle.org/latest_releases.html,建立過程中還需要使用JAVA SDK自帶的keytool工具。

3. 對于Cert轉JKS/BKS檔案,也可以使用UI工具(portecle)進行轉換,下載下傳位址:https://sourceforge.net/projects/portecle/

生成證書對過程如下:

  1. 生成keystone檔案:
    keytool -genkey -alias YourAliasName -keystore C:\path\keystonname.keystone -validity 365
    這行指令建立一個别名為YourAliasName 的密鑰(key), 生成的檔案名是 keystonname.keystore. 執行檔案生成過程中會要求輸入密鑰(key)跟keystore的密碼。這裡需要注意下,當要求你錄入Common name 的時候,要填你的主機名。
  2. 生成cer證書檔案:
    keytool -export -alias YourAliasName -keystore C:\path\keystonname.keystone -file C:\path\certificationname.cer
    這樣指令是将keystone檔案導入生成X509證書檔案,生成cer檔案。
  3. 為Android用戶端生成BKS檔案(3/4互斥):
    keytool -import -alias YourAliasName -file C:\path\clientcertificationname.cer -keystore C:\path\yourbksfile.bks -storetype BKS -providerClass org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath C:\path\bcprov-jdk15on-XXX.jar
    此指令是将.cer檔案轉換為BKS檔案,BKS是Android用戶端引入SSL所必須的公鑰檔案格式,也就是說BKS檔案是cer證書檔案的一種變形,給Android用戶端Trust keystone使用。
  4. 為Java服務端生成JKS檔案(3/4互斥):
    keytool -exportcert -alias YourAliasName -file C:\path\servercertificationname.cer -keystore yourjksfile.jks
    此指令是将.cer檔案轉換為JKS檔案,JKS是JAVA伺服器端引入SSL的常用公鑰檔案格式,給Java服務端 Trust keystone使用。

Naga SSL 使用代碼

在上一篇:Socket 與 Android Socket AIO 庫 Naga 的介紹中,我已經介紹了Naga庫的基本用法,在本節中,我會介紹一下,經過我的源碼修改後的Naga庫在SSL通信是的使用方法,修改後的源碼,我使用方法已經釋出在我的Github項目NagaForSSL中的example子目錄中,連結為:https://github.com/fy2462/NagaForSSL

用戶端使用代碼如下:

new Thread() {
    public void run() {
        try {
            NIOService service = new NIOService();
            // 填寫keystone密碼
            char[] password = "keyston password".toCharArray();
            KeyStore keyStore = KeyStore.getInstance("BKS");
            KeyStore trustStore = KeyStore.getInstance("BKS");
            //加載用戶端keystone檔案,第一參數為檔案路徑, 讀取client檔案私鑰client.keytone
            keyStore.load(getResources().openRawResource(R.raw.clientkeystone), password);
            //加載伺服器BKS證書檔案,第一參數為檔案路徑,讀取server端公鑰server.bks
            trustStore.load(getResources().openRawResource(R.raw.ssltrustserver), password);
            //定義證書實體,并加載BKS檔案
            TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
            tmf.init(trustStore);
            //生成SSLEngine 
            SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, tmf.getTrustManagers(), null);
            SSLEngine engine = sslContext.createSSLEngine();
            // 使用Naga
            if (null != engine) {
                SSLSocketChannelResponder socket = service.openSSLSocket(engine, SERVICR_HOST, SERVICR_PORT);
                socket.listen(new SSLSocketObserver() {

                    @Override   
                    public void packetSent(NIOSocket socket, Object tag) {
                        System.out.println("Packet sent");
                    }

                    @Override
                    public void connectionOpened(NIOSocket nioSocket) {
                        try {
                            //連接配接建立後開始發起握手
                            ((NIOSocketSSL) nioSocket).beginHandshake();
                        } catch (SSLException e) {
                            e.printStackTrace();
                        }
                        System.out.println("*Connection opened");
                    }

                    @Override
                    public void connectionBroken(NIOSocket nioSocket, Exception exception) {
                        System.out.println("*Connection broken");
                        if (exception != null)
                            exception.printStackTrace();
                    }

                    @Override
                    public void packetReceived(NIOSocket socket, byte[] packet) {
                        System.out.println("*Unencrypted Packet received " + packet.length);
                        System.out.println(new String(packet));
                    }

                    @Override
                    public void handleFinished(NIOSocket socket) {
                        // 發資料
                        socket.write("hello word -- 11111".getBytes());
                    }
                });
                // 輪詢Task并處理
                while (true) {
                    service.selectBlocking();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    };
}.start();
           

服務端使用代碼如下:

public static void main(String... args) {
    try {
        final NIOService service = new NIOService();
        NIOServerSocketSSL socket = service.openSSLServerSocket(getSSLContext(), SERVICR_PORT);
        System.out.println("Server listens on port " + SERVICR_PORT + "... ...");

        socket.listen(new ServerSocketObserverAdapter() {
            @Override
            public void newConnection(final NIOSocket nioSocket) {

                nioSocket.listen(new SSLSocketObserver() {
                    @Override
                    public void packetReceived(NIOSocket socket, byte[] packet) {
                        System.out.println("Received " + socket.getIp() + ": " +new String(packet));

                        socket.write("This is an SSL Server".getBytes());
                        System.out.println("Server sent: " + "This is an SSL Server");
                    }

                    @Override
                    public void connectionBroken(NIOSocket nioSocket, Exception exception) {
                        System.out.println("Client " + nioSocket.getIp()
                                + " disconnected. Exception: " + exception.getMessage());
                    }

                    @Override
                    public void connectionOpened(NIOSocket nioSocket) {
                        try {
                            ((NIOSocketSSL) nioSocket).beginHandshake();
                        } catch (SSLException e) {
                            e.printStackTrace();
                        }

                        System.out.println("Client " + nioSocket.getIp() + " connected.");
                    }

                    @Override
                    public void packetSent(NIOSocket socket, Object tag) {
                        System.out.println("packetSent");

                    }

                    @Override
                    public void handleFinished(NIOSocket socket) {
                        socket.write("This is an SSL Server".getBytes());
                        System.out.println("Server sent: " + "This is an SSL Server");

                    }
                });
            }
        });

        while (true) {
            service.selectBlocking();
        }
    } catch (Exception e) {
        e.printStackTrace(); 
    }
}

private static SSLContext getSSLContext() throws GeneralSecurityException,
        FileNotFoundException, IOException {
    KeyStore ks = KeyStore.getInstance("JKS");
    KeyStore ts = KeyStore.getInstance("JKS");

    char[] passphrase = "testssl".toCharArray();

    // 載入server端私鑰
    ks.load(new FileInputStream("ssl/serverkey.keystone"), passphrase);
    // 載入client可信證書,擷取用戶端公鑰
    ts.load(new FileInputStream("ssl/ssltrustclient.jks"), passphrase);

    KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
    kmf.init(ks, passphrase);

    TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
    tmf.init(ts);

    SSLContext sslCtx = SSLContext.getInstance("SSL");

    sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

    return sslCtx;
}
           

小結

本篇文章,主要介紹了SSL的概念以及加密通信的原理,并且描述了在日常開發中SSLEngine的使用,以及Naga Socket庫在SSL方面的使用方法,由于時間倉促,未免會出現錯誤,大家可以留言交流、指正。

繼續閱讀