目录:
- 概述
- 基础
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();