使用 OpenSSL 以编程方式创建 X509 证书

Posted

技术标签:

【中文标题】使用 OpenSSL 以编程方式创建 X509 证书【英文标题】:Programmatically Create X509 Certificate using OpenSSL 【发布时间】:2010-09-20 08:41:13 【问题描述】:

我有一个 C/C++ 应用程序,我需要创建一个包含公钥和私钥的 X509 pem 证书。证书可以是自签名的,也可以是未签名的,没关系。

我想在应用程序中执行此操作,而不是从命令行。

什么 OpenSSL 函数可以为我做这件事?任何示例代码都是奖励!

【问题讨论】:

【参考方案1】:

Nathan Osman 详细解释了它,在 C++ 中也有同样的问题需要解决,所以这里是我的小补充,cpp 风格的重写概念,并考虑了一些注意事项:

bool generateX509(const std::string& certFileName, const std::string& keyFileName, long daysValid)

    bool result = false;

    std::unique_ptr<BIO, void (*)(BIO *)> certFile   BIO_new_file(certFileName.data(), "wb"), BIO_free_all  ;
    std::unique_ptr<BIO, void (*)(BIO *)> keyFile  BIO_new_file(keyFileName.data(), "wb"), BIO_free_all ;

    if (certFile && keyFile)
    
        std::unique_ptr<RSA, void (*)(RSA *)> rsa  RSA_new(), RSA_free ;
        std::unique_ptr<BIGNUM, void (*)(BIGNUM *)> bn  BN_new(), BN_free ;

        BN_set_word(bn.get(), RSA_F4);
        int rsa_ok = RSA_generate_key_ex(rsa.get(), RSA_KEY_LENGTH, bn.get(), nullptr);

        if (rsa_ok == 1)
        
            // --- cert generation ---
            std::unique_ptr<X509, void (*)(X509 *)> cert  X509_new(), X509_free ;
            std::unique_ptr<EVP_PKEY, void (*)(EVP_PKEY *)> pkey  EVP_PKEY_new(), EVP_PKEY_free;

            // The RSA structure will be automatically freed when the EVP_PKEY structure is freed.
            EVP_PKEY_assign(pkey.get(), EVP_PKEY_RSA, reinterpret_cast<char*>(rsa.release()));
            ASN1_INTEGER_set(X509_get_serialNumber(cert.get()), 1); // serial number

            X509_gmtime_adj(X509_get_notBefore(cert.get()), 0); // now
            X509_gmtime_adj(X509_get_notAfter(cert.get()), daysValid * 24 * 3600); // accepts secs

            X509_set_pubkey(cert.get(), pkey.get());

            // 1 -- X509_NAME may disambig with wincrypt.h
            // 2 -- DO NO FREE the name internal pointer
            X509_name_st* name = X509_get_subject_name(cert.get());

            const uchar country[] = "RU";
            const uchar company[] = "MyCompany, PLC";
            const uchar common_name[] = "localhost";

            X509_NAME_add_entry_by_txt(name, "C",  MBSTRING_ASC, country, -1, -1, 0);
            X509_NAME_add_entry_by_txt(name, "O",  MBSTRING_ASC, company, -1, -1, 0);
            X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, common_name, -1, -1, 0);

            X509_set_issuer_name(cert.get(), name);
            X509_sign(cert.get(), pkey.get(), EVP_sha256()); // some hash type here


            int ret  = PEM_write_bio_PrivateKey(keyFile.get(), pkey.get(), nullptr, nullptr, 0, nullptr, nullptr);
            int ret2 = PEM_write_bio_X509(certFile.get(), cert.get());

            result = (ret == 1) && (ret2 == 1); // OpenSSL return codes
        
    

    return result;

当然,应该更多检查函数的返回值,实际上应该检查所有,但这会使样本过于“分支”并且很漂亮无论如何都很容易改进。

【讨论】:

看起来我们忘记在设置到期时将指针 behind 发送到唯一指针...。我会粘贴代码,但显然我无法让它看起来像代码。 @tripulse emm...因为?为什么?【参考方案2】:

您需要先熟悉术语和机制。

根据定义,X.509 证书不包括私钥。相反,它是公钥的 CA 签名版本(以及 CA 放入签名中的任何属性)。 PEM 格式实际上只支持单独存储密钥和证书 - 尽管您可以将两者连接起来。

无论如何,您都需要调用 20 多个不同的 OpenSSL API 函数来创建密钥和自签名证书。一个例子是 OpenSSL 源代码本身,在 demos/x509/mkcert.c

如需更详细的解答,请参阅下方的Nathan Osman's explanation。

【讨论】:

是的 - 我确实需要更多地熟悉 ssl 概念。我将查看示例,感谢链接(虽然链接有问题,但我会解决的。)我也使用 Crypto++ 做一些事情,在这种情况下它可能比 OpenSSL 更容易使用。跨度> 谢谢!由于提供的链接而选择了此答案。【参考方案3】:

我意识到这是一个很晚(而且很长)的答案。但考虑到这个问题在搜索引擎结果中的排名,我认为它可能值得写一个像样的答案。

您将在下面阅读的很多内容都是从this demo 和 OpenSSL 文档中借用的。下面的代码适用于 C 和 C++。


在我们真正创建证书之前,我们需要创建一个私钥。 OpenSSL 提供了EVP_PKEY 结构,用于在内存中存储与算法无关的私钥。此结构在openssl/evp.h 中声明,但包含在openssl/x509.h 中(稍后我们将需要它),因此您实际上不需要显式包含标头。

为了分配EVP_PKEY结构,我们使用EVP_PKEY_new

EVP_PKEY * pkey;
pkey = EVP_PKEY_new();

还有一个用于释放结构的相应函数 - EVP_PKEY_free - 它接受一个参数:上面初始化的 EVP_PKEY 结构。

现在我们需要生成一个密钥。对于我们的示例,我们将生成一个 RSA 密钥。这是通过在openssl/rsa.h 中声明的RSA_generate_key 函数完成的。此函数返回一个指向 RSA 结构的指针。

函数的简单调用可能如下所示:

RSA * rsa;
rsa = RSA_generate_key(
    2048,   /* number of bits for the key - 2048 is a sensible value */
    RSA_F4, /* exponent - RSA_F4 is defined as 0x10001L */
    NULL,   /* callback - can be NULL if we aren't displaying progress */
    NULL    /* callback argument - not needed in this case */
);

如果RSA_generate_key 的返回值为NULL,则说明出现问题。如果没有,那么我们现在有一个 RSA 密钥,我们可以将它分配给我们之前的 EVP_PKEY 结构:

EVP_PKEY_assign_RSA(pkey, rsa);

RSA 结构将在EVP_PKEY 结构被释放时自动释放。


现在是证书本身。

OpenSSL 使用X509 结构来表示内存中的 x509 证书。这个结构的定义在openssl/x509.h。我们需要的第一个函数是X509_new。它的使用相对简单:

X509 * x509;
x509 = X509_new();

EVP_PKEY 一样,有一个相应的函数用于释放结构 - X509_free

现在我们需要使用一些X509_* 函数来设置证书的一些属性:

ASN1_INTEGER_set(X509_get_serialNumber(x509), 1);

这会将我们证书的序列号设置为“1”。一些开源 HTTP 服务器拒绝接受序列号为“0”的证书,这是默认设置。下一步是指定证书实际有效的时间跨度。我们通过以下两个函数调用来做到这一点:

X509_gmtime_adj(X509_get_notBefore(x509), 0);
X509_gmtime_adj(X509_get_notAfter(x509), 31536000L);

第一行将证书的notBefore 属性设置为当前时间。 (X509_gmtime_adj 函数将指定的秒数添加到当前时间 - 在本例中为无。)第二行将证书的 notAfter 属性设置为从现在起 365 天(60 秒 * 60 分钟 * 24 小时 * 365 天)。

现在我们需要使用之前生成的密钥为我们的证书设置公钥:

X509_set_pubkey(x509, pkey);

由于这是一个自签名证书,我们将颁发者的名称设置为主题的名称。该过程的第一步是获取主题名称:

X509_NAME * name;
name = X509_get_subject_name(x509);

如果您之前曾在命令行上创建过自签名证书,您可能还记得被要求输入国家/地区代码。下面是我们提供它以及组织 ('O') 和通用名称 ('CN') 的地方:

X509_NAME_add_entry_by_txt(name, "C",  MBSTRING_ASC,
                           (unsigned char *)"CA", -1, -1, 0);
X509_NAME_add_entry_by_txt(name, "O",  MBSTRING_ASC,
                           (unsigned char *)"MyCompany Inc.", -1, -1, 0);
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
                           (unsigned char *)"localhost", -1, -1, 0);

(我在这里使用值“CA”,因为我是加拿大人,这是我们的国家代码。另外请注意,参数 #4 需要显式转换为 unsigned char *。)

现在我们可以实际设置发行者名称了:

X509_set_issuer_name(x509, name);

最后我们准备好执行签名过程。我们使用之前生成的密钥调用X509_sign。代码非常简单:

X509_sign(x509, pkey, EVP_sha1());

请注意,我们使用SHA-1 散列算法对密钥进行签名。这与我在此答案开头提到的 mkcert.c 演示不同,它使用 MD5。


我们现在有一个自签名证书!但我们还没有完成——我们需要将这些文件写入磁盘。值得庆幸的是,OpenSSL 也为我们提供了 PEM_* 函数,这些函数在 openssl/pem.h 中声明。我们需要的第一个是 PEM_write_PrivateKey 用于保存我们的私钥。

FILE * f;
f = fopen("key.pem", "wb");
PEM_write_PrivateKey(
    f,                  /* write the key to the file we've opened */
    pkey,               /* our key from earlier */
    EVP_des_ede3_cbc(), /* default cipher for encrypting the key on disk */
    "replace_me",       /* passphrase required for decrypting the key on disk */
    10,                 /* length of the passphrase string */
    NULL,               /* callback for requesting a password */
    NULL                /* data to pass to the callback */
);

如果您不想加密私钥,则只需将NULL 传递给上面的第三个和第四个参数。无论哪种方式,您都肯定希望确保该文件不是世界可读的。 (对于 Unix 用户,这意味着 chmod 600 key.pem。)

哇!现在我们只需要一个功能——我们需要将证书写入磁盘。为此我们需要的函数是PEM_write_X509:

FILE * f;
f = fopen("cert.pem", "wb");
PEM_write_X509(
    f,   /* write the certificate to the file we've opened */
    x509 /* our certificate */
);

我们完成了!希望此答案中的信息足以让您大致了解一切是如何工作的,尽管我们几乎没有触及 OpenSSL 的表面。

对于那些有兴趣了解上述所有代码在实际应用程序中的样子的人,我整理了一个 Gist(用 C++ 编写),您可以查看 here。

【讨论】:

感谢您的出色回答和解释!只有一个小疑问:这句话Now we need to set the public key for our certificate using the key we generated earlier:是不是错字? public key 不应该是 private key 吗? 我必须在最后添加 fclose(f) 。否则正在写入的文件是 0B 漂亮而全面的清晰答案。还有一件事,如何在 openssl.cnf 文件中找到的证书添加更多参数。例如添加扩展主题AltName? 这个答案是一个巨大的帮助。对于想要为其证书添加扩展名的人,请参阅:***.com/questions/35616853/… 来自文档:“RSA_generate_key() 在 OpenSSL 0.9.8 中已弃用;请改用 RSA_generate_key_ex()。”两者都将在 OpenSSL 3.0 中弃用。【参考方案4】:

是否有机会通过您的应用程序中的system 调用来做到这一点?这样做的几个充分理由:

许可:调用openssl 可执行文件可以将其与您的应用程序分开,并可能提供某些优势。 免责声明:就此咨询律师。

文档:OpenSSL 附带现象命令行文档,极大地简化了可能很复杂的工具。

可测试性:您可以从命令行练习 OpenSSL,直到您完全了解如何创建证书。有很多很多选项;预计要花大约一天的时间,直到您正确了解所有细节。之后,将命令合并到您的应用中就很简单了。

如果您选择使用 API,请查看 www.openssl.org 上的 openssl-dev 开发人员列表。

祝你好运!

【讨论】:

OpenSSL 是 apache 风格许可证下的许可证,它可以像任何其他非 copyleft 许可证一样用于商业应用程序。人们可能仍想咨询律师以确保他们所做的一切都正常,但不存在与 GPL 相关的问题 已记录和更新 -- 谢谢。将开源代码与闭源代码分开通常是一个好主意,除非效率至关重要,否则其他原因是使用独立 openssl 实用程序的好理由。 我宁愿不使用系统调用来执行此操作。您关于文档的观点非常有效 - OpenSSL 的 SSL 方面的文档没有多大帮助。 其实还有GPL相关的问题:Apache 1.0 License和OpenSSL分发的4条款BSD许可证都与GPL软件不兼容。操作系统提供的库在 GPL 中有一个例外,因此如果您链接到您的发行版提供的 OpenSSL,您可能会侥幸逃脱。 See also

以上是关于使用 OpenSSL 以编程方式创建 X509 证书的主要内容,如果未能解决你的问题,请参考以下文章

是否可以仅使用 C# 以编程方式生成 X509 证书?

如何使用包括 CRL 分发点的 openssl 创建证书?

如何创建一个最小的虚拟 X509Certificate2?

使用 openssl 以编程方式提取 pem 证书信息

OpenSSL之X509证书用法

OpenSSL之X509系列