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 |
站点组织名 | Lynch |
站点DNS主机名 | lynch-lee.me |
站点公钥 | xxxx |
证书颁发者 | RSA Data Security |
数字签名 | xxxx |
服务端把证书(内含服务端的公钥)发给客户端,客户端使用颁布证书的机构的公钥来解密,检查数字签名,取出公钥。取出服务端的公钥,将后面请求用的对称密钥 X 传递给服务端,后面就用该密钥进行加密传输信息
3 TLS 原理
HTTPS 是在 HTTP 和 TCP 之间加了一层 TLS,这个 TLS 协商了一个对称密钥来进行 HTTP 加密
img_https.png同时,SSL/TLS 不仅仅可以用在 HTTP,也可以用在 FTP,Telnet 等应用层协议上
SSL/TLS 实际上混合使用了对称和非对称密钥,主要分成这几步
- 使用非对称密钥建立安全的通道
- 客户端请求 Https 连接,发送可用的 TLS 版本和可用的密码套件
- 服务端返回证书,密码套件和 TLS 版本
- 用安全的通道产生并发送临时的随机对称密钥
- 生成随机对称密钥,使用证书中的服务端公钥加密,发送给服务端
- 服务端使用私钥解密获取对称密钥
- 使用对称密钥加密信息,进行交互
简单的过程如下
TLS 握手详细如下
SSL Messages
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 复用,创建新的连接过程如下
-
private void buildConnection(int connectTimeout, int readTimeout, int writeTimeout,
-
ConnectionSpecSelector connectionSpecSelector) throws IOException {
-
connectSocket(connectTimeout, readTimeout);
-
establishProtocol(readTimeout, writeTimeout, connectionSpecSelector);
-
}
connectSocket
三次握手,创建 TCP 连接
establishProtocal
在 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 创建。会配置
如果 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 会先使用现代的规格进行连接,如果失败会采用回退策略选择下一个
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 来完成配置
-
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
-
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();