目錄:
- 概述
- 基礎
2.1. 加密
2.2. 數字簽名
2.3. 數字證書 - TLS 原理
- 主要的類和接口
4.1. JDK
4.2. OkHttp - 源碼分析
5.1. 創建安全 Socket
5.2. 配置
5.3. 握手
5.4. 驗證
5.5. 完成 - 應用實例
6.1. 信任所有證書
6.2. 信任自簽名證書
6.4. 自定義 TLS 連接規格
6.5. 使用證書鎖定 - 資料
1. 概述
TLS 是進行 HTTPS 連接的重要環節,通過了 TLS 層進行協商,后續的 HTTP 請求就可以使用協商好的對稱密鑰進行加密
SSL 是 Netscape 開發的專門用來保護 Web 通訊,目前版本為 3.0。TLS 是 IETF 制定的新協議,建立在 SSL 3.0 之上。所以 TLS 1.0 可以認為是 SSL 3.1
TLS(Transport Layer Security Protocol) 協議分為兩部分
- TLS 記錄協議
- TLS 握手協議
2. 基礎
2.1. 加密
2.1.1. 對稱密鑰加密
編碼和解碼使用同一個密鑰,e = d
加密算法有
- DES
- Triple-DES
- RC2
- RC4(在 OkHttp 2.3 已經下降支持)
位數越多,枚舉攻擊花費的時間越長
痛點:發送者和接收者建立對話前,需要一個共享密鑰
2.1.2. 非對稱密鑰加密
兩個密鑰,一個加密,一個解密。私鑰持有,公鑰公開
- RSA
破解私鑰的難度相當于對極大數進行因式分解
RSA 加密系統中,D 和 E 會相互抵消
E(D(stuff)) = stuff
D(E(stuff)) = stuff
所以具體哪個是私鑰,哪個是公鑰是由用戶選擇的
2.2 數字簽名
加了密的校驗和
- 證明是原作者,只有原作者可以私鑰來進行加密
- 證明沒有篡改,中途篡改校驗和就不再匹配
校驗和使用摘要算法生成,比如 MD5,SHA
2.3. 數字證書
受信任組織擔保的用戶或公司的信息,沒有統一的標準
服務端大部分使用 x509 v3 派生證書,主要信息有
字段 | 舉例 |
---|---|
證書序列號 | 12:34:56:78 |
證書過期時間 | Wed,Sep 17,2017 |
站點組織名 | StevenLee |
站點DNS主機名 | steven-lee.me |
站點公鑰 | xxxx |
證書頒發者 | RSA Data Security |
數字簽名 | xxxx |
服務端把證書(內含服務端的公鑰)發給客戶端,客戶端使用頒布證書的機構的公鑰來解密,檢查數字簽名,取出公鑰。取出服務端的公鑰,將后面請求用的對稱密鑰 X 傳遞給服務端,后面就用該密鑰進行加密傳輸信息
3. TLS 原理
HTTPS 是在 HTTP 和 TCP 之間加了一層 TLS,這個 TLS 協商了一個對稱密鑰來進行 HTTP 加密
同時,SSL/TLS 不僅僅可以用在 HTTP,也可以用在 FTP,Telnet 等應用層協議上。
SSL/TLS 實際上混合使用了對稱和非對稱密鑰,主要分成這幾步:
使用非對稱密鑰建立安全的通道。
- 客戶端請求 Https 連接,發送可用的 TLS 版本和可用的密碼套件
- 服務端返回證書,密碼套件和 TLS 版本
用安全的通道產生并發送臨時的隨機對稱密鑰。
- 生成隨機對稱密鑰,使用證書中的服務端公鑰加密,發送給服務端
- 服務端使用私鑰解密獲取對稱密鑰
使用對稱密鑰加密信息,進行交互。
簡化后的流程圖如下:
詳細的流程圖如下:
4. 主要的類和接口
4.1. JDK
主要由 JDK 的 java.security,javax.net 和 javax.net.ssl 提供的
- SSLSocketFactory
- SSLSocket
- SSLSession
- TrustManager
- X509TrustManager
- Certificate
- X509Certificate
- HostNameVerifier
核心類的關系圖
4.2. OkHttp
- RealConnection
- ConnectionSpecSelector
- ConnectionSpec
- CipherSuite
- CertificatePinner
5. 源碼分析
連接的所有實現,在 RealConnection 中。如果沒有從 ConnectionPool 復用,創建新的連接過程,見 RealConnection.buildConnection
:
private void buildConnection(int connectTimeout, int readTimeout, int writeTimeout, ConnectionSpecSelector connectionSpecSelector) throws IOException {
connectSocket(connectTimeout, readTimeout);
establishProtocol(readTimeout, writeTimeout, connectionSpecSelector);
}
connectSocket ,三次握手,創建 TCP 連接。
establishProtocol ,在 TCP 連接的基礎上,開始根據不同版本的協議,來完成連接過程。主要有 HTTP/1.1,HTTP/2 和 SPDY 協議。如果是 HTTPS 類型的,則開始 TLS 建聯。
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;
}
...
}
只關注 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. 創建安全 Socket
這里的安全 Socket 就是 SSLSocket,是握手成功后的 TCP Socket 進行的封裝。
如果 SSLSocketFactory 沒有自定義配置的話,會使用 OkHttp 的默認創建。比如在 OkHttpClient 中有這樣的代碼來構造默認的 SSLSocketFactory
X509TrustManager trustManager = systemDefaultTrustManager();
this.sslSocketFactory = systemDefaultSslSocketFactory(trustManager);
this.certificateChainCleaner = CertificateChainCleaner.get(trustManager);
systemDefaultSslSocketFactory 方法使用 SSLContext 來構造 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.
}
}
這樣就是用了系統默認的 X509TrustManager。
該 SSLSocketFactory 為系統 SDK 提供,包括它生產的 SSLSocket,所以和系統平臺版本強相關,底層為 OpenSSL 庫。對 TLS 版本的支持情況不一樣,接口也有所不同。
SSLSocket 配置信息有兩大類:
- 支持的 TLS 協議
- 支持的密碼套件(CipherSuite)
OkHttp 不包括自己的 SSL/TLS 庫,所以 SSLSocket 使用 Android 提供的標準 SSLSocket
5.2. 配置
經過上面創建過程后,SSLSocket 已經有了一些操作系統提供的默認配置。但不完全安全,OkHttp 會有自己的連接規格,來過濾掉過時的 TLS 版本和弱密碼套件。
OkHttp 內置了三套規格,
- ConnectionSepc.MODEN_TLS, 現代的 TLS 配置。
- ConnectionSpec.COMPATIABLE_TLS,不是現代的,但安全 TLS 配置。
- ConnectionSpec.CLEARTEXT, 不安全的 TLS 配置。
這三套規格跟著版本走,例如,在OkHttp 2.2,下降支持響應POODLE攻擊的SSL 3.0。而在OkHttp 2.3 下降的支持RC4
所以與桌面Web瀏覽器,保持最新的OkHttp是保持安全的最好辦法
OkHttp 還會通過反射的方式,來對 SSLSocket 的 TLS 的擴展功能進行配置
- SNI 和 Session tickets
- ALPN
OkHttp 會先使用現代的規格(ConnectionSepc.MODEN_TLS)進行連接,如果失敗會采用回退策略選擇下一個。
5.2.1. TLS 連接規格選擇
該步驟選擇適合客戶端的 TLS連接規格。一個很大的作用,就是盡可能地使用高版本的 TLS,和最新的密碼套件,來提供最安全的連接。
連接規格都封裝在 ConnectionSpec 中,主要內容就是 TLS 版本和密碼套件
連接規格選擇的策略由 ConnectSpecSelector 進行,默認使用 OkHttp 的三套規格
最后會調用 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 方法中,會對選擇好的規格,和 SSLSocket 可用的配置取中交集,過濾掉那些不安全的低版本的 TLS 和弱密碼套件和 SSLSocket 不支持的配置。
這個階段后,SSLSocket 中的一些不安全的 TLS 版本和弱密碼套件就被過濾了,將會使用 OkHttp 配置規范中認為的安全版本和強密碼套件開始正式的握手過程。
5.2.2. TLS 連接規格回退
最開始會嘗試現代的 TLS 規格,如果不支持的話,會有回退策略(Fallback Strategy),回退到非現代但安全的 TLS 規格
回退策略由 RealConnection 和 ConnectSpecSelector 一起配合提供。
比如它會先選擇最新的 ConnectionSpec.MODEN_TLS,不支持的話,再更換為 ConnectionSpec.COMPATIABLE_TLS,最后選擇 ConnectionSpec.CLEARTEXT。
策略很簡單,就是連接失敗的時候,更換下一套規范重新進行連接。
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 采用發射的方式嘗試加載擴展,讓這些機型的擴展配置生效。
如果 ConectionSpec 支持 TLS 的擴展,這里還會配置 SNI,session tickets 和 ALPN。
5.3. 握手
調用 SSLSocket.startHandShake
開始進行握手:
// Force handshake. This can throw!
sslSocket.startHandshake();
Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());
這里客戶端正式向服務端發出數據包,內容為可選擇的密碼和請求證書。服務端會返回相應的密碼套件,tls 版本,節點證書,本地證書等等,然后封裝在 Handshake 類中
主要內容有:
- CipherSuite, 密碼套件。
- TlsVersion, TLS 版本。
- Certificate[] peerCertificates, 站點的證書。
- Certificate[] localCertificates, 本地的證書。一些安全級別更高的應用,會使用雙向的證書認證。
該過程中,SSLSocket 內部會對服務端返回的 Certificate 進行判斷,是否是可信任的 CA 發布的。如果不是的話,會拋出異常
5.4. 驗證
到了這一步,服務端返回的證書已經被系統所信任,也就是頒發的機構 CA 在系統的可信任 CA 列表中了。但是為了更加安全,還會進行以下兩種驗證。
5.4.1. 站點身份驗證
使用 HostnameVerifier 來驗證 host 是否合法,如果不合法會拋出 SSLPeerUnverifiedException
默認的實現是 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)
到了該階段,證書已經被信任,是屬于平臺的可信任證書授權機構(CA)的。但是這個會受到證書頒發機構的攻擊,比如 2011 DigiNotar 的攻擊。
所以,還可以使用 CertificatePinner 來鎖定,哪些證書和 CA 是可信任的。
缺點,限制了服務端更新 TLS 證書的能力,所以證書鎖定一定要經過服務端管理員的同意。
5.5. 完成
成功創建,保存這些信息:
- Socket,安全的連接。
- Handshake,握手信息。
- Protocol,使用的 HTTP 協議。
后面和服務端的交互,都會被 TLS 過程中協商好的對稱密鑰進行加密。
6. 應用實例
6.1. 信任所有證書
- 跳過系統檢驗,不再使用系統默認的 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 來進行測試,先從官網上下載證書 srca.cer
- 將自簽名證書,比如 12306 的 srca.cer,保存到 assets
- 讀取自簽名證書集合,保存到 KeyStore 中
- 使用 KeyStore 構建 X509TrustManager
- 使用 X509TrustManager 初始化 SSLContext
- 使用 SSLContext 創建 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 構建一個 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連接規格
比如使用三個安全級別很高的密碼套件,并且限制 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();
該連接規格的配置是否能夠生效,還需要和 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();