摘要
之前写的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为例,部署证书
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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\n SM\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 ) {
}
}
}