我有一个C / C ++应用程序,我需要创建一个包含公钥和私钥的X509 pem证书。证书可以是自签名的,也可以是未签名的,无关紧要。
我想在app中执行此操作,而不是从命令行执行此操作。
OpenSSL的功能会为我做什么?任何示例代码都是奖金!
答案 0 :(得分:168)
我意识到这是一个非常晚(和很长)的答案。但考虑到这个问题在搜索引擎结果中的排名有多好,我认为可能值得写一个体面的答案。
下面您将阅读的许多内容都是从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
函数将指定的秒数添加到当前时间 - 在本例中为none。)第二行将证书的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也使用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。
答案 1 :(得分:44)
您首先需要熟悉术语和机制。
根据定义,X.509 证书不包含私钥。相反,它是公钥的CA签名版本(以及CA放入签名的任何属性)。 PEM格式实际上只支持密钥和证书的单独存储 - 尽管您可以将两者连接起来。
在任何情况下,您都需要调用OpenSSL API的20多个不同功能来创建密钥和自签名证书。一个例子是OpenSSL源本身,demos/x509/mkcert.c
如需更详细的解答,请参阅下面的Nathan Osman's explanation。
答案 2 :(得分:2)
是否有可能通过您应用内的system
来电来完成此操作?这样做的几个很好的理由:
许可:调用openssl
可执行文件可以将其与您的应用程序区分开来,并可能提供某些优势。 免责声明:请咨询律师。
文档:OpenSSL附带现象命令行文档,可大大简化可能复杂的工具。
可测试性:您可以从命令行执行OpenSSL,直到您完全了解如何创建证书。有一个很多的选项;期待花一天时间,直到你得到所有细节。之后,将命令合并到您的应用程序中是微不足道的。
如果您选择使用API,请查看www.openssl.org上的openssl-dev
开发人员列表。
答案 3 :(得分:0)
关于从代码中执行这些命令,我不确定。
答案 4 :(得分:0)
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;
}
当然,应该对函数的返回值进行更多检查,实际上应检查所有所有,但这会使样本过于“简陋”,非常漂亮无论如何都容易改进。