在一些前端开发场景中,需要在本地创建 https 服务,Node.js 提供了 https 模块帮助开发者快速创建 https 的服务器,示例代码如下:
js
const https = require('https')
const fs = require('fs')
const options = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem'),
}
const server = https.createServer(options, (req, res) => {
res.writeHead(200)
res.end('hello world\n')
})
server.listen(8080)
与创建 http 服务最大的区别在于:https 服务需要证书。因此需要在 options 选项中提供 key
和 cert
两个字段。大部分前端不知道如何创建 key
和 cert
,虽然网上能查到一些 openssl 命令,但也不知道是什么含义。所谓授人以鱼不如授人以渔,这里先从一些基本概念讲起,然后一步步教大家如何创建一个可以被浏览器绝对信任的自签名证书。
密码学知识
首先要知道加密学中非常重要的一个算法:公开密钥算法 (Public Key Cryptography),也称为非对称加密算法 (Asymmetrical Cryptography),算法的密钥是一对,分别是公钥 (public key)和私钥(private key),一般私钥由密钥对的生成方(比如服务器端)持有,避免泄露,而公钥任何人都可以持有,也不怕泄露。
一句话总结:公钥加密、私钥解密。
公私钥出了用于加解密之外,还能用于数字签名。因为私钥只有密钥对的生成者持有,用私钥签署(注意不是加密)一条消息,然后发送给任意的接收方,接收方只要拥有私钥对应的公钥,就能成功反解被签署的消息。
一句话总结:私钥加签、公钥验证。
由于只有私钥持有者才能"签署"消息,如果不考虑密钥泄露的问题,就不能抵赖说不是自己干的。
数字签名和信任链
基于数字签名技术,假设 A 授权给 B,B 授权给 C,C 授权给 D,那么 D 就相当于拿到了 A 的授权,这就形成了一个完整的信任链。因此:
- 信任链建立了一条从根证书颁发机构(Root CA)到最终证书持有人的信任路径
- 每个证书都有一个签名,验证这个签名需要使用颁发该证书的机构的公钥
- 信任链的作用是确保接收方可以验证数字签名的有效性,并信任签名所代表的身份
以 https 证书在浏览器端被信任为例,整个流程如下:
可以看到,在这套基础设施中,涉及到很多参与方和新概念,例如:
- 服务器实体:需要申请证书的实体(如某个域名的拥有者)
- CA机构:签发证书的机构
- 证书仓库:CA 签发的证书全部保存到仓库中,证书可能过期或被吊销。
- 证书校验方:校验证书真实性的软件,例如浏览器、客户端等。
这些参与方、概念和流程的集合被称为公钥基础设施(Public Key Infrastructure)
X.509 标准
为了能够将这套基础设施跑通,需要遵循一些标准,最常用的标准是 X.509,其内容包括:
- 如何定义证书文件的结构(使用 ANS.1 来描述证书)
- 如何管理证书(申请证书的流程,审核身份的标准,签发证书的流程)
- 如何校验证书(证书签名校验,校验实体属性,比如的域名、证书有效期等)
- 如何对证书进行撤销(包括 CRL 和 OCSP 协议等概念)
X.509标准在网络安全中广泛使用,特别是在 TLS/SSL 协议中用于验证服务器和客户端的身份。在 Node.js 中,当 tls 通道创建之后,可以通过下面两个方法获取客户端和服务端 X.509 证书:
- 获取本地 X.509 证书:getX509Certificate
- 获取对方 X.509 证书:getPeerX509Certificate
生成证书
要想生成证书,首要有一个私钥(private key),私钥的生成方式为:
sh
$ openssl genrsa -out key.pem 2048
会生成一个 key.pem 文件,内容如下:
txt
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCB....
-----END PRIVATE KEY-----
然后,还需要一个与私钥相对应的公钥(public key),生成方式:
sh
$ openssl req -new -sha256 -key key.pem -out csr.pem
按照提示操作即可,下面是示例输入(中国-浙江省-杭州市-西湖-苏堤-keliq):
sh
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:Zhejiang
Locality Name (eg, city) []:Hangzhou
Organization Name (eg, company) [Internet Widgits Pty Ltd]:West Lake
Organizational Unit Name (eg, section) []:Su Causeway
Common Name (e.g. server FQDN or YOUR name) []:keliq
Email Address []:email@example.com
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:123456
An optional company name []:XiHu
生成的文件内容如下:
txt
-----BEGIN CERTIFICATE REQUEST-----
MIIDATCCAekCAQAwgY8xCzAJBgNVBAYTAkNOMREw...
-----END CERTIFICATE REQUEST-----
公钥文件创建之后,接下来有两个选择:
- 将其发送给 CA 机构,让其进行签名
- 自签名
自签名
如果是本地开发,我们选择自签名的方式就行了,openssl 同样提供了命令:
sh
$ openssl x509 -req -in csr.pem -signkey key.pem -out cert.pem
Certificate request self-signature ok
subject=C = CN, ST = Zhejiang, L = Hangzhou, O = West Lake, OU = Su Causeway, CN = keliq, emailAddress = email@example.com
最终得到了 cert.pem,内容如下:
txt
-----BEGIN CERTIFICATE-----
MIIDpzCCAo8CFAf7LQmMUweTSW+ECkjc7g1uy3jCMA0...
-----END CERTIFICATE-----
到这里,所有环节都走完了,再来回顾一下,总共生成了三个文件,环环相扣:
- 首先生成私钥文件
key.pem
- 然后生成与私钥对应的公钥文件
csr.pem
- 最后用公私钥生成证书
cert.pem
实战------信任根证书
用上面自签名创建的证书来创建 https 服务:
js
const options = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem'),
}
启动之后,如果你在浏览器中访问,会发现出错了:
命名是 https 服务,为什么浏览器说不是私密连接呢?因为自签名证书默认是不被浏览器信任的,只需要将 cert.pem 拖到钥匙里面即可,然后修改为「始终信任」,过程中需要验证指纹或者输入密码:
如果你觉得上述流程比较繁琐,可以用下面的命令行来完成:
sh
$ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain cert.pem
实战------指定主题备用名称
然而,即使添加了钥匙串信任,问题似乎并没有解决,报错还是依旧:
仔细看,其实报错信息发生了变化,从原来的 NET::ERR_CERT_AUTHORITY_INVALID
变成了 NET::ERR_CERT_COMMON_NAME_INVALID
,这又是怎么回事呢?我们点开高级按钮看一下详细报错:
这段话的意思是:当前网站的 SSL 证书中的通用名称(Common Name)与实际访问的域名不匹配。
证书中会包含了一个通用名称字段,用于指定证书的使用范围。如果证书中的通用名称与您访问的域名不匹配,浏览器会出现NET::ERR_CERT_COMMON_NAME_INVALID错误。
一句话描述,证书缺少了主题备用名称(subjectAltName),而浏览器校验证书需要此字段。为了更好的理解这一点,我们可以用下面的命令查看证书的完整信息:
sh
$ openssl x509 -in cert.pem -text -noout
输出结果如下:
txt
Certificate:
Data:
Version: 1 (0x0)
Serial Number:
07:fb:2d:09:8c:53:07:93:49:6f:84:0a:48:dc:ee:0d:6e:cb:78:c2
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = CN, ST = Zhejiang, L = Hangzhou, O = West Lake, OU = Su Causeway, CN = keliq, emailAddress = email@example.com
Validity
Not Before: Nov 15 06:29:36 2023 GMT
Not After : Dec 15 06:29:36 2023 GMT
Subject: C = CN, ST = Zhejiang, L = Hangzhou, O = West Lake, OU = Su Causeway, CN = keliq, emailAddress = email@example.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:ac:63:b1:f1:7a:69:aa:84:ef:9d:0e:be:c1:f7:
80:3f:6f:59:e1:7d:c5:c6:db:ff:2c:f3:99:12:7f:
...
Exponent: 65537 (0x10001)
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
70:d9:59:10:46:dc:7b:b3:19:c8:bd:4b:c5:70:4f:89:b6:6a:
53:1c:f2:35:27:c8:0a:ed:a8:0a:13:1f:46:3e:e7:a7:ff:1f:
...
我们发现,这个证书并没有 Subject Alternative Name 这个字段,那如何增加这个字段呢?有两种方式:
指定 extfile 选项
sh
$ openssl x509 -req \
-in csr.pem \
-signkey key.pem \
-extfile <(printf "subjectAltName=DNS:localhost") \
-out cert.pem
再次用命令查看证书详情,可以发现 Subject Alternative Name 字段已经有了:
txt
...
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:localhost
X509v3 Subject Key Identifier:
21:65:8F:93:49:BC:DF:8C:17:1B:6C:43:AC:31:3C:A9:34:3C:CB:77
...
用新生成的 cert.pem 启动 https 服务,再次访问就正常了,可以点击小锁查看证书详细信息:
但是如果把 localhost 换成 127.0.0.1 的话,访问依然被拒绝,因为 subjectAltName 只添加了 localhost 这一个域名,所以非 localhost 域名使用此证书的时候,浏览器就会拒绝。
新建 .cnf 文件
这次我们新建一个 ssl.cnf 文件,并在 alt_names 里面多指定几个域名:
ini
[req]
prompt = no
default_bits = 4096
default_md = sha512
distinguished_name = dn
x509_extensions = v3_req
[dn]
C=CN
ST=Zhejiang
L=Hangzhou
O=West Lake
OU=Su Causeway
CN=keliq
emailAddress=keliq@example.com
[v3_req]
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName=@alt_names
[alt_names]
DNS.1 = localhost
IP.2 = 127.0.0.1
IP.3 = 0.0.0.0
然后一条命令直接生成密钥和证书文件:
sh
$ openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 \
-config openssl.cnf \
-keyout key.pem \
-out cert.pem
再次查看证书详情,观察 Subject Alternative Name 字段:
txt
...
X509v3 extensions:
X509v3 Key Usage:
Digital Signature, Non Repudiation, Key Encipherment
X509v3 Subject Alternative Name:
DNS:localhost, IP Address:127.0.0.1, IP Address:0.0.0.0
X509v3 Subject Key Identifier:
B6:FC:1E:68:CD:8B:97:D0:80:0E:F1:18:D3:39:86:29:90:0B:9D:1F
...
这样无论是访问 localhost 还是 127.0.0.1 或者 0.0.0.0,浏览器都能够信任。