使用 openSSL 验证我的自签名证书

Verifying my self-signed certificate with openSSL

我拥有服务器和客户的可执行文件。我想在它们之间建立安全的 TLS 连接。

我可以将我想要的任何内容嵌入到客户端可执行文件中,但我不确定如何验证我的客户端从服务器连接收到的自分配证书,即来自 SSL_get_peer_certificate 调用。

我读到证书只是 public 密钥,元数据部分用私钥签名。我可以通过将 public 密钥嵌入我的客户端应用程序来以某种方式验证服务器发送给我的证书确实包含正确签名的所有元数据吗?这可能吗(如果可能,怎么办?)

I'm not sure how to validate a self-assigned certificate that my client received from a connection to the server ...

根据您使用的 OpenSSL 库,您必须执行两到三个步骤进行验证。这两个版本在 OpenSSL 1.1.0 处一分为二。 OpenSSL 1.1.0 及更高版本执行主机名验证,因此只需两个步骤。 OpenSSL 1.0.2 及以下版本不执行主机名验证,因此需要三个步骤。

下面详述的步骤来自 SSL/TLS Client OpenSSL wiki。

服务器证书

OpenSSL 1.0.2 和 1.1.0 都要求您检查证书是否存在。如果您使用 ADH(匿名 Diffie-Hellman)、TLS-PSK(预共享密钥)、TLS_SRP(安全远程密码),则可能没有要验证的服务器证书。

您使用 SSL_get_peer_certificate 获得服务器证书。如果它 returns 非 NULL,则存在证书。缺少证书可能会也可能不会成为失败的原因。

证书链

OpenSSL 1.0.2 和 1.1.0 都要求您检查链验证的结果。链验证是路径构建的一部分,在 RFC 4158, Certification Path Building.

中有详细说明

你得到路径验证的结果SSL_get_verify_result

证书名称

OpenSSL 1.0.2 及以下要求您验证主机名是否与证书中列出的名称相匹配。这是一个很大的话题,但简而言之:任何主机名或 dns 名称都需要出现在证书的 Subject Alternative Name (SAN)[=54 中=]不是 通用名称 (CN)。另请参阅 How do you sign Certificate Signing Request with your Certification Authority and How to create a self-signed certificate with openssl? 它提供了大量有关 X.509 服务器证书的背景信息、如何显示名称以及各种规则的来源。

实际上,您使用 X509_get_ext_d2i(cert, NID_subject_alt_name, ...) 获取 SAN。然后循环列表并使用 sk_GENERAL_NAME_num 提取每个名称。然后,您提取 GENERAL_NAME 条目和 ASN1_STRING_to_UTF8,并查看它是否与您尝试连接的名称匹配。

下面是打印 主题备用名称 (SAN) 通用名称 (CN) 的例程。它来自 OpenSSL wiki 页面上的示例。

void print_san_name(const char* label, X509* const cert)
{
    int success = 0;
    GENERAL_NAMES* names = NULL;
    unsigned char* utf8 = NULL;

    do
    {
        if(!cert) break; /* failed */

        names = X509_get_ext_d2i(cert, NID_subject_alt_name, 0, 0 );
        if(!names) break;

        int i = 0, count = sk_GENERAL_NAME_num(names);
        if(!count) break; /* failed */

        for( i = 0; i < count; ++i )
        {
            GENERAL_NAME* entry = sk_GENERAL_NAME_value(names, i);
            if(!entry) continue;

            if(GEN_DNS == entry->type)
            {
                int len1 = 0, len2 = -1;

                len1 = ASN1_STRING_to_UTF8(&utf8, entry->d.dNSName);
                if(utf8) {
                    len2 = (int)strlen((const char*)utf8);
                }

                if(len1 != len2) {
                    fprintf(stderr, "  Strlen and ASN1_STRING size do not match (embedded null?): %d vs %d\n", len2, len1);
                }

                /* If there's a problem with string lengths, then     */
                /* we skip the candidate and move on to the next.     */
                /* Another policy would be to fails since it probably */
                /* indicates the client is under attack.              */
                if(utf8 && len1 && len2 && (len1 == len2)) {
                    fprintf(stdout, "  %s: %s\n", label, utf8);
                    success = 1;
                }

                if(utf8) {
                    OPENSSL_free(utf8), utf8 = NULL;
                }
            }
            else
            {
                fprintf(stderr, "  Unknown GENERAL_NAME type: %d\n", entry->type);
            }
        }

    } while (0);

    if(names)
        GENERAL_NAMES_free(names);

    if(utf8)
        OPENSSL_free(utf8);

    if(!success)
        fprintf(stdout, "  %s: <not available>\n", label);        
}

void print_cn_name(const char* label, X509_NAME* const name)
{
    int idx = -1, success = 0;
    unsigned char *utf8 = NULL;

    do
    {
        if(!name) break; /* failed */

        idx = X509_NAME_get_index_by_NID(name, NID_commonName, -1);
        if(!(idx > -1))  break; /* failed */

        X509_NAME_ENTRY* entry = X509_NAME_get_entry(name, idx);
        if(!entry) break; /* failed */

        ASN1_STRING* data = X509_NAME_ENTRY_get_data(entry);
        if(!data) break; /* failed */

        int length = ASN1_STRING_to_UTF8(&utf8, data);
        if(!utf8 || !(length > 0))  break; /* failed */

        fprintf(stdout, "  %s: %s\n", label, utf8);
        success = 1;

    } while (0);

    if(utf8)
        OPENSSL_free(utf8);

    if(!success)
        fprintf(stdout, "  %s: <not available>\n", label);
}

Verifying my self-signed certificate with openSSL

因为它的你的自签名证书,你可以做得比上面更好。您对主机的 public 密钥有 先验 知识。您可以固定 public 密钥,然后仅使用证书来交付 public 密钥或作为演示详细信息。

要固定 public 键,请参阅 OWASP 上的 Public Key Pinning

你也应该避开 IETF 的 RFC 7469, Public Key Pinning Extension for HTTP with Overrides。 IETF 的再现允许攻击者破坏已知良好的 pinset,以便攻击者可以中间人连接。他们还禁止报告问题,因此用户代理成为掩盖事实的同谋。