证书请求简介

前言与摘要

背景要求

  • 证书申请的基本原理:可以参考《图解密码技术》-- 结城浩 – 第十章 证书。
  • 了解基本的openssl 命令行使用。

摘要

本文没有介绍证书申请的背景知识和相关的openssl使用。本文直接开始介绍,使用openssl命令,生成证书请求。为了搞明白证书请求需要包含哪些内容,我们去阅读了证书请求的相关RFC规范。接着,我们阅读openssl命令执行证书请求的源码执行流程。阅读源码的过程中,我们浅尝辄止,没有深入阅读具体的函数实现。所以,我们不知道,在证书申请这块,openssl是否完全遵守了RFC规范。最后,我们使用C++封装了openssl api,实现了一个简单的证书请求类。


openssl命令行-生成证书请求

openssl reqreq - PKCS#10 certificate request and certificate generating utility.

openssl-genrsagenrsa - generate an RSA private key.

我们使用genrsa生成临时RSA密钥。接着,我们使用req生成证书请求。关于命令使用的详细介绍,参见上面两个官方文档链接。

# 生成一个私钥
➜  openssl genrsa -out private_key.pem 2048 # pem格式

# 交互式的构造证书请求
➜  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中包含两部分内容:DataSignature

Data中包含Subjectpublic 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:由请求标识符、证书内容模板和可选的控制信息序列组成。(上一节中的subjectpublicKey均为证书模板内容中的一部分)

popo:需要证明请求证书有对应的私钥。(上一节中这一部分是签名值)

regInfo:仅包含与证书请求上下文相关的补充信息。(不知道这啥东西)

那有个问题:openssl的实现是否满足上面规范,或者说,openssl如何实现上面规范

这个问题跳过。下面虽然阅读了点openssl源码,但具体的函数内部没有去阅读。


openssl源码阅读-生成证书请求

调试下面命令对应openssl源码的执行过程。

openssl req -key private_key.pem -new -config ruler.conf -out ruler_csr.pem

# 调试下面命令会失败。因为如果没有指定配置文件,会加载默认配置文件openssl.conf。
# 而我没有源码安装openssl,所以会报错。
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为私钥;fsbjX509_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的链接如下:

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

da1234cao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值