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

发布于 2024-07-08 10:16:01 字数 163 浏览 7 评论 0原文

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

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

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

I have a C/C++ application and I need to create a X509 pem certificate containing both a public and private key. The certificate can be self signed, or unsigned, doesn't matter.

I want to do this inside an app, not from command line.

What OpenSSL functions will do this for me? Any sample code is a bonus!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

我要还你自由 2024-07-15 10:16:01

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

您将在下面读到的很多内容都借自 此演示 和 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);

EVP_PKEY 被调用时,RSA 结构将自动释放。 结构被释放。


现在来说说证书本身。

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 哈希算法签署密钥。 这与我在本答案开头提到的使用 MD5 的 mkcert.c 演示不同。


我们现在有了一个自签名证书! 但我们还没有完成 - 我们需要将这些文件写入磁盘。 值得庆幸的是,OpenSSL 也为我们提供了在 openssl/pem.h 中声明的 PEM_* 函数。 我们需要的第一个是 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++ 编写),您可以查看 此处

I realize that this is a very late (and long) answer. But considering how well this question seems to rank in search engine results, I figured it might be worth writing a decent answer for.

A lot of what you will read below is borrowed from this demo and the OpenSSL docs. The code below applies to both C and C++.


Before we can actually create a certificate, we need to create a private key. OpenSSL provides the EVP_PKEY structure for storing an algorithm-independent private key in memory. This structure is declared in openssl/evp.h but is included by openssl/x509.h (which we will need later) so you don't really need to explicitly include the header.

In order to allocate an EVP_PKEY structure, we use EVP_PKEY_new:

EVP_PKEY * pkey;
pkey = EVP_PKEY_new();

There is also a corresponding function for freeing the structure - EVP_PKEY_free - which accepts a single argument: the EVP_PKEY structure initialized above.

Now we need to generate a key. For our example, we will generate an RSA key. This is done with the RSA_generate_key function which is declared in openssl/rsa.h. This function returns a pointer to an RSA structure.

A simple invocation of the function might look like this:

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 */
);

If the return value of RSA_generate_key is NULL, then something went wrong. If not, then we now have an RSA key, and we can assign it to our EVP_PKEY structure from earlier:

EVP_PKEY_assign_RSA(pkey, rsa);

The RSA structure will be automatically freed when the EVP_PKEY structure is freed.


Now for the certificate itself.

OpenSSL uses the X509 structure to represent an x509 certificate in memory. The definition for this struct is in openssl/x509.h. The first function we are going to need is X509_new. Its use is relatively straightforward:

X509 * x509;
x509 = X509_new();

As was the case with EVP_PKEY, there is a corresponding function for freeing the structure - X509_free.

Now we need to set a few properties of the certificate using some X509_* functions:

ASN1_INTEGER_set(X509_get_serialNumber(x509), 1);

This sets the serial number of our certificate to '1'. Some open-source HTTP servers refuse to accept a certificate with a serial number of '0', which is the default. The next step is to specify the span of time during which the certificate is actually valid. We do that with the following two function calls:

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

The first line sets the certificate's notBefore property to the current time. (The X509_gmtime_adj function adds the specified number of seconds to the current time - in this case none.) The second line sets the certificate's notAfter property to 365 days from now (60 seconds * 60 minutes * 24 hours * 365 days).

Now we need to set the public key for our certificate using the key we generated earlier:

X509_set_pubkey(x509, pkey);

Since this is a self-signed certificate, we set the name of the issuer to the name of the subject. The first step in that process is to get the subject name:

X509_NAME * name;
name = X509_get_subject_name(x509);

If you've ever created a self-signed certificate on the command line before, you probably remember being asked for a country code. Here's where we provide it along with the organization ('O') and common name ('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);

(I'm using the value 'CA' here because I'm Canadian and that's our country code. Also note that parameter #4 needs to be explicitly cast to an unsigned char *.)

Now we can actually set the issuer name:

X509_set_issuer_name(x509, name);

And finally we are ready to perform the signing process. We call X509_sign with the key we generated earlier. The code for this is painfully simple:

X509_sign(x509, pkey, EVP_sha1());

Note that we are using the SHA-1 hashing algorithm to sign the key. This differs from the mkcert.c demo I mentioned at the beginning of this answer, which uses MD5.


We now have a self-signed certificate! But we're not done yet - we need to write these files out to disk. Thankfully OpenSSL has us covered there too with the PEM_* functions which are declared in openssl/pem.h. The first one we will need is PEM_write_PrivateKey for saving our private key.

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 */
);

If you don't want to encrypt the private key, then simply pass NULL for the third and fourth parameter above. Either way, you will definitely want to ensure that the file is not world-readable. (For Unix users, this means chmod 600 key.pem.)

Whew! Now we are down to one function - we need to write the certificate out to disk. The function we need for this is 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 */
);

And we're done! Hopefully the information in this answer is enough to give you a rough idea of how everything works, although we've barely scratched the surface of OpenSSL.

For those interested in seeing what all of the code above looks like in a real application, I've thrown together a Gist (written in C++) that you can view here.

懒猫 2024-07-15 10:16:01

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

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

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

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

You'll need to familiarize yourself with the terminology and mechanisms first.

An X.509 certificate, by definition, does not include a private key. Instead, it is a CA-signed version of the public key (along with any attributes the CA puts into the signature). The PEM format really only supports separate storage of the key and the certificate - although you can then concatenate the two.

In any case, you'll need to invoke 20+ different functions of the OpenSSL API to create a key and a self-signed certificate. An example is in the OpenSSL source itself, in demos/x509/mkcert.c

For a more detailed answer, please see Nathan Osman's explanation below.

沦落红尘 2024-07-15 10:16:01

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;
}

当然,应该对函数的返回值进行更多检查,实际上应该检查所有它们,但这会使样本太“分支”并且很容易改进。

Nathan Osman explained it greatly and fully, had the same problem to be solved in C++ so here is my small addition, cpp-style rewritten concept with a couple of caveats taken into account:

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;
}

Of course, there should be more checks of function's return values, actually all of them should be checked but that would make a sample too "branchy" and is pretty easy to improve anyway.

奶茶白久 2024-07-15 10:16:01

有机会通过应用程序内的系统调用来执行此操作吗? 这样做有几个很好的理由:

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

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

  • 可测试性:您可以从命令行运行 OpenSSL,直到您确切了解如何创建证书。 有很多选择; 预计要花大约一天的时间,直到您弄清所有细节为止。 之后,将该命令合并到您的应用程序中就很简单了。

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

祝你好运!

Any chance of doing this via a system call from within your app? Several good reasons for doing this:

  • Licensing: Calling the openssl executable arguably separates it from your application and may provide certain advantages. Disclaimer: consult a lawyer on this.

  • Documentation: OpenSSL comes with phenomenal command-line documentation that greatly simplifies a potentially complicated tool.

  • Testability: you can exercise OpenSSL from the command line until you understand exactly how to create your certs. There are a lot of options; expect to spend about a day on this until you get all the details right. After that, it's trivial to incorporate the command into your app.

If you choose to use the API, check the openssl-dev developers' list on www.openssl.org.

Good luck!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文