Abstract
Keywords 数字签名证书  Let’s Encrypt证书  SSL证书  数字签名证书  Let’s Encrypt证书  SSL证书 
Citation Yao Qing-sheng.自建数字签名证书.FUTURE & CIVILIZATION Natural/Social Philosophy & Infomation Sciences,20240808. https://yaoqs.github.io/20240808/zi-jian-shu-zi-qian-ming-zheng-shu/

局域网内搭建浏览器可信任的 SSL 证书 [1]

首先是为什么要干这个事情,你可能会说随便搞个自签名证书难道不能用吗?答案是还真的不能用,的确对于开发来说搞个自签名的证书就行了。但是一旦放到生产环境浏览器对证书有效性进行验证的时候便是不可信状态,这时就必须要用户点击一下继续访问,但是对于我们即将实施项目的自动化要求来说没法这样干。你可能又会说了现在这个环境在阿里云、华为云这些平台上随便申请一个免费的证书难道不行吗?答案是真的不行,因为项目的特殊要求最终我们部署的环境是完全没有外网访问的,就只能在局域网环境下运行及意味着不光是 SSL 证书的问题我们连 DNS 服务器都要自己建。这时候你可能又要说了那么直接用 http 访问就可以了,干嘛要用 ssl 证书呀?答案是这个项目需要使用 WebRTC 进行音视频多人会议,而 WebRTC 只能在 https 下运行。

其实上面的说法有一个点需要更正一下,自签名证书其实也可以但是一旦对超过 100 个客户端进行分发简直是要命的事情,所以我们通过 Windows 域控的方式统一对下属计算机进行证书分发保证可用性。

1. 原理

SSL 证书的信任机制其实是非常简单的,第一需要一个机构证书,第二是需要服务端证书,一般来说机构证书被称为 CA 证书,而服务端证书就称为服务器证书吧。那么为啥 https 非常安全呢?答案其实不复杂,下面就是一段逻辑性描述来说明为啥 https 是安全的。

通常情况下我们在给 Nginx、Tomcat、IIS 上配置的证书便是服务器证书,那么它是怎么保证客户端访问的地址绝对没有被拦截修改的呢?其实也不复杂,当我们的浏览器发起一个请求的时候到服务端上时,对应 web 服务器会通过证书的秘钥将 http 响应值进行一次加密,然后将密文与明文同时返回出来,客户端浏览器接收到响应之后会将密文对称解码然后和明文进行对比,这样一来便可以保证响应值没有被串改。

这个时候逻辑上稍微厉害一点都会发现一个问题,客户端是怎么解码的?这里的答案就是服务端在响应的时候同时会将证书的公钥也返回,这个公钥只能解码对应私钥加密的信息,同时这个公钥无法加密只能解密,这样一来如果如果某人想要拦截 http 请求便必须知道对应的私钥才行,否则浏览器一旦发现解密信息对不上便知道了响应数据已经被拦截修改过了。

如果你反应过来了你会发现一个新的问题,那么假设拦截这自己搞了一对有效的私钥和公钥然后伪装为服务器不就行了,恭喜你盲生发现了华点。这里就需要 CA 证书来处理了。其实服务器证书的公钥是由 CA 证书的秘钥配对加密来的,这样一来当请求返回的服务器公钥和通过 CA 证书进行验证时便会发现这个公钥是不是由机构签发的公钥,一旦对应不上则说明服务器不是原来 CA 证书签发服务器证书,这就证明你的请求被第三方拦截了。同时 CA 证书对于浏览器而言只有公钥,也就是说安装证书时本质上就是将 CA 证书的公钥导入到你的电脑上了,至此除开 CA 机构的证书发放者没有知道 CA 证书的秘钥是什么这样一来便可以保证下面几个非常关键的安全性:

  • 你请求的服务绝对是官方的服务器,绝对不是黑客自建的服务器。
  • 服务器响应给你的数据绝对是正确的,期间黑客绝对无法对其进行修改。

证书的结构如下:

这里还有一个问题便是这些 CA 证书是哪来的,自己的电脑上又重来没有导入过什么证书。这里便是一个非常无耻躺着赚钱的商业模式了,微软、谷歌、苹果等公司提供了操作系统和浏览器,他们便是第一方的 CA 机构,他们的系统自己肯定信任自己对吧?所以系统安装的时候他们的 CA 公钥已经安装到你们的系统里面了,然后这几家巨头合伙说那么这些 CA 公钥在每种系统都有,然后就是一写第三方公司和这些巨头打成了合作,这些公司的机构证书也被巨头们信任所以理所当然的入库了,这些三方机构便是大名鼎鼎的 SymantecGeoTrust 几个巨头,这些机构一个单域名的签名证书都敢直接拿出来卖,一年好几千,对他们而言无法就是给下发的证书进行一次签名而已,真正的躺着赚钱。

2. 开始制作证书

这里我使用的证书工具是 openssl,经典工具,坦白的说非常难用。

2.1 创建 CA 证书

首先第一步肯定是制作一个机构证书也就是 CA 证书出来,这里有两种方案,第一是直接用 openssl 创建 CA 证书,另一种是 windows 域控生成域组织的 CA 证书,我们分开说。

2.1.1 通过 openssl 创建 CA 证书

第一步是创建一个秘钥,这个便是 CA 证书的根本,之后所有的东西都来自这个秘钥:

1
2
# 通过rsa算法生成2048位长度的秘钥
openssl genrsa -out myCA.key 2048

第二步是通过秘钥加密机构信息形成公钥:

1
2
3
# 公钥包含了机构信息,在输入下面的指令之后会有一系列的信息输入,这些信息便是机构信息,公司名称地址什么的
# 这里还有一个过期信息,CA证书也会过期,openssl默认是一个月,我们直接搞到100年
openssl req -utf8 -new -x509 -key myCA.key -out myCA.cer -days 36500

这一步需要输入的机构信息有点,分别说一下:

参数名称参数值
Country Name国家代码,比如中国就是 CN
State or Province Name省名称
Locality Name城市名称
Organization Name机构名称
Organizational Unit Name机构单位名称
Common Name重点参数:授权给什么,因为机构是根节点所以是授权给自己
Email Address邮件地址

2.1.2 通过 windows 域控创建 CA 证书

这种便是我采用的方案,执行上比直接用 openssl 创建证书复杂多了,但是好处也非常多,一方面域控下级的所有计算机天然对域控服务就是信任状态,第二是域控制器能够通过组策略域内同步 CA 证书,本质上来讲相对于多了一个 CA 证书同步与分发的机制。我这边使用的 Windows Server 2016,其他版本区别也不大。

第一步是在域控上启用证书服务

第二步是安装完毕之后配置证书

这里非常简单,我都不想说了,直接根据提示输入相关信息就行了,在过期时间那一步最好将时间拉长,我还是使用的 100 年。

第三步是通过组策略进行分发

策略路径是:计算机策略/Windows设置/安全设置/公钥策略/受信任的根证书颁发机构计算机策略/Windows设置/安全设置/公钥策略/受信任的发布者证书。将上面创建的证书导出之后,在这里导入即可。

2.2 创建服务器证书

在得到 CA 证书之后,需要通过 openssl 工具对证书进行转换得到公钥(.crt文件)和密钥(.key文件),无论 CA 证书是怎么来的到这里之后就没有任何区别了,服务器证书的制作流程相较 CA 证书要复杂一点点。

第一步通过 openssl 工具创建服务器的秘钥:

1
2
# 通过RSA算法生成长度2048位的秘钥
openssl genrsa -out server.key 2048

第二步这里是创建一个签名请求

需要将服务器信息写入到请求文件之中,然后通过 CA 机构证书对请求签名形成服务器证书公钥,这一步要复杂一些,很多网上的教程在这里都 GG 了主要原因没有把原理搞清楚。

首先 https 证书的公钥不同于自定义情况下的加密证书,这里需要安装浏览器标准进行配置,首先 openssl 默认的证书版本是 V1,V1 在支持 https 时部分浏览器依旧会认为不安全,所以需要使用 V3 版本;同时 openssl 即便是使用 V3 版本依旧没有附带 V3 的 subjectAltName 字段数据(这里是证书对应的 IP 地址或者域名,可以用通配符)。但是这些东西命令行没法指定所以需要配置文件,我这里准备了一个:

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# openssl.cnf
tsa_policy2 = 1.2.3.4.5.6
tsa_policy3 = 1.2.3.4.5.7

[ ca ]
default_ca = CA_default # The default ca section

[ CA_default ]
dir = ./demoCA # Where everything is kept
certs = $dir/certs # Where the issued certs are kept
crl_dir = $dir/crl # Where the issued crl are kept
database = $dir/index.txt # database index file.
new_certs_dir = $dir/newcerts # default place for new certs.
certificate = $dir/cacert.pem # The CA certificate
serial = $dir/serial # The current serial number
crlnumber = $dir/crlnumber # the current crl number
crl = $dir/crl.pem # The current CRL
private_key = $dir/private/cakey.pem# The private key
RANDFILE = $dir/private/.rand # private random number file
x509_extensions = usr_cert # The extentions to add to the cert
name_opt = ca_default # Subject Name options
cert_opt = ca_default # Certificate field options
default_days = 365 # how long to certify for
default_crl_days= 30 # how long before next CRL
default_md = default # use public key default MD
preserve = no # keep passed DN ordering
policy = policy_match

[ policy_match ]
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
commonName = supplied
emailAddress = optional

[ policy_anything ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional

[ req ]
default_bits = 1024
default_keyfile = privkey.pem
distinguished_name = req_distinguished_name
attributes = req_attributes
x509_extensions = v3_ca # The extentions to add to the self signed cert
string_mask = utf8only
req_extensions = v3_req # The extensions to add to a certificate request

[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = CN
countryName_min = 2
countryName_max = 2

stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = BeiJing

localityName = Locality Name (eg, city)

0.organizationName = Organization Name (eg, company)
0.organizationName_default = myca
organizationalUnitName = Organizational Unit Name (eg, section)
commonName = Common Name (e.g. server FQDN or YOUR name)
commonName_max = 64
emailAddress = Email Address
emailAddress_max = 64

[ req_attributes ]
challengePassword = A challenge password
challengePassword_min = 4
challengePassword_max = 20
unstructuredName = An optional company name

[ usr_cert ]
basicConstraints=CA:FALSE
nsCertType = client, email, objsign
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
nsComment = "OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer

[ svr_cert ]
basicConstraints=CA:FALSE
nsCertType = server
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
extendedKeyUsage = serverAuth,clientAuth

[ v3_req ]
subjectAltName = @alt_names

# 这里是重点,需要将里面配置为最终服务端需要的域名或者IP
# 这里可以写多个,能够自行添加DNS.X = XXXXXX
[ alt_names ]
DNS.1 = xunshi.com
DNS.2 = *.xunshi.com
IP.1 = 192.168.0.2
IP.2 = 192.168.0.3

[ v3_ca ]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
basicConstraints = CA:true

[ crl_ext ]
authorityKeyIdentifier=keyid:always

[ proxy_cert_ext ]
basicConstraints=CA:FALSE
nsComment = "OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo

[ tsa ]
default_tsa = tsa_config1 # the default TSA section

[ tsa_config1 ]
dir = ./demoCA # TSA root directory
serial = $dir/tsaserial # The current serial number (mandatory)
crypto_device = builtin # OpenSSL engine to use for signing
signer_cert = $dir/tsacert.pem # The TSA signing certificate
# (optional)
certs = $dir/cacert.pem # Certificate chain to include in reply
# (optional)
signer_key = $dir/private/tsakey.pem # The TSA private key (optional)

default_policy = tsa_policy1 # Policy if request did not specify it
# (optional)
other_policies = tsa_policy2, tsa_policy3 # acceptable policies (optional)
digests = md5, sha1 # Acceptable message digests (mandatory)
accuracy = secs:1, millisecs:500, microsecs:100 # (optional)
clock_precision_digits = 0 # number of digits after dot. (optional)
ordering = yes # Is ordering defined for timestamps?
# (optional, default: no)
tsa_name = yes # Must the TSA name be included in the reply?
# (optional, default: no)
ess_cert_id_chain = no # Must the ESS cert id chain be included?
# (optional, default: no)

将上面的配置内容保存为 openssl.cnf 放到生成的服务器证书文件的目录下(注意:修改 alt_names 里面的域名或者 IP 为最终部署需要的地址,支持通配符),然后执行创建签名申请文件即可,执行运行:

1
2
3
# 和创建CA时一样这里需要输入一堆服务器信息,输入项也是相同的。
# 不过在输入Common Name(CN)最好直接输入服务器的IP地址或者域名。
openssl req -utf8 -config openssl.cnf -new -out server.req -key server.key

PS:上述配置文件使用 sha1 算法生产的证书,部分浏览器已经已经不信任该算法了,如果你使用的时候遇到 sha1 相关的问题,可以参考评论区的 kevin 同学提供的方案

如果你遇到 sha1 问题,用稍微新一点的 openssl.cnf 文件 https://github.com/openssl/openssl/blob/master/apps/openssl.cnf
同时还要在这个文件里稍微改一下,把下述的配置加入进去

1
2
3
4
5
6
7
[ v3_req ]  
subjectAltName = @alt_names
# 这里是重点,需要将里面配置为最终服务端需要的域名或者IP
# 这里可以写多个,能够自行添加DNS.X = XXXXXX
[ alt_names ]
DNS.1 = xunshi.com
DNS.2 = *.xunshi.com

加上。
最后用请求生成密钥的时候 用下面这个指令 使用 sha384 代替默认的 sha1
openssl x509 -req -extfile openssl.cnf -extensions v3_req -in server.req -out server.cer -CAkey myCA.key -CA myCA.cer -sha384 -days 36500 -CAcreateserial -CAserial serial

第三步通过 CA 机构证书对服务器证书进行签名认证

1
2
# 这里没有什么需要说的,本质上就是将签名请求文件进行签名最终得到服务器的公钥
openssl x509 -req -extfile openssl.cnf -extensions v3_req -in server.req -out server.cer -CAkey myCA.key -CA myCA.cer -days 36500 -CAcreateserial -CAserial serial

第四步部署证书

这里应该没有什么需要说的了,我们通过 Nginx 部署,最终得到 server.key 就是秘钥,server.cer 文件就是公钥只需要配置给 Nginx 就行了。

3. 信任 CA 机构证书

如果通过 Windows 域控创建的 CA 证书,其证书本身通过组策略便可以给每一个域下计算机添加机构信任。如果你没有域控只是通过 openssl 创建的 CA 证书也没有关系,只需要将 CA 证书的公钥(myCA.cer文件)导入到系统信任的根证书颁发机构里面就行了:

这个界面在 windows 的 internet选型->内容->证书可以打开,导入即可,也可以直接双击 cer 文件进行证书安装,最终不光是 windows 系统,任何操作系统都可以安装证书来进行对 CA 机构的进行信任操作。

在对证书进行信任之后通过 https 打开浏览器进入内网 DNS 或者 host 配置的域名便可以得到没有任何警告的内容的安全连接:

如果是 Mac 系统访问逻辑也是一样的通过安装 CA 证书并且在钥匙串内添加信任之后依然可以正常访问:

在 Android 手机上也是一样,安装并且信任证书之后可以正常访问:

4. 总结

本来对我对 https 的认证逻辑其实理解没有多深入,以前也只是用过 SSL 证书进行 TCP 传输加密而已,经过对 openssl 的学习现在至少在理解上达到了及格水平,不过这次学习论证与探索的过程我个人极其不愉快,本来这东西在有了理解之后大家都看得出来不是什么很难的东西,事实上我也只用了一天半就搞定了。但是网上充斥大量垃圾内容,不光没有什么正向内容甚至不少内容还 TM 起了误导的作用,整个中文互联网检索体系下就没有找到一篇文章稍微详细描述整个搭建逻辑与流程,简直了,最终我只能从 https 原理和 openssl 的官方文档开始看起,过于离谱了。基本上可以得到一个结论现在天天写一些所谓干货的博主简直就是滥竽充数,其内容千篇一律大多数也是抄袭来的基本上什么都没有说清楚简直浪费时间。

最后说一下 https 的原理,在解释清楚之后其实不是绝对上的安全,结合本文各位可以想一下怎样去伪造一个页面出来?假设我是黑客来搞入侵其实只需要一个小小的脚本就可以了,我们自行制作 CA 和服务证书之后,通过修改 HOST 文件对域名解析进行劫持将其引导到我们自己的服务器,然后将我们自己制作 CA 证书注入目标电脑的受信任证书组,这样一来对于被入侵者已经看到是安全连接但是其请求已经被我们拦截了。所以各位不要看到 https 就以为安全了,一旦你的电脑本身就被入侵了那么 https 也是形同虚设的,所以在执行高风险操作的时候最好还是点开站点的证书看看对应的 CA 机构是不是被修改过。

免费的 Let’s Encrypt 证书 [2]

一个比较离谱的事情发生了,我每年在阿里云搞得免费证书现在有效期只有 3 个月,相当于我 TM 每三个月就要去手动更新一下证书,之前的博客也写了 SSL 这个东西简直就是一本万利的事情(局域网内搭建浏览器可信任的 SSL 证书),这种完全不能忍,所以拜拜了资本家,老子不用你了,免费证书不香吗?

Let’s Encrypt 就是免费证书的代表,这玩意几乎所有设备都支持,并且能申请通配证书,步骤如下:

安装 snapd,这是为了安装 certbot 的工具

1
2
3
yum install snapd
systemctl enable --now snapd.socket
ln -s /var/lib/snapd/snap /snap

安装 certbot,这个工具就是拿来申请证书的

1
2
snap install --classic certbot
ln -s /snap/bin/certbot /usr/bin/certbot

申请证书

1
certbot certonly --manual --preferred-challenges dns
  1. 在运行这个命令之后会开始证书的申请步骤,具体如下:
  2. 如果需要提供邮箱就按照提示输入邮箱即可;
  3. 按照提示输出域名,用空格分隔多个域名
    1. 注意:tangyuecan.com 和 www.tangyuecan.com 是两个不同的域名,如果使用通配域名就是 *.tangyuecan.com
  4. 通过提示进行 DNS 验证,这里按照要求添加一个 TXT 解析即可
    1. 注意:连续超过 5 次解析失败,服务器在未来五个小时都无法继续申请
  5. 成功之后就会生成对应的证书文件;

使用证书

certbot 生成的证书文件很多,其中对于 nginx 等常规的 web server 就只有两个有用:

  • fullchain.pem:这个是证书文件,兼容大部分 web server;
  • privkey.pem:这个是秘钥文件;

以 nginx 为例,最终使用证书配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;

server_name tangyuecan.com;
ssl_certificate /ssl/fullchain.pem;
ssl_certificate_key /ssl/privkey.pem;

root /var/www/html;
index index.php;

location / {
try_files $uri $uri/ /index.php?$args;
}

location ~ .php$ {
include fastcgi.conf;
fastcgi_intercept_errors on;
fastcgi_pass xxx.xxx.xxx.xxx:xxxx;
}
}

自动更新证书

没有想到吧,还有这个功能,原理也很简单,但是使用上存在一个问题,每一次更新的证书的时候都需要对域名所有权进行一次校验,只有通过了校验 Let’s Encrypt 才会知道这个域名是你在管理,进而才会为你更新证书,上文使用 DNS 进行验证的话,运行 renew 进行重新校验可能是没法通过的,因为 certbot 无法访问你的 DNS 解析对吧,所以这里最佳方案是通过 web 服务器进行验证。

1
2
certbot certonly --webroot -w /var/www/example -d www.example.com -d example.com --dry-run
# 其中--dry-run的意思是测试,正式更新的时候去掉就行了;-w指的是webroot的路径,对应-d就是该路径下的域名,明显一个路径下有多个域名

特别注意:如果你使用了泛域名解析就不能直接通过 webroot 进行更新证书,毕竟泛域名不大可能指向同一个 webroot 路径,这种情况下想要实现自动更新证书只有通过 DNS 插件才能实现,可以参考这个项目


  1. https://www.tangyuecan.com/2021/12/17 / 局域网内搭建浏览器可信任的 ssl 证书 / ↩︎

  2. https://www.tangyuecan.com/2023/12/03 / 免费的 lets-encrypt 证书 / ↩︎

References