
Certificate Authority
以密钥交换(如 DHKX)、非对称加密、对称加密等密码学为基础的 TLS 能够让双方在不可信信道上协商出加密通信。
问题在于,你能和对方加密通讯了,可是你怎么知道对方是谁呢?
(如何防御 MITM 攻击)
答案其实挺无聊:引入先验知识。
如果对方能出示一个你们事先约定好的暗号,那你就知道对方是谁了。
但是如果每个人都有一个唯一暗号,这个暗号量会变得太大。 比如全世界有 14 亿个网站,你大可能存 14 亿个暗号,而且还得经常更新。
所以现实中采用 x509 证书信任链的方式。
所谓证书,可以简单粗暴的将其理解为是一个身份证,一个公钥,以及上级部门用私钥生成的签名。
「私钥签名,公钥验签」
假设 Alice 要去和 Bob 通信,
每次握手时,Bob 都出示它及其它所有上级的全部证书。
Alice 仅需要保存最顶级证书(根证书),然后用根证书的公钥校验中间证书,用中间证书的公钥校验叶子证书, 以此类推,理论上 Alice 仅需要保存一份证书,就可以校验无数多个叶子证书的有效性。

打开 GitHub 看看它的证书链

打开系统设置看看,我还真有这个根证书

总结:操作系统中内置了一些权威机构(CA)的根证书,这些 CA 机构可以去下发其他证书,只要是这些 CA 下发的证书,我们都会统统信任。
换言之,我们信任 CA,将检查 Bob 是不是真是 Bob 的责任交给了 CA。
Bob 想去找 CA 生成一个证书,这个证书会绑定到一个域名,所以问题变成了,Bob 怎么证明这个域名真是自己的。
答案又是「暗号」。
CA 给 Bob 一个暗号,Bob 把它放在域名的一个 url 下,CA 去访问一下这个 url,如果发现暗号存在,那就说明 Bob 确实是域名的管理者

那如果我是想申请如 *.laisky.com 这样的 wildcard 证书,又该怎么证明呢?
最常见的办法就是给 CA 机构提供一个 DNS server 的 Access Token, 让 CA 去域名托管机构上去 check 一下,确定这域名真是你的。
任何人,只要往你的系统根证书池里插入一个自己的根证书,那么他就可以随心所欲的签发任何域名的任何证书,而且你都会毫无察觉的信任它。
根证书的破坏力是如此巨大,只要一个恶意根证书,就能让你整个的 HTTPS 防御瞬间瓦解。
清华大学在 2022 年联合 360 浏览器做了一次采样,发现中国人的根证书池被严重污染。

既然我们现在无条件信任 CA,但是 CA 能保证自己不作恶吗?

最出名的可能就是沃通公司给第三方签发了 github.com 的证书。
暴露出 CA 公司的内部管理可能极其混乱,甚至自己都搞不清楚自己发过什么证书。
顺带吐槽一下沃通这公司,
最早原名 WoTrust,后来改名 WoSign,然后因为乱签证书被 Mozilla 和 Google 禁用,后来又改名 WoTrus 重回 CA 大家族。
WoSign 被封时曾提出的申诉是:只服务于中国区用户。但是该辩解未被接受。

https://certificate.transparency.dev/
由 Google 等公司牵头成立了证书透明日志(Certificate Transparency)。
这是个第三方日志审计机构,要求所有的 CA,在签发证书时,必须上传所有新证书的审计日志。

CT log 可以起到如下作用:
这是一个我订阅的 CT Log 的例子

Tips:CT Log 本质上和 HPKP(HTTP Public Key Pinning) 是一个效果。
但实际上这几年下来,那一系列的 HTTPS 增强 Headers 好像都逐步被淘汰了。
可能是因为实际应用下来,发现因缓存等原因,非常容易误用并导致严重的后果。 唯一还在广泛被使用的可能只有 HSTS 😂
Cloudflare 作为现代互联网的事实入口,负载了 80% 的互联网 CDN 流量。
源站不再直接为用户提供公网服务,而是依托 CDN 在全球的边缘机房,将数据传输到 CDN 后,由 CDN 代为提供服务。
所有的 CDN 都绑定同一个域名,用户通过智能 DNS 解析到最近的 CDN 站点。

CDN 扮演的就像是一个缓存,客户先去请求 CDN,如果 CDN 没有,再去请求源站(穿透)。
因为 CDN 需要知道客户的请求信息,所以 CDN 必须对 HTTPS 做卸载。
用户和 CDN 进行 HTTPS,CDN 再和源站通信。
那其实,CDN 本身也可以扮演起 CA 的角色,为域名申请 HTTPS 证书,并持有该证书和客户通讯。
CF 自动为托管的域名提供 HTTPS 服务,然后你还可以选择,源站是否要使用 HTTPS

顺路吹一波 CloudFlare,不仅可以实现自动 HTTPS、HTTP/2、HTTP/3、IPv6,更有 Worker、Tunnel 等一系列大杀器应用。
而且 全部免费!
在过去,申请一张 HTTPS 证书是很贵的,一般来说,普通的单域名证书在 10 刀/年 左右,wildcard 证书在 100 刀/年 左右。
过去也没有 cloudflare 这样的慈善家会帮你的定制域名做 HTTPS。
CDN 往往是使用 CDN 自己的域名做 HTTPS,然后给你提供个子路径来使用。
形如 https://cdn.cloudflare.com/userspace/resouece_name
赞美 LetsEncrypt,让每个人都可以有自己的 wildcard 证书!
LetsEncrypt 的 Founder 前段时间去世了,R.I.P.

使用 LetsEncrypt 最简单的方法就是通过 SWAG - Secure Web Application Gateway

这就是一个 Nginx docker container,你把 site-confs 挂载进去,它自动给你申请 LetsEncrypt 证书。
(使用体验类似于 Go Caddy)

用 docker comose 启动
yml
swag:
image: lscr.io/linuxserver/swag:latest
container_name: swag
cap_add:
- NET_ADMIN
environment:
- PUID=1000
- PGID=1000
- TZ=UTC
- URL=tk.laisky.com
- VALIDATION=dns
- SUBDOMAINS=wildcard
- DNSPLUGIN=cloudflare
volumes:
- /var/www:/var/www
- /var/log/nginx:/var/log/nginx
- /opt/configs/swag/tk:/config
- /opt/configs/nginx/conf.d/tk:/config/nginx/site-confs
ports:
- 443:443
- 80:80
restart: unless-stopped
搞定!

自签证书其实也挺爽的😄,尤其是不需要对公众开放的私有服务。
自己搞一套 PKI 体系完全不受制于人,在自有设备上导入 rootCA 即可。
不过有个缺点是,像 iOS 只支持有效期不超过 398 天的证书,为证书更换维护带来了一些麻烦。
一个服务器(同一个 IP)可能托管多个站点,每个站点都有不同的域名。服务器需要按照域名将流量分发给不同的站点。
所以这个域名,即使在 HTTPS 时代,也得保持明文,这就是 ClientHello 中的 Server Name Indication。
这个 SNI 成为了 HTTPS 的巨大软肋,它完全泄漏了用户的隐私,让第三方知道你在访问什么站点。
TLS 1.3 draft-10 提出了 0-RTT 方案,如果服务端的公钥长期未变,客户端可以跳过 ClientHello 直接使用旧公钥和服务器建立加密通信。
不过该方案没有被 TLS 1.3 最终版接纳。因为本质上并未解决首次握手时的明文状态。

0-RTT 的设计理念是,要想加密握手信息,那么客户端就应该先取得服务端的公钥。
其后的 ESNI(Encrypted SNI) 协议延续了这个思路,服务端将自己的公钥注册到 DNS 里,客户端先去 DNS 查询服务端公钥,再加密 SNI 信息并发起握手。
为了防止 DNS 明文查询导致信息被篡改,ESNI 需要同步启用 DoH(DNS-Over-HTTPS) 后才能生效。

但是实用了一段时间后发现 ESNI 有很多缺点:
ESNI 继续进化就成为了 ECH(Encrypted Client Hello) 协议。
首先看名字就知道,它实现了对握手的全加密。
其次公钥还是注册到 DNS,但是 DNS 中增加了 HTTPS RR 类型,一个域名可以为不同 IP 解析地址注册不同的公钥。

我的理解是在网络服务商的服务器上,在源网站的前面多套了一层 client-facing server。
客户端先和 client-facing server 握手,成功后再用 share key 加密握手信息去和真正的网站握手。
这样 SNI 只会泄露我和哪个网络服务商通信了(透露的信息不比 IP 多)。
DNS 内可以为域名配置多个 HTTPS endpoint,且每个 endpoint 都可以配置不同的元信息(如公钥等)

ECH 协议解决 DNS 缓存的问题也很简单暴力。
当你和 client-facing server 建立连接后,如果 client-facing server 发现客户端持有的公钥是无效的(已过期), 那就直接把当前有效的公钥传给客户端。
DNS 能透露的信息相当多,针对 DNS 的攻击也非常多:
为 DNS 加密的需求日益增长,所以催生出了 DNSSEC(DNS Security Extensions)。
这一方案和 HTTPS 的 CA 类似,要求用户下载递归签名的信任锚(trust anchor)。
用户持有 root DNSKEY 作为信任根,而且还需要启用类似于 HSTS 的强制验证,对客户端要求太重了。 实际上也很少有网站启用了对 DNSSEC 的支持。
目前最流行的解决方案还是 DoT(DNS over TLS) 和 DoH(DNS over HTTPS),尤其是 DoH 已占主流。
实现原理非常简单,DNS Server 提供一个 HTTPS 接口,然后客户端发起 HTTPS 请求去查询 DNS。
本质上就是把 DNS 服务器封装为一个 HTTPS 服务。
用 docker compose 部署一个 DoH 服务:
yml
doh:
image: satishweb/doh-server:v2.3.2-ubuntu
restart: always
dns:
- 8.8.8.8
- 1.1.1.1
logging:
driver: "json-file"
options:
max-size: "10m"
environment:
DEBUG: "0"
UPSTREAM_DNS_SERVER: "udp:1.1.1.1:53"
DOH_HTTP_PREFIX: "/query"
DOH_SERVER_LISTEN: "0.0.0.0:8053"
DOH_SERVER_TIMEOUT: "10"
DOH_SERVER_TRIES: "3"
DOH_SERVER_VERBOSE: "true"
该服务会监听 8053 HTTP 接口,外面套一层 SWAG HTTPS 反向代理即可。
给 Chrome 配置 DoH

iOS 用户可以通过 ShadowRocket 等 App 配置 DoH

Cloudflare 牵头着手解决这个问题:在客户端和 DNS 服务器间再加一层反向代理来隐藏客户端信息。
客户端和服务器间通过 DoH 加密来防止代理窃听。 这就让 DNS 服务器也不知道是谁在查询,增强了客户的隐私。 称为 Oblivious DNS over HTTPS(ODoH)

但是 ODoH 仍然有一个缺点,就是 DNS 服务商虽然不知道谁在查,但是它知道在查什么。
为什么这点很重要呢,因为很多国内的 ODoH 服务,处于各种“不可言喻”的原因,不给返回境外网站的地址。
这就很恶心,要想解决这个问题,可能需要基于如匿踪查询 PIR 等技术了。

为了解决 HTTP/1 的各类不足,人们做出了很多努力,既改进了 HTTP 协议,也尝试改进过 TCP 协议:
但是 TCP 其协议设计之初就没有优先考虑过性能,导致了两个最大的问题:
Tips:
HTTP/2.0 解决了 HTTP HoL,HTTP 请求不再需要按照顺序响应。
但是 TCP 本身的发包仍然是有顺序的,所以并未能解决 TCP HoL。
人们也尝试过优化 TCP 协议,但是遇到了一些难以预料的困难。
这毕竟是一个即将 50 岁的协议,ISP 基础设施层面已经在硬件上对其做了大量“事实上已不可维护“的优化。
但凡这个协议有丝毫的改动,都可能导致难以预测的问题。
这一现象也被称为 Ossification(骨化),本来不可见的网络层,突然变成有形的阻碍
所以 HTTP/3 协议选用 UDP 作为底层协议最重要的原因其实是:兼容性。
UDP 也是一个历史悠久的协议,ISP 能够非常好的支持。而且因为它协议简单,所以底层基础设施不会为它做太多优化。
HTTP/3 在 UDP 的基础上,在应用层重新实现了可靠传输、多路并发、地址漂移等等功能。
TCP 时代,通过四元组来唯一识别一个连接(client ip, client port, server ip, server port)。一旦客户端发生漂移(如基站切换),就会导致重建一个全新的连接,重新进行一系列的握手等。
QUIC 通过为每一个连接引入 Coonection Identifier(CID) 来实现对 ip、port 迁移的兼容。
QUIC 实际上是让客户端和服务端每次都协商好一组 CIDs 来标记某一个连接。每当网络发生迁移时, 都会使用一个新的 CID 来恢复连接,这样可以防止用 CID 来跟踪用户。
比起 HTTP/3 这个名字,我们也许更应该称之为 QUIC/1。
它的本质是在 UDP 上重新实现了一套优化过的 TCP 协议。
这只是开始,未来可能还会有更多的升级(毕竟升级 QUIC 比升级 TCP 要简单太多)。 而且为了防止基础设施偷偷为 QUIC 做一些难以维护的定制化优化,HTTP/3 默认全加密。
QUIC 是一种通用型传输协议,可以通过设定 FRAME 类型来传输不同的数据。
比如通过 STREAM 来传输 TCP 字节流。
甚至你可以指定 DATAGRAM 类型来传输 UDP
(基于 UDP 重新实现了 TCP,然后拿来传输 UDP😂)