HTTP1.x傳輸資料時,所有傳輸的內容都是明文,客戶端和伺服器端都無法驗證對方的身份,存在的問題如下:
其實這些問題不只在HTTP上出現,其他未加密的協定中也會有這類問題。
(1) 通訊使用明文可能會被竊聽
#按TCP/IP協定族的工作機制,網路上的任何角落都存在通訊內容被竊聽的風險。而HTTP協定本身不具備加密的功能,所傳輸的都是明文。即使已經經過加密處理的通信,也會被窺視到通信內容,這點和未加密的通信是相同的。只是說如果通訊經過加密,就有可能讓人無法破解封包訊息的含義,但加密處理後的訊息訊息本身還是會被看到的。
(2) 不驗證通訊方的身分可能遭遇偽裝
#在HTTP協定通訊時,由於不存在確認通訊方的處理步驟,因此任何人都可以發起請求。另外,伺服器只要接收到請求,不管對方是誰都會回傳一個回應。因此不確認通信方,存在以下隱患:
(3) 無法證明封包完整性,可能已被竄改
所謂完整性是指資訊的準確度。若無法證明其完整性,通常也意味著無法判斷資訊是否準確。 HTTP協定無法證明通訊的封包完整性,在要求或回應送出之後直到對方接收之前的這段時間內,即使請求或回應的內容遭到竄改,也沒有辦法獲悉。
例如,從某個Web網站下載內容,是無法確定客戶端下載的檔案和伺服器上存放的檔案是否前後一致的。文件內容在傳輸途中可能已經被竄改為其他的內容。即使內容真的已改變,作為接收方的客戶端也是覺察不到的。像這樣,請求或回應在傳輸途中,遭攻擊者攔截並篡改內容的攻擊稱為中間人攻擊(Man-in-the-Middle attack,MITM)。
#(4) 安全的HTTP版本應該具備的幾個特徵
由於上述的幾個問題,需要一種能夠提供以下功能的HTTP安全技術:
(1) 伺服器認證(客戶端知道它們是在與真正的而不是偽造的伺服器通話);
(2) 用戶端認證(伺服器知道它們是在與真正的而不是偽造的客戶端通話);
(3) 完整性(客戶端和伺服器的資料不會被修改);
(4) 加密(客戶端和伺服器的對話是私密的,無需擔心被竊聽);
(5) 效率(一個運行的足夠快的演算法,以便低端的客戶端和伺服器使用);
(6) 普適性(基本上所有的客戶端和伺服器都支援這些協定);
在這樣的需求背景下,HTTPS技術誕生了。 HTTPS協議的主要功能基本上都依賴TLS/SSL協議,提供了身份驗證、資訊加密和完整性校驗的功能,可以解決HTTP存在的安全性問題。本節就重點探討HTTPS協定的幾個關鍵技術點。
#(1) 加密技術
加密演算法一般分為兩種:
對稱加密:加密與解密的金鑰相同。以DES演算法為代表;
非對稱加密:加密與解密的金鑰不相同。以RSA演算法為代表;
對稱加密強度非常高,一般破解不了,但存在一個很大的問題就是無法安全地產生和保管密鑰,假如客戶端和伺服器之間每次會話都使用固定的、相同的密鑰加密和解密,肯定存在很大的安全隱憂。
在非對稱金鑰交換演算法出現以前,對稱加密一個很大的問題就是不知道如何安全地產生和保管金鑰。非對稱金鑰交換過程主要就是為了解決這個問題,使金鑰的產生和使用更加安全。但同時也是HTTPS效能和速度嚴重降低的「罪魁禍首」。
HTTPS採用對稱加密和非對稱加密兩者並用的混合加密機制,在交換金鑰環節使用非對稱加密方式,之後的建立通訊交換封包階段則使用對稱加密方式。
(2) 驗證–證明公開金鑰正確性的憑證
非對稱加密最大的問題,就是無法證明公鑰本身就是貨真價實的公鑰。例如,正準備和某台伺服器建立公開金鑰加密方式下的通訊時,如何證明收到的公開金鑰就是原本預想的那台伺服器發行的公開金鑰。或許在公開金鑰傳輸途中,真正的公開金鑰已經被攻擊者取代掉了。
如果不驗證公鑰的可靠性,至少會存在以下的兩個問題:中間人攻擊和資訊抵賴。
#為了解決上述問題,可以使用由數位憑證認證機構(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) 憑證=公鑰 申請者與頒發者資訊 簽署;
(1) HTTPS的歷史
HTTPS協定歷史簡介:
(2) 協定實作
宏觀上,TLS以記錄協定(record protocol)實作。記錄協定負責在傳輸連線上交換所有的底層訊息,並且可以設定加密。每一筆TLS記錄以一個短標頭起始。標頭包含記錄內容的類型(或子協定)、協定版本和長度。訊息資料緊接在標頭之後,如下圖所示:
#TLS的主規格說明書定義了四個核心子協定:
(3) 握手協定
握手是TLS協定中最精密複雜的部分。在這個過程中,通訊雙方協商連接參數,並且完成身份驗證。根據使用的功能的不同,整個過程通常需要交換6~10個訊息。根據配置和支援的協定擴展的不同,交換過程可能有許多變種。在使用中經常可以觀察到以下三種流程:
(4) 單向驗證的握手流程
本節以QQ信箱的登入流程為例,透過抓包來分析單向驗證的握手流程。單向驗證的一次完整的握手流程如下所示:
#主要分為四個步驟:
以下對此過程進行詳細的分析。
1.ClientHello
在握手流程中,ClientHello是第一個訊息。這則訊息將客戶端的功能和首選項傳送給伺服器。包含客戶端支援的SSL的指定版本、加密元件(Cipher Suite)清單(所使用的加密演算法及金鑰長度等)。
#2.ServerHello
ServerHello訊息將伺服器選擇的連線參數傳送回客戶端。這個訊息的結構與ClientHello類似,只是每個欄位只包含一個選項。伺服器的加密元件內容以及壓縮方法等都是從接收到的客戶端加密元件內篩選出來的。
#3.Certificate
之後伺服器發送Certificate報文,封包中包含公開金鑰證書,伺服器必須保證它所傳送的證書與選擇的演算法套件一致。不過Certificate訊息是可選的,因為並非所有套件都使用身份驗證,也並非所有驗證方法都需要憑證。
#4.ServerKeyExchange
ServerKeyExchange訊息的目的是攜帶密鑰交換的額外資料。訊息內容對於不同的協商演算法套件都會存在差異。在某些場景中,伺服器不需要發送任何內容,在這些場景中就不需要發送ServerKeyExchange訊息。
#5.ServerHelloDone
ServerHelloDone訊息表示伺服器已經將所有預期的握手訊息傳送完畢。在此之後,伺服器會等待客戶端發送訊息。
#6.ClientKeyExchange
ClientKeyExchange訊息攜帶客戶端為金鑰交換提供的所有資訊。這個訊息受協商的密碼套件的影響,內容隨著不同的協商密碼套件而有所不同。
#7.ChangeCipherSpec
ChangeCipherSpec訊息表示發送端已取得用以產生連接參數的足夠訊息,已產生加密金鑰,並且將切換到加密模式。客戶端和伺服器在條件成熟時都會發送這個訊息。注意:ChangeCipherSpec不屬於握手訊息,它是另一種協議,只有一條訊息,作為它的子協議進行實現。
#8.Finished
Finished訊息表示握手已經完成。訊息內容將會加密,以便雙方可以安全地交換驗證整個握手完整性所需的資料。客戶端和伺服器在條件成熟時都會發送這個訊息。
(5) 雙向驗證的握手流程
在一些對安全性要求更高的場景下,可能會出現雙向驗證的需求。完整的雙向驗證流程如下:
#可以看到,同單向驗證流程相比,雙向驗證多瞭如下兩條訊息:CertificateRequest與CertificateVerify,其餘流程大致相同。
1.Certificate Request
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;
可以選擇發送一份自己接受的證書頒發機構列表,這些機構都用其可分辨名稱來表示.
2.CertificateVerify
##當需要做客戶端認證時,客戶端會傳送CertificateVerify訊息,證明自己確實擁有客戶端憑證的私鑰。這則訊息僅在客戶端憑證有簽署能力的情況下發送。 CertificateVerify必須緊接在ClientKeyExchange之後。訊息結構如下:
struct { Signature handshake_messages_signature; } CertificateVerify;
(6) 應用資料協定(application data protocol)
應用資料協定帶著應用訊息,只以TLS的角度考慮的話,這些就是資料緩衝區。記錄層使用目前連線安全參數對這些訊息進行打包、碎片整理和加密。如下圖所示,可以看到傳輸的資料已經是加密後的了。
#(7) 警報協定(alert protocol)
警報的目的是以簡單的通知機制告知對端通訊出現異常狀況。它通常會攜帶close_notify異常,在連線關閉時使用,並報告錯誤。警報非常簡單,只有兩個欄位:
struct { AlertLevel level; AlertDescription description; } Alert;
(1) 服务器证书验证错误
这是最常见的一种问题,通常会抛出如下类型的异常:
出现此类错误通常可能由以下的三种原因导致:
当服务器的CA不被系统信任时,就会发生 SSLHandshakeException。可能是购买的CA证书比较新,Android系统还未信任,也可能是服务器使用的是自签名证书(这个在测试阶段经常遇到)。
解决此类问题常见的做法是:指定HttpsURLConnection信任特定的CA集合。在本文的第5部分代码实现模块,会详细的讲解如何让Android应用信任自签名证书集合或者跳过证书校验的环节。
(2) 域名验证失败
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);
(3) 客户端证书验证
SSL支持服务端通过验证客户端的证书来确认客户端的身份。这种技术与TrustManager的特性相似。本文将在第5部分代码实现模块,讲解如何让Android应用支持客户端证书验证的方式。
(4) Android上TLS版本兼容问题
之前在接口联调的过程中,测试那边反馈过一个问题是在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协议的版本支持如下:
也就是说,按官方的文档显示,在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遇到的一些常见的问题,给出一个比较系统的解决方案。
(1) 整体结构
不管是使用自签名证书,还是采取客户端身份验证,核心都是创建一个自己的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使用自定义的校验逻辑。整体设计上使用策略模式决定采用哪种验证机制:
(2) 单向验证并自定义信任的证书集合
在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; } } }
(3) 跳过证书校验过程
和上面的过程类似,只不过这里提供的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; }
以上是分析 HTTPS 原理以及在 Android 中的使用的詳細內容。更多資訊請關注PHP中文網其他相關文章!