游戏服务端这块,之前是很少用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
没有校验通过,后续再看看是什么问题。