本文主要翻译自 [Practical-Cryptography-for-Developers-Book][cryptobook],但是笔者也补充了 HMAC 的 Python 实现以及 scrypt 使用示例。
《写给开发人员的实用密码学》系列文章目录:
现代人的日常生活中,HTTPS 协议几乎无处不在,我们每天浏览网页时、用手机刷京东淘宝时、甚至每天秀自己绿色的健康码时,都在使用 HTTPS 协议。
作为一个开发人员,我想你应该多多少少有了解一点 HTTPS 协议。
你可能知道 HTTPS 是一种加密传输协议,能保证数据传输的保密性。
如果你拥有部署 HTTPS 服务的经验,那你或许还懂如何申请权威 HTTPS 证书,并配置在 Nginx 等 Web 程序上。
但是你是否清楚 HTTPS 是由 HTTP + TLS 两种协议组合而成的呢?
更进一步你是否有抓包了解过 TLS 协议的完整流程?是否清楚它加解密的底层原理?是否清楚 Nginx 的 HTTPS 配置中一堆密码学参数的真正含义?是否知道 TLS 协议有哪些弱点、存在哪些攻击手段、如何防范?
我们在《写给开发人员的实用密码学》的前七篇文章中已经学习了许多的密码学概念与算法,接下来我们就利用这些知识,深度剖析下 HTTPS 协议中的数字证书以及 TLS 协议。
我们在前面已经学习了「对称密码算法」与「非对称密码算法」两个密码学体系,这里做个简单的总结。
但是非对称密码算法仍然存在一些问题:
数字证书与公钥基础架构就是为了解决上述问题而设计的。
首先简单介绍下公钥基础架构(Public Key Infrastructure),它是一组由硬件、软件、参与者、管理政策与流程组成的基础架构,其目的在于创造、管理、分配、使用、存储以及撤销数字证书。
PKI 是一个总称,而并非指单独的某一个规范或标准,因此显然数字证书的规范(X.509)、存储格式(PKCS系列标准、DER、PEM)等都是 PKI 的一部分。
我们下面从公钥证书开始逐步介绍 PKI 中的各种概念及架构。
前面我们介绍了公钥密码系统存在的一个问题是「在分发公钥时,难以确认公钥的真实性、完整性及其来源身份」。
PKI 通过「数字证书」+「证书认证机构」来解决这个问题,下面先简单介绍下「数字证书」。
数字证书
指的其实就是
公钥证书
(也可直接简称为
证书
)。
在现代网络通讯中通行的公钥证书标准名为
X.509
v3, 由
RFC5280
定义。
X.509 v3 格式被广泛应用在 TLS/SSL 等众多加密通讯协议中,它规定公钥证书应该包含如下内容:
RSA-SHA-256
与
ECDSA-SHA-256
每个证书都有唯一的 ID,这样在私钥泄漏的情况下,我们可以通过公钥基础设施的 OCSP(Online Certificate Status Protocol)协议吊销某个证书。
吊销证书的操作还是比较罕见的,毕竟私钥泄漏并不容易遇到,因此这里就略过不提了,有需要的可以自行搜索。
使用 Firefox 查看网站
https://www.google.com
的证书信息如下:
前面介绍证书内容时,提到了每个证书都包含「签发者(Issuer)」信息,并且还包含「签发者」使用「证书内容」与「签发者私钥」生成的数字签名。
那么在证书交换时,如何验证证书的真实性、完整性及来源身份呢?
根据「数字签名」算法的原理,显然需要使用「签发者公钥」来验证「被签发证书」中的签名。
仍然辛苦 Alice 与 Bob 来演示下这个流程:
PKI 引入了一个
可信赖的第三者
(Trusted third party,TTP)来解决这个问题。
在 Alice 与 Bob 的案例中,就是说还有个第三者
Eve
,他使用自己的私钥为自己的公钥证书签了名,生成了一个「自签名证书」,并且已经提前将这个「自签名证书」分发(比如当面交付、物理分发 emmm)给了 Alice 跟 Bob.
在现实世界中,Eve 这个角色被称作「 证书认证机构 (Certification Authority, CA)」,全世界只有几十家这样的权威机构,它们都通过了各大软件厂商的严格审核,从而将根证书(CA 证书)直接内置于主流操作系统与浏览器中,也就是说早就提前分发给了因特网世界的几乎所有用户。由于许多操作系统或软件的更新迭代缓慢(2022 年了还有人用 XP 你敢信?),根证书的有效期通常都在十年以上。
但是,如果 CA 机构直接使用自己的私钥处理各种证书签名请求,这将是非常危险的。
因为全世界有海量的 HTTPS 网站,也就是说有海量的证书需求,可一共才几十家 CA 机构。
频繁的动用私钥会产生私钥泄漏的风险,如果这个私钥泄漏了,那将直接影响海量网站的安全性。
PKI 架构使用「 数字证书链 (也叫做 信任链 )」的机制来解决这个问题:
画个图来表示大概是这么个样子:
CA 机构也可能会在经过严格审核后,为其他机构签发中间证书,这样就能赋予其他机构签发证书的权利,而且根证书的安全性不受影响。
如果你访问某个 HTTPS 站点发现浏览器显示小绿锁,那就说明这个证书是由某个权威 认证机构 签发的,其信息是经过这些机构认证的。
部分个人、企业或其他机构(比如金融机构),可能会因为各种原因,生成自己的根证书与中间证书,然后自行签发证书。
但是这种自己生成的根证书是未内置在操作系统与浏览器中的,为了确保安全性,用户就需要先手动在设备上安装好这个数字证书。
自行签发证书的案例有:
现在再拿出前面
https://www.google.com
的证书截图看看,最上方有三个标签页,从左至右依次是「服务器证书」、「中间证书」、「根证书」,可以点进去分别查看这三个证书的各项参数,各位看官可以自行尝试:
按前面的描述,每个权威认证机构都拥有一个正在使用的根证书,使用它签发出几个中间证书后,就会把它离线存储在安全地点,平常仅使用中间证书签发终端实体证书。
这样实际上每个权威认证机构的证书都形成一颗证书树,树的顶端就是根证书。
实际上在 PKI 体系中,一些证书链上的中间证书会被使用多个根证书进行签名——我们称这为交叉签名。
交叉签名的主要目的是提升证书的兼容性——客户端只要安装有其中任何一个根证书,就能正常验证这个中间证书。
从而使中间证书在较老的设备也能顺利通过证书验证。
X509 证书可以以多种格式存储,一般拓展名来标识证书文件的格式。
现代网络通讯中,常用的证书格式都在
RFC7468
中被标准化。
主要有 PEM、PKCS#7、PKCS#12 三种格式。
DER 是由国际电信联盟(ITU)在
ITU-T X.690
标准中定义的一种数据编码规则,用于将 ASN.1 结构的信息编码为二进制数据。
直接以 DER 格式存储的证书,大都使用
.cer
.crt
.der
拓展名,在 Windows 系统比较常见。
而 PEM 格式,即 Privacy-Enhanced Mail,是 openssl 默认使用的证书格式。可用于编码公钥、私钥、公钥证书等多种密码学信息。
PEM 其实就是在 DER 的基础上多做一步——使用 Base64 将 DER 编码出的二进制数据再处理一次,编码成字符串再存储。好处是存储、传输要方便很多。
一个 2048 位 RSA 公钥的 PEM 文件内容如下:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyl6q6BkEcEUi9V1/Q7il
bngnh1YzG1tM4Hd6XCZQ35OzDN4my9eXWtjoL8YvLYqlYTJqhTHpuptgjF/lmlhg
WIMKNNcuDAbvmWExRyZateVrjO9OtgkyJCuGhaum0TIUC+dbZ9L9xsdK/fU1L5BB
nPRSYMloH8uE1CbK/DhFUiKp36aHZFfqLPicY3c6/N+k2kIJCEWBY0SROqpqy2Iz
yCIP54JSoOoGz6pdtWhd5cEeicr9e7f/WixEES6fgavqIHzhSJBVctpMgFPjFZ/x
JJhQVf23WKb3YQQ/0Uc8O7OTDXoUfuJP9UgqvKNh4hPfJA+a4nxkDYhTPfrLHfKY
YwIDAQAB
-----END PUBLIC KEY-----
PEM 格式的数据通常以 .pem
.key
.crt
.cer
等拓展名存储,直接 cat
一下是不是字符串,就能确认该文件是否是 PEM 格式了。
因为纯文本格式处理起来很方便,大部分场景下证书、公钥、私钥等信息都会被编码成 PEM 格式再进行存储、传输。
openssl 默认使用的输入输出均 PEM 格式。
PKCS#1
PKCS#1 是专用于编码 RSA 公私钥的标准,通常被编码为 PEM 格式存储。openssl 生成的 RSA 密钥对默认使用此格式。
这是一个比较陈旧的格式,openssl 之所以默认使用它,主要是为了兼容性。通常建议使用更安全的 PKCS#8 而不是这个。
一个使用 PKCS#1 标准的 2048 位 RSA 公钥文件,内容如下:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyl6q6BkEcEUi9V1/Q7il
bngnh1YzG1tM4Hd6XCZQ35OzDN4my9eXWtjoL8YvLYqlYTJqhTHpuptgjF/lmlhg
WIMKNNcuDAbvmWExRyZateVrjO9OtgkyJCuGhaum0TIUC+dbZ9L9xsdK/fU1L5BB
nPRSYMloH8uE1CbK/DhFUiKp36aHZFfqLPicY3c6/N+k2kIJCEWBY0SROqpqy2Iz
yCIP54JSoOoGz6pdtWhd5cEeicr9e7f/WixEES6fgavqIHzhSJBVctpMgFPjFZ/x
JJhQVf23WKb3YQQ/0Uc8O7OTDXoUfuJP9UgqvKNh4hPfJA+a4nxkDYhTPfrLHfKY
YwIDAQAB
-----END PUBLIC KEY-----
PKCS#7 / CMS
PKCS#7,也被称作 CMS(Cryptographic Message Syntax),是一个数据填充标准,常被用于需要数据填充的加密、数字签名等算法中。
PKCS#7 通常并不用于存储,数据在被 PKCS#7 填充后,立即用于加密或签名,生成出对应的密文或签名,然后就可以丢弃了。
PKCS#7 本身只定义了数据结构,如果需要存储的话,通常使用 DER 编码规则编码后直接存储,或者再继续编码为 PEM 格式。
在 Windows 系统中, DER 与 PEM 格式的 PKCS#7 文件都使用 .p7b
拓展名。
一个 PKCS#7 格式的文件内容如下:
‑‑‑‑BEGIN PKCS7‑‑‑‑‑
‑‑‑‑‑END PKCS7‑‑‑‑‑
PKCS#8
PKCS#8 是一个专门用于编码私钥的标准,可用于编码 DSA/RSA/ECC 私钥。它通常被编码成 PEM 格式存储。
前面介绍了专门用于编码 RSA 的 PKCS#1 标准比较陈旧,而且曾经出过漏洞。因此通常建议使用更安全的 PKCS#8 来取代 PKCS#1.
C# Java 等编程语言通常要求使用此格式的私钥,而 Python 的 pyca/cryptography 则支持多种编码格式。
一个非加密 ECC 私钥的 PKCS#8 格式如下:
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQglQanBRiYVPX7F2Rd
4CqyjEN0K4qfHw4tM/yMIh21wamhRANCAARsxaI4jT1b8zbDlFziuLngPcExbYzz
ePAHUmgWL/ZCeqlODF/l/XvimkjaWC2huu1OSWB9EKuG+mKFY2Y5k+vF
-----END PRIVATE KEY-----
一个加密 PKCS#8 私钥的 PEM 格式私钥如下:
-----BEGIN ENCRYPTED PRIVATE KEY-----
Base64 编码内容
-----END ENCRYPTED PRIVATE KEY-----
可使用如下 openssl 命令将 RSA/ECC 私钥转换为 PKCS#8 格式:
# RSA
openssl pkcs8 -topk8 -inform PEM -in rsa-private-key.pem -outform PEM -nocrypt -out rsa-private-key-pkcs8.pem
# ECC 的转换命令与 RSA 完全一致
openssl pkcs8 -topk8 -inform PEM -in ecc-private-key.pem -outform PEM -nocrypt -out ecc-private-key-pkcs8.pem
PKCS#12
PKCS#12 是一个归档文件格式,用于实现存储多个私钥及相关的 X.509 证书。
因为保存了私钥,为了安全性它通常是加密的,需要使用 passphrase 解密后才能使用。
PKCS#12 的常用拓展名为 .p12
.pfx
.
PKCS#12 的主要使用场景是安全地保存、传输私钥及相关的 X.509 证书,比如:
微信/支付宝等支付相关的数字证书,通常使用 PKCS#12 格式存储,使用商户号做加密密码,然后编码为 base64 再提供给用户
安卓的 APK 签名证书通常使用 PKCS#12 格式存储,拓展名为 .keystore
或者 .jks
.
PEM 格式转 PKCS#12(公钥和私钥都放里面):
openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12
# 按提示输入保护密码
从 PKCS#12 中分别提取出 PEM 格式的公钥与私钥:
openssl pkcs12 -in xxx.p12 -out xxx.crt -clcerts -nokeys
openssl pkcs12 -in xxx.p12 -out xxx.key -nocerts -nodes
4. 证书支持保护的域名类型
TLS 证书支持配置多个域名,并且支持所谓的通配符(泛)域名。
但是通配符域名证书的匹配规则,和 DNS 解析中的匹配规则并不一致!
根据证书选型和购买 - **阿里云文档 的解释,通配符证书只支持同级匹配,详细说明如下**:
一级通配符域名: 可保护该通配符域名(主域名)自身和该域名所有的一级子域名。
例如: 一级通配符域名 *.aliyun.com
可以用于保护 aliyun.com
、www.aliyun.com
以及其他所有一级子域名。
但是不能用于保护任何二级子域名,如 xx.aa.aliyun.com
二级或二级以上通配符域名: 只能保护该域名同级的所有通配域名,不支持保护该通配符域名本身。
例如: *.a.aliyun.com
只支持保护它的所有同级域名,不能用于保护三级子域名。
要想保护多个二三级子域,只能在生成 TLS 证书时,添加多个通配符域名。
因此设计域名规则时,要考虑到这点,尽量不要使用层级太深的域名!有些信息可以通过 -
来拼接以减少域名层级,比如阿里云的 oss 域名:
公网: oss-cn-shenzhen.aliyuncs.com
内网: oss-cn-shenzhen-internal.aliyuncs.com
此外也可直接为 IP 地址签发证书,IP 地址可以记录在证书的 SAN 属性中。
在自己生成的证书链中可以为局域网 IP 或局域网域名生成本地签名证书。
此外在因特网中也有一些权威认证机构提供为公网 IP 签发证书的服务,一个例子是 Cloudflare 的 https://1.1.1.1, 使用 Firefox 查看其证书,可以看到是一个由 DigiCert 签发的 ECC 证书,使用了 P-256 曲线:
5. 生成自己的证书链
OpenSSL 是目前使用最广泛的网络加密算法库,这里以它为例介绍证书的生成。
另外也可以考虑使用 cfssl.
前面介绍了,在局域网通信中通常使用本地证书链来保障通信安全,这通常有如下几个原因。
在内网环境下,管理员将本地 CA 证书安装到所有局域网设备上,因此并无必要向权威 CA 机构申请证书
内网环境使用的可能是非公网域名(xxx.local
/xxx.lan
/xxx.srv
等),甚至可能直接使用局域网 IP 通信,权威 CA 机构不签发这种类型的证书
下面介绍下如何使用 OpenSSL 生成一个本地 CA 证书链,并签发用于安全通信的服务端证书,可用于 HTTPS/QUIC 等协议。
1. 生成 RSA 证书链
到目前为止 RSA 仍然是应用最广泛的非对称加密方案,几乎所有的根证书都是使用的 2048 位或者 4096 位的 RSA 密钥对。
对于 RSA 算法而言,越长的密钥能提供越高的安全性,当前使用最多的 RSA 密钥长度仍然是 2048 位,但是 2048 位已被一些人认为不够安全了,密码学家更建议使用 3072 位或者 4096 位的密钥。
生成一个 2048 位的 RSA 证书链的流程如下:
OpenSSL 的 CSR 配置文件官方文档: https://www.openssl.org/docs/manmaster/man1/openssl-req.html
编写证书签名请求的配置文件 csr.conf
:[ req ]
prompt = no
default_md = sha256 # 在签名算法中使用 SHA-256 计算哈希值
req_extensions = req_ext
distinguished_name = dn
[ dn ]
C = CN # Contountry
ST = Guangdong
L = Shenzhen
O = Xxx
OU = Xxx-SRE
CN = *.svc.local # 泛域名,这个字段已经被 chrome/apple 弃用了。
[ alt_names ] # 备用名称,chrome/apple 目前只信任这里面的域名。
DNS.1 = *.svc.local # 一级泛域名
DNS.2 = *.aaa.svc.local # 二级泛域名
DNS.3 = *.bbb.svc.local # 二级泛域名
[ req_ext ]
subjectAltName = @alt_names
[ v3_ext ]
subjectAltName=@alt_names # Chrome 要求必须要有 subjectAltName(SAN)
authorityKeyIdentifier=keyid,issuer:always
basicConstraints=CA:FALSE
keyUsage=keyEncipherment,dataEncipherment,digitalSignature
extendedKeyUsage=serverAuth,clientAuth
此文件的详细文档: OpenSSL file formats and conventions
生成证书链与服务端证书:# 1. 生成本地 CA 根证书的私钥
openssl genrsa -out ca.key 2048
# 2. 使用私钥签发出 CA 根证书
## CA 根证书的有效期尽量设长一点,因为不方便更新换代,这里设了 100 年
openssl req -x509 -new -nodes -key ca.key -subj "/CN=MyLocalRootCA" -days 36500 -out ca.crt
# 3. 生成服务端证书的 RSA 私钥(2048 位)
openssl genrsa -out server.key 2048
# 4. 通过第一步编写的配置文件,生成证书签名请求(公钥+申请者信息)
openssl req -new -key server.key -out server.csr -config csr.conf
# 5. 使用 CA 根证书直接签发服务端证书,这里指定服务端证书的有效期为 3650 天
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 3650 \
-extensions v3_ext -extfile csr.conf
简单起见这里没有生成中间证书,直接使用根证书签发了用于安全通信的服务端证书。
2. 生成 ECC 证书链
在上一篇文章中我们已经介绍过了,ECC 加密方案是新一代非对称加密算法,是 RSA 的继任者,在安全性相同的情况下,ECC 拥有比 RSA 更快的计算速度、更少的内存以及更短的密钥长度。
对于 ECC 加密方案而言,不同的椭圆曲线生成的密钥对提供了不同程度的安全性。
各个组织(ANSI X9.62、NIST、SECG)命名了多种曲线,可通过如下命名查看 openssl 支持的所有椭圆曲线名称:
openssl ecparam -list_curves
目前在 TLS 协议以及 JWT 签名算法中,目前应该最广泛的椭圆曲线仍然是 NIST 系列:
P-256
: 到目前为止 P-256 应该仍然是应用最为广泛的椭圆曲线
在 openssl 中对应的名称为 prime256v1
P-384
在 openssl 中对应的名称为 secp384r1
P-521
在 openssl 中对应的名称为 secp521r1
生成一个使用 P-384
曲线的 ECC 证书的示例如下:
编写证书签名请求的配置文件 ecc-csr.conf
:[ req ]
prompt = no
default_md = sha256 # 在签名算法中使用 SHA-256 计算哈希值
req_extensions = req_ext
distinguished_name = dn
[ dn ]
C = CN # Contountry
ST = Guangdong
L = Shenzhen
O = Xxx
OU = Xxx-SRE
CN = *.svc.local # 泛域名,这个字段已经被 chrome/apple 弃用了。
[ alt_names ] # 备用名称,chrome/apple 目前只信任这里面的域名。
DNS.1 = *.svc.local # 一级泛域名
DNS.2 = *.aaa.svc.local # 二级泛域名
DNS.3 = *.bbb.svc.local # 二级泛域名
[ req_ext ]
subjectAltName = @alt_names
[ v3_ext ]
subjectAltName=@alt_names # Chrome 要求必须要有 subjectAltName(SAN)
authorityKeyIdentifier=keyid,issuer:always
basicConstraints=CA:FALSE
keyUsage=keyEncipherment,dataEncipherment,digitalSignature
extendedKeyUsage=serverAuth,clientAuth
此文件的详细文档: OpenSSL file formats and conventions
生成证书链与服务端证书:# 1. 生成本地 CA 根证书的私钥,使用 P-384 曲线,密钥长度 384 位
openssl ecparam -genkey -name secp384r1 -out ecc-ca.key
# 2. 使用私钥签发出 CA 根证书
## CA 根证书的有效期尽量设长一点,因为不方便更新换代,这里设了 100 年
openssl req -x509 -new -nodes -key ecc-ca.key -subj "/CN=MyLocalRootCA" -days 36500 -out ecc-ca.crt
# 3. 生成服务端证书的 EC 私钥,使用 P-384 曲线,密钥长度 384 位
openssl ecparam -genkey -name secp384r1 -out ecc-server.key
# 4. 通过第一步编写的配置文件,生成证书签名请求(公钥+申请者信息)
openssl req -new -key ecc-server.key -out ecc-server.csr -config ecc-csr.conf
# 5. 使用 CA 根证书直接签发 ECC 服务端证书,这里指定服务端证书的有效期为 3650 天
openssl x509 -req -in ecc-server.csr -CA ecc-ca.crt -CAkey ecc-ca.key \
-CAcreateserial -out ecc-server.crt -days 3650 \
-extensions v3_ext -extfile ecc-csr.conf
简单起见这里没有生成中间证书,直接使用根证书签发了用于安全通信的服务端证书,而且根证书跟服务端证书都使用了 ECC 证书。
现实中由于根证书更新缓慢,几乎所有的根证书都还是 RSA 证书,而中间证书与终端实体证书的迭代要快得多,目前已经有不少网站在使用 ECC 证书了。
6. 证书的类型
按照数字证书的生成方式进行分类,证书有三种类型:
由权威 CA 机构签名的证书: 这类证书会被浏览器、小程序等第三方应用/服务商信任
申请证书时需要验证你对域名/IP 的所有权,也就使证书无法伪造
如果你的 API 需要提供给第三方应用/服务商/用户访问,那就需要向权威 CA 机构申请此类证书
本地签名证书 - tls_locally_signed_cert
: 即由本地 CA 证书签名的 TLS 证书
本地 CA 证书,就是自己使用 openssl
等工具生成的 CA 证书
这类证书的缺点是无法与第三方应用/服务商建立安全的连接
如果客户端是完全可控的(比如是自家的 APP,或者是接入了域控的企业局域网设备),完全可以在所有客户端都安装上自己生成的 CA 证书。这种场景下使用此类证书是安全可靠的,可以不向权威 CA 机构申请证书
自签名证书 - tls_self_signed_cert
: 前面介绍了根证书是一个自签名证书,它使用根证书的私钥为根证书签名
这里的「自签名证书」是指直接使用根证书进行网络通讯,缺点是证书的更新迭代会很麻烦,而且安全性低。
总的来说,权威CA机构颁发的证书,可以被第三方应用信任,但是自己生成的不行。
而越贵的权威证书,安全性与可信度就越高,或者可以保护更多的域名。
在客户端可控的情况下,可以考虑自己生成证书链并签发「本地签名证书」,将本地 CA 证书预先安装在客户端中用于验证。
而「自签名证书」主要是方便,能不用还是尽量不要使用。
7. 向权威 CA 机构申请「受信证书」
免费的「受信证书」有两种方式获取:
申请 Let's Encrypt 免费证书
很多代理工具都有提供 Let's Encrypt 证书的 Auto Renewal,比如:
Traefik
Caddy
docker-letsencrypt-nginx-proxy-companion
网上也有一些 certbot 插件,可以通过 DNS 提供商的 API 进行 Let's Encrypt 证书的 Auto Renewal,比如:
certbot-dns-aliyun
terraform 也有相关 provider: terraform-provider-acme
部分认证机构有提供免费证书的申请,有效期为一年,但是不支持泛域名
收费证书可以在各云服务商处购买,比如国内的阿里云、腾讯云等。
完整的证书申请流程如下:
为了方便用户,图中的申请人(Applicant)自行处理的部分,目前很多证书申请网站也可以自动处理,用户只需要提供相关信息即可。
8. 证书的寿命
对于公开服务,服务端证书的有效期不要超过 825 天(27 个月)!
另外从 2020 年 11 月起,新申请的服务端证书有效期已经缩短到了 398 天(13 个月)。
目前 Apple/Mozilla/Chrome 都发表了相应声明,证书有效期超过上述限制的,将被浏览器/Apple设备禁止使用。
而对于其他用途的证书,如果更换起来很麻烦,可以考虑放宽条件。
比如 kubernetes 集群的加密证书,可以考虑有效期设长一些,比如 10 年。
据云原生安全破局|如何管理周期越来越短的数字证书?所述,大量知名企业如特斯拉/微软/领英/爱立信都曾因未及时更换 TLS 证书导致服务暂时不可用。
因此 TLS 证书最好是设置自动轮转!人工维护不可靠!
目前很多 Web 服务器/代理,都支持自动轮转 Let's Encrypt 证书。
另外 Vault 等安全工具,也支持自动轮转私有证书。
9. 使用 OpenSSL 验证证书、查看证书信息
# 查看证书(crt)信息
openssl x509 -noout -text -in server.crt
# 查看证书请求(csr)信息
openssl req -noout -text -in server.csr
# 查看 RSA 私钥(key)信息
openssl rsa -noout -text -in server.key
# 验证证书是否可信
## 1. 使用系统的证书链进行验证
openssl verify server.crt
## 2. 使用指定的 CA 证书进行验证
openssl verify -CAfile ca.crt server.crt
二、TLS 协议
在讲 TLS 协议前,还是先复习下「对称密码算法」与「非对称密码算法」两个密码体系的特点。
对称密码算法(如 AES/ChaCha20): 计算速度快、安全强度高,但是缺乏安全交换密钥的手段、密钥的保存和管理也很困难
非对称密码算法(如 RSA/ECC): 解决了上述对称密码算法的两个缺陷——通过数字证书 + PKI 公钥基础架构实现了身份认证,再通过 DHE/ECDHE 实现了安全的对称密钥交换
TLS 协议借助数字证书与 PKI 公钥基础架构、DHE/ECDHE 密钥交换协议以及对称加密算法这三者,实现了安全的加密通讯。
基于经典 DHKE 协议的 TLS 握手流程如下:
而在支持「完美前向保密(Perfect Forward Secrecy)」的 TLS1.2 或 TLS1.3 协议中,经典 DH 协议被 ECDHE 协议取代。
变化之一是进行最初的握手协议从经典 DHKE 换成了基于 ECC 的 ECDH 协议,
变化之二是在每次通讯过程中也在不断地进行密钥交换,生成新的对称密钥供下次通讯使用,其细节参见 写给开发人员的实用密码学(五)—— 密钥交换 DHKE 与完美前向保密 PFS。
TLS 协议通过应用 ECDHE 密钥交换协议,提供了「完美前向保密(Perfect Forward Secrecy)」特性,也就是说它能够保护过去进行的通讯不受密钥在未来暴露的威胁。
即使攻击者破解出了一个「对称密钥」,也只能获取到一次事务中的数据,其他事务的数据安全性完全不受影响。
另外注意一点是,CA 证书和服务端证书都只在 TLS 协议握手的前三个步骤中有用到,之后的通信就与它们无关了。
1. 密码套件与 TLS 历史版本
密码套件(Cipher_suite)是 TLS 协议中一组用于实现安全通讯的密码学算法,类似于我们前面学习过的加密方案。
不同密码学算法的组合形成不同的密码套件,算法组合的差异使这些密码套件具有不同的性能与安全性,另外 TLS 协议的更新迭代也导致各密码套件拥有不同的兼容性。
通常越新推出的密码套件的安全性越高,但是兼容性就越差(旧设备不支持)。
密码套件的名称由它使用的各种密码学算法名称组成,而且有固定的格式,以 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
为例介绍下:
TLS
: 定义了此套件适用的协议,通常固定为 TLS
ECDHE
: 密钥交换算法
RSA
: 数字证书认证算法
AES_128_GCM
: 使用的对称加密方案,这是一个基于 AES 与 GCM 模式的对称认证加密方案,使用 128 位密钥
SHA256
: 哈希函数,用于 HMAC 算法实现消息认证
TLS 固定使用 HMAC 算法进行消息认证
TLS 协议的前身是 SSL 协议,TLS/SSL 的发展历程展示如下:
{{< figure src="/images/about-tls-cert/history-of-ssl-tls.png" title="SSL/TLS 的历史版本" >}}
SSL 协议早在 2015 年就被各大主流浏览器废除了,TLS1.0 感觉也基本没站点在用了,这俩就直接跳过了。
下面分别介绍下 TLS1.1 TLS1.2 与 TLS1.3.
TLS 1.1
TLS 1.1 在 RFC4346 中定义,于 2006 年 4 月发布。
TLS 1.1 是 TLS 1.0 的一个补丁,主要更新包括:
添加对CBC攻击的保护
隐式初始向量 IV 被替换成一个显式的 IV
修复分组密码模式中填充算法的 bug
支持 IANA 登记的参数
TLS 1.1及其之前的算法曾经被广泛应用,它目前已知的缺陷如下:
不支持 PFS 完全前向保密
不支持 AEAD 认证加密算法
为了兼容性,保留了很多不安全的算法
TLS 1.1 已经不够安全了,不过一些陈年老站点或许还在使用它。
TLS 1.2
TLS 1.2 在 RFC5246 中定义,于 2008 年 8 月发发布。
可选支持 PFS 完全前向保密
移除对 MD5 与 SHA-1 签名算法的支持
添加对 HMAC-SHA-256 及 HMAC-SHA-384 消息认证算法的支持
添加对 AEAD 加密认证方案的支持
去除 forback 回到 SSL 协议的能力,提升安全性
为了兼容性,保留了很多不安全的算法
如果你使用 TLS 1.2,需要小心地选择密码套件,避开不安全的套件,就能实现足够高的安全性。
TLS 1.3
TLS 1.3 做了一次大刀阔斧的更新,是一个里程碑式的版本,其更新总结如下:
移除对如下算法的支持
哈希函数 SHA1/MD5
所有非 AEAD 加密认证的密码方案(CBC 模式)
移除对 RC4 与 3DES 加密算法的支持
移除了静态 RSA 与 DH 密钥交换算法
支持高性能的 Ed25519/Ed448 签名认证算法、X25519 密钥协商算法
支持高性能的 ChaCha20-Poly1305 对称认证加密方案
将密钥交换算法与公钥认证算法从密码套件中分离出来
比如原来的 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
密码套件将被拆分为 ECDHE
算法、RSA
身份认证算法、以及 TLS_AES_128_GCM_SHA256
密码套件
这样密码套件就只包含一个 AEAD 认证加密方案,以及一个哈希函数了
仅支持前向安全的密钥交换算法 DHE 或 ECDHE
支持最短 0-RTT 的 TLS 握手(会话恢复)
TLS 1.3 从协议中删除了所有不安全的算法或协议,可以说只要你的通讯用了 TLS 1.3,那你的数据就安全了(当然前提是你的私钥没泄漏)。
如何设置 TLS 协议的版本、密码套件参数
我们前面已经学习了对称加密、非对称加密、密钥交换三部分知识,对照 TLS 套件的名称,应该能很容易判断出哪些是安全的、哪些不够安全,哪些支持前向保密、哪些不支持。
一个非常好用的「站点 HTTPS 安全检测」网站是 https://myssl.com/,使用它测试知乎网的检测结果如下:
能看到知乎为了兼容性,目前仍然支持 TLS1.0 与 TLS1.1,另外目前还不支持 TLS1.3.
此外,知乎仍然支持很多已经不安全的加密套件,myssl.com 专门使用黄色标识出了这些不安全的加密套件,我们总结下主要特征:
部分密码套件使用了不安全的对称加密算法 3DES
其他被标识为黄色的套件虽然使用了安全的对称加密算法,但是不支持 PFS 前向保密
此外 myssl.com 还列出了许多站点更详细的信息,包括 TLS1.3 的会话恢复,以及后面将会介绍的公钥固定、HTTP严格传输安全等信息:
Nginx 的 TLS 协议配置
以前为 Nginx 等程序配置 HTTPS 协议时,我最头疼的就是其中密码套件参数 ssl_ciphers
,为了安全性,需要配置超长的一大堆选用的密码套件名称,我可以说一个都看不懂,但是为了把网站搞好还是得硬着头皮搜索复制粘贴,实际上也不清楚安全性导致咋样。
为了解决这个问题,Mozilla/DigitalOcean 都搞过流行 Web 服务器的 TLS 配置生成工具,比如 ssl-config - **mozilla,这个网站提供三个安全等级的配置**:
「Intermediate」: 查看生成出的 ssl-cipher
属性,发现它只支持 ECDHE
/DHE
开头的算法。因此它保证前向保密。
对于需要通过浏览器访问的 API,推荐选择这个等级。
「Mordern」: 只支持 TLSv1.3
,该协议废弃掉了过往所有不安全的算法,保证前向保密,安全性极高,性能也更好。
对于不需要通过浏览器等旧终端访问的 API,请直接选择这个等级。
「Old」: 除非你的用户使用非常老的终端进行访问,否则请不要考虑这个选项!
另外阿里云负载均衡器配置前向保密的方法参见: 管理TLS安全策略 - 负载均衡 - 阿里云文档
1. TLS 双向认证(Mutual TLS authentication, mTLS)
TLS 协议(tls1.0+,RFC: TLS1.2 - RFC5246)中,定义了服务端请求验证客户端证书的方法。这
个方法是可选的。如果使用上这个方法,那客户端和服务端就会在 TLS 协议的握手阶段进行互相认证。这种验证方式被称为双向 TLS 认证(mTLS, mutual TLS)。
传统的「TLS 单向认证」技术,只在客户端去验证服务端是否可信。
而「TLS 双向认证(mTLS)」,则添加了服务端验证客户端是否可信的步骤(第三步):
客户端发起请求
「验证服务端是否可信」: 服务端将自己的 TLS 证书发送给客户端,客户端通过自己的 CA 证书链验证这个服务端证书。
「验证客户端是否可信」: 客户端将自己的 TLS 证书发送给服务端,服务端使用它的 CA 证书链验证该客户端证书。
协商对称加密算法及密钥
使用对称加密进行后续通信。
因为相比传统的 TLS,mTLS 只是添加了「验证客户端」这样一个步骤,所以这项技术也被称为「Client Authetication」.
mTLS 需要用到两套 TLS 证书:
服务端证书: 这个证书签名已经介绍过了。
客户端证书: 客户端证书貌似对证书信息(如 CN/SAN 域名)没有任何要求,只要证书能通过 CA 签名验证就行。
使用 openssl 生成 TLS 客户端证书(ca 和 csr.conf 可以直接使用前面生成服务端证书用到的,也可以另外生成):
# 1. 生成 2048 位 的 RSA 密钥
openssl genrsa -out client.key 2048
# 2. 通过第一步编写的配置文件,生成证书签名请求
openssl req -new -key client.key -out client.csr -config csr.conf
# 3. 生成最终的证书,这里指定证书有效期 3650 天
### 使用前面生成的 ca 证书对客户端证书进行签名(客户端和服务端共用 ca 证书)
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out client.crt -days 3650 \
-extensions v3_ext -extfile csr.conf
mTLS 的应用场景主要在「零信任网络架构」,或者叫「无边界网络」中。
比如微服务之间的互相访问,就可以使用 mTLS。
这样就能保证每个 RPC 调用的客户端,都是其他微服务(或者别的可信方),防止黑客入侵后为所欲为。
目前查到如下几个Web服务器/代理支持 mTLS:
Traefik: Docs - Client Authentication (mTLS)
Nginx: Using NGINX Reverse Proxy for client certificate authentication
主要参数是两个: ssl_client_certificate /etc/nginx/client-ca.pem
和 ssl_verify_client on
mTLS 的安全性
如果将 mTLS 用在 App 安全上,存在的风险是:
客户端中隐藏的证书是否可以被提取出来,或者黑客能否 Hook 进 App 中,直接使用证书发送信息。
如果客户端私钥设置了「密码(passphrase)」,那这个密码是否能很容易被逆向出来?
mTLS 和「公钥锁定/证书锁定」对比:
公钥锁定/证书锁定: 只在客户端进行验证。
但是在服务端没有进行验证。这样就无法鉴别并拒绝第三方应用(爬虫)的请求。
加强安全的方法,是通过某种算法生成动态的签名。爬虫生成不出来这个签名,请求就被拒绝。
mTLS: 服务端和客户端都要验证对方。
保证双边可信,在客户端证书不被破解的情况下,就能 Ban 掉所有的爬虫或代理技术。
2. TLS 协议攻防战
1. 证书锁定(Certifacte Pining)技术
即使使用了 TLS 协议对流量进行加密,并且保证了前向保密,也无法保证流量不被代理!
这是因为客户端大多是直接依靠了操作系统内置的证书链进行 TLS 证书验证,而 Fiddler 等代理工具可以将自己的 TLS 证书添加到该证书链中。
为了防止流量被 Fiddler 等工具使用上述方式监听流量,出现了「证书锁定」技术。
方法是在客户端中硬编码证书的指纹(Hash值,或者直接保存整个证书的内容也行),在建立 TLS 连接前,先计算使用的证书的指纹是否匹配,否则就中断连接。
这种锁定方式需要以下几个前提才能确保流量不被监听:
客户端中硬编码的证书指纹不会被篡改。
指纹验证不能被绕过。
目前有公开技术(XPosed+JustTrustMe)能破解 Android 上常见的 HTTPS 请求库,直接绕过证书检查。
针对上述问题,可以考虑加大绕过的难度。或者 App 检测自己是否运行在 Xposed 等虚拟环境下。
用于 TLS 协议的证书不会频繁更换。(如果更换了,指纹就对不上了。)
而对于第三方的 API,因为我们不知道它们会不会更换 TLS 证书,就不能直接将证书指纹硬编码在客户端中。
这时可以考虑从服务端获取这些 API 的证书指纹(附带私钥签名用于防伪造)。
为了实现证书的轮转(rotation),可以在新版本的客户端中包含多个证书指纹,这样能保证同时有多个可信证书,达成证书的轮转。(类比 JWT 的公钥轮转机制)
证书锁定技术几乎等同于 SSH 协议的 StrictHostKeyChecking
选项,客户端会验证服务端的公钥指纹(key fingerprint),验证不通过则断开连接。
2. 公钥锁定(Public Key Pining)技术
前面提到过,TLS 证书其实就是公钥+申请者(你)和颁发者(CA)的信息+签名(使用 CA 私钥加密),因此我们也可以考虑只锁定其中的公钥。
「公钥锁定」比「证书锁定」更灵活,这样证书本身其实就可以直接轮转了(证书有过期时间),而不需要一个旧证书和新证书共存的中间时期。
如果不考虑实现难度的话,「公钥锁定」是更推荐的技术。
3. TLS 协议的逆向手段
要获取一个应用的数据,有两个方向:
服务端入侵: 现代应用的服务端突破难度通常都比较客户端高,注入等漏洞底层框架就有处理。
客户端逆向+爬虫: 客户端是离用户最近的地方,也是最容易被突破的地方。
mTLS 常见的破解手段,是找到老版本的安装包,发现很容易就能提取出客户端证书。。
wiki 列出了一些 TLS 协议的安全问题:https://en.wikipedia.org/wiki/Transport_Layer_Security#Security
TO BE DONE...
HTTPS 温故知新(三) —— 直观感受 TLS 握手流程(上)
A complete overview of SSL/TLS and its cryptographic system
Certificates - Kubernetes Docs
另外两个关于 CN(Common Name) 和 SAN(Subject Altnative Name) 的问答:
Can not get rid of net::ERR_CERT_COMMON_NAME_INVALID
error in chrome with self-signed certificates
SSL - How do Common Names (CN) and Subject Alternative Names (SAN) work together?
关于证书锁定/公钥锁定技术:
Certificate and Public Key Pinning - OWASP
Difference between certificate pinning and public key pinning
其他推荐读物:
图解密码技术 - [日]结城浩