自签发证书配置 HTTPS 单向双向验证

好久以前阅读《HTTP/2 in Action》一书起了个头,又重新放回了书架。近来再次对 HTTPS/TLS 来了劲,自己的博客用的是 Let's Encrypt 签发的证书,这次实践一下自签发证书的过程与配置,并实现单向和双向的认证方式。

如果是配置单向认证的过程需要有以下三个证书

  1. 根(CA) 证书: root.crt
  2. 服务端私钥文件: server.key
  3. 服务端公钥证书: server.crt

证书是含有组织与域名或(CA) 信息以及公钥的文件, root.key 和 root.crt 将被用于签发其他的证书。这里的 crt 证书是 x509 格式的。

浏览器只会信任某些 CA 机构签发的证书,如 DigiCert, GlobalSign, GoDaddy, Amazon Root CA,Let's Encrypt 等。如果是不被信任 CA 签发的证书,我们在浏览器中打开相应的 HTTPS url 就会看到 'Not Secure - Your connection is not private' 的提示,要继续访问需自行承担可能的安全责任。

如果是公司内部,或相互信任的公司之间的通信,用自签发的证书也不成问题。不过直接用浏览器或其他 HTTPS 客户端信任又免费的 Let's Encrypt 来签发证书可免去浏览器的警告或某些特殊配置。

根证书,服务端证书或后面将要讲到的客户端证书的完整生成过程都是三步

  1. openssl genrsa 生成私钥 xxx.key
  2. openssl req -new -out xxx.csr -key xxx.key 产生证书请求文件
  3. openssl x509 -req in xxx.csr -out xxx.crt -signkey xxx.key -CAcreateserial -days 365 生成相应证书文件。生成根证书时无需指定 -CA 参数,因为自己就是 CA,其他证书则需用 -CA root.crt 让根证书予以签发

根据不同的情景,以上某些步骤可以合并操作。

生成根(CA)证书

最终要得到的 root.key, root.crt 用于签发服务端或客户端证书。如果对中间过程没有兴趣的,可用该节最后的 openssl req -x509... 一条命令完成所有。

1. 生成根私钥 root.key

虽然 openssl genrsa 支持最小的长度是 512,但 Nginx 要求最短长度为 2048。

2. 生成根证书请求文件 root.csr

-subj 参数指定所有组织相关的信息,免得按提示要多次输入,C, ST, O 分别是国家,省(州), 组织名,这里省略了 OU 部门名称,根据实际进行输入即可。最后 CN,此处是根证书,可随便指定,无需真正的域名。

生成证书请求文件是一个中间步骤,目的是准备组织和域名信息,再加上私钥用于签发最后的证书文件(包含公钥)

3. 生成自签发的根证书 root.crt

CA 就是自己,所以不用指定 -CA 参数

其实最终只需要 root.key 和 root.csr 文件,所以 #2, #3 两步可合而为一,并且不生成中间过程的 root.csr 请求文件,只生成  root.crt 文件

** 由于是自签发根证书,只需一条命令便能生成需要的 root.key 和 root.crt 文件

如果是在 macOS 中可用命令 open root.crt 查看,默认会用 Keychain Access/Add Certificates 打开,然后可看到信息

或者用 openssl 查看,下面显示了部分信息

生成服务端私钥与证书

正常的过程是要先有私钥 server.key,证书请求文件 server.csr, 然后用前面的根(CA)证书 root.key 和 root.crt 签发服务端证书,可参考上节中生成 root.key 和 root.csr 的步骤。

1. 生成服务端私钥与证书请求文件

这里用一条命令同时生成 server.key 和 server.csr 两文件

-subj 参数要留意 CN 必须与服务器的访问方式对应,如果将用 IP 地址(如 192.168.86.101) 访问, 则 CN=192.168.86.101。这里假设将用 server.local 域名来访问,所以设置 CN=server.local,后面在客户端将通过 /etc/hosts 中配置 127.0.0.1 server.local 来访问本地的 https://server.local 服务。 

2. 用根证书签发服务端证书

到现在我们就有了 server.key 和 server.crt 文件,同样的方式可以查看 server.crt 的信息。

私钥,证书请求文件和证书的内容格式

不管是私钥,公钥还是证书,或更多我们常见到  key, csr, crt, cer, der, pem, pfx, p12, jks 等扩展名,其实它们内部的格式都差不多,基本像下面那样

-----BEGIN XXX-----
<ASCII 字符串, 通常是 BASE64 格式>
-----END XXX-----

以上扩展名的缩写为

  1. PEM(Privacy-Enhanced Mail)
  2. DER(Distinguished Encoding Rules)
  3. CRT/CER(Certificate), KEY(key)
  4. P12(PKCS#12)
  5. PFX(Predecessor of PKCS#12)
  6. JKS(Java Key Storage)

比如我们查看前面生成的 key, csr, crt 文件内部如下:

cat server.key
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCnGmYO71gf4bQF
......
3TFxXQvsM5LjL/JCckYRscUiVQiaZzkNi044XeTRLKLrrsEKxOKJ+Dn7R5c4lBIQ
+kP41vHSScLrhltkjKZfQiHQ
-----END PRIVATE KEY-----
cat server.csr
-----BEGIN CERTIFICATE REQUEST-----
MIICpDCCAYwCAQAwXzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCElsbGlub2lzMRAw
......
jr3ySo1hczjdhn4TiYR7AtLMTQj4X8aTnSfDBpn95+/jR1x9KjeCwLBgCJWXmtwo
Fu5hcsQXfjc=
-----END CERTIFICATE REQUEST-----
cat server.crt
-----BEGIN CERTIFICATE-----
MIIDhzCCAm+gAwIBAgIUb2sb3c3BaZk+z1fOprzKO6Zcu18wDQYJKoZIhvcNAQEL
......
4YXaB54t+I8x/eiYu1W77hJjAG52SsVb/2QiJdbFT0VDwcVrMdMRutMjlg==
-----END CERTIFICATE-----

在 csr, crt 文件中内容进行 Base64 解码可看到组织及域名信息。有时候我们看到的  *.pem 文件,它可能就是一个 *.crt 文件,比如愿意的外把上面的 server.crt 改名成 server.pem 也行,关键看其中的内容是 BEGIN xxx。

配置 Nginx 支持 HTTPS 单向验证 (TLS)

有了前面生成的服务端私钥 server.key, 证书 server.crt 后,首先应用到 Nginx Web 服务中。以 Docker 启用的 Ubuntu:24.04 容器为例, 假定 server.key, server.crt 在当前目录中

覆盖 /usr/shar/nginx/html/index.html 的内容为 "Ok", 以免后的 curl 命令输出过长。

编辑 /etc/nginx/nginx.cfg, 在 http 块中加上

然后启动 nginx 或重新启动 nginx -s reload

用 curl 测试

已在 /etc/hosts 中配置了 127.0.0.1 server.local 映射。或可在另一个 Docker 容器中测试(docker run -it --net host ubuntu:24.04)

root@docker-desktop:~# curl https://server.local
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

因为服务器端的证书是自签发的,默认不被信任,需用 -k 信任证书,重试,顺便加上 -v 参数

控制台输出明确显示了完整的 TLS 握手过程

* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20): 

用浏览器访问,同样因为我们自己的 rootCA 是不被浏览器信任的,所以看到的是这样

点击地址栏 "Not Secure" 可查看服务端证书以及 CA 的信息。点 "Advanced" -> "Processed to server.local (unsafe)" 自担安全的信任该网站 server.local 而访问它的内容

还能用 openssl s_client 命令访问

openssl s_client 更有助于我们理解 TLS 握手的详细过程。

Apache2 中的配置 HTTPS

和 Nginx 差不多,先要启用相应的模块和站点

再编辑 default-ssl.conf,配置一个 443 虚拟机

apachectl -k restart 重启即可

SpringBoot Tomcat 中的配置

如果开发的一个 SpringBoot Web 应用直接面对客户,它的前面没有 Nginx/Apache 或其他的 Load Balancer, 这一节或许对我们会有所帮助。所有的关于 server.ssl 的配置可参考 SpringBoot 官方文档 Common Application Properties / Server Properties

相关的配置项有

从中我们发现 server.ssl.certificateserver.ssl.certificate-private-key 可实现与 Nginx/Apache 类似的配置

试着在 application.properties 中加上

启动 SpringBoot Web 项目,看到控制台输出

Tomcat started on port 8080 (https) with context path '/'

测试

curl -kv https://server.local:8080/

没问题,一样的效果,需要注意的是 SpringBoot Tomcat 不同时启动 HTTP 和 HTTPS 服务,配置了相应的 server.ssl 则只启动 HTTPS 服务,并且端口号复用 server.port 的配置。

如果采用 key-store 的配置方式则要用 Java 的 keytool 命令把 openssl 生成的  server.key, server.crt 生成 keystore.jks 文件,或者直接用 keytool 替代 openssl 来生成它所需的 keystore.jks 文件。

配置 Nginx 支持 HTTPS 双向验证 (mTLS)

为配置 mTLS(mutual TLS),除了前面的 root.key, root.crt, server.key, server.crt 外,还需要为客户端生成相应的 key 与证书。何谓双向验证是除了传统的客户端信任服务端,服务端还要检查客户端的证书,决定是否信任,是否提供服务给该客户端。我们可以为不同的客户生独立的客户端私钥与证书,或多个客户可共享。服务端单向验证在客户端可选择信任或不信任,即使不信任也可以使用服务; 而双向认证让服务端更有主动权,服务认为是一个非法的客户(未提供客户端证书或提供了非法的客户端证书)能够拒绝服务。双向认证让 HTTPS 通信更安全,不易被中间人攻击,以至于让无比缺德的 zscaler 都会无能为力。

生成客户端 client1.key, client1.crt

编辑 /etc/nginx/nginx.cfg, server {} 块

nginx -s reload

现在用  curl -k 完全信息服务端也没用,服务端可不买这个账

400 Bad Request, 因为 No required SSL certificate was sent

主动带上 client1.key 和  client1.crt 就行,只要服务端的证书是不被广泛接受的,就要用 -k 来手动接受证书,即使是服务端与客户端相互承认也不能省略 -k

除了用命令 curl,同样可用 openssl s_clinet 携带 client1.key 和 client1.crt 来访问

openssl s_client -connect server.local:443 -key client1.key -cert client1.crt

那浏览器该怎么办呢?

我们可用 openssl 由 client1.crt 和 client1.key 生成 PKCS#12 格式的客户端证书上,然后导入到浏览器当中

如果不想设置的密码的话在两次提示输入密码时直接回车跳过,生成 client1.p12

比如在 Chrome 浏览器,可找到 Settings/Privacy and security/Security/Advanced/Manage certificates/Personal/Imports..., 然后导入前面生成的 client1.p12 文件,有密码的话就输入相同的密码。再浏览 https://server.local 就没问题了。 

用 Postman 直接浏览 https://server.local 也同样得到 400 Bad Request 的错误,也需要配置客户端证书才能正常访问。Postman 同时支持 client1.crt,client1.key 组合,和 client1.p12(有密码则输入), 请看下方的截图

完后 Postman 就能看到 https://server.local 的正确输出了。

curl 也可以用 p12 格式的证书

Apache2 中的 mTLS 双向认证配置

类似的在 Apache2 中的配置就是在启用 HTTPS 单向验证的基础上于 default-ssl.conf 中再加上

SpringBoot Tomcat 的 mTLS 双向认证

在启用了 HTTPS 单身验证的情况下加上 server.ssl.trust-certificate 和  server.ssl.client-auth 配置,完整的相关配置是

可分别用下面的 curl  进行测试

$ curl -k https://server.local:8080
curl: (56) LibreSSL SSL_read: LibreSSL/3.3.6: error:1404C412:SSL routines:ST_OK:sslv3 alert bad certificate, errno 0
$ curl --cert client1.crt --key client1.key -k https://server.local:8080
Ok

其他相关内容

以下相关内容在本文中不作详细展开,具体做法可参考 基于证书的双向认证(mTLS)技术方案。比

  1. keytool 替代 openssl 生成证书,在 crt, jdk, p12 之间的证书转换
  2. Tomcat 中如何使用 key-store 方式配置 HTTPS
  3. Tomcat 中启用了 HTTPS 后,@EnableWebSecurity 之后如何信任 x509 证书
  4. Java 作为客户端调用相应服务,如何处理 TLS 和 mTLS
  5. Nginx 作反向代理时作为 HTTPS 客户端时的配置

链接:

  1. 基于证书的双向认证(mTLS)技术方案
  2. nginx 自签证书实现配置 https 双向认证
  3. 巧用 Nginx 快速实现 HTTPS 双向认证
  4. springboot2.0 配置ssl证书详解

本文链接 https://yanbin.blog/self-certs-https-tls-mtls-config/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

1 Comment
Inline Feedbacks
View all comments
trackback

[…] 自签发证书配置 HTTPS 单向双向验证 中配置的 Nginx 为例,如果正常在浏览器中访问 https://server.local, 则在 […]