正如你所访问的这个网站,你会发现 Chrome(或者你用的其他浏览器),一般情况下(只要不是版本太老)都会在网站的 URL 前面加上绿色标志,表示你访问的网站是真的我部署的,而不是受到恶意拦截或者 DNS 污染等而伪造的假网站。一般来说,普通的网友想要拥有一个被信任的 HTTPS 网站,一般都是需要向 SSL 证书提供商处认证然后获得被信任的证书,这里之所以能被信任,是因为你电脑中的浏览器内置了很多根证书,通过这个根证书首先能证明这些提供商是正版的,当你的浏览器确认这些提供商是正版的了之后,再帮我的网站证明我的网站是正版的,从而告诉你这个正在使用浏览器的人,你看的网站是真的。

说起来有点绕,但是,这里涉及到几个在 TLS 中很重要的几个概念,理解了这几个概念对你阅读这篇文章有很大的帮助:

听上去有点绕,但是简单来讲就差不多是这么回事,因为今天不想讲太多理论上的东西,等周末闲下来再写一篇画画图,和有兴趣的同学聊一聊,今天要将的是实践,如何自己构造一对安全可靠的 SSL 证书,并且在自己的测试环境跑起来。先预先说一下,这里是以 CentOS 下的 Nginx 为例进行的:

第一步:安装Nginx和OpenSSL

$ yum install nginx openssl -y

因为后面会产生比较多的文件,所以为了能更好得管理这些文件,有必要新建一个目录,所以:

$ mkdir /home/liqiang.io
$ cd /home/liqiang.io
$ mkdir certs private config server

这里解释一下每个目录的作用:

第二步:创建 openssl 配置文件

里面的内容就写这些:

```
[ ca ]
default_ca      = foo                   # The default ca section

[ foo ]
dir            = ./         # top dir
database       = ./index.txt          # index file.
new_certs_dir  = ./newcerts           # new certs dir

certificate    = ./private/ca.crt         # The CA cert
serial         = ./serial             # serial no file
private_key    = ./private/ca.key  # CA private key
RANDFILE       = ./private/.rand      # random number file

default_days   = 365                     # how long to certify for
default_crl_days= 30                     # how long before next CRL
default_md     = sha256                     # message digest method to use
unique_subject = no                      # Set to 'no' to allow creation of
                                         # several ctificates with same subject.
policy         = policy_any              # default policy

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

[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req

[req_distinguished_name]
countryName = Country Name (2 letter code)
countryName_default = CN
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = GD
localityName = Locality Name (eg, city)
localityName_default = Shenzhen
organizationalUnitName  = Organizational Unit Name (eg, section)
organizationalUnitName_default  = Domain Control Validated
commonName = 192.168.57.16
commonName_max  = 64

[ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:TRUE  # 如果生成 CA 要改成 TRUE,server 证书要改成 FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = 192.168.57.16
IP.1 = 192.168.1.1
IP.2 = 192.168.57.16  
```

第三步:创建根证书

前面说过了,因为是非对称加密,所以会有公钥私钥一说,公钥是会给别人的,而私钥是证明你自己的存在,必须收藏好。其实根证书也是一种服务证书,所以你会发现步骤和后面的服务器证书步骤差不多:

3.1 生成私钥 key 文件

$ openssl genrsa -out private/ca.key 2048

3.2 生成证书请求 csr 文件

$ openssl req -new -key private/ca.key -out private/ca.csr -config "./config/openssl.conf"

3.3 生成凭证 crt 文件

$ openssl x509 -req -days 365 -in private/ca.csr -signkey private/ca.key -out private/ca.crt -extensions v3_req -extfile "./config/openssl.conf"

3.4 为 key 设置起始序列号和创建 CA 键库

$ echo RAND > serial  
$ touch index.txt
$ touch index.txt.attr

3.5 创建证书撤销列表

$ openssl ca -gencrl -out ./private/ca.crl -crldays 7 -config "./config/openssl.conf"

整套连起来就是这样的:

第四步:服务器证书的生成

当有了根证书之后,我们就可以来设置一个服务器的证书了,其实,如果要从简的话,直接将根证书作为服务器证书自验也行,不过,出于一个好习惯,我还是推荐根证书和服务器证书分开好一些,这也是工业的一个好实践。其实刚刚说了,这个步骤和创建根证书的步骤差不多,所以也就不多赘述了,直接走起:

$ openssl genrsa -out server/server.key 2048  
$ openssl req -new -key server/server.key -out server/server.csr -config "./config/openssl.conf"
# 不同的地方:使用私有的 CA key 为 key 签名
$ openssl x509 -req -sha256 -extensions v3_req -extfile "./config/openssl.conf" -CA private/ca.crt -CAkey  private/ca.key -CAcreateserial -in server/server.csr -out server/server.crt

执行的效果图差不多是这样的:

第五步:Nginx 配置 SSL 证书

到这一步的时候其实你在目录中已经生成好了需要的经根证书认证过的 SSL 证书了,下一步是时候上环境了,其实 Nginx 的配置也是极其简单的,我这里上一个配置看看:

你可以将这个配置添加到你的 Nginx 中,然后记得 reload 一下 Nginx:

$ nginx -t reload

这个时候打开浏览器查看一下,我这里的情况是这样的:

这是很明显的呀,因为你的证书使用自己的根证书签名认证的,而浏览器默认是不认你的根证书的,所以肯定是不安全的。那么可以怎么让他信任呢?最简单的方式就是将文件 private/ca.crt 导入到浏览器中,这样你就让你的浏览器信任你自己的根证书了,这其实也是很多企业自己签证书的常用操作,很多内网服务都是通过这种方式进行的,不用买外部的服务,省钱。。。

我这个版本的 Chrome 是这么设置的:

添加根证书之后,世界一切都是那么美好,只是绿的不够。

第六步:客户端证书的生成

如果你觉得只验证服务器不够,同时对访客也要进行一波验证,那么没问题,你也可以要求客户端提供自己的证书,然后服务端进行验证;当然,首先还是要有客户端的证书对吧,还是通过自己的根证书签发:

6.1 创建存放 key 的目录 users

$ mkdir client

6.2 创建一个客户端 key

$ openssl genrsa -des3 -out ./client/client.key 2048

6.3 为客户端 key 创建一个证书签名请求文件(csr)

$ openssl req -new -key ./client/client.key -out ./client/client.csr

6.4 使用私有的 CA key 为客户端 key 签名

$ openssl ca -in ./client/client.csr -cert ./private/ca.crt -keyfile ./private/ca.key -out ./client/client.crt -config "./config/openssl.conf"
$ openssl pkcs12 -export -clcerts -in ./client/client.crt -inkey ./client/client.key -out ./client/client.p12

这前 4 部分的执行情况大概是这样的:

6.5 修改 Nginx 配置如下:

然后你重启一下 nginx 再访问看看:

嘿嘿预想不到吧。这是因为 https 双向验证需要客户端安装证书,而你此时默认是没有客户端的证书的,这里也提示得很明显了,所以下面需要将客户端证书添加进去。

6.6 添加客户端证书

拿到生成的客户端证书 client.p12,直接双击它,进入 "证书导入向导":导入证书,直接双击下一步,下一步,输入密码,添加成功。同时 MAC 下需要手动添加为信任钥匙串,并使用safari打开测试

然后再次打开浏览器,你就会看到这个了:

然后确定共用这个之后,Mac 处于安全会要求你输入密码:

当输入密码之后,又是刚才的页面了,万事大吉:

总结

本文对创建自签名的 SSL 证书进行一个可实践的指导,并且给出了服务器和客户端双向认证的方案,并且是可以保证你安全认证的,希望对有需要的同学有所帮助。同时,最近我看到有几个网站在做一些抄袭的事情,甚至于连本文的原地址都去掉了,不知道这些网站意欲何为?本博客的文章,除了标注了转载和翻译之外,其他所有文章均为本人在工作业余时间完成,大部分都是熬夜写的,希望能够尊重个人劳动成果。同时,我也对那些于我学习这些知识有帮助的文章表示感谢,并在下方贴回引用。

Reference

  1. Configuring HTTPS servers
  2. Introducing TLS with Client Authentication
  3. Secure gRPC with TLS/SSL