OpenSSL证书认证过程

游戏服务端这块,之前是很少用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连接分为二种:

  1. 单向认证
    客户端认证服务端,服务端不认证客户端,这时服务端需要一个证书,客户端不需要。网站的HTTPS认证通常属于这一类,即网站的内容是公开的,它并不在意谁来访问这些内容,因此无需校验客户端。但对客户端而言,它要保证所访问的网站是正确的网站,而不是被劫持修改、假冒的钓鱼网站,因此需要校验服务端。

  2. 双向认证
    客户端认证服务端,服务端也需要认证客户端,这时候服务端和客户端都需要有证书。假如一个员工下班后,需要在家登录公司的内部管理系统,那这时候,服务端需要确认登录的用户属于自己公司的员工,就需要校验客户端。

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去加载。如果用的是自签证书,则是加载自签根证书而不是系统根证书。它也不会去做CRLOCSP的检测,但你可以用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 没有校验通过,后续再看看是什么问题。

原文地址:https://www.cnblogs.com/coding-my-life/p/13910569.html