本文主要關注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();
}
}