目錄:
- 概述
- 基礎(chǔ)
2.1. 加密
2.2. 數(shù)字簽名
2.3. 數(shù)字證書 - TLS 原理
- 主要的類和接口
4.1. JDK
4.2. OkHttp - 源碼分析
5.1. 創(chuàng)建安全 Socket
5.2. 配置
5.3. 握手
5.4. 驗證
5.5. 完成 - 應(yīng)用實例
6.1. 信任所有證書
6.2. 信任自簽名證書
6.4. 自定義 TLS 連接規(guī)格
6.5. 使用證書鎖定 - 資料
1. 概述
TLS 是進行 HTTPS 連接的重要環(huán)節(jié)震放,通過了 TLS 層進行協(xié)商,后續(xù)的 HTTP 請求就可以使用協(xié)商好的對稱密鑰進行加密
SSL 是 Netscape 開發(fā)的專門用來保護 Web 通訊驼修,目前版本為 3.0殿遂。TLS 是 IETF 制定的新協(xié)議,建立在 SSL 3.0 之上乙各。所以 TLS 1.0 可以認為是 SSL 3.1
TLS(Transport Layer Security Protocol) 協(xié)議分為兩部分
- TLS 記錄協(xié)議
- TLS 握手協(xié)議
2. 基礎(chǔ)
2.1. 加密
2.1.1. 對稱密鑰加密
編碼和解碼使用同一個密鑰墨礁,e = d
加密算法有
- DES
- Triple-DES
- RC2
- RC4(在 OkHttp 2.3 已經(jīng)下降支持)
位數(shù)越多,枚舉攻擊花費的時間越長
痛點:發(fā)送者和接收者建立對話前觅丰,需要一個共享密鑰
2.1.2. 非對稱密鑰加密
兩個密鑰,一個加密妨退,一個解密妇萄。私鑰持有,公鑰公開
- RSA
破解私鑰的難度相當(dāng)于對極大數(shù)進行因式分解
RSA 加密系統(tǒng)中咬荷,D 和 E 會相互抵消
E(D(stuff)) = stuff
D(E(stuff)) = stuff
所以具體哪個是私鑰冠句,哪個是公鑰是由用戶選擇的
2.2 數(shù)字簽名
加了密的校驗和
- 證明是原作者,只有原作者可以私鑰來進行加密
- 證明沒有篡改幸乒,中途篡改校驗和就不再匹配
校驗和使用摘要算法生成懦底,比如 MD5,SHA
2.3. 數(shù)字證書
受信任組織擔(dān)保的用戶或公司的信息罕扎,沒有統(tǒng)一的標準
服務(wù)端大部分使用 x509 v3 派生證書聚唐,主要信息有
字段 | 舉例 |
---|---|
證書序列號 | 12:34:56:78 |
證書過期時間 | Wed,Sep 17,2017 |
站點組織名 | StevenLee |
站點DNS主機名 | steven-lee.me |
站點公鑰 | xxxx |
證書頒發(fā)者 | RSA Data Security |
數(shù)字簽名 | xxxx |
服務(wù)端把證書(內(nèi)含服務(wù)端的公鑰)發(fā)給客戶端,客戶端使用頒布證書的機構(gòu)的公鑰來解密腔召,檢查數(shù)字簽名杆查,取出公鑰。取出服務(wù)端的公鑰臀蛛,將后面請求用的對稱密鑰 X 傳遞給服務(wù)端亲桦,后面就用該密鑰進行加密傳輸信息
3. TLS 原理
HTTPS 是在 HTTP 和 TCP 之間加了一層 TLS,這個 TLS 協(xié)商了一個對稱密鑰來進行 HTTP 加密
同時浊仆,SSL/TLS 不僅僅可以用在 HTTP客峭,也可以用在 FTP,Telnet 等應(yīng)用層協(xié)議上抡柿。
SSL/TLS 實際上混合使用了對稱和非對稱密鑰舔琅,主要分成這幾步:
使用非對稱密鑰建立安全的通道。
- 客戶端請求 Https 連接洲劣,發(fā)送可用的 TLS 版本和可用的密碼套件
- 服務(wù)端返回證書搏明,密碼套件和 TLS 版本
用安全的通道產(chǎn)生并發(fā)送臨時的隨機對稱密鑰鼠锈。
- 生成隨機對稱密鑰,使用證書中的服務(wù)端公鑰加密星著,發(fā)送給服務(wù)端
- 服務(wù)端使用私鑰解密獲取對稱密鑰
使用對稱密鑰加密信息购笆,進行交互。
簡化后的流程圖如下:
詳細的流程圖如下:
4. 主要的類和接口
4.1. JDK
主要由 JDK 的 java.security虚循,javax.net 和 javax.net.ssl 提供的
- SSLSocketFactory
- SSLSocket
- SSLSession
- TrustManager
- X509TrustManager
- Certificate
- X509Certificate
- HostNameVerifier
核心類的關(guān)系圖
4.2. OkHttp
- RealConnection
- ConnectionSpecSelector
- ConnectionSpec
- CipherSuite
- CertificatePinner
5. 源碼分析
連接的所有實現(xiàn)同欠,在 RealConnection 中。如果沒有從 ConnectionPool 復(fù)用横缔,創(chuàng)建新的連接過程铺遂,見 RealConnection.buildConnection
:
private void buildConnection(int connectTimeout, int readTimeout, int writeTimeout, ConnectionSpecSelector connectionSpecSelector) throws IOException {
connectSocket(connectTimeout, readTimeout);
establishProtocol(readTimeout, writeTimeout, connectionSpecSelector);
}
connectSocket ,三次握手茎刚,創(chuàng)建 TCP 連接襟锐。
establishProtocol ,在 TCP 連接的基礎(chǔ)上膛锭,開始根據(jù)不同版本的協(xié)議粮坞,來完成連接過程。主要有 HTTP/1.1初狰,HTTP/2 和 SPDY 協(xié)議莫杈。如果是 HTTPS 類型的,則開始 TLS 建聯(lián)奢入。
private void establishProtocol(int readTimeout, int writeTimeout,
ConnectionSpecSelector connectionSpecSelector) throws IOException {
if (route.address().sslSocketFactory() != null) {
connectTls(readTimeout, writeTimeout, connectionSpecSelector);
} else {
protocol = Protocol.HTTP_1_1;
socket = rawSocket;
}
...
}
只關(guān)注 TLS 連接過程
private void connectTls(int readTimeout, int writeTimeout,
ConnectionSpecSelector connectionSpecSelector) throws IOException {
Address address = route.address();
SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
boolean success = false;
SSLSocket sslSocket = null;
try {
// Create the wrapper over the connected socket.
sslSocket = (SSLSocket) sslSocketFactory.createSocket(
rawSocket, address.url().host(), address.url().port(), true /* autoClose */);
// Configure the socket's ciphers, TLS versions, and extensions.
ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
if (connectionSpec.supportsTlsExtensions()) {
Platform.get().configureTlsExtensions(
sslSocket, address.url().host(), address.protocols());
}
// Force handshake. This can throw!
sslSocket.startHandshake();
Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());
// Verify that the socket's certificates are acceptable for the target host.
if (!address.hostnameVerifier().verify(address.url().host(), sslSocket.getSession())) {
X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
throw new SSLPeerUnverifiedException("Hostname " + address.url().host() + " not verified:"
+ "\n certificate: " + CertificatePinner.pin(cert)
+ "\n DN: " + cert.getSubjectDN().getName()
+ "\n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
}
// Check that the certificate pinner is satisfied by the certificates presented.
address.certificatePinner().check(address.url().host(),
unverifiedHandshake.peerCertificates());
// Success! Save the handshake and the ALPN protocol.
String maybeProtocol = connectionSpec.supportsTlsExtensions()
? Platform.get().getSelectedProtocol(sslSocket)
: null;
socket = sslSocket;
source = Okio.buffer(Okio.source(socket));
sink = Okio.buffer(Okio.sink(socket));
handshake = unverifiedHandshake;
protocol = maybeProtocol != null
? Protocol.get(maybeProtocol)
: Protocol.HTTP_1_1;
success = true;
} catch (AssertionError e) {
if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
} finally {
if (sslSocket != null) {
Platform.get().afterHandshake(sslSocket);
}
if (!success) {
closeQuietly(sslSocket);
}
}
}
5.1. 創(chuàng)建安全 Socket
這里的安全 Socket 就是 SSLSocket筝闹,是握手成功后的 TCP Socket 進行的封裝。
如果 SSLSocketFactory 沒有自定義配置的話腥光,會使用 OkHttp 的默認創(chuàng)建关顷。比如在 OkHttpClient 中有這樣的代碼來構(gòu)造默認的 SSLSocketFactory
X509TrustManager trustManager = systemDefaultTrustManager();
this.sslSocketFactory = systemDefaultSslSocketFactory(trustManager);
this.certificateChainCleaner = CertificateChainCleaner.get(trustManager);
systemDefaultSslSocketFactory 方法使用 SSLContext 來構(gòu)造 SSLSocketFactory
private SSLSocketFactory systemDefaultSslSocketFactory(X509TrustManager trustManager) {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { trustManager }, null);
return sslContext.getSocketFactory();
} catch (GeneralSecurityException e) {
throw new AssertionError(); // The system has no TLS. Just give up.
}
}
這樣就是用了系統(tǒng)默認的 X509TrustManager。
該 SSLSocketFactory 為系統(tǒng) SDK 提供武福,包括它生產(chǎn)的 SSLSocket解寝,所以和系統(tǒng)平臺版本強相關(guān),底層為 OpenSSL 庫艘儒。對 TLS 版本的支持情況不一樣聋伦,接口也有所不同。
SSLSocket 配置信息有兩大類:
- 支持的 TLS 協(xié)議
- 支持的密碼套件(CipherSuite)
OkHttp 不包括自己的 SSL/TLS 庫界睁,所以 SSLSocket 使用 Android 提供的標準 SSLSocket
5.2. 配置
經(jīng)過上面創(chuàng)建過程后觉增,SSLSocket 已經(jīng)有了一些操作系統(tǒng)提供的默認配置。但不完全安全翻斟,OkHttp 會有自己的連接規(guī)格逾礁,來過濾掉過時的 TLS 版本和弱密碼套件。
OkHttp 內(nèi)置了三套規(guī)格,
- ConnectionSepc.MODEN_TLS嘹履, 現(xiàn)代的 TLS 配置腻扇。
- ConnectionSpec.COMPATIABLE_TLS,不是現(xiàn)代的砾嫉,但安全 TLS 配置幼苛。
- ConnectionSpec.CLEARTEXT, 不安全的 TLS 配置焕刮。
這三套規(guī)格跟著版本走舶沿,例如,在OkHttp 2.2配并,下降支持響應(yīng)POODLE攻擊的SSL 3.0括荡。而在OkHttp 2.3 下降的支持RC4
所以與桌面Web瀏覽器,保持最新的OkHttp是保持安全的最好辦法
OkHttp 還會通過反射的方式溉旋,來對 SSLSocket 的 TLS 的擴展功能進行配置
- SNI 和 Session tickets
- ALPN
OkHttp 會先使用現(xiàn)代的規(guī)格(ConnectionSepc.MODEN_TLS)進行連接畸冲,如果失敗會采用回退策略選擇下一個。
5.2.1. TLS 連接規(guī)格選擇
該步驟選擇適合客戶端的 TLS連接規(guī)格观腊。一個很大的作用邑闲,就是盡可能地使用高版本的 TLS,和最新的密碼套件恕沫,來提供最安全的連接监憎。
連接規(guī)格都封裝在 ConnectionSpec 中纱意,主要內(nèi)容就是 TLS 版本和密碼套件
連接規(guī)格選擇的策略由 ConnectSpecSelector 進行婶溯,默認使用 OkHttp 的三套規(guī)格
最后會調(diào)用 ConnectionSpec 的 apply 方法,來配置 SSLSocket
/** Applies this spec to {@code sslSocket}. */
void apply(SSLSocket sslSocket, boolean isFallback) {
ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback);
if (specToApply.tlsVersions != null) {
sslSocket.setEnabledProtocols(specToApply.tlsVersions);
}
if (specToApply.cipherSuites != null) {
sslSocket.setEnabledCipherSuites(specToApply.cipherSuites);
}
}
在 supportedSpec 方法中偷霉,會對選擇好的規(guī)格迄委,和 SSLSocket 可用的配置取中交集,過濾掉那些不安全的低版本的 TLS 和弱密碼套件和 SSLSocket 不支持的配置类少。
這個階段后叙身,SSLSocket 中的一些不安全的 TLS 版本和弱密碼套件就被過濾了,將會使用 OkHttp 配置規(guī)范中認為的安全版本和強密碼套件開始正式的握手過程硫狞。
5.2.2. TLS 連接規(guī)格回退
最開始會嘗試現(xiàn)代的 TLS 規(guī)格信轿,如果不支持的話,會有回退策略(Fallback Strategy)残吩,回退到非現(xiàn)代但安全的 TLS 規(guī)格
回退策略由 RealConnection 和 ConnectSpecSelector 一起配合提供财忽。
比如它會先選擇最新的 ConnectionSpec.MODEN_TLS,不支持的話泣侮,再更換為 ConnectionSpec.COMPATIABLE_TLS即彪,最后選擇 ConnectionSpec.CLEARTEXT。
策略很簡單活尊,就是連接失敗的時候隶校,更換下一套規(guī)范重新進行連接漏益。
5.2.3. TLS 擴展配置
Android 平臺,最終在 AndroidPlatform.configureTlsExtensions
來完成配置
@Override public void configureTlsExtensions(
SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
// Enable SNI and session tickets.
if (hostname != null) {
setUseSessionTickets.invokeOptionalWithoutCheckedException(sslSocket, true);
setHostname.invokeOptionalWithoutCheckedException(sslSocket, hostname);
}
// Enable ALPN.
if (setAlpnProtocols != null && setAlpnProtocols.isSupported(sslSocket)) {
Object[] parameters = {concatLengthPrefixed(protocols)};
setAlpnProtocols.invokeWithoutCheckedException(sslSocket, parameters);
}
}
因為某些手機機型是支持 TLS 擴展的深胳,OkHttp 采用發(fā)射的方式嘗試加載擴展绰疤,讓這些機型的擴展配置生效。
如果 ConectionSpec 支持 TLS 的擴展稠屠,這里還會配置 SNI峦睡,session tickets 和 ALPN。
5.3. 握手
調(diào)用 SSLSocket.startHandShake
開始進行握手:
// Force handshake. This can throw!
sslSocket.startHandshake();
Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());
這里客戶端正式向服務(wù)端發(fā)出數(shù)據(jù)包权埠,內(nèi)容為可選擇的密碼和請求證書榨了。服務(wù)端會返回相應(yīng)的密碼套件,tls 版本攘蔽,節(jié)點證書龙屉,本地證書等等,然后封裝在 Handshake 類中
主要內(nèi)容有:
- CipherSuite满俗, 密碼套件转捕。
- TlsVersion, TLS 版本唆垃。
- Certificate[] peerCertificates五芝, 站點的證書。
- Certificate[] localCertificates辕万, 本地的證書枢步。一些安全級別更高的應(yīng)用,會使用雙向的證書認證渐尿。
該過程中醉途,SSLSocket 內(nèi)部會對服務(wù)端返回的 Certificate 進行判斷,是否是可信任的 CA 發(fā)布的砖茸。如果不是的話隘擎,會拋出異常
5.4. 驗證
到了這一步,服務(wù)端返回的證書已經(jīng)被系統(tǒng)所信任凉夯,也就是頒發(fā)的機構(gòu) CA 在系統(tǒng)的可信任 CA 列表中了货葬。但是為了更加安全,還會進行以下兩種驗證劲够。
5.4.1. 站點身份驗證
使用 HostnameVerifier 來驗證 host 是否合法震桶,如果不合法會拋出 SSLPeerUnverifiedException
默認的實現(xiàn)是 OkHostnameVerifier.verify
:
public boolean verify(String host, SSLSession session) {
try {
Certificate[] certificates = session.getPeerCertificates();
return verify(host, (X509Certificate) certificates[0]);
} catch (SSLException e) {
return false;
}
}
具體的驗證策略比較簡單,主要是檢查證書里的 IP 和 hostname 是否是我們的目標地址
5.4.2. 證書鎖定(Certificate Pinner)
到了該階段再沧,證書已經(jīng)被信任尼夺,是屬于平臺的可信任證書授權(quán)機構(gòu)(CA)的。但是這個會受到證書頒發(fā)機構(gòu)的攻擊,比如 2011 DigiNotar 的攻擊淤堵。
所以寝衫,還可以使用 CertificatePinner 來鎖定,哪些證書和 CA 是可信任的拐邪。
缺點慰毅,限制了服務(wù)端更新 TLS 證書的能力,所以證書鎖定一定要經(jīng)過服務(wù)端管理員的同意扎阶。
5.5. 完成
成功創(chuàng)建汹胃,保存這些信息:
- Socket,安全的連接东臀。
- Handshake着饥,握手信息。
- Protocol惰赋,使用的 HTTP 協(xié)議宰掉。
后面和服務(wù)端的交互,都會被 TLS 過程中協(xié)商好的對稱密鑰進行加密赁濒。
6. 應(yīng)用實例
6.1. 信任所有證書
- 跳過系統(tǒng)檢驗轨奄,不再使用系統(tǒng)默認的 SSLSocketFactory
- 自定義 TrustManager,信任所有證書
X509TrustManager trustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{trustManager}, null);
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, trustManager)
.build();
Request request = new Request.Builder()
.url("https://kyfw.12306.cn/otn/")
.build();
Call call = client.newCall(request);
Response response = call.execute();
Logger.d("response " + response.code());
response.close();
6.2. 信任自簽名證書
還是以 12306 來進行測試拒炎,先從官網(wǎng)上下載證書 srca.cer
- 將自簽名證書挪拟,比如 12306 的 srca.cer,保存到 assets
- 讀取自簽名證書集合击你,保存到 KeyStore 中
- 使用 KeyStore 構(gòu)建 X509TrustManager
- 使用 X509TrustManager 初始化 SSLContext
- 使用 SSLContext 創(chuàng)建 SSLSocketFactory
// 獲取自簽名證書集合玉组,由證書工廠管理
InputStream inputStream = HttpsActivity.this.getAssets().open("srca.cer");
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Collection<? extends java.security.cert.Certificate> certificates = certificateFactory.generateCertificates(inputStream);
if (certificates.isEmpty()) {
throw new IllegalArgumentException("expected non-empty set of trusted certificates");
}
// 將證書保存到 KeyStore 中
char[] password = "password".toCharArray();
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, password);
int index = 0;
for (Certificate certificate : certificates) {
String certificateAlias = String.valueOf(index++);
keyStore.setCertificateEntry(certificateAlias, certificate);
}
// 使用包含自簽名證書的 KeyStore 構(gòu)建一個 X509TrustManager
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, password);
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:"
+ Arrays.toString(trustManagers));
}
// 使用 X509TrustManager 初始化 SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{trustManagers[0]}, null);
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustManagers[0])
.build();
Request request = new Request.Builder()
.url("https://kyfw.12306.cn/otn/")
.build();
Call call = client.newCall(request);
Response response = call.execute();
Logger.d("response " + response.code());
response.close();
6.3. 自定義TLS連接規(guī)格
比如使用三個安全級別很高的密碼套件,并且限制 TLS 版本為 1_2
ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2)
.cipherSuites(
CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256)
.build();
OkHttpClient client = new OkHttpClient.Builder()
.connectionSpecs(Collections.singletonList(spec))
.build();
該連接規(guī)格的配置是否能夠生效果漾,還需要和 SSLSocket 的支持情況取交集球切,SSLSocket 不支持也就用不了
所以這三個密碼套件只能在 Android 5.0 以上的機子生效了
6.4. 使用證書鎖定
比如鎖定了指定 publicobject.com 的證書谷誓。
pin 的取值為绒障,先對證書公鑰信息使用 SHA-256 或者 SHA-1 取哈希,然后進行 Base64 編碼捍歪,再加上 sha256 或者 sha1 的前綴户辱。
這樣 publicobject.com 只能使用指定公鑰的證書了,安全性進一步提高糙臼,但靈活性降低:
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
.add("publicobject.com", "sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=")
.add("publicobject.com", "sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=")
.add("publicobject.com", "sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=")
.build();
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build();