做者:曹豐斌html
本文首先分析HTTP協議在安全性上的不足,進而闡述HTTPS實現安全通訊的關鍵技術點和原理。而後經過抓包分析HTTPS協議的握手以及通訊過程。最後總結一下本身在開發過程當中遇到的HTTPS相關的問題,並給出當前項目中對HTTPS問題的系統解決方案,以供總結和分享。若有不當之處,歡迎批評和指正。java
HTTP1.x在傳輸數據時,全部傳輸的內容都是明文,客戶端和服務器端都沒法驗證對方的身份,存在的問題以下:android
- 通訊使用明文(不加密),內容可能會被竊聽;
- 不驗證通訊方的身份,有可能遭遇假裝;
- 沒法證實報文的完整性,因此有可能已遭篡改;
其實這些問題不只在HTTP上出現,其餘未加密的協議中也會存在這類問題。git
按TCP/IP協議族的工做機制,互聯網上的任何角落都存在通訊內容被竊聽的風險。而HTTP協議自己不具有加密的功能,所傳輸的都是明文。即便已經通過過加密處理的通訊,也會被窺視到通訊內容,這點和未加密的通訊是相同的。只是說若是通訊通過加密,就有可能讓人沒法破解報文信息的含義,但加密處理後的報文信息自己仍是會被看到的。github
在HTTP協議通訊時,因爲不存在確認通訊方的處理步驟,所以任何人均可以發起請求。另外,服務器只要接收到請求,無論對方是誰都會返回一個響應。所以不確認通訊方,存在如下隱患:算法
- 沒法肯定請求發送至目標的Web服務器是不是按真實意圖返回響應的那臺服務器。有多是已假裝的 Web 服務器;
- 沒法肯定響應返回到的客戶端是不是按真實意圖接收響應的那個客戶端。有多是已假裝的客戶端;
- 沒法肯定正在通訊的對方是否具有訪問權限。由於某些Web服務器上保存着重要的信息,只想發給特定用戶通訊的權限;
- 沒法斷定請求是來自何方、出自誰手;
- 即便是無心義的請求也會照單全收,沒法阻止海量請求下的DoS攻擊;
所謂完整性是指信息的準確度。若沒法證實其完整性,一般也就意味着沒法判斷信息是否準確。HTTP協議沒法證實通訊的報文完整性,在請求或響應送出以後直到對方接收以前的這段時間內,即便請求或響應的內容遭到篡改,也沒有辦法獲悉。瀏覽器
好比,從某個Web網站下載內容,是沒法肯定客戶端下載的文件和服務器上存放的文件是否先後一致的。文件內容在傳輸途中可能已經被篡改成其餘的內容。即便內容真的已改變,做爲接收方的客戶端也是覺察不到的。像這樣,請求或響應在傳輸途中,遭攻擊者攔截並篡改內容的攻擊稱爲中間人攻擊(Man-in-the-Middle attack,MITM)。安全
因爲上述的幾個問題,須要一種可以提供以下功能的HTTP安全技術:服務器
- (1) 服務器認證(客戶端知道它們是在與真正的而不是僞造的服務器通話);
- (2) 客戶端認證(服務器知道它們是在與真正的而不是僞造的客戶端通話);
- (3) 完整性(客戶端和服務器的數據不會被修改);
- (4) 加密(客戶端和服務器的對話是私密的,無需擔憂被竊聽);
- (5) 效率(一個運行的足夠快的算法,以便低端的客戶端和服務器使用);
- (6) 普適性(基本上全部的客戶端和服務器都支持這些協議);
在這樣的需求背景下,HTTPS技術誕生了。HTTPS協議的主要功能基本都依賴於TLS/SSL協議,提供了身份驗證、信息加密和完整性校驗的功能,能夠解決HTTP存在的安全問題。本節就重點探討一下HTTPS協議的幾個關鍵技術點。微信
加密算法通常分爲兩種:
- 對稱加密:加密與解密的密鑰相同。以DES算法爲表明;
- 非對稱加密:加密與解密的密鑰不相同。以RSA算法爲表明;
對稱加密強度很是高,通常破解不了,但存在一個很大的問題就是沒法安全地生成和保管密鑰,假如客戶端和服務器之間每次會話都使用固定的、相同的密鑰加密和解密,確定存在很大的安全隱患。
在非對稱密鑰交換算法出現之前,對稱加密一個很大的問題就是不知道如何安全生成和保管密鑰。非對稱密鑰交換過程主要就是爲了解決這個問題,使密鑰的生成和使用更加安全。但同時也是HTTPS性能和速度嚴重下降的「罪魁禍首」。
HTTPS採用對稱加密和非對稱加密二者並用的混合加密機制,在交換密鑰環節使用非對稱加密方式,以後的創建通訊交換報文階段則使用對稱加密方式。
非對稱加密最大的一個問題,就是沒法證實公鑰自己就是貨真價實的公鑰。好比,正準備和某臺服務器創建公開密鑰加密方式下的通訊時,如何證實收到的公開密鑰就是本來預想的那臺服務器發行的公開密鑰。或許在公開密鑰傳輸途中,真正的公開密鑰已經被攻擊者替換掉了。
若是不驗證公鑰的可靠性,至少會存在以下的兩個問題:中間人攻擊和信息抵賴。
爲了解決上述問題,可使用由數字證書認證機構(CA,Certificate Authority)和其相關機關頒發的公開密鑰證書。
CA使用具體的流程以下:
- (1) 服務器的運營人員向數字證書認證機構(CA)提出公開密鑰的申請;
- (2) CA經過線上、線下等多種手段驗證申請者提供信息的真實性,如組織是否存在、企業是否合法,是否擁有域名的全部權等;
- (3) 若是信息審覈經過,CA會對已申請的公開密鑰作數字簽名,而後分配這個已簽名的公開密鑰,並將該公開密鑰放入公鑰證書後綁定在一塊兒。
證書包含如下信息:申請者公鑰、申請者的組織信息和我的信息、簽發機構CA的信息、有效時間、證書序列號等信息的明文,同時包含一個簽名;
簽名的產生算法:首先,使用散列函數計算公開的明文信息的信息摘要,而後,採用CA的私鑰對信息摘要進行加密,密文即簽名;- (4) 客戶端在HTTPS握手階段向服務器發出請求,要求服務器返回證書文件;
- (5) 客戶端讀取證書中的相關的明文信息,採用相同的散列函數計算獲得信息摘要,而後,利用對應CA的公鑰解密簽名數據,對比證書的信息摘要,若是一致,則能夠確認證書的合法性,即公鑰合法;
- (6) 客戶端而後驗證證書相關的域名信息、有效時間等信息;
- (7) 客戶端會內置信任CA的證書信息(包含公鑰),若是CA不被信任,則找不到對應CA的證書,證書也會被斷定非法。
在這個過程注意幾點:
- (1) 申請證書不須要提供私鑰,確保私鑰永遠只能被服務器掌握;
- (2) 證書的合法性仍然依賴於非對稱加密算法,證書主要是增長了服務器信息以及簽名;
- (3) 內置CA對應的證書稱爲根證書;頒發者和使用者相同,本身爲本身簽名,叫自簽名證書;
- (4) 證書=公鑰+申請者與頒發者信息+簽名;
HTTPS協議歷史簡介:
- (1) SSL協議的第一個版本由Netscape公司開發,但這個版本從未發佈過;
- (2) SSL協議第二版於1994年11月發佈。第一次部署是在Netscape Navigator1.1瀏覽器上,發行於1995年3月;
- (3) SSL 3於1995年年末發佈,雖然名稱與早先的協議版本相同,但SSL3是徹底從新設計的協議,該設計一直沿用到今天。
- (4) TLS 1.0於1999年1月問世,與SSL 3相比,版本修改並不大;
- (5) 2006年4月,下一個版本TLS 1.1才問世,僅僅修復了一些關鍵的安全問題;
- (6) 2008年8月,TLS1.2發佈。該版本添加了對已驗證加密的支持,而且基本上刪除了協議說明中全部硬編碼的安全基元,使協議徹底彈性化;
宏觀上,TLS以記錄協議(record protocol)實現。記錄協議負責在傳輸鏈接上交換全部的底層消息,並能夠配置加密。每一條TLS記錄以一個短標頭起始。標頭包含記錄內容的類型(或子協議)、協議版本和長度。消息數據緊跟在標頭以後,以下圖所示:
TLS的主規格說明書定義了四個核心子協議:
- 握手協議(handshake protocol);
- 密鑰規格變動協議(change cipher spec protocol);
- 應用數據協議(application data protocol);
- 警報協議(alert protocol);
握手是TLS協議中最精密複雜的部分。在這個過程當中,通訊雙方協商鏈接參數,而且完成身份驗證。根據使用的功能的不一樣,整個過程一般須要交換6~10條消息。根據配置和支持的協議擴展的不一樣,交換過程可能有許多變種。在使用中常常能夠觀察到如下三種流程:
- (1) 完整的握手,對服務器進行身份驗證(單向驗證,最多見);
- (2) 對客戶端和服務器都進行身份驗證的握手(雙向驗證);
- (3) 恢復以前的會話採用的簡短握手;
本節以QQ郵箱的登陸過程爲例,經過抓包來對單向驗證的握手流程進行分析。單向驗證的一次完整的握手流程以下所示:
主要分爲四個步驟:
- (1) 交換各自支持的功能,對須要的鏈接參數達成一致;
- (2) 驗證出示的證書,或使用其餘方式進行身份驗證;
- (3) 對將用於保護會話的共享主密鑰達成一致;
- (4) 驗證握手消息是否被第三方團體修改;
下面對這一過程進行詳細的分析。
在握手流程中,ClientHello是第一條消息。這條消息將客戶端的功能和首選項傳送給服務器。包含客戶端支持的SSL的指定版本、加密組件(Cipher Suite)列表(所使用的加密算法及密鑰長度等)。
ServerHello消息將服務器選擇的鏈接參數傳送回客戶端。這個消息的結構與ClientHello相似,只是每一個字段只包含一個選項。服務器的加密組件內容以及壓縮方法等都是從接收到的客戶端加密組件內篩選出來的。
以後服務器發送Certificate報文,報文中包含公開密鑰證書,服務器必須保證它發送的證書與選擇的算法套件一致。不過Certificate消息是可選的,由於並不是全部套件都使用身份驗證,也並不是全部身份驗證方法都須要證書。
ServerKeyExchange消息的目的是攜帶密鑰交換的額外數據。消息內容對於不一樣的協商算法套件都會存在差別。在某些場景中,服務器不須要發送任何內容,在這些場景中就不須要發送ServerKeyExchange消息。
ServerHelloDone消息代表服務器已經將全部預計的握手消息發送完畢。在此以後,服務器會等待客戶端發送消息。
ClientKeyExchange消息攜帶客戶端爲密鑰交換提供的全部信息。這個消息受協商的密碼套件的影響,內容隨着不一樣的協商密碼套件而不一樣。
ChangeCipherSpec消息代表發送端已取得用以生成鏈接參數的足夠信息,已生成加密密鑰,而且將切換到加密模式。客戶端和服務器在條件成熟時都會發送這個消息。注意:ChangeCipherSpec不屬於握手消息,它是另外一種協議,只有一條消息,做爲它的子協議進行實現。
Finished消息意味着握手已經完成。消息內容將加密,以便雙方能夠安全地交換驗證整個握手完整性所需的數據。客戶端和服務器在條件成熟時都會發送這個消息。
在一些對安全性要求更高的場景下,可能會出現雙向驗證的需求。完整的雙向驗證流程以下:
能夠看到,同單向驗證流程相比,雙向驗證多了以下兩條消息:CertificateRequest與CertificateVerify,其他流程大體相同。
Certificate Request是TLS規定的一個可選功能,用於服務器認證客戶端的身份。經過服務器要求客戶端發送一個證書實現,服務器應該在ServerKeyExchange以後當即發送CertificateRequest消息。
消息結構以下:
enum { rsa_sign(1), dss_sign(2), rsa_fixed_dh(3),dss_fixed_dh(4), rsa_ephemeral_dh_RESERVED(5),dss_ephemeral_dh_RESERVED(6), fortezza_dms_RESERVED(20), ecdsa_sign(64), rsa_fixed_ecdh(65), ecdsa_fixed_ecdh(66), (255) } ClientCertificateType; opaque DistinguishedName<1..2^16-1>;struct { ClientCertificateType certificate_types<1..2^8-1>; SignatureAndHashAlgorithm supported_signature_algorithms<2^16-1>; DistinguishedName certificate_authorities<0..2^16-1>; } CertificateRequest;
能夠選擇發送一份本身接受的證書頒發機構列表,這些機構都用其可分辨名稱來表示.
當須要作客戶端認證時,客戶端發送CertificateVerify消息,來證實本身確實擁有客戶端證書的私鑰。這條消息僅僅在客戶端證書有簽名能力的狀況下發送。CertificateVerify必須緊跟在ClientKeyExchange以後。消息結構以下:
struct { Signature handshake_messages_signature; } CertificateVerify;
應用數據協議攜帶着應用消息,只以TLS的角度考慮的話,這些就是數據緩衝區。記錄層使用當前鏈接安全參數對這些消息進行打包、碎片整理和加密。以下圖所示,能夠看到傳輸的數據已是通過加密以後的了。
警報的目的是以簡單的通知機制告知對端通訊出現異常情況。它一般會攜帶close_notify異常,在鏈接關閉時使用,報告錯誤。警報很是簡單,只有兩個字段:
struct { AlertLevel level; AlertDescription description; } Alert;
- AlertLevel字段:表示警報的嚴重程度;
- AlertDescription:直接表示警報代碼;
這是最多見的一種問題,一般會拋出以下類型的異常:
出現此類錯誤一般可能由如下的三種緣由致使:
- (1) 頒發服務器證書的CA未知;
- (2) 服務器證書不是由CA簽署的,而是自簽署(比較常見);
- (3) 服務器配置缺乏中間 CA;
當服務器的CA不被系統信任時,就會發生 SSLHandshakeException。多是購買的CA證書比較新,Android系統還未信任,也多是服務器使用的是自簽名證書(這個在測試階段常常遇到)。
解決此類問題常見的作法是:指定HttpsURLConnection信任特定的CA集合。在本文的第5部分代碼實現模塊,會詳細的講解如何讓Android應用信任自簽名證書集合或者跳過證書校驗的環節。
SSL鏈接有兩個關鍵環節。首先是驗證證書是否來自值得信任的來源,其次確保正在通訊的服務器提供正確的證書。若是沒有提供,一般會看到相似於下面的錯誤:
出現此類問題的緣由一般是因爲服務器證書中配置的域名和客戶端請求的域名不一致所致使的。
有兩種解決方案:
- (1) 從新生成服務器的證書,用真實的域名信息;
- (2) 自定義HostnameVerifier,在握手期間,若是URL的主機名和服務器的標識主機名不匹配,則驗證機制能夠回調此接口的實現程序來肯定是否應該容許此鏈接。能夠經過自定義HostnameVerifier實現一個白名單的功能。
代碼以下:
HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { // 設置接受的域名集合 if (hostname.equals(...)) { return true; } } }; HttpsURLConnection.setDefaultHostnameVerifier(DO_NOT_VERIFY);
SSL支持服務端經過驗證客戶端的證書來確認客戶端的身份。這種技術與TrustManager的特性類似。本文將在第5部分代碼實現模塊,講解如何讓Android應用支持客戶端證書驗證的方式。
以前在接口聯調的過程當中,測試那邊反饋過一個問題是在Android 4.4如下的系統出現HTTPS請求不成功而在4.4以上的系統上卻正常的問題。相應的錯誤以下:
03-09 09:21:38.427: W/System.err(2496): javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0xb7fa0620: Failure in SSL library, usually a protocol error 03-09 09:21:38.427: W/System.err(2496): error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure (external/openssl/ssl/s23_clnt.c:741 0xa90e6990:0x00000000)
按照官方文檔的描述,Android系統對SSL協議的版本支持以下:
Name | Supported (API Levels) |
---|---|
Default | 10+ |
SSL | 10–TBD |
SSLv3 | 10–TBD |
TLS | 1+ |
TLSv1 | 10+ |
TLSv1.1 | 16+ |
TLSv1.2 | 16+ |
也就是說,按官方的文檔顯示,在API 16+以上,TLS1.1和TLS1.2是默認開啓的。可是實際上在API 20+以上才默認開啓,4.4如下的版本是沒法使用TLS1.1和TLS 1.2的,這也是Android系統的一個bug。
參照stackoverflow上的一些方式,比較好的一種解決方案以下:
SSLSocketFactory noSSLv3Factory; if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { noSSLv3Factory = new TLSSocketFactory(mSSLContext.getSSLSocket().getSocketFactory()); } else { noSSLv3Factory = mSSLContext.getSSLSocket().getSocketFactory(); }
對於4.4如下的系統,使用自定義的TLSSocketFactory,開啓對TLS1.1和TLS1.2的支持,核心代碼:
public class TLSSocketFactory extends SSLSocketFactory { private SSLSocketFactory internalSSLSocketFactory; public TLSSocketFactory() throws KeyManagementException, NoSuchAlgorithmException { SSLContext context = SSLContext.getInstance("TLS"); context.init(null, null, null); internalSSLSocketFactory = context.getSocketFactory(); } public TLSSocketFactory(SSLSocketFactory delegate) throws KeyManagementException, NoSuchAlgorithmException { internalSSLSocketFactory = delegate; } ...... @Override public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)); } // 開啓對TLS1.1和TLS1.2的支持 private Socket enableTLSOnSocket(Socket socket) { if(socket != null && (socket instanceof SSLSocket)) { ((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.1", "TLSv1.2"}); } return socket; } }
本部分主要基於第四部分提出的Android應用中使用HTTPS遇到的一些常見的問題,給出一個比較系統的解決方案。
無論是使用自簽名證書,仍是採起客戶端身份驗證,核心都是建立一個本身的KeyStore,而後使用這個KeyStore建立一個自定義的SSLContext。總體類圖以下:
類圖中的MySSLContext能夠應用在HTTPUrlConnection的方式與服務端鏈接的過程當中:
if (JarConfig.__self_signed_https) { SSLContextByTrustAll mSSLContextByTrustAll = new SSLContextByTrustAll(); MySSLContext mSSLContext = new MySSLContext(mSSLContextByTrustAll); SSLSocketFactory noSSLv3Factory; if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { noSSLv3Factory = new TLSSocketFactory(mSSLContext.getSSLSocket().getSocketFactory()); } else { noSSLv3Factory = mSSLContext.getSSLSocket().getSocketFactory(); } httpsURLConnection.setSSLSocketFactory(noSSLv3Factory); httpsURLConnection.setHostnameVerifier(MY_DOMAIN_VERIFY); }else { httpsURLConnection.setSSLSocketFactory((SSLSocketFactory) SSLSocketFactory.getDefault()); httpsURLConnection.setHostnameVerifier(DO_NOT_VERIFY); }
核心是經過httpsURLConnection.setSSLSocketFactory使用自定義的校驗邏輯。總體設計上使用策略模式決定採用哪一種驗證機制:
- makeContextWithCilentAndServer 單向驗證方式(自定義信任的證書集合)
- makeContextWithServer 雙向驗證方式(自定義信任的證書集合,並使用客戶端證書)
- makeContextToTrustAll (信任全部的CA證書,不安全,僅供測試階段使用)
在App中,把服務端證書放到資源文件下(一般是asset目錄下,由於證書對於每個用戶來講都是相同的,而且也不會常常發生改變),可是也能夠放在設備的外部存儲上。
public class SSLContextWithServer implements GetSSLSocket { // 在這裏進行服務器正式的名稱的配置 private String[] serverCertificateNames = {"serverCertificateNames1" ,"serverCertificateNames2"}; @Override public SSLContext getSSLSocket() { String[] caCertString = new String[serverCertificateNames.length]; for(int i = 0 ; i < serverCertificateNames.length ; i++) { try { caCertString[i] = readCaCert(serverCertificateNames[i]); } catch(Exception e) { } } SSLContext mSSLContext = null; try { mSSLContext = SSLContextFactory.getInstance().makeContextWithServer(caCertString); } catch(Exception e) { } return mSSLContext; }
serverCertificateNames中定義了App所信任的證書名稱(這些證書文件必需要放在指定的文件路徑下,並其要保證名稱相同),然後就能夠加載服務端證書鏈到keystore,經過獲取到的可信任並帶有服務端證書的keystore,就能夠用它來初始化自定義的SSLContext了:
@Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { try { originalX509TrustManager.checkServerTrusted(chain, authType); } catch(CertificateException originalException) { try { X509Certificate[] reorderedChain = reorderCertificateChain(chain); CertPathValidator validator = CertPathValidator.getInstance("PKIX"); CertificateFactory factory = CertificateFactory.getInstance("X509"); CertPath certPath = factory.generateCertPath(Arrays.asList(reorderedChain)); PKIXParameters params = new PKIXParameters(trustStore); params.setRevocationEnabled(false); validator.validate(certPath, params); } catch(Exception ex) { throw originalException; } } }
和上面的過程相似,只不過這裏提供的TrustManager不須要提供信任的證書集合,默認接受任意客戶端證書便可:
public class AcceptAllTrustManager implements X509TrustManager { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { //do nothing,接受任意客戶端證書 } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { //do nothing,接受任意服務端證書 } @Override public X509Certificate[] getAcceptedIssuers() { return null; }
然後構造相應的SSLContext:
public SSLContext makeContextToTrustAll() throws Exception { AcceptAllTrustManager tm = new AcceptAllTrustManager(); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[] { tm }, null); return sslContext; }
1. TLS協議分析(5) handshake協議 證書與密鑰交換
2. HTTPS深刻理解
3. HTTPS原理介紹以內容加密
4. How to disable SSLv3 in android for HttpsUrlConnection?
5. Android 4.1+ enable TLS 1.1 and TLS 1.2
6. Google關於https的官方文檔
7. 使用HTTPS與SSL來保證安全性
8. Android App 安全的HTTPS 通訊
9. 詳解https是如何確保安全的?
10. 圖解HTTP--第七章
11. HTTP: The Definitive Guide
12. Bulletproof SSL and TLS:
Understanding and Deploying SSL/TLS and PKI to Secure Servers and Web Applications
更多精彩內容歡迎關注騰訊 Bugly的微信公衆帳號:
騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!