随着互联网整体的发展,https 也被越来越多的应用。甚至苹果去年还曾经放言要强制所有的 app 都使用 https,可见在如今的互联网它的重要性。前面的文章说了 OSI 七层模型,https 可以保证数据在传输层是安全的。那么如果使用了 https,传输的数据还需要做二次加密吗?是否有过度设计的感觉,让我们来分析是否有这个必要性。
目录:
1. 何为 https
上一篇文章讲了网络篇 - http协议从入门到精通,http 协议是基于 tcp协议的,https 协议也是一样。
https 与 http 都是 OSI 模型中应用层协议,而唯一不同的就是 https 中在 http 的应用层和 TCP/IP 中间增加了一个 SSL /TLS 层,主要用来对数据进行加解密,保证数据的传输的正确性。
http 协议最大的问题就是,它是明文传输的。其中一个最突出的问题就是信息劫持。什么叫做劫持呢?简单来说,以我们现在所使用的互联网架构,在发送和接受一条网络请求时,并不是直接发送到目标主机的,而是要经过中间很多个路由器,才能到达主机。因为 http 是明文传输的,那么所有中间经过的路由器都可以看到你的信息,还可以修改你的信息。比如中间某个路由器篡改了数据,然后再把数据转发给下一个路由器,那么最后送达数据肯定是不正确的。
如果坚持用 http,如何解决上面的问题呢?肯定就是对数据做加密然后再传输。https 正是基于这个原理,但是它做的很完善。先来从加密说起,常用的加密算法分为对称加密和非对称加密。对称加密就是用一个秘钥进行加密和解密,非对称加密就是加密的时候用一个秘钥,解密的时候用另一个秘钥。这样来看是不是非对称加密会更安全一些呢? 那为什么还会有对称加密的存在呢? 因为性能,非对称加密要比对称加密更加消耗计算性能。
使用加密算法可以加强传输过程的安全性,但单纯的加密还是不够的。为什么这么说呢,如果你开发的是一个 app 客户端,这样的设计勉强够用,在你的服务器和客户端上面用同样的秘钥对信息加密和解密就可以让传输安全得到很大的提升,只要做到秘钥保存的安全性。但如果你开发的是一个 web 程序,那这种机制几乎就没办法使用了。因为访问你服务的是浏览器,而不是受你控制的客户端,你没有办法让一个通用浏览器来适配你服务器的特殊加密规则。所以这时候就需要一个更加标准化的传输加密协议,也就是 https 了。
2. 证书
数据传输安全就是得保证数据在送达后的一致性和完整性,https 和 http 协议相比提供了:
互联网有太多的服务需要使用证书来验证身份,以至于客户端(操作系统或浏览器等)无法内置所有证书,需要通过服务端将证书发送给客户端。证书包含以下这些内容:
防止中间人攻击。
先来看看什么是数字签名 - Digital Signature:
将一段文本通过哈希和私钥加密处理后生成数字签名,常用的哈希算法有MD、SHA、MAC。
那么接收方如何验证消息就是发送方发生的呢?发送方将文本数据和数字签名一起发生给接收方,接收方收到文本数据和数字签名,同时接收方已知发送方的公钥,所以可以这样验证:
这样就能验证是不是发送方发送的数据,并且验证数据是否完整与未被篡改。
前提是接收方如何安全地拿到发送方的公钥呢?接收者对手中的公钥的真实性并无法做出判断,例如A给B通信,A生成了一个公密钥对,A拥有密钥,B拥有公钥,A使用私钥加密,B使用A的公钥解密。现在C自己来生成一个公私钥对,并且使用生成的公钥将B手上持有的A的公钥替换掉,然后C使用生成的私钥就可以给B发消息,此时B还以为跟A在通信,其实他在跟C通信。
这时就有另一个概念:证书颁发机构(Certificate Authority,简称CA)。
数字证书是由证书签证机关(CA)签发的对用户的公钥的认证。通俗地讲,数字证书就是个人或单位在Internet的身份证。数字证书主要包括三方面的内容:证书所有者的信息、证书所有者的公钥和证书颁发机构的签名。
那么如何对我们的公钥进行认证呢?向一个证书发布机构CA申请证书,我们需要将证书的信息告诉给CA,CA就会将这些信息写到证书中去,然后使用自己的私钥对证书进行加密,这样我们就可以将这种证书投入使用,当将这个证书发生给对方之后,对方会在自己操作系统中受信任的发布机构的证书中去找CA的证书,如果找不到,那说明证书可能有问题,程序会给出一个错误信息。如果在系统中找到了CA的证书,那么应用程序就会从证书中取出CA的公钥,这样使用这个公钥就可以对接收到的数字证书进行解密,解密之后就可以拿到我们自己的公钥信息。
文字可能比较难理解,我画了张图:
3. https 证书传递流程
整个过程就是证书的传递、校验,客户端私钥的生成、传递,用非对称加密传递秘钥,用对称加密传输数据。
4. https 传输的数据是否需要二次加密
https 能保证的是连接的安全,当我们打开网站的时候,是需要和服务器进行通信的,那么 https 能够保证在通信时数据传输是安全的。
如果我们打开一个网页,在网址的前面有标明"https"的话,那么就说明这个网站有证书并且有进行加密的操作。但是,不少钓鱼网页其实也可以使用 https,这就说明了 https 并不能保证网站本身是安全的。举个例子,如果你在一个网页上输入账号密码的话,使用 https 的网页能够保证你的账号密码在传到网站服务器的过程中不会被其他的东西干扰或者盗取,但是,你输入的账号密码却有可能被你正在访问的这个网页盗用。
再举个例子:移动端手机种了木马,终端被攻击了,那么对于传输通道进行加密就毫无意义,试想人家都跑到你家里面去了,所有的东西在内存跟硬盘里面都有,再进行网络监听毫无意义。
所以 https 本身是安全的,至少在网络传输过程中,能够保证传输数据的完整性和一致性。但是并不代表它的宿主是安全的,所以如果是金融类,支付类等对安全等级要求很高的软件,最好进行二次加密。
5. Android 中 https 反劫持
在密码学和计算机安全领域中,中间人攻击 (Man-in-the-middle attack,通常缩写为 MITM) 是指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为它们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者完全控制。在中间人攻击中,攻击者可以拦截通讯双方的通话并插入新的内容。
Android https 中间人攻击漏洞源于:
攻击者可通过中间人攻击,盗取账户密码明文、聊天内容、通讯地址、电话号码以及信用卡支付信息等敏感信息,甚至通过中间人劫持将原有信息替换成恶意链接或恶意代码程序,以达到远程控制、恶意扣费等攻击意图。有大量存在 https 证书不校验漏洞,例如国内绝大部分 Android app 存在信任所有证书漏洞、亚马逊最新官方 Android 版存在一处信任所有证书漏洞、Yahoo 雅虎在国内访问遭遇 SSL 中间人攻击、携程旅游网最新 Android 客户端 https 未校验证书导致 https 通信内容完全被捕获。
(1) 中间人攻击漏洞位置
X509TrustManager 、HostnameVerifier 、 setHostnameVerifier (X509HostnameVerifier hostnameVerifier)。
(2) 漏洞触发前提条件
(3) 漏洞原理
由于客户端没有校验服务端的证书,因此攻击者就能与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为它们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者完全控制。在中间人攻击中,攻击者可以拦截通讯双方的通话并插入新的内容。
客户端不校验 SSL 证书 (包含签名 CA 是否合法、域名是否匹配、是否自签名证书、证书是否过期) 包含以下几种编码错误情况:
(1) 自实现的不校验证书的 X509TrustManager 接口 (其中的 checkServerTrusted() 方法实现为空,即不检查服务器是否可信)
private static class UnSafeTrustManager implements 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[]{};
}
}
(2) 不检查站点域名与站点证书的域名是否匹配
private class UnSafeHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
}
(3) 接受任意域名
SSLSocketFactory sf;
……
sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
校验签名 CA 是否合法、证书是否是自签名、主机域名是否匹配、证书是否过期等。
实现 checkServerTrusted():
// certificate 为客户端预埋的证书,可以放在 assets 或 raw 文件夹下
public static X509TrustManager getX509TrustManager(final Certificate certificate) {
return new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
for (X509Certificate cert : chain) {
// 校验证书没有过期
cert.checkValidity();
// 校验证书公钥
try {
cert.verify(certificate.getPublicKey());
} catch (NoSuchAlgorithmException e) {
LogUtils.printStackTrace(e);
} catch (InvalidKeyException e) {
LogUtils.printStackTrace(e);
} catch (NoSuchProviderException e) {
LogUtils.printStackTrace(e);
} catch (SignatureException e) {
LogUtils.printStackTrace(e);
}
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
}
实现 HostnameVerifier 的 verify() 方法:
httpsURLConnection.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
return hv.verify("*.waterhole.io", session);;
}
});
另外一种写法证书锁定,直接用预埋的证书来生成 TrustManger,过程如下:
public final class SecureSocketSslContextFactory {
/** 协议 tls 3.0 */
private static final String PROTOCOL = "TLS";
/** 算法 x509 */
private static final String ALGORITHM = "X509";
private static final SSLContext CLIENT_CONTEXT;
static {
try {
Security.setProperty("jdk.tls.disabledAlgorithms", "");
System.setProperty("sun.security.ssl.allowUnsafeRenegotiation", "true");
synchronized (SecureSocketSslContextFactory.class) {
// client key store
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream inputStream = clientAsInputStream();
keyStore.load(inputStream, getKeyStorePassword());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(ALGORITHM);
keyManagerFactory.init(keyStore, getCertificatePassword());
// client trust manager
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(ALGORITHM);
trustManagerFactory.init(keyStore);
// client context
SSLContext clientContext = SSLContext.getInstance(PROTOCOL);
clientContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
CLIENT_CONTEXT = clientContext;
}
} catch (Exception e) {
throw new Error("Failed to initialize the client-side SSLContext", e);
}
}
public SecureSocketSslContextFactory() {
throw new RuntimeException("SecureSocketSslContextFactory stub!");
}
public static SSLContext getClientContext() {
return CLIENT_CONTEXT;
}
}
使用时:
httpsURLConnection.setSSLSocketFactory(CLIENT_CONTEXT.getSocketFactory());
6. Webview 的 https 安全
目前很多 app 都用 webview 加载 h5 页面,如果服务端采用的是可信 CA 颁发的证书,在 webView.setWebViewClient(webviewClient) 时重载 WebViewClient 的 onReceivedSslError() ,如果出现证书错误,直接调用handler.proceed() 会忽略错误继续加载证书有问题的页面,如果调用 handler.cancel() 可以终止加载证书有问题的页面,证书出现问题了,可以提示用户风险,让用户选择加载与否,如果是需要安全级别比较高,可以直接终止页面加载,提示用户网络环境有风险。