HTTPS協議詳解

前言


由於前不久蘋果公司已經強制IOS應用必須使用HTTPS協議開發,雖然Google沒有強制開發者使用HTTPS,但相信不久的將來Android也會跟隨IOS全面轉向HTTPS。因此,HTTPS的學習也是相當重要。本篇文章涉及到的代碼不多,主要內容是對HTTPS協議的講解,最後將結合Retrofit實現HTTPS的單雙向認證。


HTTPS概述


什麼是HTTPS? 

我們看維基百科給HTTPS的定義:

HTTPS(Hypertext Transfer Protocol Secure)是一種通過計算機網絡進行安全通信的傳輸協議。HTTPS經由HTTP進行通信,但利用TLS來加密數據包。HTTPS開發的主要目的,是提供對網站服務器的身份認證,保護交換數據的隱私與完整性。

原來HTTPS就是在HTTP協議的基礎上加入了TLS協議。目的是保證我們的數據在網絡上傳輸的安全性。

TLS是傳輸層加密協議,前身是SSL協議。由網景公司於1995年發佈。後改名爲TLS。常用的 TLS 協議版本有:TLS1.2, TLS1.1, TLS1.0 和 SSL3.0。其中 SSL3.0 由於 POODLE 攻擊已經被證明不安全。TLS1.0 也存在部分安全漏洞,比如 RC4 和 BEAST 攻擊。

由於HTTP協議採用明文傳輸,我們可以通過抓包很輕鬆的獲取到HTTP所傳輸的數據。因此,採用HTTP協議是不安全的。這才催生了HTTPS的誕生。HTTPS相對HTTP提供了更安全的數據傳輸保障。主要體現在三個方面:

  1. 內容加密。客戶端到服務器的內容都是以加密形式傳輸,中間者無法直接查看明文內容。 

  2. 身份認證。通過校驗保證客戶端訪問的是自己的服務器。 

  3. 數據完整性。防止內容被第三方冒充或者篡改。


HTTPS實現原理


在學習HTTPS原理之前我們先了解一下兩種加密方式: 對稱加密和非對稱加密。 對稱加密 即加密和解密使用同一個密鑰,雖然對稱加密破解難度很大,但由於對稱加密需要在網絡上傳輸密鑰和密文,一旦被黑客截取很容就能被破解,因此對稱加密並不是一個較好的選擇。 

非對稱加密 即加密和解密使用不同的密鑰,分別稱爲公鑰和私鑰。我們可以用公鑰對數據進行加密,但必須要用私鑰才能解密。在網絡上只需要傳送公鑰,私鑰保存在服務端用於解密公鑰加密後的密文。但是非對稱加密消耗的CPU資源非常大,效率很低,嚴重影響HTTPS的性能和速度。因此非對稱加密也不是HTTPS的理想選擇。

那麼HTTPS採用了怎樣的加密方式呢?其實爲了提高安全性和效率HTTPS結合了對稱和非對稱兩種加密方式。即客戶端使用對稱加密生成密鑰(key)對傳輸數據進行加密,然後使用非對稱加密的公鑰再對key進行加密。因此網絡上傳輸的數據是被key加密的密文和用公鑰加密後的密文key,因此即使被黑客截取,由於沒有私鑰,無法獲取到明文key,便無法獲取到明文數據。所以HTTPS的加密方式是安全的。

接下來我們以TLS1.2爲例來認識HTTPS的握手過程。

  • 客戶端發送 client_hello,包含一個隨機數 random1。 

  • 服務端回覆 server_hello,包含一個隨機數 random2,攜帶了證書公鑰 P。 

  • 客戶端接收到 random2 之後就能夠生成 premaster_secrect (對稱加密的密鑰)以及 master_secrect(用premaster_secret加密後的數據)。 

  • 客戶端使用證書公鑰 P 將 premaster_secrect 加密後發送給服務器 (用公鑰P對premaster_secret加密)。 

  • 服務端使用私鑰解密得到 premaster_secrect。又由於服務端之前就收到了隨機數 1,所以服務端根據相同的生成算法,在相同的輸入參數下,求出了相同的 master secrect。

HTTPS的握手過程如下圖: 



數字證書


我們上面提到了HTTPS的工作原理,通過對稱加密和非對稱加密實現數據的安全傳輸。我們也知道非對稱加密過程需要用到公鑰進行加密。那麼公鑰從何而來?其實公鑰就被包含在數字證書中。數字證書通常來說是由受信任的數字證書頒發機構CA,在驗證服務器身份後頒發,證書中包含了一個密鑰對(公鑰和私鑰)和所有者識別信息。數字證書被放到服務端,具有服務器身份驗證和數據傳輸加密功能。

除了CA機構頒發的證書之外,還有非CA機構頒發的證書和自簽名證書。

  • 非CA機構即是不受信任的機構頒發的證書,理所當然這樣的證書是不受信任的。

  • 自簽名證書,就是自己給自己頒發的證書。當然自簽名證書也是不受信任的。

例如大(chou)名(ming)鼎(zhao)鼎(zhu)的12306網站使用的就是非CA機構頒發的證書(最近發現12306購票頁面已經改爲CA證書了),12306的證書是由SRCA頒發,SRCA中文名叫中鐵數字證書認證中心,簡稱中鐵CA。這是個鐵道部自己搞的機構,相當於是自己給自己頒發證書。因此我們訪問12306時通常會看到如下情景: 


說了這麼多,我們來總結一下數字證書的兩個作用:

  • 分發公鑰。每個數字證書都包含了註冊者生成的公鑰。在 TLS握手時會通過 certificate 消息傳輸給客戶端。 

  • 身份授權。確保客戶端訪問的網站是經過 CA 驗證的可信任的網站。(在自簽名證書的情況下可以驗證是否是我們自己的服務器)

最後我們從別處搬來一箇中間人攻擊的例子,來認識證書是如何保證我們的數據安全的。 
對於一個正常的網絡請求,其流程通常如下: 


但是,如果有黑客在通信過程中攔截了這個請求。試想在客戶端和服務端中間有一箇中間人,兩者之間的傳輸對中間人來說是透明的,那麼中間人完全可以獲取兩端之間的任何數據並加以修改,然後轉發給兩端。其流程如下圖: 


此時惡意服務端完全可以發起雙向攻擊:對上可以欺騙服務端,對下可以欺騙客戶端,更嚴重的是客戶端段和服務端完全感知不到已經被攻擊了。這就是所謂的中間人攻擊。

中間人攻擊(MITM攻擊)是指,黑客攔截並篡改網絡中的通信數據。又分爲被動MITM和主動MITM,被動MITM只竊取通信數據而不修改,而主動MITM不但能竊取數據,還會篡改通信數據。最常見的中間人攻擊常常發生在公共wifi或者公共路由上。

現在可以看看使用證書是怎麼樣提高安全性,避免中間人攻擊的,用一張簡單的流程圖來說明:



HTTPS單項認證


所謂單項認證只要服務端配置證書,客戶端在請求服務端時驗證服務器的證書即可。我們上述講到的內容其實都是說的HTTPS單項認證。通常來說對於安全性要求不高的網站單項認證就可以滿足我們的需求了。因此我們訪問的HTTPS網站大部分都是單項認證。

關於HTTPS的使用存在的誤區

由於我們對安全性的認識不夠重視,通常對於HTTPS存在一些誤區,這些誤區可能直接給我們帶來一些安全隱患。 

誤區(1):對於CA機構頒發的證書客戶端無須內置

上面提到訪問HTTPS服務器是需要在客戶端配置服務器證書的。有些小夥伴可能就納悶了,說我們用的就是HTTPS但是並沒有在客戶端配置證書呢?比如請求百度的網站https://www.baidu.com/,和請求HTTP服務器沒什麼區別。其實這是因爲在Android系統中已經內置了所有CA機構的根證書,也就是只要是CA機構頒發的證書,Android是直接信任的。對於此種情況,雖然可以正常訪問到服務器,但是仍然存在安全隱患。假如黑客自家搭建了一個服務器並申請到了CA證書,由於我們客戶端沒有內置服務器證書,默認信任所有CA證書(客戶端可以訪問所有持有由CA機構頒發的證書的服務器),那麼黑客仍然可以發起中間人攻擊劫持我們的請求到黑客的服務器,實際上就成了我們的客戶端和黑客的服務器建立起了連接。 

誤區(2):對於非CA機構頒發的證書和自簽名證書,可以忽略證書校驗

另外一種情況,如果我們服務器的證書是非認證機構頒發的 (例如12306)或者自簽名證書,那麼我們是無法直接訪問到服務器的,直接訪問通常會拋出如下異常:

網上很多解決SSLHandshakeException異常的方案是自定義TrustManager忽略證書校驗。代碼如下:

avax.net.ssl.SSLHandshakeException: 
    java.security.cert.CertPathValidatorException: 
        Trust anchor for certification path not found.

網上很多解決SSLHandshakeException異常的方案是自定義TrustManager忽略證書校驗。代碼如下:

public static SSLSocketFactory getSSLSocketFactory() throws Exception {
        //創建一個不驗證證書鏈的證書信任管理器。
        final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
            @Override
            public void checkClientTrusted(
                    java.security.cert.X509Certificate[] chain,
                    String authType) throws CertificateException {
            }

            @Override
            public void checkServerTrusted(
                    java.security.cert.X509Certificate[] chain,
                    String authType) throws CertificateException {
            }

            @Override
            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                return new java.security.cert.X509Certificate[0];
            }
        }};

        // Install the all-trusting trust manager
        final SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustAllCerts,
                new java.security.SecureRandom());
        // Create an ssl socket factory with our all-trusting manager
        return sslContext
                .getSocketFactory();
    }


  //使用自定義SSLSocketFactory
  private void onHttps(OkHttpClient.Builder builder) {
       try {
            builder.sslSocketFactory(getSSLSocketFactory()).hostnameVerifier(org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
 對於這樣的處理方式雖然解決了SSLHandshakeException異常,但是卻存在更大的安全隱患。因爲此種做法直接使我們的客戶端信任了所有證書(包括CA機構頒發的證書和非CA機構頒發的證書以及自簽名證書),因此,這樣配置將比第一種情況危害更大。

Retrofit綁定證書實現HTTPS單項認證

對於上述兩種情況中存在的安全隱患,我們應該如何應對?最簡單的解決方案就是在客戶端內置服務器的證書,我們在校驗服務端證書的時候只比對和App內置的證書是否完全相同,如果不同則斷開連接。那麼此時再遭遇中間人攻擊劫持我們的請求時由於黑客服務器沒有相應的證書,此時HTTPS請求校驗不通過,則無法與黑客的服務器建立起連接。

那麼接下來我們就結合Retrofit以訪問12306爲例來實現HTTPS的單項認證。 
首先從12306網站下載簽名證書,並放置到我們項目資源目錄raw下。然後根據證書構造SSLSocketFactory,代碼如下:

**
     * 單項認證
     */
    public static SSLSocketFactory getSSLSocketFactoryForOneWay(InputStream... certificates) {
        try {
            CertificateFactory certificateFactory = CertificateFactory.getInstance(CLIENT_TRUST_MANAGER, CLIENT_TRUST_PROVIDER);
            KeyStore keyStore = KeyStore.getInstance(CLIENT_TRUST_KEYSTORE);
            keyStore.load(null);
            int index = 0;
            for (InputStream certificate : certificates) {
                String certificateAlias = Integer.toString(index++);
                keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));
                try {
                    if (certificate != null)
                        certificate.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            SSLContext sslContext = SSLContext.getInstance(CLIENT_AGREEMENT);

            TrustManagerFactory trustManagerFactory =
                    TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());

            trustManagerFactory.init(keyStore);
            sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
            return sslContext.getSocketFactory();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

接下來爲OKHttpClient設置SslSocketFactory以及hostnameVerifier,代碼如下:

InputStream certificate12306 = Utils.getContext().getResources().openRawResource(R.raw.srca);
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .readTimeout(Constants.DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)
                .connectTimeout(Constants.DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)
                .addInterceptor(interceptor)
                .addInterceptor(new HttpHeaderInterceptor())
                .addNetworkInterceptor(new HttpCacheInterceptor())
                .sslSocketFactory(SslContextFactory.getSSLSocketFactoryForOneWay(certificate12306))  
                .hostnameVerifier(new SafeHostnameVerifier())
                .cache(cache)
                .build();

上述代碼中hostnameVerifier是對服務器的校驗,SafeHostnameVerifier代碼如下:

private class SafeHostnameVerifier implements HostnameVerifier {
        @Override
        public boolean verify(String hostname, SSLSession session) {
            if (Constants.IP.equals(hostname)) {//校驗hostname是否正確,如果正確則建立連接
                return true;
            }
            return false;
        }
    }

verify方法中對比了請求的IP和服務器的IP是否一致,一致則返回true表示校驗通過,否則返回false,檢驗不通過,斷開連接。對於網上有些處理是直接返回true,即不對請求的服務器IP做校驗,我們不推薦這樣使用。而且現在谷歌應用商店已經對此種做法做了限制,禁止在verify方法中直接返回true的App上線。


HTTPS雙項認證


對於HTTPS雙向認證,用到的情況不多。但是對於像金融行業等對安全性要求較高的企業,通常都會使用雙向認證。所謂雙向認證就是客戶端校驗服務器證書,同時服務器也需要校驗客戶端的證書。因此,雙向認證就另需一張證書放到客戶端待服務端去驗證。

單項認證保證了我們自己的客戶端只能訪問我們自己的服務器,但並不能保證我們自己的服務器只能被我們自己的客戶端訪問(第三方客戶端忽略證書校驗即可)。那麼雙向認證則保證了我們的客戶端只能訪問我們自己的服務器,同時我們的服務器也只能被我們自己的客戶端訪問。因此雙向認證可以說相比單項認證安全性足足提高一個等級。

雙向認證流程

接下來我們來了解下雙向認證的流程,以加深對雙向認證的理解:

  • 客戶端發送一個連接請求給服務器。 

  • 服務器將自己的證書,以及同證書相關的信息發送給客戶端。 

  • 客戶端檢查服務器送過來的證書是否和App內置證書相同。如果是,就繼續執行協議;如果不是則終止此次請求。 

  • 接着客戶端比較證書裏的消息,例如域名和公鑰,與服務器剛剛發送的相關消息是否一致,如果是一致的,客戶端認可這個服務器的合法身份。 

  • 服務器要求客戶發送客戶自己的證書。收到後,服務器驗證客戶端的證書,如果沒有通過驗證,拒絕連接;如果通過驗證,服務器獲得用戶的公鑰。 

  • 客戶端告訴服務器自己所能夠支持的通訊對稱密碼方案。 

  • 服務器從客戶發送過來的密碼方案中,選擇一種加密程度最高的密碼方案,用客戶的公鑰加過密後通知客戶端。 

  • 客戶端針對這個密碼方案,選擇一個通話密鑰,接着用服務器的公鑰加過密後發送給服務器。 

  • 服務器接收到客戶端送過來的消息,用自己的私鑰解密,獲得通話密鑰。 

  • 服務器通過密鑰解密客戶端發送的被加密數據,得到明文數據。

Retrofit實現HTTPS雙向認證

對於雙向認證,我們以華爲北向平臺登錄接口爲例來進行學習。地址如下:

http://developer.huawei.com/ict/cn/doc/site-oceanconnect-northbound_api_reference-zh/index.html/zh-cn_topic_0103199657

我們直接通過瀏覽器訪問登錄接口可以看到如下情景: 


哈,驚喜不?直接被拒絕了!這就是雙向認證,沒有證書想訪問服務器門都沒有。那麼對於雙向認證我們應該做怎樣的配置?我們可以參考華爲開源出來的代碼,源碼中由兩個證書文件ca.jks和outgoing.CertwithKey.pkcs12,其中ca.jks是在客戶端配置的證書,outgoing.CertwithKey.pkcs12是在服務端配置的證書。因爲我們當前客戶端是Android系統,由於Android系統不支持jks格式的證書,因此需要把jks轉成Android支持的bks格式。轉換方式不再貼出,可自行查閱。 

有了證書,接下來看獲取SSLSocketFactory的代碼:

/**
     * 雙向認證
     *
     * @return SSLSocketFactory
     */
    public static SSLSocketFactory getSSLSocketFactoryForTwoWay() {
        try {
            InputStream certificate = Utils.getContext().getResources().openRawResource(R.raw.capk);
            //  CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509", "BC");
            KeyStore keyStore = KeyStore.getInstance(CLIENT_TRUST_KEY);
            keyStore.load(certificate, SELF_CERT_PWD.toCharArray());
            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            kmf.init(keyStore, SELF_CERT_PWD.toCharArray());

            try {
                if (certificate != null)
                    certificate.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

            //初始化keystore
            KeyStore clientKeyStore = KeyStore.getInstance(CLIENT_TRUST_KEYSTORE);
            clientKeyStore.load(Utils.getContext().getResources().openRawResource(R.raw.cabks), TRUST_CA_PWD.toCharArray());

            SSLContext sslContext = SSLContext.getInstance(CLIENT_AGREEMENT);
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.
                    getInstance(TrustManagerFactory.getDefaultAlgorithm());

            trustManagerFactory.init(clientKeyStore);

            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            keyManagerFactory.init(clientKeyStore, SELF_CERT_PWD.toCharArray());

            sslContext.init(kmf.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
            return sslContext.getSocketFactory();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

接下來同樣需要配置OKHttpClient,代碼如下:

OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .readTimeout(Constants.DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)
                .connectTimeout(Constants.DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)
                .addInterceptor(interceptor)
                .addInterceptor(new HttpHeaderInterceptor())
                .addNetworkInterceptor(new HttpCacheInterceptor())
                .sslSocketFactory(SslContextFactory.getSSLSocketFactoryForTwoWay()) 
                .hostnameVerifier(new SafeHostnameVerifier())
                .cache(cache)
                .build();

這樣就完成了HTTPS的配置,接下來就可以愉快的訪問HTTPS 雙向認證的接口了。由於北向登錄接口中需要appId和secret兩個參數,因此,登錄相關代碼就不再貼出。


結語


好了,到此關於HTTPS的學習就結束了,如果有不明白的地方可以參看文末源碼。以上內容純屬個人對HTTPS的一些認識,如果文中有錯誤之處還請多多包涵,歡迎留言指正。

原鏈接:《關於HTTPS那些事

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