前言与摘要
背景要求
- 证书申请的基本原理:可以参考《图解密码技术》-- 结城浩 – 第十章 证书。
- 了解基本的openssl 命令行使用。
摘要
本文没有介绍证书申请的背景知识和相关的openssl使用。本文直接开始介绍,使用openssl命令,生成证书请求。为了搞明白证书请求需要包含哪些内容,我们去阅读了证书请求的相关RFC规范。接着,我们阅读openssl命令执行证书请求的源码执行流程。阅读源码的过程中,我们浅尝辄止,没有深入阅读具体的函数实现。所以,我们不知道,在证书申请这块,openssl是否完全遵守了RFC规范。最后,我们使用C++封装了openssl api,实现了一个简单的证书请求类。
openssl命令行-生成证书请求
openssl req :req - PKCS#10 certificate request and certificate generating utility.
openssl-genrsa:genrsa - generate an RSA private key.
我们使用genrsa 生成临时RSA密钥。接着,我们使用req 生成证书请求。关于命令使用的详细介绍,参见上面两个官方文档链接。
? openssl genrsa -out private_key.pem 2048
? openssl req -key private_key.pem -new -out cert_req.pem
我个人更喜欢使用配置文件生成证书请求。下面内容,保存在ruler.conf 中。(简单起见,我注释掉了扩展项部分)
[req]
prompt = no # 如果设置为值 no 这将禁用证书字段的提示,并且直接从配置文件中获取值。
utf8 = yes
distinguished_name = req_distinguished_name
# req_extensions = v3_req
[req_distinguished_name]
organizationName = galactic alliance
emailAddress = unknow@haha.gal
commonName = ruler of earth
# [v3_req]
# basicConstraints = critical, CA:false
接下来,我们使用openssl命令行,生成证书请求。证书请求输出在ruler_csr.pem文件中。
? openssl req -key private_key.pem -new -config ruler.conf -out ruler_csr.pem
我们来解析证书请求,看下ruler_csr.pem文件中,有哪些内容。
? openssl req -in ruler_csr.pem -text -noout
Certificate Request:
Data:
Version: 1 (0x0)
Subject: O = galactic alliance, emailAddress = unknow@haha.gal, CN = ruler of earth
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:ad:29:30:ec:c7:26:ae:69:89:68:81:d5:76:c6:
7e:af:f9:32:b8:d1:03:f1:ed:a6:5f:fa:3a:f2:a6:
bf:39:6a:61:e4:6a:cc:1b:12:b5:ea:f9:ef:4f:2f:
43:6a:78:c3:62:39:8c:26:b1:ac:1d:6f:66:60:32:
8d:4d:53:6f:98:5c:f2:04:2c:2a:78:bf:74:29:7a:
5c:99:2a:c1:6f:fe:07:c2:d7:7a:bf:a2:73:22:29:
db:45:3e:dc:5d:e8:22:d6:4a:81:7d:fa:d5:be:bd:
43:09:e9:0a:63:a9:e7:62:9f:cc:67:a4:39:38:61:
ed:f8:b9:79:1c:1c:b0:d9:47:7b:4c:1e:57:17:89:
8a:9a:d0:4e:da:e8:47:fe:87:db:6c:1f:34:2e:2f:
77:d2:7a:76:ed:c3:be:ef:a2:0a:23:72:9e:44:31:
8c:a6:4f:bc:93:48:42:cd:c2:05:db:4e:ab:a3:74:
84:9e:3f:73:04:f5:da:de:ab:95:d3:ce:30:a5:1d:
4b:3a:90:85:b6:5e:e6:b2:0b:a9:33:91:33:b6:f5:
24:e9:f4:3a:ba:33:4d:8f:92:74:15:76:f8:23:ca:
f3:c0:0b:71:a3:fc:50:50:49:06:ee:9a:b6:57:4e:
dd:ac:ee:f7:cf:9b:0d:52:21:24:38:ab:62:1c:e8:
b1:7f
Exponent: 65537 (0x10001)
Attributes:
a0:00
Signature Algorithm: sha256WithRSAEncryption
89:29:6e:ee:3a:00:bd:e9:0a:c0:31:c8:3d:96:a5:38:88:18:
e0:7d:31:84:a2:ea:e0:fa:bd:03:9a:4a:58:9a:28:c8:02:a6:
50:02:03:d1:25:23:a3:16:14:5b:d4:8d:48:32:95:48:0d:55:
38:88:f4:d6:bf:f4:1e:e7:c2:7c:e6:4b:df:34:ce:2c:82:58:
6e:0c:52:8e:d5:d1:d0:cc:06:61:84:9e:d1:19:8b:07:9b:ab:
75:b8:e9:bc:a4:ca:bd:e1:9d:ab:cf:ad:eb:6d:2b:a0:90:b7:
0b:33:6f:bc:a2:13:36:d7:1a:d7:e9:7d:f0:1f:63:93:1a:e9:
c5:d1:5c:f4:1f:7e:ce:e6:c4:95:77:2b:d7:68:c2:f0:53:23:
e2:10:ae:d4:54:0f:f1:89:a4:7a:dd:78:49:ad:d0:7f:19:de:
ee:e1:ee:87:f0:91:4b:53:f8:99:2f:d9:20:30:a9:52:95:6e:
f0:7a:c6:81:1c:e3:04:33:5d:b0:d0:4f:ca:38:82:d5:35:59:
49:4c:16:9e:ff:65:d7:8c:c3:a7:da:b3:9f:07:8c:6d:b1:a1:
b9:e1:2f:42:6f:2c:2e:91:cb:c4:3a:61:7b:4c:5d:05:47:e5:
76:a6:fe:b4:d8:10:b0:11:91:d7:10:6a:39:b7:0c:a4:e3:56:
06:d4:65:bb
根据缩进,我们可以看到Certificate Request 中包含两部分内容:Data 和Signature 。
Data 中包含Subject 和public key 信息。Signature 为摘要之后的签名。
此时,我们怀揣一个疑问:证书请求应该包含哪些内容,它的规范是什么?
下一节,我们来尝试解决这个问题。
RFC - 证书请求的消息格式( Certificate Request Message Format)
需要翻阅如下的RFC文档。关于如何查找这些文档,见“附录-RFC文件的查看方法”。
虽然这三个RFC文档,我都翻了一遍。但我并没有看懂。详见上面链接。
哈哈,但问题不大。最起码知道,证书申请包含三部分内容:证书请求信息、证明证书主体的实体实际拥有相应私钥,证书请求上下文相关的补充信息。
CertReqMessages ::= SEQUENCE SIZE (1..MAX) OF CertReqMsg
CertReqMsg ::= SEQUENCE {
certReq CertRequest,
popo ProofOfPossession OPTIONAL,
-- content depends upon key type
regInfo SEQUENCE SIZE(1..MAX) of AttributeTypeAndValue OPTIONAL
}
certReq :由请求标识符、证书内容模板和可选的控制信息序列组成。(上一节中的subject 和publicKey 均为证书模板内容中的一部分)
popo :需要证明请求证书有对应的私钥。(上一节中这一部分是签名值)
regInfo :仅包含与证书请求上下文相关的补充信息。(不知道这啥东西)
那有个问题:openssl 的实现是否满足上面规范,或者说,openssl 如何实现上面规范。
这个问题跳过。下面虽然阅读了点openssl源码,但具体的函数内部没有去阅读。
openssl源码阅读-生成证书请求
调试下面命令对应openssl源码的执行过程。
openssl req -key private_key.pem -new -config ruler.conf -out ruler_csr.pem
openssl req -new -sha256 -key private_key.pem -utf8 -subj /O=galactic alliance/emailAddress=unknow@haha.gal/CN=ruler of earth -out cert_req.pem
-
进入命令行指定的程序req:
pname = opt_progname(argv[0]); 获取程序名。do_cmd(prog, argc, argv); 进入统一跳转函数。fp = lh_FUNCTION_retrieve(prog, &f); 、return fp->func(argc, argv); 进入req_main 函数。 -
循环获取参数:while ((o = opt_next()) != OPT_EOF) -
根据参数进行处理:
pkey = load_key(keyfile, keyform, 0, passin, e, "private key"); 加载私钥- …
-
生成证书申请。
req = X509_REQ_new_ex(app_get0_libctx(), app_get0_propq()); 。创建证书申请空间。make_REQ(req, pkey, fsubj, multirdn, !gen_x509, chtype) 。填充证书。pkey 为私钥;fsbj 为X509_NAME 格式,里面为subject信息。
X509_REQ_set_version(req, X509_REQ_VERSION_1) 设置版本号。X509_REQ_set_subject_name(req, fsubj); 设置主题信息。X509_REQ_set_pubkey(req, pkey) 设置公钥。 PEM_write_bio_X509(out, new_x509); 默认为pem格式输出。X509_REQ_free(req); ,X509_NAME_free(fsubj);``EVP_PKEY_free(pkey); 释放空间。 -
此次调试没有进过parse_name 函数。但由于下一节我自己写了一个简化版的这个函数。所以,还是看下这个函数。当调用-subj /type0=value0/type1=value1/type2=..., 会触发这个函数 – 使用命令行的信息填充subject。
n = X509_NAME_new(); 开辟X509_NAME 格式空间。nid = OBJ_txt2nid(typestr); ,X509_NAME_add_entry_by_NID(n, nid, chtype, valstr, strlen((char *)valstr), -1, ismulti ? -1 : 0)) 添加subject条目。
调用openssl api - 生成证书请求
参考自:《Openssl 编程》-- 赵春平 – 第二十五章 证书申请、OpenSSL中文手册 – 蓝月心语 – OpenSSL中文手册之X509库详解(未完待续)
下面为简略代码,删除了调用函数的返回值检查。需要用的时候,自行添加。
#pragma once
#include "exception.hpp"
#include "load_key.hpp"
#include <openssl/x509.h>
#include <openssl/err.h>
#include <boost/utility/string_ref.hpp>
#include <iostream>
#include <sstream>
#include <string>
class req {
private:
X509_REQ* x509_req = nullptr;
char ERR[1024] = {0};
X509_NAME* get_subject(const std::string subject_contents); // 通过字符串subject_contents,生成X509_REQ结构
public:
req(const std::string digest_name, const std::string private_key_path,
const std::string subject_contents);
void out_to_file(int out_format, const std::string out_path); //证书申请输出到文件
void out_to_stdout(int out_format); //证书申请输出到标准输出
~req();
};
req::req(const std::string digest_name, const std::string private_key_path,
const std::string subject_contents)
{
// 填充版本号, 主题名,公钥, 摘要并私钥签名
x509_req = X509_REQ_new();
// 0对应版本1;
// 1对应的是版本2;2对应版本3;
long version = 0;
X509_REQ_set_version(x509_req, version);
X509_NAME* subject_name = get_subject(subject_contents);
EVP_PKEY* pkey = load_private_key(private_key_path, _FORMAT_UNDEF);
X509_REQ_set_pubkey(x509_req, pkey);
const EVP_MD* md = EVP_get_digestbyname(digest_name.c_str());
X509_REQ_sign(x509_req, pkey, md);
// 释放不需要的资源
X509_NAME_free(subject_name);
EVP_PKEY_free(pkey);
}
X509_NAME* req::get_subject(const std::string subject_contents)
{
// 通过字符串subject,生成X509_REQ结构
X509_NAME* subject_name = X509_NAME_new();
std::istringstream ins(subject_contents);
std::string tmp;
while(std::getline(ins,tmp,'/')) {
std::string::size_type equal_sin_loc = tmp.find('=');
if(equal_sin_loc == std::string::npos) {
BOOST_THROW_EXCEPTION(arg_err()
<<err_str("cant find = in subject agrs"));
}
std::string key = tmp.substr(0, equal_sin_loc);
std::string value = tmp.substr(equal_sin_loc+1, tmp.length()- equal_sin_loc -1);
// https://www.openssl.org/docs/manmaster/man3/X509_NAME_add_entry.html
// For almost all applications loc can be set to -1 and set to 0.
if(key == "C") {
X509_NAME_add_entry_by_txt(subject_name, "countryName", MBSTRING_UTF8,
(const unsigned char*)value.c_str(), value.length(), -1, 0);
} else if(key == "ST") {
X509_NAME_add_entry_by_txt(subject_name, "stateOrProvinceName", MBSTRING_UTF8,
(const unsigned char*)value.c_str(), value.length(), -1, 0);
} else if(key == "L") {
X509_NAME_add_entry_by_txt(subject_name, "localityName", MBSTRING_UTF8,
(const unsigned char*)value.c_str(), value.length(), -1, 0);
} else if(key == "O") {
X509_NAME_add_entry_by_txt(subject_name, "organizationName", MBSTRING_UTF8,
(const unsigned char*)value.c_str(), value.length(), -1, 0);
} else if(key == "OU") {
X509_NAME_add_entry_by_txt(subject_name, "organizationalUnitName", MBSTRING_UTF8,
(const unsigned char*)value.c_str(), value.length(), -1, 0);
} else if(key == "EMAIL") {
X509_NAME_add_entry_by_txt(subject_name, "emailAddress", MBSTRING_UTF8,
(const unsigned char*)value.c_str(), value.length(), -1, 0);
} else if(key == "CN") {
X509_NAME_add_entry_by_txt(subject_name, "commonName", MBSTRING_UTF8,
(const unsigned char*)value.c_str(), value.length(), -1, 0);
} else {
BOOST_THROW_EXCEPTION(arg_err()
<<err_str("Illegal name appears in parameter. The legal names are follow: C/ST/L/O/OU/EMAIL/CN"));
}
}
return subject_name;
}
void req::out_to_file(int out_format, const std::string out_path)
{
// 将证书申请,写入文件
BIO* out = BIO_new_file(out_path.c_str(), "w");
if(out_format == _PEM) {
PEM_write_bio_X509_REQ(out, x509_req);
}else if(out_format == _DER) {
i2d_X509_REQ_bio(out,x509_req);
}
BIO_free(out);
}
void req::out_to_stdout(int out_format)
{
// 将证书申请,写入文件
BIO* out = BIO_new_fp(stdout, BIO_NOCLOSE);
if(out_format == _PEM) {
PEM_write_bio_X509_REQ(out, x509_req);
}else if(out_format == _DER) {
i2d_X509_REQ_bio(out,x509_req);
}
BIO_free(out);
}
req::~req()
{
X509_REQ_free(x509_req);
}
附录
RFC文件的查看方法
RFC-wiki中有一篇博客链接:[译] 如何阅读 RFC 文档。这篇博客给出了在RFC Editor查找需要的RFC文档。
以本篇博客为例,我想查找证书请求的RFC文档,可以得到如下结果。
RFC 2511 中规定的证书请求消息格式已经被RFC 4211淘汰。RFC 4211 中使用算法,在RFC 9045中有更新。
所以想了解证书的请求规范,需要翻阅:RFC 2511、RFC 4211、RFC 9045。
我不喜欢直接读英文文档,所以我去找翻译。
可以在这四个站点,找到大多数RFC的文档翻译:http://rfc.ac.cn/、https://rfc2cn.com/、https://docs.huihoo.com/rfc/、http://www.cnpaf.net/Class/RFC/
所以本篇博客需要阅读的RFC的链接如下:
|