我們知道,HTTP請求都是明文傳輸的,所謂的明文指的是沒有經過加密的資訊,如果HTTP請求被黑客攔截,並且裡面含有銀行卡密碼等敏感資料的話,會非常危險。為了解決這個問題,Netscape 公司制定了HTTPS協議,HTTPS可以將資料加密傳輸,也就是傳輸的是密文,即便黑客在傳輸過程中攔截到資料也無法破譯,這就保證了網路通訊的安全。
密碼學基礎
在正式講解HTTPS協議之前,我們首先要知道一些密碼學的知識。
明文: 明文指的是未被加密過的原始資料。
密文:明文被某種加密演算法加密之後,會變成密文,從而確保原始資料的安全。密文也可以被解密,得到原始的明文。
金鑰:金鑰是一種引數,它是在明文轉換為密文或將密文轉換為明文的演算法中輸入的引數。金鑰分為對稱金鑰與非對稱金鑰,分別應用在對稱加密和非對稱加密上。
對稱加密:對稱加密又叫做私鑰加密,即資訊的傳送方和接收方使用同一個金鑰去加密和解密資料。對稱加密的特點是演算法公開、加密和解密速度快,適合於對大資料量進行加密,常見的對稱加密演算法有DES、3DES、TDEA、Blowfish、RC5和IDEA。
其加密過程如下:明文 + 加密演算法 + 私鑰 => 密文
解密過程如下:密文 + 解密演算法 + 私鑰 => 明文
對稱加密中用到的金鑰叫做私鑰,私鑰表示個人私有的金鑰,即該金鑰不能被洩露。
其加密過程中的私鑰與解密過程中用到的私鑰是同一個金鑰,這也是稱加密之所以稱之為“對稱”的原因。由於對稱加密的演算法是公開的,所以一旦私鑰被洩露,那麼密文就很容易被破解,所以對稱加密的缺點是金鑰安全管理困難。
非對稱加密:非對稱加密也叫做公鑰加密。非對稱加密與對稱加密相比,其安全性更好。對稱加密的通訊雙方使用相同的金鑰,如果一方的金鑰遭洩露,那麼整個通訊就會被破解。而非對稱加密使用一對金鑰,即公鑰和私鑰,且二者成對出現。私鑰被自己儲存,不能對外洩露。公鑰指的是公共的金鑰,任何人都可以獲得該金鑰。用公鑰或私鑰中的任何一個進行加密,用另一個進行解密。
被公鑰加密過的密文只能被私鑰解密,過程如下:
明文 + 加密演算法 + 公鑰 => 密文, 密文 + 解密演算法 + 私鑰 => 明文
被私鑰加密過的密文只能被公鑰解密,過程如下:
明文 + 加密演算法 + 私鑰 => 密文, 密文 + 解密演算法 + 公鑰 => 明文
由於加密和解密使用了兩個不同的金鑰,這就是非對稱加密“非對稱”的原因。
非對稱加密的缺點是加密和解密花費時間長、速度慢,只適合對少量資料進行加密。
在非對稱加密中使用的主要演算法有:RSA、Elgamal、Rabin、D-H、ECC(橢圓曲線加密演算法)等。
HTTPS通訊過程
HTTPS協議 = HTTP協議 + SSL/TLS協議,在HTTPS資料傳輸的過程中,需要用SSL/TLS對資料進行加密和解密,需要用HTTP對加密後的資料進行傳輸,由此可以看出HTTPS是由HTTP和SSL/TLS一起合作完成的。
SSL的全稱是Secure Sockets Layer,即安全套接層協議,是為網路通訊提供安全及資料完整性的一種安全協議。SSL協議在1994年被Netscape發明,後來各個瀏覽器均支援SSL,其最新的版本是3.0
TLS的全稱是Transport Layer Security,即安全傳輸層協議,最新版本的TLS(Transport Layer Security,傳輸層安全協議)是IETF(Internet Engineering Task Force,Internet工程任務組)制定的一種新的協議,它建立在SSL 3.0協議規範之上,是SSL 3.0的後續版本。在TLS與SSL3.0之間存在著顯著的差別,主要是它們所支援的加密演算法不同,所以TLS與SSL3.0不能互操作。雖然TLS與SSL3.0在加密演算法上不同,但是在我們理解HTTPS的過程中,我們可以把SSL和TLS看做是同一個協議。
HTTPS為了兼顧安全與效率,同時使用了對稱加密和非對稱加密。資料是被對稱加密傳輸的,對稱加密過程需要客戶端的一個金鑰,為了確保能把該金鑰安全傳輸到伺服器端,採用非對稱加密對該金鑰進行加密傳輸,總的來說,對資料進行對稱加密,對稱加密所要使用的金鑰通過非對稱加密傳輸。
以下圖片來自於limboy的部落格
HTTPS在傳輸的過程中會涉及到三個金鑰:
- 伺服器端的公鑰和私鑰,用來進行非對稱加密
- 客戶端生成的隨機金鑰,用來進行對稱加密
一個HTTPS請求實際上包含了兩次HTTP傳輸,可以細分為8步。
- 客戶端向伺服器發起HTTPS請求,連線到伺服器的443埠。
- 伺服器端有一個金鑰對,即公鑰和私鑰,是用來進行非對稱加密使用的,伺服器端儲存著私鑰,不能將其洩露,公鑰可以傳送給任何人。
- 伺服器將自己的公鑰傳送給客戶端。
- 客戶端收到伺服器端的公鑰之後,會對公鑰進行檢查,驗證其合法性,如果發現發現公鑰有問題,那麼HTTPS傳輸就無法繼續。嚴格的說,這裡應該是驗證伺服器傳送的數字證照的合法性,關於客戶端如何驗證數字證照的合法性,下文會進行說明。如果公鑰合格,那麼客戶端會生成一個隨機值,這個隨機值就是用於進行對稱加密的金鑰,我們將該金鑰稱之為client key,即客戶端金鑰,這樣在概念上和伺服器端的金鑰容易進行區分。然後用伺服器的公鑰對客戶端金鑰進行非對稱加密,這樣客戶端金鑰就變成密文了,至此,HTTPS中的第一次HTTP請求結束。
- 客戶端會發起HTTPS中的第二個HTTP請求,將加密之後的客戶端金鑰傳送給伺服器。
- 伺服器接收到客戶端發來的密文之後,會用自己的私鑰對其進行非對稱解密,解密之後的明文就是客戶端金鑰,然後用客戶端金鑰對資料進行對稱加密,這樣資料就變成了密文。
- 然後伺服器將加密後的密文傳送給客戶端。
- 客戶端收到伺服器傳送來的密文,用客戶端金鑰對其進行對稱解密,得到伺服器傳送的資料。這樣HTTPS中的第二個HTTP請求結束,整個HTTPS傳輸完成。
數字證照
通過觀察HTTPS的傳輸過程,我們知道,當伺服器接收到客戶端發來的請求時,會向客戶端傳送伺服器自己的公鑰,但是黑客有可能中途篡改公鑰,將其改成黑客自己的,所以有個問題,客戶端怎麼信賴這個公鑰是自己想要訪問的伺服器的公鑰而不是黑客的呢? 這時候就需要用到數字證照。
在講數字證照之前,先說一個小例子。假設一個鎮裡面有兩個人A和B,A是個富豪,B想向A借錢,但是A和B不熟,怕B借了錢之後不還。這時候B找到了鎮長,鎮長給B作擔保,告訴A說:“B人品不錯,不會欠錢不還的,你就放心借給他吧。” A聽了這話後,心裡想:“鎮長是全鎮最德高望重的了,他說B沒問題的話那就沒事了,我就放心了”。 於是A相信B的為人,把錢借給了B。
與此相似的,要想讓客戶端信賴公鑰,公鑰也要找一個擔保人,而且這個擔保人的身份必須德高望重,否則沒有說服力。這個擔保人的就是證照認證中心(Certificate Authority),簡稱CA。 也就是說CA是專門對公鑰進行認證,進行擔保的,也就是專門給公鑰做擔保的擔保公司。 全球知名的CA也就100多個,這些CA都是全球都認可的,比如VeriSign、GlobalSign等,國內知名的CA有WoSign。
那CA怎麼對公鑰做擔保認證呢?CA本身也有一對公鑰和私鑰,CA會用CA自己的私鑰對要進行認證的公鑰進行非對稱加密,此處待認證的公鑰就相當於是明文,加密完之後,得到的密文再加上證照的過期時間、頒發給、頒發者等資訊,就組成了數字證照。
不論什麼平臺,裝置的作業系統中都會內建100多個全球公認的CA,說具體點就是裝置中儲存了這些知名CA的公鑰。當客戶端接收到伺服器的數字證照的時候,會進行如下驗證:
- 首先客戶端會用裝置中內建的CA的公鑰嘗試解密數字證照,如果所有內建的CA的公鑰都無法解密該數字證照,說明該數字證照不是由一個全球知名的CA簽發的,這樣客戶端就無法信任該伺服器的數字證照。
- 如果有一個CA的公鑰能夠成功解密該數字證照,說明該數字證照就是由該CA的私鑰簽發的,因為被私鑰加密的密文只能被與其成對的公鑰解密。
- 除此之外,還需要檢查客戶端當前訪問的伺服器的域名是與數字證照中提供的“頒發給”這一項吻合,還要檢查數字證照是否過期等。
通過瀏覽器直接獲取伺服器的公鑰很容易,各個瀏覽器操作大同小異。百度現在已經實現了全站點HTTPS,我們就以百度為例如何從Chrome中獲取其公鑰。
- 用Chrome開啟百度首頁,在https左側我們會發現有一個綠色的鎖頭。
2.點選該鎖頭,出現一個彈出皮膚,點選皮膚中的“詳細資訊”幾個字。
3.這是會開啟Chrome的Developer Tool,並自動切換到Security這個頁面。
4.這是會開啟Chrome的Developer Tool,並自動切換到Security這個頁面。
在“常規”這個皮膚中,我們從中可以檢視該證照是又Symantec頒發給baidu.com這個網站的,有效期是從2015年9月17到2016年9月1日。
5.切換到“詳細資訊”皮膚,可以檢視證照的一些詳細資訊,比如證照所使用的數字簽名的演算法,如下圖所示:
上面有個“複製到檔案”的按鈕,點選該按鈕就可以將百度的數字證照匯出成檔案,從而我們就可以儲存到自己的機器上,介面如下所示:
我們將其匯出成X.509格式的證照,以.cer作為副檔名,最後儲存到本地機器如下所示:
6.切換到“證照路徑”皮膚,可以檢視證照的證照鏈。
- 這裡先解釋一下什麼是證照鏈。我們之前提到,VeriSign是一個全球知名的CA,但是一般情況下,CA不會用自己的私鑰去直接簽名某網站的數字證照,一般CA會首先簽發一種證照,然後用這種證照再去簽發百度等的數字證照。在此例中,VeriSign簽發了Symantec證照,然後Symantec又簽發了baiduc.om,VeriSign位於最頂端,類似於根結點,因此叫做根CA,Symatec位於中間,叫做中間CA,當然,有可能有多箇中間CA,這樣從根CA到中間CA,再到最終的網站的證照,這樣自上而下形成了一條證照鏈。如果想要檢視證照鏈中的某個證照,只需要選中它,比如選中了Symantec,然後點選下面的“檢視證照”按鈕就會彈出另一個對話方塊,在其中可以檢視Symantec的數字證照,當然也可以將其匯出成證照檔案儲存在硬碟上。
Android中訪問HTTPS
在Android中我們也會經常傳送HTTPS請求,這時需要使用HttpsURLConnection這個類,HttpsURLConnection是繼承自HttpURLConnection的,其用法跟HttpURLConnection是一樣的,比如我們想用HTTPS訪問百度的首頁,程式碼如下所示:
1 2 3 4 |
URL url = new URL("https://www.baidu.com"); HttpsURLConnection conn = (HttpsURLConnection)url.openConnection(); InputStream is = conn.getInputStream(); ... |
我們通過得到的InputStream就可以解析伺服器的返回結果了。
如果對應伺服器的數字證照存在問題,那麼客戶端就無法信任該證照,從而無法建立HTTPS連結,我們以國內的12306.cn網站為例進行說明。
12306.cn的使用者登入是需要HTTPS的訪問的,如果瀏覽器第一次開啟頁面https://kyfw.12306.cn/otn/regist/init,那麼瀏覽器要麼顯示證照警告資訊,要麼索性直接不顯示頁面,因為12306.cn的數字證照存在問題。
其證照鏈如下所示:
大家可以看到,該12306.cn的證照是由SRCA這個機構簽發的,也就是說SRCA是證照鏈上的根CA。
但是SRCA是啥呢?沒聽過啊!
我們選中SRCA後,點選“檢視證照”按鈕,SRCA的證照如下所示:
也就是說SRCA的全稱是Sinorail Certification Authority, 在百度裡面搜尋該名稱,可以查到一個叫做中鐵資訊工程集團的網站,http://www.sinorail.com/ProductInfos.aspx?id=185,裡面有這麼一段描述:
也就是說SRCA是鐵道部給旗下的網站等做簽名的一個所謂CA,但是它不具備公信力,因為它不是一個全球知名的CA,所以客戶端根本不認可它。
我們在Android中直接訪問https://kyfw.12306.cn/otn/regist/init時,會得到如下異常:
java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
這是因為Android的客戶端內建的可信任CA列表中沒有包含所謂的SRCA,這樣就出現了12306.cn的證照不被客戶端信任的異常。
為了解決客戶端不信任伺服器數字證照的問題,網路上大部分的解決方案都是讓客戶端不對證照做任何檢查,這是一種有很大安全漏洞的辦法,如下所示:
首先定義一個自己的SSLTrustAllManager,其繼承自X509TrustManager,具體來說,用SSLTrustAllManager建立一個SSLContext,然後用SSLContext生成一個SSLSocketFactory,然後通過呼叫HttpURLConnectoin的setSSLSocketFactory方法將其給某個具體的連線物件使用。由於SSLTrustAllManager沒有對其中的三個核心方法進行具體實現,也就是不對證照做任何審查。這樣無論伺服器的證照如何,都能建立HTTPS連線,因為客戶端直接忽略了驗證伺服器證照這一步。
這樣雖然能建立HTTPS連線,但是存在很大的安全漏洞。因為黑客有可能攔截到我們的HTTPS請求,然後黑客用自己的假證照冒充12306.cn的數字證照,由於客戶端不對證照做驗證,這樣客戶端就會和黑客的伺服器建立連線,這樣會導致客戶端把自己的12306的賬號和密碼傳送給了黑客,所以客戶端不對證照做任何驗證的做法有很大的安全漏洞。
解決此問題的辦法是讓Android客戶端信任12306的證照,而不是不對證照做任何檢查。
我們通過上面提到的方法得到12306的證照12306.cer,將其放到assets目錄下,也就是將12306.cer打包到我們的App中,然後用如下程式碼訪問12306的HTTPS站點。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
class DownloadThread extends Thread{ @Override public void run() { HttpsURLConnection conn = null; InputStream is = null; try { URL url = new URL("https://kyfw.12306.cn/otn/regist/init"); conn = (HttpsURLConnection)url.openConnection(); //建立X.509格式的CertificateFactory CertificateFactory cf = CertificateFactory.getInstance("X.509"); //從asserts中獲取證照的流 InputStream cerInputStream = getAssets().open("12306.cer");//SRCA.cer //ca是java.security.cert.Certificate,不是java.security.Certificate, //也不是javax.security.cert.Certificate Certificate ca; try { //證照工廠根據證照檔案的流生成證照Certificate ca = cf.generateCertificate(cerInputStream); System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN()); } finally { cerInputStream.close(); } // 建立一個預設型別的KeyStore,儲存我們信任的證照 String keyStoreType = KeyStore.getDefaultType(); KeyStore keyStore = KeyStore.getInstance(keyStoreType); keyStore.load(null, null); //將證照ca作為信任的證照放入到keyStore中 keyStore.setCertificateEntry("ca", ca); //TrustManagerFactory是用於生成TrustManager的,我們建立一個預設型別的TrustManagerFactory String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); //用我們之前的keyStore例項初始化TrustManagerFactory,這樣tmf就會信任keyStore中的證照 tmf.init(keyStore); //通過tmf獲取TrustManager陣列,TrustManager也會信任keyStore中的證照 TrustManager[] trustManagers = tmf.getTrustManagers(); //建立TLS型別的SSLContext物件, that uses our TrustManager SSLContext sslContext = SSLContext.getInstance("TLS"); //用上面得到的trustManagers初始化SSLContext,這樣sslContext就會信任keyStore中的證照 sslContext.init(null, trustManagers, null); //通過sslContext獲取SSLSocketFactory物件 SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); //將sslSocketFactory通過setSSLSocketFactory方法作用於HttpsURLConnection物件 //這樣conn物件就會信任我們之前得到的證照物件 conn.setSSLSocketFactory(sslSocketFactory); is = conn.getInputStream(); //將得到的InputStream轉換為字串 final String str = getStringByInputStream(is); handler.post(new Runnable() { @Override public void run() { textView.setText(str); btn.setEnabled(true); } }); }catch (Exception e){ e.printStackTrace(); final String errMessage = e.getMessage(); handler.post(new Runnable() { @Override public void run() { btn.setEnabled(true); Toast.makeText(MainActivity.this, errMessage, Toast.LENGTH_LONG).show(); } }); }finally { if(conn != null){ conn.disconnect(); } } } |
上面的註釋寫的比較詳細,此處我們還是對以上程式碼進行一下說明。
- 首先從asserts目錄中獲取12306.cer證照的檔案流,然後用CertificateFactory的generateCertificate方法將該檔案流轉化為一個證照物件Certificate,該證照就是12306網站的數字證照。
- 建立一個預設型別的KeyStore例項,keyStore用於儲存著我們信賴的數字證照,將12306的數字證照放入keyStore中。
- 我們獲取一個預設的TrustManagerFactory的例項,並用之前的keyStore初始化它,這樣TrustManagerFactory的例項也會信任keyStore中12306.cer證照。通過TrustManagerFactory的getTrustManagers方法獲取TrustManager陣列,該陣列中的TrustManager也會信任12306.cer證照。
- 建立一個TLS型別的SSLContext例項,並用之前的TrustManager陣列初始化sslContext例項,這樣該sslContext例項也會信任12306.cer證照。
- 通過sslContext獲取SSLSocketFactory物件,將sslSocketFactory通過setSSLSocketFactory方法作用於HttpsURLConnection物件,這樣conn物件就會信任keyStore中的12306.cer證照。這樣一來,客戶端就會信任12306的證照,從而正確建立HTTPS連線。
以上的處理過程是Google官方建議的流程,步驟流程總結如下:
證照檔案流 InputStream -> Certificate -> KeyStore -> TrustManagerFactory -> TrustManager[] -> SSLContext -> SSLSocketFactory -> HttpsURLConnection
以上步驟的起點是獲取證照檔案的檔案流,不一定非要從assets目錄中獲取,也可以通過其他途徑得到,只要能拿到證照的檔案流即可。
上面的過程是對的,但是還存在一點問題。我們將12306網站自身的12306.cer放到了assets目錄中,然後讓我們建立的HttpsURLConnection的例項信任了12306.cer。但是,數字證照都是有過期時間的,如果12306網站的數字證照到期了,那麼12306會去SRCA那裡重新生成一個數字證照,這時候12306網站的公鑰和私鑰都會更新,那這樣就存在問題了。我們App的assets目錄中儲存的是老的12306.cer證照,這樣12306網站重新生成了新的數字證照,那麼老的數字證照就自動作廢了,因為我們App中的12306.cer中的老的公鑰無法解密12306網站最新的私鑰了(公鑰和私鑰只能成對出現,舊的公鑰只能解密舊的私鑰)。
很不幸的是,網上大部分的解決方案就是直接信任12306.cer這種網站自身的數字證照,雖然這種辦法暫時可以解決HTTPS問題,但是不是長久之計,會為以後的數字證照的更新埋下隱患。
那怎麼解決這個問題呢?
最好的辦法不是讓我們的App直接信任12306.cer,而是讓我們的App直接信任12306數字證照的簽發者SRCA的數字證照。
我們用之前提到過的辦法將12306的簽發者SRCA的數字證照匯出,取名SRCA.cer,將其放到assets目錄下,我們只需要改一行程式碼裡面的引數即可,我們將程式碼:
1 |
InputStream cerInputStream = getAssets().open("12306.cer"); |
修成該
1 |
InputStream cerInputStream = getAssets().open("SRCA.cer"); |
也就是我們讀取的是SRCA.cer證照的檔案流,而不再是12306.cer的。
通過讓HttpsURLConnection例項信任SRCA.cer,也能夠正常建立HTTPS連線,這是為什麼呢?
我們之前提到了證照鏈的概念,假設存在如下證照鏈:
CA -> A -> B -> C
CA是根CA證照,例如SRCA.cer,C是最終的網站的數字證照,例如12306.cer,A和B都是中間證照,理論上來說,只要客戶端信任了該數字證照鏈中的任何一個證照,那麼C證照都會被信任。比如客戶端信任了根證照CA,由於CA信任A,所以客戶端也會信任A,由於A信任B,那麼客戶端也信任B,由於B信任C,那麼客戶端也信任C。所以在12306的例子中,只要信任了SRCA.cer,那麼客戶端就信任12306網站自身的12306.cer數字證照了。
Android客戶端不信任伺服器證照的原因主要是因為客戶端不信任證照鏈中的根證照CA。12306網站的自身的數字證照可能會過幾年就會重新生成,發生變動,但是SRCA作為其簽發者,發生變動的次數會少的多,或者說是很長時間內不會改動,所以我們的App去信任SRCA.cer比直接去信任12306.cer要更穩定一些。
總結:
- HTTPS的傳輸過程涉及到了對稱加密和非對稱加密,對稱加密加密的是實際的資料,非對稱加密加密的是對稱加密所需要的客戶端的金鑰。
- 為了確保客戶端能夠確認公鑰就是想要訪問的網站的公鑰,引入了數字證照的概念,由於證照存在一級一級的簽發過程,所以就出現了證照鏈,在證照鏈中的頂端的就是根CA。
- Android客戶端不信任伺服器證照的原因主要是因為客戶端不信任證照鏈中的根證照CA,我們應該讓我們的App去信任該根證照CA,而不是直接信任網站的自身的數字證照,因為網站的數字證照可能會發生變化。
希望本身對大家有所幫助!
參考:
Limboy的《圖解HTTPS》
阮一峰的《數字簽名是什麼?》
Google官方的《Security with HTTPS and SSL》