本文主要关注Https的两个核心问题:Https如何加密,以及Https如何保证安全
Https加密过程
Https加密过程直接用下面这张图可以加以说明:
Https加密有三个关键点:
-
Https传输过程中用到了对称加密和非对称加密。对称加密:通信双方采用相同密钥进行加解密;非对称加密:传输数据采用公钥加密,必须采用公钥对应的私钥才能解密。
对称加密:encrypt(明文,秘钥) = 密文 decrypt(密文,秘钥) = 明文
非对称加密:
encrypt(明文,公钥) = 密文 decrypt(密文,私钥) = 明文
Https在握手过程中采用非对称加密协定双方数据传输中使用的密钥,具体过程如图示,Client端拿到Server端的公钥后,生成一个随机密钥,密钥采用公钥加密后传给Server端,Server可以采用私钥解密,至此,Client Server双方都持有了新的数据传输密钥;
实际的数据传输采用了新的实时生成的密钥进行加密传输,这种数据传输属于对称加密
Https如何保证安全
Https是在Http基础上进行数据传输的协议,其本质是Http层上面加了一个安全层,称之为TLS。TLS也是SSL的升级版。其主要提供三个基本服务:
- 加密
- 身份验证
- 消息完整性校验
加密
详细加密过程在第一节中讲到,通过这种机制保证了握手阶段密钥的协定,通过新的密钥保证了数据传输过程中的安全
身份验证
在TLS握手过程中服务端会提供给客户端它的证书。这个证书可不是随意生成的,而是通过指定的权威机构申请颁发的。服务端如果能够提供一个合法的证书,说明这个服务端是合法的,可以被信任。身份验证过程就是证书链的验证过程。
- 客户端获取到了站点证书,拿到了站点的公钥;
- 要验证站点可信后,才能使用其公钥,因此客户端找到其站点证书颁发者的信息;
- 站点证书的颁发者验证了服务端站点是可信的,但客户端依然不清楚该颁发者是否可信;
- 再往上回溯,找到了认证了中间证书商的源头证书颁发者。由于源头的证书颁发者非常少,我们浏览器之前就认识了,因此可以认为根证书颁发者是可信的;
- 一路倒推,证书颁发者可信,那么它所颁发的所有站点也是可信的,最终确定了我们所访问的服务端是可信的;
- 客户端使用证书中的公钥,继续完成TLS的握手过程。
Https中间人攻击与防范
所谓中间人攻击,指的是整个网络请求被中间人所劫持,Client信任了中间人证书,传输请求被中间人接管,中间人将请求解析之后重新发送给Server端。对于Server端来讲,中间人是实际请求的Client端,对于Client端来讲,中间人是实际请求的Server端。
Charles如何抓HTTPS包,回想一下这个过程,本质就是中间人攻击方式。其核心就是将私有CA签发的数字证书安装到手机中并且作为受信任证书保存,然后Charles接管整个传输过程,实现对数据的完全掌握。
中间人攻击的原因在于:没有对服务端证书及域名做校验或者校验不完整。
App中防范中间人攻击主要要考虑两个地方:
- 网络请求中的证书、域名强校验,针对安全性要求比较高的APP,如银行类App,可以采用预制证书的方式,只有服务端证书和本地证书完全一致才能进行网络请求,这种方式还需要考虑到证书过期如何更新问题,具体可以通过网络层面的封装来解决;除了预制证书,还可以考虑对证书和域名的强校验来保证安全性;
- WebView中的Https安全问题:WebViewClient中的重载方法, 如果直接忽略SSL错误,可采用handler.proceed();继续加载网络,安全的措施是在收到错误之后强校验证书,再决定是否加载网页
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
String channel = "";
ApplicationInfo appInfo = null;
try {
appInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
channel = appInfo.metaData.getString("TD_CHANNEL_ID");
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
if (!TextUtils.isEmpty(channel) && channel.equals("play.google.com")) {
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
String message = context.getString(R.string.ssl_error);
switch (error.getPrimaryError()) {
case SslError.SSL_UNTRUSTED:
message = context.getString(R.string.ssl_error_not_trust);
break;
case SslError.SSL_EXPIRED:
message = context.getString(R.string.ssl_error_expired);
break;
case SslError.SSL_IDMISMATCH:
message = context.getString(R.string.ssl_error_mismatch);
break;
case SslError.SSL_NOTYETVALID:
message = context.getString(R.string.ssl_error_not_valid);
break;
}
message += context.getString(R.string.ssl_error_continue_open);
builder.setTitle(R.string.ssl_error);
builder.setMessage(message);
builder.setPositiveButton(R.string.continue_open, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
handler.proceed();
}
});
builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
handler.cancel();
}
});
final AlertDialog dialog = builder.create();
dialog.show();
} else {
handler.proceed();
}
}