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


76

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

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

哪些OpenSSL函数将为我执行此操作?任何示例代码都是奖励!

Answers:


49

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

根据定义,X.509证书不包含私钥。相反,它是公共密钥的CA签名版本(以及CA放入签名的所有属性)。PEM格式实际上仅支持密钥和证书的单独存储-尽管您可以随后将两者进行串联。

无论如何,您都需要调用OpenSSL API的20多种不同功能来创建密钥和自签名证书。OpenSSL源代码本身在demos / x509 / mkcert.c中提供了一个示例

有关更详细的答案,请参阅下面的Nathan Osman的说明


是的-我确实需要更加熟悉ssl概念。我将检查示例,感谢链接(尽管链接有问题,但是我会弄清楚的。)我还使用了Crypto ++进行某些操作,在这种情况下,它可能比OpenSSL更容易使用。

谢谢!由于提供了链接,因此选择了此答案。

200

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

您将在下面阅读的很多内容都来自此演示和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密钥。这是通过RSA_generate_key中声明的函数完成的openssl/rsa.h。此函数返回指向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。)

ew!现在我们只需要一个功能-我们需要将证书写到磁盘上。为此,我们需要的功能是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 ++编写),您可以在这里查看。


感谢您的出色回答和解释!只是一个小疑问:这句话Now we need to set the public key for our certificate using the key we generated earlier:是错字吗?不public key应该private key吗?
Kelvin Hu

3
我必须在末尾添加fclose(f)。否则,正在写入的文件是0B
Qamar Suleiman

全面的明确答案。还有一点,如何向openssl.cnf文件中找到的证书添加更多参数。例如添加扩展subjectAltName?
karim 2015年

这个答案是巨大的帮助。对于想为其证书添加扩展名的人员,请参阅:stackoverflow.com/questions/35616853/…–
Bryan

1
从文档中:“ RSA_generate_key()在OpenSSL 0.9.8中已弃用;请改用RSA_generate_key_ex()。” 两者都将在OpenSSL 3.0中弃用。
菲利普·克拉森(PhilippClaßen)

2

是否有可能通过system您的应用内的电话进行此操作?这样做有几个很好的理由:

  • 许可:openssl可以说调用可执行文件将其与应用程序分离开来,并且可能会提供某些优势。 免责声明:请咨询律师。

  • 文档:OpenSSL附带了引人注目的命令行文档,极大地简化了潜在的复杂工具。

  • 可测试性:您可以从命令行使用OpenSSL,直到您完全了解如何创建证书为止。有很多选择。希望在此上花费大约一天的时间,直到您正确掌握所有细节为止。之后,将命令合并到您的应用程序中很简单。

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

祝好运!


5
OpenSSL是apache样式许可下的许可,它可以像任何其他非copyleft许可一样在商业应用中使用。人们仍然可能希望咨询律师,以确保他们所做的一切都很好,但是它没有GPL相关问题
Louis Gerbarg's

注意和更新-谢谢。将开放源代码与封闭源代码分开通常是一个好主意,除非效率至关重要,否则其他原因将成为使用独立的openssl实用程序的一个很好的案例。
亚当·利斯

2
我宁愿不使用系统调用来执行此操作。您对文档的观点非常有效-OpenSSL的SSL方面的文档无济于事。

1
实际上存在与GPL相关的问题:分发OpenSSL时使用的Apache 1.0许可证和4条款BSD许可证均与GP​​L软件不兼容。GPL中操作系统提供的库中有一个例外,因此,如果您与发行版提供的OpenSSL链接,则可能无法使用它。另请参见
Mathias Brossard

2

内森·奥斯曼(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), 0); // now
            X509_gmtime_adj(X509_get_notAfter(cert), 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;
}

当然,应该对函数的返回值进行更多检查,实际上应该检查所有返回值,但这会使样本过于“简陋”,并且无论如何还是很容易改进的。


好像我们在设置到期时忘记了将指针发送到唯一指针的后面....我会粘贴代码,但显然我无法使它看起来像代码。
AcidTonic19年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.