言成言成啊 | Kit Chen's Blog

SSL/TLS证书以及H2与H2C

发布于2024-11-09 19:20:07,更新于2025-06-23 01:25:45,标签:java http tls  文章会持续修订,转载请注明来源地址:https://meethigher.top/blog

之前写的HTTP反向代理工具,在实际使用时,碰到反代失败的问题。跟踪了一下,才发现是由于对方使用了自签名SSL证书,导致发起HTTP请求时,验证失败。因此简单记录一下。

针对该问题的复现,从两个方面来展开

  1. 理解SSL/TLS
  2. 忽略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证书,由于操作系统已经内置了一系列根证书,当访问一个使用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";
}
};
}

三、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
0 1 1 3,6,9,12 * /usr/bin/certbot renew --force-renew

表示在每年的 3 月、6 月、9 月和 12 月的 1 日,凌晨 1:00 运行 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 等。

协商的过程大致是这样:

  1. 客户端发起 TLS 握手时,告诉服务器:“我支持 HTTP/2、HTTP/1.1!”
  2. 服务器收到后选择一个协议,比如:“那就用 HTTP/2 吧!”
  3. 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) {
}
}
}
发布:2024-11-09 19:20:07
修改:2025-06-23 01:25:45
链接:https://meethigher.top/blog/2024/ssl-certificate/
标签:java http tls 
付款码 打赏 分享
Shift+Ctrl+1 可控制工具栏