游戲服務端這塊,之前是很少用SSL的,畢竟游戲里的數據沒有什么保密的必要,登錄、充值也是傳輸簽名,不涉及密碼什么的。不過這幾年,HTTPS普及得比較快,H5游戲發展迅速。H5游戲是基於web的,和后端通信一般走websocket,加不加SSL其實對於游戲影響不大。但是不少平台都要求加SSL的,一是用戶通過瀏覽器玩游戲時,地址欄里有個鎖頭體驗還是好點,二是喪心病狂的黑產會劫持鏈接加上廣告,想象一下在玩游戲時,右下角彈個廣告是啥體驗。
雖說用上SSL,不過並不一定就要自己實現SSL,比如用Nginx做一層代理,在Nginx處理SSL,在游戲服務器處理邏輯。不過最近有時間,還是研究下SSL。這里不涉及如何用OpenSSL實現一個SSL鏈接,網上的例子已經太多。而是研究下證書的認證,即
瀏覽器地址欄里的這個鎖頭是怎么來的?它怎么判斷當前連接是安全?
這里以OpenSSL為例,簡單說下SSL的的建立過程。普通的socket通過accept/connect
來建立連接,然后用SSL_set_fd
把socket和OpenSSL關聯起來,接着調用SSL_do_handshake
來進行SSL握手,握手成功后,就可以通過SSL_read/SSL_write
來進行加密的數據通信。
那證書的認證在哪里處理?證書認證屬於SSL握手(SSL_do_handshake
)的一部分,SSL連接分為二種:
-
單向認證
客戶端認證服務端,服務端不認證客戶端,這時服務端需要一個證書,客戶端不需要。網站的HTTPS認證通常屬於這一類,即網站的內容是公開的,它並不在意誰來訪問這些內容,因此無需校驗客戶端。但對客戶端而言,它要保證所訪問的網站是正確的網站,而不是被劫持修改、假冒的釣魚網站,因此需要校驗服務端。 -
雙向認證
客戶端認證服務端,服務端也需要認證客戶端,這時候服務端和客戶端都需要有證書。假如一個員工下班后,需要在家登錄公司的內部管理系統,那這時候,服務端需要確認登錄的用戶屬於自己公司的員工,就需要校驗客戶端。
PS: 測試了下,不存在雙方都不需要證書的SSL連接。
現在使用的證書一般是x509標准證書。可以用openssl s_client -servername cnblogs.com -connect cnblogs.com:443 </dev/null 2>/dev/null | openssl x509 -text
來查看證書的內容,一般包括以下內容:
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
0d:2e:94:94:ec:65:e6:e6:4a:a7:a9:4d:1b:bf:8d:e4
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = RapidSSL RSA CA 2018
Validity
Not Before: Mar 6 00:00:00 2020 GMT
Not After : Mar 6 12:00:00 2021 GMT
Subject: CN = *.cnblogs.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:bd:39:42:6b:62:fc:e7:24:3d:23:f9:1a:db:c4:
d1:4e:bc:8c:d4:b6:71:54:11:b1:24:d4:0f:a1:fe:
1e:8d:a8:cb:81:fb:72:e7:fe:2c:c1:40:1d:1b:4c:
96:f3:28:3c:cf:ba:20:3c:d7:6d:d6:18:bf:7f:a9:
f8:3e:6a:a6:50:46:b1:1a:36:b7:81:6f:b8:81:f0:
6a:64:77:20:05:6e:7c:5a:17:6b:4f:f5:0d:f5:59:
3c:93:7c:50:22:95:a0:4a:3c:43:63:f2:28:81:a2:
4e:e1:41:7e:6c:c9:c7:9b:56:72:1a:ce:6c:b5:78:
f9:0f:62:14:9e:38:e4:f4:4e:e7:40:dc:de:fa:4f:
21:6e:9f:88:7e:d5:0b:58:f3:36:a4:2a:92:63:fb:
91:e8:93:86:3e:21:e5:df:8c:79:5e:03:e1:05:57:
f3:13:df:e7:8b:6f:a8:80:86:82:85:30:2b:21:f5:
e8:bb:25:ae:8a:26:17:46:d7:28:11:b5:e0:26:a4:
90:b8:2a:bb:44:27:59:4a:f3:40:ec:1e:78:58:ef:
83:c9:df:0a:55:bb:f4:de:25:2c:89:00:30:45:81:
db:f6:fc:46:b2:03:e9:e9:47:97:c8:0e:a8:a0:55:
81:c5:21:c6:e0:e7:8b:77:c9:e5:28:c2:8e:09:12:
c6:f9
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Authority Key Identifier:
keyid:53:CA:17:59:FC:6B:C0:03:21:2F:1A:AE:E4:AA:A8:1C:82:56:DA:75
X509v3 Subject Key Identifier:
4A:82:6C:3C:60:F8:58:F5:FB:18:0B:82:65:8D:9F:3E:ED:18:13:F7
X509v3 Subject Alternative Name:
DNS:*.cnblogs.com, DNS:cnblogs.com
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
X509v3 CRL Distribution Points:
Full Name:
URI:http://cdp.rapidssl.com/RapidSSLRSACA2018.crl
X509v3 Certificate Policies:
Policy: 2.16.840.1.114412.1.2
CPS: https://www.digicert.com/CPS
Policy: 2.23.140.1.2.1
Authority Information Access:
OCSP - URI:http://status.rapidssl.com
CA Issuers - URI:http://cacerts.rapidssl.com/RapidSSLRSACA2018.crt
X509v3 Basic Constraints:
CA:FALSE
CT Precertificate SCTs:
Signed Certificate Timestamp:
Version : v1 (0x0)
Log ID : F6:5C:94:2F:D1:77:30:22:14:54:18:08:30:94:56:8E:
E3:4D:13:19:33:BF:DF:0C:2F:20:0B:CC:4E:F1:64:E3
Timestamp : Mar 6 08:06:35.594 2020 GMT
Extensions: none
Signature : ecdsa-with-SHA256
30:46:02:21:00:DA:3B:5E:67:CF:E7:2E:83:66:5F:12:
52:F9:36:C1:51:00:04:2F:D6:69:A6:F2:0E:90:43:47:
F6:28:09:C9:1C:02:21:00:8D:9A:5C:F3:42:87:47:61:
0C:0A:63:80:5C:DB:F9:EC:E7:DC:EF:93:04:46:9F:3F:
B0:49:29:33:FA:84:D6:FC
Signed Certificate Timestamp:
Version : v1 (0x0)
Log ID : 44:94:65:2E:B0:EE:CE:AF:C4:40:07:D8:A8:FE:28:C0:
DA:E6:82:BE:D8:CB:31:B5:3F:D3:33:96:B5:B6:81:A8
Timestamp : Mar 6 08:06:35.535 2020 GMT
Extensions: none
Signature : ecdsa-with-SHA256
30:44:02:20:0E:4B:F7:24:22:D2:01:7A:9D:0E:30:76:
21:37:0A:42:2C:7B:0C:A2:62:0C:7A:74:07:9B:7E:CB:
23:E1:EF:AD:02:20:14:6D:B0:C9:2E:C2:97:42:78:F6:
0B:DE:A6:40:FE:20:09:7E:A0:A6:12:0A:48:F3:1B:65:
7E:05:A4:A9:2F:90
Signature Algorithm: sha256WithRSAEncryption
97:19:57:2b:1a:48:7a:47:f3:52:f6:cb:9f:cc:8c:b2:ab:68:
61:cd:6d:c4:0a:f4:a9:4c:6a:cc:1d:e7:f4:b6:da:47:4e:e6:
d5:11:ef:a5:80:73:e4:25:c0:42:71:77:53:2e:11:04:d8:4a:
9f:39:43:54:8a:f0:71:e3:18:49:79:25:73:ef:26:ff:d9:d2:
09:bb:e5:2d:8f:d1:60:d2:4c:55:6f:c4:3d:76:be:d1:49:ac:
89:7c:fe:63:71:ec:32:d9:c5:00:f6:5e:b1:7f:f4:5b:05:40:
a3:e0:95:6b:6d:7e:49:d5:0f:ee:45:9d:e1:4b:9d:55:c1:60:
2a:2b:23:5f:4f:78:cd:e2:dc:f7:af:bb:df:43:4e:b4:f2:72:
a4:1b:4b:15:28:e3:8f:67:e6:32:73:93:81:d9:be:bb:bd:e8:
8f:fe:e6:35:d0:ec:92:09:50:34:14:28:61:65:04:94:9a:3c:
c4:56:09:e4:bf:48:4e:93:82:bd:40:35:e8:0a:7b:32:46:73:
74:8d:0d:3b:5a:02:ff:17:be:e5:aa:65:92:3c:76:e2:f1:f6:
82:32:7b:d7:db:ed:2e:38:36:e3:63:5f:0e:d1:f3:c8:44:0a:
0a:5b:a1:bb:02:2e:de:e9:13:6b:68:5b:12:a8:60:a5:c8:c0:
40:4a:15:2d
我們選取幾個重要的點來說:
- 時間
Validity
Not Before: Mar 6 00:00:00 2020 GMT
Not After : Mar 6 12:00:00 2021 GMT
時間的校驗比較簡單,檢測時間在Validity的[Not Before, Not After]區間內即可,具體的實現可以看OpenSSL源碼int x509_check_cert_time(X509_STORE_CTX *ctx, X509 *x, int depth)
- 域名
X509v3 Subject Alternative Name:
DNS:*.cnblogs.com, DNS:cnblogs.com
證書里包含域名。連接建立的時候,肯定有對方的IP,可以查看對方的IP是否在域名列表里,在就合法,不在那就是非法,具體可以看OpenSSl源碼X509_check_host
的實現
- 認證鏈
Issuer: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = RapidSSL RSA CA 2018
Authority Information Access:
OCSP - URI:http://status.rapidssl.com
CA Issuers - URI:http://cacerts.rapidssl.com/RapidSSLRSACA2018.crt
Signature Algorithm: sha256WithRSAEncryption
97:19:57:2b:1a:48:7a:47:f3:52:f6:cb:9f:cc:8c:b2:ab:68:
61:cd:6d:c4:0a:f4:a9:4c:6a:cc:1d:e7:f4:b6:da:47:4e:e6:
d5:11:ef:a5:80:73:e4:25:c0:42:71:77:53:2e:11:04:d8:4a:
9f:39:43:54:8a:f0:71:e3:18:49:79:25:73:ef:26:ff:d9:d2:
09:bb:e5:2d:8f:d1:60:d2:4c:55:6f:c4:3d:76:be:d1:49:ac:
89:7c:fe:63:71:ec:32:d9:c5:00:f6:5e:b1:7f:f4:5b:05:40:
a3:e0:95:6b:6d:7e:49:d5:0f:ee:45:9d:e1:4b:9d:55:c1:60:
2a:2b:23:5f:4f:78:cd:e2:dc:f7:af:bb:df:43:4e:b4:f2:72:
a4:1b:4b:15:28:e3:8f:67:e6:32:73:93:81:d9:be:bb:bd:e8:
8f:fe:e6:35:d0:ec:92:09:50:34:14:28:61:65:04:94:9a:3c:
c4:56:09:e4:bf:48:4e:93:82:bd:40:35:e8:0a:7b:32:46:73:
74:8d:0d:3b:5a:02:ff:17:be:e5:aa:65:92:3c:76:e2:f1:f6:
82:32:7b:d7:db:ed:2e:38:36:e3:63:5f:0e:d1:f3:c8:44:0a:
0a:5b:a1:bb:02:2e:de:e9:13:6b:68:5b:12:a8:60:a5:c8:c0:
40:4a:15:2d
證書最核心的功能,就是用於識別對方的身份。那客戶端是怎么校驗這個證書是不是被修改過呢?畢竟數據是通過網絡下發的,別人可以攔截並替換成其他證書。
首先,申請證書的時候,需要提交一系列的材料,包括有效日期、公司名字等等之類的東西,然后頒發者(Issuer)會把這些東西通過算法(比如sha256)得到一份摘要(就是類似md5的一長串字符串),然后用自己的私鑰把這份摘要加密,得到簽名(就是上面Signature Algorithm
之后那個長長的字符串)。具體的算法可以看RFC 5280。
客戶端要校驗這個證書,就是把這個過程逆向。收到證書后,根據證書里頒發者提供的地址CA Issuers - URI:http://cacerts.rapidssl.com/RapidSSLRSACA2018.crt
下載頒發者的證書,然后取出頒發者證書里的公鑰(Subject Public Key Info
,注意是頒發者的不是當前證書的),用這個公鑰對當前證書的簽名進行解密,就會得到頒發時的那份摘要。
接着客戶端用頒發時一樣的算法,對當前證書的有效日期、公司名字...等數據計算一份摘要,和上面解密得到的摘要進行對比,如果一致,說明這個證書是經過頒發者認證的。
但是證書是經過頒發者認證的
並不能說明這個證書就是合法的,別人劫持數據包修改證書的時候,一樣可以修改頒發者的證書URL,這時候就需要繼續對頒發者的證書進行校驗。由於頒發者的證書是使用同樣的標准,重復上面的驗證過程即可。直到最后一個頒發者的時候,已經沒有更高級的頒發者了,他的證書稱為根證書。這時沒有下載證書的地址了,就需要根據頒發者的名字搜索根證書目錄,如果發現對應的證書,則取出證書的公鑰來校驗。
那這個根證書目錄
是從哪來呢?一個是系統自帶的,在安裝系統時就固定配置了一些證書,比如Debian下就是放在/etc/ssl/certs/ca-certificates.crt
,一些瀏覽器可能也自帶了一些根證書,在安裝瀏覽器的時候固定配置了證書。這些目錄一般是不更新的,因為公認頒發根證書機構就那幾個,基本不會變的。如果是需要添加一些自簽的證書,則可以手動添加。
- CRL & OCSP
X509v3 CRL Distribution Points:
Full Name:
URI:http://cdp.rapidssl.com/RapidSSLRSACA2018.crl
Authority Information Access:
OCSP - URI:http://status.rapidssl.com
在證書里一般還會包含這兩個地址,CRL(Certificate Revocation List)提供一個接口查詢哪些證書已經被注銷,一般是發錯了證書,但證書這個東西又沒法收回,只能通過這種方式注銷。OCSP(Online Certificate Status Protocol)可以實時查詢該證書是否還有效,證書是否被注銷也可以通過這個接口查詢。
上面是一般情況下證書的認證過程,但OpenSSL這個庫默認情況下不會做證書認證。在調用OpenSSl實現SSL連接時,我們可以通過SSL_CTX_set_verify
來指定是否校驗對方的證書
// 一個ctx可以給多個連接使用,因此一個證書就創建一個ctx就可以了
SSL_CTX *ctx = SSL_CTX_new(method);
if (!ctx)
{
ssl_error("new_ssl_ctx:can NOT create ssl content");
return -1;
}
// 指定了根ca證書路徑,說明需要校驗對方證書的正確性
if (ca)
{
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr);
/*加載CA FILE*/
if (SSL_CTX_load_verify_locations(ctx, ca, nullptr) != 1)
{
SSL_CTX_free(ctx);
ssl_error("load verify fail");
return -1;
}
}
當不調用SSL_CTX_set_verify
或者參數為SSL_VERIFY_NONE
時,表示不校驗對方的證書,即連接依然是SSL(數據還是經過加密),但對方的證書可能是無效的(自簽的、過期的),這還是有一些應用場景的,比如說H5游戲用自簽證書就可以防止別人劫持連接加廣告。
當參數為SSL_VERIFY_PEER
,在SSL握手時,將會校驗對方的證書。校驗過程是在SSL握手時進行的,如果校驗不通過,SSL握手將失敗(SSL_do_handshake
返回失敗)例如無法驗證根證書時
[T1CE10-28 10:24:28] error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed
[T1CE10-28 10:24:28] error:14094418:SSL routines:ssl3_read_bytes:tlsv1 alert unknown ca
那啟用校驗時,OpenSSL是不是就會自動完成上面所說的那些校驗呢?肯定不是的。OpenSSL作為一個庫它提供了一些接口,但並不會主動去做這些東西。例如它不會自動加載/etc/ssl/certs/ca-certificates.crt
下的根證書,需要調用SSL_CTX_load_verify_locations
去加載。如果用的是自簽證書,則是加載自簽根證書而不是系統根證書。它也不會去做CRL
和OCSP
的檢測,但你可以用X509_STORE_add_crl
來加載自己已下載的CRL列表,以及OCSP的實現
另外,對於SSL_CTX_set_verify
這個接口,我一直以為verify_callback
這個參數是自定義證書校驗函數。
typedef int (*SSL_verify_cb)(int preverify_ok, X509_STORE_CTX *x509_ctx);
void SSL_CTX_set_verify(SSL_CTX *ctx, int mode, SSL_verify_cb verify_callback);
If no special callback was set before, the default callback for the underlying ctx is used
然而在看原碼的時候,所謂的default callback是這樣的
// x509_vfy.c
static int null_callback(int ok, X509_STORE_CTX *e)
{
return ok;
}
這個callback的作用是每次校驗證書的時候,觸發一次回調,用於做一些自定義的操作,但是這個回調並不影響校驗結果的。真正的默認校驗函數在
// x59_vfy.c
/* verify the issuer signatures and cert times of ctx->chain */
static int internal_verify(X509_STORE_CTX *ctx)
{
// ...
}
可以用X509_STORE_CTX_set_verify_cb
來自定義校驗函數。
PS: 還有一個問題沒搞明白,那就是做證書鏈認證的時候,OpenSSL到底有沒有自動下載Issuer的證書
open verify
的實現
在程序里,通過設置ca文件為/etc/ssl/certs/ca-certificates.crt
后,postman-echo.com可以測試通過。但當我在瀏覽器里點擊https://postman-echo.com/get
的鎖頭-證書-詳細信息-導出后,用openssl verify -verbose -CAfile /etc/ssl/certs/ca-certificates.crt postman-echo.com
沒有校驗通過,后續再看看是什么問題。