摘要
之前写的HTTP反向代理工具,在实际使用时,碰到反代失败的问题。跟踪了一下,才发现是由于对方使用了自签名SSL证书,导致发起HTTP请求时,验证失败。因此简单记录一下。
正文
之前写的HTTP反向代理工具,在实际使用时,碰到反代失败的问题。跟踪了一下,才发现是由于对方使用了自签名SSL证书,导致发起HTTP请求时,验证失败。因此简单记录一下。
针对该问题的复现,从两个方面来展开
- 理解SSL/TLS
- 忽略SSL/TLS
一、理解SSL/TLS
1.1 HTTPS与SSL/TLS
SSL是用于加密传输的协议,也是最初的加密标准,目前已被TLS取代,但由于历史原因,大家还是会称为SSL。
HTTPS是HTTP上实现加密传输的协议,依赖SSL/TLS来确保安全性。
从不求甚解的角度来理解,HTTPS=HTTP+SSL/TLS
1.2 证书分类
SSL常见的证书分类有两种
这两者的区别如下
| 特性 | 自签名证书 | 公共CA证书 |
|---|
| 签发机构 | 由证书持有者自己签发 | 由受信任的证书颁发机构(CA)签发 |
| 信任级别 | 默认不被浏览器或操作系统信任,需手动安装信任 | 浏览器和操作系统默认信任 |
| 身份验证 | 无身份验证,持有者自行生成证书 | CA会对证书持有者进行身份验证 |
| 安全性 | 安全性较低,可能被伪造或滥用 | 高安全性,通过身份验证保障证书真实性 |
| 应用场景 | 适用于开发、测试和内部网络 | 适用于生产环境和面向互联网的服务 |
| 成本 | 免费 | 需要付费,费用根据证书类型和CA机构不同而异 |
| 浏览器警告 | 会弹出“不安全连接”警告 | 不会弹出警告,用户信任度高 |
| 管理复杂度 | 管理简单,但不适合公开环境 | 管理较复杂,需要向CA申请和续期 |
互联网服务使用的一般都是CA证书,由于操作系统已经内置了一系列根证书,当访问一个使用CA签发证书的HTTPS网站时,就不会出现“不安全连接”的警告。
而自签名证书,由于操作系统缺少对其的信任,访问就会被拦截了。此时服务提供方,需要给调用方提供自签名证书,以便调用方可以信任该连接。
1.3 OpenSSL生成自签名证书
1.3.1 扩展名说明
像我购买的CA证书,部署到Nginx时,一般都是.pem和.key文件。但在自己生成证书的过程中,发现还有.crt文件。直观的感受是,这些扩展名特别的混乱。经过查阅资料,下面简单记录这些扩展名的区别。
- crt: 存储证书(公钥)。该证书可提供给第三方使用,比如HTTPS客户端
- key: 私钥。该私钥文件只应给服务提供者使用。
- csr: 向证书颁发机构申请签署密钥的请求,不包含密钥本身。
- pem: 基于Base64编码的文本格式。它可以是上述任何文件。
- der: 基于二进制编码的文本格式。它可以是上述任何文件。
参考
ssl - Difference between pem, crt, key files - Stack Overflow
Difference between .pem and .crt and how to use them - Help - Let's Encrypt Community Support
1.3.2 自签名证书
下面使用OpenSSL生成自签名的公钥和私钥证书。
1
2
3
4
5
6
7
8
9
10
11
12
13
| # 生成一个2048位的RSA私钥,并保存到private.key文件中
openssl genrsa -out private.key 2048
# 根据私钥,生成一个证书签名请求
openssl req -new -key private.key -out request.csr
# X.509是SSL/TLS中最常用的公钥证书标准
# 通过私钥和证书签名请求,生成一个时效为365天的公钥证书
openssl x509 -req -days 365 -in request.csr -signkey private.key -out public.pem
# 验证证书内容
openssl x509 -in public.pem -noout -text
|
上面这段是标准的自签名证书生成流程,但缺少 Subject Alternative Name (SAN) 字段(未指定域名),所以不能用于现代浏览器或 curl、Java 客户端。
下面放置一个可用的,也就是指定域名。
1
2
3
4
5
6
7
8
9
| # 生成一个2048位的RSA私钥,并保存到private.key文件中
openssl genrsa -out private.key 2048
# 生成自签名证书,包含 SAN 扩展字段,指定使用的域名为meethigher.com和www.meethigher.com
openssl req -x509 -new -nodes -key private.key -sha256 -days 365 -out public.pem -subj "/CN=meethigher.com" -addext "subjectAltName=DNS:meethigher.com,DNS:www.meethigher.com"
# 验证证书内容
openssl x509 -in public.pem -noout -text | grep "Subject Alternative Name" -A 1
|
二、部署与忽略SSL/TLS
2.1 服务端部署证书
2.1.1 Nginx
以Nginx为例,部署证书
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
server {
listen 443 ssl;
server_name 10.0.0.1;
ssl_certificate /usr/local/nginx/conf/cert/public.pem;
ssl_certificate_key /usr/local/nginx/conf/cert/private.key;
location / {
root /usr/local/nginx/html;
index index.html;
}
}
}
2.1.2 Vertx
使用Java中的Vertx 4.5.10版本开启HTTPServer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()//使用自签名证书开启ssl
.addCertPath("/usr/local/nginx/conf/cert/public.pem")
.addKeyPath("/usr/local/nginx/conf/cert/private.key");
Future<HttpServer> serverFuture = vertx.createHttpServer(new HttpServerOptions()
.setSsl(true)
.setKeyCertOptions(pemKeyCertOptions))
//注册路由
.requestHandler(router)
.listen(port);
serverFuture.onComplete(re -> {
if (re.succeeded()) {
log.info("http server started on port {}", port);
} else {
log.error("http server failed to start", re.cause());
}
});
|
2.2 客户端忽略校验
2.2.1 CURL
curl忽略ssl校验比较简单,添加-k参数即可。
1
| curl -k "https://10.0.0.10:443"
|
2.2.2 Apache HttpPClient and OkHttpClient
设置忽略SSL的核心逻辑如下,具体的写法还需根据框架而定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| /**
* 信任所有SSL证书,包括CA证书和自签名证书。实现效果类似于`curl -k`
* @see <a href="https://blog.csdn.net/qq_20683411/article/details/142996223">Apache HttpClient 4.3.2 和 4.5.13 - 忽略证书问题_apache 4.3.5 忽略ssl-CSDN博客</a>
*/
public SSLContext trustAllCerts() {
try {
TrustManager[] trustManagers = {
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
};
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustManagers, new SecureRandom());
return sslContext;
} catch (Exception ignore) {
}
return null;
}
public HostnameVerifier getNoopHostnameVerifier() {
return new HostnameVerifier() {
@Override
public boolean verify(final String s, final SSLSession sslSession) {
return true;
}
@Override
public final String toString() {
return "NO_OP";
}
};
}
|
2.3 Chrome 命令行忽略
在 Chrome 浏览器中,如果因为不信任的证书而被拦截。可以通过键盘直接输入 thisisunsafe 来进行临时的跳过。
注意,该操作仅对当前标签页、当前会话有效。
三、Certbot自动更新Nginx证书
3.1 安装与配置
具体的安装步骤,可自行查阅官网,很详细。
3.1.1 snapd
安装snap软件包管理工具
1
2
3
4
5
6
7
8
| # 安装 Snapd(Snap 软件包管理工具)
sudo yum install -y snapd
# 启用并立即启动 snapd.socket,使其开机自动启动
sudo systemctl enable --now snapd.socket
# 创建符号链接,将 /var/lib/snapd/snap 映射到 /snap,避免路径问题
sudo ln -s /var/lib/snapd/snap /snap
|
3.1.2 certbot
1.) 使用snpa安装certbot
1
2
3
4
5
| # 通过 Snap 安装 Certbot,并使用 --classic 选项启用传统模式
sudo snap install --classic certbot
# 创建符号链接,使 certbot 命令可直接通过 /usr/bin/certbot 访问
sudo ln -s /snap/bin/certbot /usr/bin/certbot
|
2.) 初次生成证书
1
2
3
4
5
6
7
8
9
10
| # 使用 Certbot 为域名 meethigher.top 申请 SSL 证书,并配置 Nginx
# 生成一个证书,该证书可用于所有-d的域名
sudo certbot --nginx \
--nginx-ctl /usr/local/nginx/sbin/nginx \
--nginx-server-root /usr/local/nginx/conf \
-d meethigher.top \
-d git.meethigher.top \
-d tools.meethigher.top \
-d jetbrains.meethigher.top \
-d vaultwarden.meethigher.top
|
成功时输出内容如下
另外,他会自动进行nginx.conf的维护。
生成证书的记录可以通过crt.sh查看
3.) 模拟证书生成流程。此处我遇到报错了,不过该模拟过程的报错可以忽略,直接进行下一步。
1
| sudo certbot renew --dry-run
|
该问题在官方论坛也有讨论,First Time Problem - certbot failed to auth during secondary validation - Help - Let's Encrypt Community Support
4.) 移除自带定时任务。
1
2
3
| systemctl list-timers --all
systemctl stop snap.certbot.renew.timer
systemctl disable snap.certbot.renew.timer
|
5.) 配置自定义定时任务。强制覆盖更新证书。
输入crontab -e,追加一行,内容如下
1
| 50 0 1 2,4,6,8,10,12 * /usr/bin/certbot renew --force-renew
|
表示在每年的 2、4、6、8、10、12 月的 1 日,凌晨 00:50 运行 certbot 进行 SSL 证书的强制覆盖续期。
Certbot根据/etc/letsencrypt/renewal/*.conf文件来确定有哪些证书是它“负责”的。
3.3 参考
Certbot Instructions | Certbot
使用CertBot自动更新Nginx的ssl证书 - pyt123456 - 博客园
Nginx配置使用certbot自动申请HTTPS证书-腾讯云开发者社区-腾讯云
四、h2与h2c
本文的示例代码meethigher/bug-test at vertx-http-alpn
4.1 理解h2与h2c
h2 与 h2c 是基于 http2 通信的两种模式。
h2 即 http/2 over tls,直观理解就是 https://xxx 形式的 http2 请求。
h2c 即 http/2 over cleartext,直观理解就是 http://xxx 形式的 http2请求。
4.2 h2与alpn协议
ALPN 的全名是 Application-Layer Protocol Negotiation,中文可以叫“应用层协议协商”。
它是用在 TLS(加密通信协议) 里的一个扩展,用来让客户端和服务器在建立加密连接之前,商量好用什么具体的应用协议,比如 HTTP/1.1、HTTP/2、HTTP/3 等。
协商的过程大致是这样:
- 客户端发起 TLS 握手时,告诉服务器:“我支持 HTTP/2、HTTP/1.1!”
- 服务器收到后选择一个协议,比如:“那就用 HTTP/2 吧!”
- TLS 握手完成后,双方就都知道接下来的通信用的是 HTTP/2。
ALPN 只能用于 HTTPS(加密通信),不能用于明文 HTTP。
4.3 h2c与prior knowledge
参考RFC 7540 - Hypertext Transfer Protocol Version 2 (HTTP/2)
h2c 是 HTTP/2 over cleartext TCP,即基于明文 TCP 的 HTTP/2 协议。h2c 支持两种连接方式
- 带 Upgrade 头的 h2c 升级方式
- h2c 明文直连(prior knowledge)
带 Upgrade 头的 h2c 升级方式,标准格式
1
2
3
4
5
6
7
8
9
10
11
| > GET /test HTTP/1.1
> Host: 127.0.0.1
> User-Agent: curl/8.12.1
> Accept: */*
> Connection: Upgrade, HTTP2-Settings
> Upgrade: h2c
> HTTP2-Settings: AAMAAABkAAQAAQAAAAIAAAAA
< HTTP/1.1 101 Switching Protocols
< connection: upgrade
< upgrade: h2c
|
h2c 明文直连(prior knowledge),建立连接后立即发送的“连接前言”(connection preface)。格式如下
1
| PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
|
Vertx 的 HTTPClient 默认并不支持 prior knowledge,因此使用 okhttp 实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| private static void h2cPriorKnowledge() {
CountDownLatch latch = new CountDownLatch(1);
OkHttpClient client = new OkHttpClient.Builder()
.protocols(new ArrayList<Protocol>() {{
add(Protocol.H2_PRIOR_KNOWLEDGE);
}})
.build();
Request request = new Request.Builder()
.url("http://meethigher.com:80/test")
.build();
try (Response resp = client.newCall(request).execute()) {
log.info("{} received:\n{}", resp.protocol(), resp.body().string());
latch.countDown();
} catch (Exception e) {
e.printStackTrace();
try {
latch.await();
} catch (Exception ignore) {
}
}
}
|