Contents

HTTP:HTTPS实现

本文采用知识共享署名 4.0 国际许可协议进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,可适当缩放并在引用处附上图片所在的文章链接。

HTTPS 介绍

HTTPS 简史

在早期HTTP诞生的这几年间,1990年~·1994年,HTTP作为一个应用层协议,它是这样工作的:

https://cdn.jsdelivr.net/gh/zhangyuhu/share_images/images/202302092016336.png

后来网景公司开发了SSL(Secure Sockets Layer)技术,然后它就变成了这样的HTTP,也就是HTTPS了:

https://cdn.jsdelivr.net/gh/zhangyuhu/share_images/images/202302092016541.png

后来爆发了与IE的世纪大战,网景败北,SSL移交给了IETF(Internat Engineering Task Force)互联网工程任务组,标准化之后变成了现在的TLS,现在一般会把它们两个放在一起称为SSL/TLS。本篇并不关注SSL/TLS具体是如何工作的,只是抽象的解释下HTTPS的一个工作流程。

HTTPS 工作流程

https://cdn.jsdelivr.net/gh/zhangyuhu/share_images/images/202302092016561.png

  1. Client发起一个HTTPS(https:/demo.linianhui.dev)的请求,根据RFC2818的规定,Client知道需要连接Server的443(默认)端口。
  2. Server把事先配置好的公钥证书(public key certificate)返回给客户端。
  3. Client验证公钥证书:比如是否在有效期内,证书的用途是不是匹配Client请求的站点,是不是在CRL吊销列表里面,它的上一级证书是否有效,这是一个递归的过程,直到验证到根证书(操作系统内置的Root证书或者Client内置的Root证书)。如果验证通过则继续,不通过则显示警告信息。
  4. Client使用伪随机数生成器生成加密所使用的会话密钥,然后用证书的公钥加密这个会话密钥,发给Server。
  5. Server使用自己的私钥(private key)解密这个消息,得到会话密钥。至此,Client和Server双方都持有了相同的会话密钥
  6. Server使用会话密钥加密“明文内容A”,发送给Client。
  7. Client使用会话密钥解密响应的密文,得到“明文内容A”。
  8. Client再次发起HTTPS的请求,使用会话密钥加密请求的“明文内容B”,然后Server使用会话密钥解密密文,得到“明文内容B”。

搭建https服务

首先确认安装OpenSSL

确定OpenSSL版本:

1
$openssl version

如果版本低于1.0.1f,建议升级,因为1.0.1f版本之下的OpenSSL有一个Heartbleed漏洞。 安装OpenSSL:

1
$sudo apt-get install openssl

自建CA

因为向CA申请签名是需要收费的,所以我们选择自己搭建一个CA来完成这个实验过程。 首先建立myCA目录用于存放CA相关信息

1
cd && mkdir -p myCA/signedcerts && mkdir myCA/private && cd myCA

myCA 用于存放 CA 根证书,证书数据库,以及后续服务器生成的证书,密钥以及请求 signedcerts:保存签名证书的 copy private: 包含私钥

之后配置myCA相关参数,在myCA目录下进行

1
echo '01'>serial && touch index.txt

然后创建 caconfig.cnf 文件

1
vim ~/myCA/caconfig.cnf

caconfig.cnf文件内容如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# My sample caconfig.cnf file.
#
# Default configuration to use when one is not provided on the command line.
#
[ ca ]
default_ca      = local_ca
#
#
# Default location of directories and files needed to generate certificates.
#
[ local_ca ]
dir             = /home/didi/myCA                    # 这里要将username替换为你的用户名
certificate     = $dir/cacert.pem
database        = $dir/index.txt
new_certs_dir   = $dir/signedcerts
private_key     = $dir/private/cakey.pem
serial          = $dir/serial
#       
#
# Default expiration and encryption policies for certificates.
#
default_crl_days        = 365
default_days            = 1825
default_md              = SHA256
#       
policy          = local_ca_policy
x509_extensions = local_ca_extensions
#       
#
# Default policy to use when generating server certificates.  The following
# fields must be defined in the server certificate.
#
[ local_ca_policy ]
commonName              = supplied
stateOrProvinceName     = supplied
countryName             = supplied
emailAddress            = supplied
organizationName        = supplied
organizationalUnitName  = supplied
#       
#
# x509 extensions to use when generating server certificates.
#
[ local_ca_extensions ]
subjectAltName          = DNS:localhost
basicConstraints        = CA:false
nsCertType              = server
#       
#
# The default root certificate generation policy.
#
[ req ]
default_bits    = 2048
default_keyfile = /home/didi/myCA/private/cakey.pem  # 这里要将username替换为你的用户名
default_md      = SHA256
#       
prompt                  = no
distinguished_name      = root_ca_distinguished_name
x509_extensions         = root_ca_extensions
#
#
# Root Certificate Authority distinguished name.  Change these fields to match
# your local environment!
#
[ root_ca_distinguished_name ]
commonName              = MyOwn Root Certificate Authority # CA机构名
stateOrProvinceName     = BJ                               # CA所在省份
countryName             = CN                               # CA所在国家(仅限2个字符)
emailAddress            = XXXX@XXX.com                     # 邮箱
organizationName        = XXX                              # 
organizationalUnitName  = XXX                              # 
#       
[ root_ca_extensions ]
basicConstraints        = CA:true

生成 CA 根证书和密钥

1
2
export OPENSSL_CONF=~/myCA/caconfig.cnf       #该命令用于给环境变量 OPENSSL_CONF 赋值为caconfig.cnf。
openssl req -x509 -newkey rsa:2048 -out cacert.pem -outform PEM -days 1825             # 生成 CA 根证书和密钥

该命令需要用户设置密码。不要忘记。 以上步骤生成了 CA 自签名根证书,和 RSA 公/私密钥对。证书的格式是 PEM,有效期是1825天。

  • /myCA/cacert.pem: CA 根证书
  • /myCA/private/cakey.pem: CA 私钥

创建服务器公私钥

生成服务器配置文件exampleserver.cnf

1
vim ~/myCA/exampleserver.cnf

exampleserver.cnf文件内容如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#
# exampleserver.cnf
#

[ req ]
prompt             = no
distinguished_name = server_distinguished_name

[ server_distinguished_name ]
commonName              = localhost          # 服务器域名
stateOrProvinceName     = BJ                 # 服务器所在省份
countryName             = CN                 # 服务器所在国家(仅限2个字符)
emailAddress            = XXXX@XXX.com       # 邮箱
organizationName        = XXX                # 
organizationalUnitName  = XXX                # 

生成服务器证书和密钥

1
2
export OPENSSL_CONF=~/myCA/exampleserver.cnf   # 该命令设置环境变量 OPENSSL_CONF,使得 openssl 更换配置文件。
openssl req -newkey rsa:1024 -keyout tempkey.pem -keyform PEM -out tempreq.pem -outform PEM

同样的,需要输入密码短语。 之后,有2种对临时秘钥的操作,选择其一即可 1.将临时私钥转换为 unencrypted key,即秘钥不加密状态。

1
openssl rsa -in tempkey.pem -out server_key.pem

需要输入密码短语。

2.如果希望将 key 保持为加密状态,直接改名

1
mv tempkey.pem server_key.pem

两者的区别是,第二种需要在服务器启动时输入私钥的密码短语,否则会导致服务器启动失败,但第二种安全性高于第一种,可以更好的保护秘钥。

使用 CA key 对服务器证书签名

1
2
export OPENSSL_CONF=~/myCA/caconfig.cnf
openssl ca -in tempkey.pem -out server_crt.pem

这里提示:

1
2
3
Can't open /home/shawn/myCA/index.txt.attr for reading, No such file or directory
140344032040704:error:02001002:system library:fopen:No such file or directory:crypto/bio/bss_file.c:74:fopen('/home/shawn/myCA/index.txt.attr','r')
140344032040704:error:2006D080:BIO routines:BIO_new_file:no such file:crypto/bio/bss_file.c:81:

是Openssl的bug,在myCA目录下再touch一个index.txt.attr即可

删除临时证书和密码文件

1
rm -f tempkey.pem && rm -f tempreq.pem

现在,自签名的服务器证书和密钥对便产生了:

  • server_crt.pem : 服务器证书文件
  • server_key.pem : 服务器密钥文件

搭建https工程

基于 cpp-httplib搭建工程 , 工程地址 :http-https

  1. server 端代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <httplib.h>
#include <fstream>
#include <iostream>
#include <string>

using namespace std::placeholders;

class TEST
{
private:
    httplib::Server *m_svr;
    void recvGetHelloHandle(const httplib::Request &req, httplib::Response &res);

public:
    TEST();
    ~TEST()
    {
        delete m_svr;
    }

    void start(int port)
    {
        m_svr->listen("0.0.0.0", port);
    }
};

TEST::TEST()
{
    m_svr = new httplib::Server;
    httplib::Server::Handler getHello_cb = std::bind(&TEST::recvGetHelloHandle, this, _1, _2);
    m_svr->Get("/hello", getHello_cb);
}

void TEST::recvGetHelloHandle(const httplib::Request &req, httplib::Response &res)
{
    if (!req.has_header("token"))
    {
        res.set_content("fail", "text/plain");
        return;
    }

    std::string token = req.get_header_value("token");
    std::cout << "token : " << token << std::endl;
}

class TESTHTTPS
{
private:
    httplib::SSLServer *m_svr;
    void recvGetHelloHandle(const httplib::Request &req, httplib::Response &res);

public:
    TESTHTTPS();
    ~TESTHTTPS()
    {
        delete m_svr;
    }

    void start(int port)
    {
        m_svr->listen("localhost", port);
    }
};

TESTHTTPS::TESTHTTPS()
{
    m_svr = new httplib::SSLServer("./data/server_crt.pem", "./data/server_key.pem");

    httplib::SSLServer::Handler getHello_cb = std::bind(&TESTHTTPS::recvGetHelloHandle, this, _1, _2);
    m_svr->Get("/dd/hello", getHello_cb);
}

void TESTHTTPS::recvGetHelloHandle(const httplib::Request &req, httplib::Response &res)
{
    if (!req.has_header("token"))
    {
        res.set_content("fail", "text/plain");
        return;
    }

    std::string token = req.get_header_value("token");
    std::cout << "token : " << token << std::endl;
}

int main(void)
{
    TESTHTTPS test;
    test.start(8887);
}
  1. client 端代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <httplib.h>
#include <fstream>
#include <iostream>
#include <string>

class TESTHTTP
{
private:
    httplib::Client *m_cli;

public:
    TESTHTTP()
    {
        m_cli = new httplib::Client("https://localhost:8887");
    }
    ~TESTHTTP()
    {
        delete m_cli;
    }
c++
    void testGetHello();
};

void TESTHTTP::testGetHello()
{
    httplib::Params params = {{"params", "test"}};
    httplib::Headers headers = {{"version", "0.0.0.1"},
                                {"token", "V66n56XJBD/4LTMsGDqE0dlRPtkVmQ0hlIQNwcBMZkU4bQeDTBlMEUluqnha1g/ZsEMK/0JHKtEBwHH2Ko9iEA=="}};
    auto res = m_cli->Get("/dd/hello", params, headers);
    std::cout << httplib::to_string(res.error()) << std::endl;
    std::cout << res->body << std::endl;
}

class TESTHTTPS
{
private:
    httplib::Client *m_cli;

public:
    TESTHTTPS()
    {
        m_cli = new httplib::Client("https://localhost:8887");
        m_cli->set_ca_cert_path("./data/cacert.pem");
        m_cli->enable_server_certificate_verification(true);
    }
    ~TESTHTTPS()
    {
        delete m_cli;
    }

    void testGetHello();
};

void TESTHTTPS::testGetHello()
{
    httplib::Params params = {{"params", "test"}};
    httplib::Headers headers = {{"version", "0.0.0.1"},
                                {"token", "V66n56XJBD/4LTMsGDqE0dlRPtkVmQ0hlIQNwcBMZkU4bQeDTBlMEUluqnha1g/ZsEMK/0JHKtEBwHH2Ko9iEA=="}};
    auto res = m_cli->Get("/dd/hello", params, headers);
    std::cout << httplib::to_string(res.error()) << std::endl;
    std::cout << res->body << std::endl;
}

int main(void)
{
    // TESTHTTP T;
    // T.testPostPush();

    TESTHTTPS T;
    T.testGetHello();
}

参考

信安实践——自建CA证书搭建https服务器 HTTPS工作流程

测试工程

http-https