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

68 openssl ssl-certificate x509 pkix

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

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

什么OpenSSL功能会为我做这个?任何示例代码都是奖金!

Nat*_*man 176

我意识到这是一个非常晚(和很长)的答案.但考虑到这个问题在搜索引擎结果中的排名有多好,我认为可能值得写一个体面的答案.

下面将介绍的很多内容都是从这个演示和OpenSSL文档中借用的.下面的代码适用于C和C++.


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

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

EVP_PKEY * pkey;
pkey = EVP_PKEY_new();
Run Code Online (Sandbox Code Playgroud)

还有一个相应的函数来释放结构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 */
);
Run Code Online (Sandbox Code Playgroud)

如果返回值RSA_generate_keyNULL,那么出事了.如果没有,那么我们现在有一个RSA密钥,我们可以EVP_PKEY从之前将它分配给我们的结构:

EVP_PKEY_assign_RSA(pkey, rsa);
Run Code Online (Sandbox Code Playgroud)

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


现在为证书本身.

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

X509 * x509;
x509 = X509_new();
Run Code Online (Sandbox Code Playgroud)

与情况一样EVP_PKEY,有一个相应的功能来释放结构 - X509_free.

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

ASN1_INTEGER_set(X509_get_serialNumber(x509), 1);
Run Code Online (Sandbox Code Playgroud)

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

X509_gmtime_adj(X509_get_notBefore(x509), 0);
X509_gmtime_adj(X509_get_notAfter(x509), 31536000L);
Run Code Online (Sandbox Code Playgroud)

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

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

X509_set_pubkey(x509, pkey);
Run Code Online (Sandbox Code Playgroud)

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

X509_NAME * name;
name = X509_get_subject_name(x509);
Run Code Online (Sandbox Code Playgroud)

如果您之前在命令行上创建过自签名证书,则可能记得被要求提供国家/地区代码.这是我们提供它以及组织('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);
Run Code Online (Sandbox Code Playgroud)

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

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

X509_set_issuer_name(x509, name);
Run Code Online (Sandbox Code Playgroud)

最后,我们准备好执行签名过程.我们X509_sign使用之前生成的密钥调用.这个代码很简单:

X509_sign(x509, pkey, EVP_sha1());
Run Code Online (Sandbox Code Playgroud)

请注意,我们使用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 */
);
Run Code Online (Sandbox Code Playgroud)

如果您不想加密私钥,那么只需传递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 */
);
Run Code Online (Sandbox Code Playgroud)

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

对于那些有兴趣看到上面的所有代码在真实应用程序中看起来像什么的人,我把一个Gist(用C++编写)放在一起,你可以在这里查看.

  • 在 OpenSSL 3.0 中,使用“EVP_RSA_gen(2048)”而不是“EVP_PKEY_new”+“RSA_generate_key”+“EVP_PKEY_assign_RSA”。通过将所有三个函数合并到一个调用中,这也恰好简化了代码。 (5认同)
  • 我不得不在最后添加fclose(f).否则写入的文件是0B (2认同)

Mar*_*wis 43

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

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

在任何情况下,您都需要调用OpenSSL API的20多个不同功能来创建密钥和自签名证书.一个例子是在OpenSSL源代码本身,在demos/x509/mkcert.c中

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


Mas*_*ler 7

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;
}
Run Code Online (Sandbox Code Playgroud)

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