国密 HTTPS 的"最后一公里":从证书管理到加密服务的架构转型
前言:国密改造的痛点正在变
过去十年,国密改造的核心问题一直是:如何申请一张 SM2 证书?
到 2026 年,这个问题已经有了相当成熟的答案——ACME 协议(RFC 8555)的 Python 实现、acme.sh 对 SM2 的支持、各类 CA 的自动化申请接口,都已经可用。(参见《ACME 协议实战:用 Python 构建自动化证书管理工具,应对 47 天有效期挑战》和《47天证书时代下的国密双证书自动化部署》。)
但一个更深层的问题正在浮现:证书拿到了,然后呢?
- Nginx 需要支持 SM2/SM3/SM4 密码套件,但原生 Nginx 不支持国密
- 浏览器需要支持国密,但 Chrome/Edge 的国密支持依赖操作系统证书库
- CDN 节点不支持国密算法,HTTPS 握手在 CDN 边缘节点就断了
- 即使 Web 服务器改造完毕,证书续期、双证书管理、密码套件升级仍然是持续负担
2026 年 6 月,北京数字认证(BJCA)发布了一篇颇具代表性的文章《证书自动化的本质是自动化交付 HTTPS 加密服务,不是自动化管理 SSL 证书》,提出了一个核心观点:用户需要的是 HTTPS 加密,不是 SSL 证书。
本文认同这个观点,并在此基础上给出一个可落地的架构方案和完整实现。
一、问题分析:为什么 CLM 模式在国密场景下走不通
1.1 国际 ACME 方案的前提条件
Let's Encrypt 通过 ACME 协议实现了证书的自动化申请和部署,极大推动了 HTTPS 的普及。但 ACME 能成功,依赖一个已经完备的加密生态:
- Web 服务器(Nginx、Apache、IIS)原生支持 TLS
- 浏览器(Chrome、Firefox、Safari)原生支持标准密码套件
- 操作系统(Windows、macOS、Linux)内置根证书库
- CDN 服务(Cloudflare、Akamai)端到端支持
1.2 中国国密生态的现实
中国的国密生态完全不同:
| 层面 | 国际生态 | 国密生态 |
|---|---|---|
| Web 服务器 | 原生 TLS 支持 | 需要 Tongsuo/BabaSSL 替换 |
| 浏览器 | 标准密码套件 | 需要操作系统国密支持 |
| CDN | 全链路支持 | 基本不支持国密 |
| 操作系统 | 内置国际根证书 | 国密根证书需手动导入 |
| 中间件 | 默认支持 TLS | 大多不支持国密套件 |
1.3 传统 CLM 的局限
传统 CLM(Certificate Lifecycle Management)系统的逻辑是:
- 发现证书
- 申请/续期证书
- 部署证书到目标系统
- 监控到期
更关键的是,在国密改造中,服务器改造的成本远高于证书管理的成本。给一台 Nginx 加上国密支持,需要:
- 编译安装 Tongsuo(替代 OpenSSL)
- 配置
ssl_conf指定国密密码套件 - 配置 SM2 双证书(签名证书 + 加密证书)
- 配置 SM3 摘要算法
- 持续跟踪国密标准的更新
二、架构转型:加密服务网关
2.1 核心思路
既然"最后一公里"的瓶颈在于服务器侧的国密支持,那就把国密支持集中到一个网关层,让后端服务器完全不需要改造。
架构如下:
客户端(浏览器/APP)
│
│ TLS 握手(国密套件)
▼
加密服务网关(Tongsuo + Nginx)
│
│ HTTP(内网明文或内部 TLS)
▼
后端服务器(无需任何国密支持)关键优势:
- 后端服务器不需要安装 Tongsuo、不需要配置国密套件、不需要管理证书
- 证书集中在网关层管理,续期、升级只在网关上操作一台
- 国密算法的演进(如未来切换到 PQC)只需升级网关层
- 适用于多服务器、CDN 不可控、甚至无法改造的老旧系统
2.2 与反向代理方案的差异
有人可能会问:这不就是一个 Nginx 反向代理吗?
表面看确实类似,但"加密服务网关"和"普通反向代理"有本质区别:
| 维度 | 普通反向代理 | 加密服务网关 |
|---|---|---|
| 密码套件 | 国际 RSA/ECC | 国密 SM2/SM3/SM4 |
| 证书类型 | 单证书 | SM2 双证书(签名+加密) |
| TLS 版本 | TLS 1.2/1.3 | NTLSP(国密 TLS)或 TLS 1.3 + 国密套件 |
| 证书管理 | 手动或 CLM | 全自动化(ACME + 自动 reload) |
| 高可用 | 通常单主 | 多实例 + 证书同步 |
| 监控指标 | 基本 SSL 指标 | 国密握手成功率、证书链完整性 |
三、实战:用 Tongsuo + Nginx 搭建国密加密服务网关
3.1 环境准备
本文基于以下环境:
- Ubuntu 22.04 LTS
- Tongsuo 3.0(原 BabaSSL,基于 OpenSSL 3.0 的国密分支)
- Nginx 1.24+(编译时链接 Tongsuo)
# 下载 Tongsuo 源码
wget https://github.com/Tongsuo-Project/Tongsuo/releases/download/8.4.0/Tongsuo-8.4.0.tar.gz
tar xzf Tongsuo-8.4.0.tar.gz
cd Tongsuo-8.4.0
# 编译安装到 /opt/tongsuo
./config --prefix=/opt/tongsuo --openssldir=/opt/tongsuo/ssl shared
make -j$(nproc)
sudo make install
# 验证安装
/opt/tongsuo/bin/openssl version
# 验证国密算法支持
/opt/tongsuo/bin/openssl enc -ciphers | grep sm4
# 应输出 SM4-CBC、SM4-GCM 等步骤 2:编译 Nginx 链接 Tongsuo
# 下载 Nginx 源码
wget http://nginx.org/download/nginx-1.26.1.tar.gz
tar xzf nginx-1.26.1.tar.gz
cd nginx-1.26.1
# 关键:指定 --with-openssl 路径为 Tongsuo
./configure \
--prefix=/etc/nginx \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_realip_module \
--with-http_stub_status_module \
--with-openssl=/opt/tongsuo \
--with-cc-opt="-I/opt/tongsuo/include" \
--with-ld-opt="-L/opt/tongsuo/lib64 -Wl,-rpath,/opt/tongsuo/lib64"
make -j$(nproc)
sudo make install3.2 生成国密双证书
国密 TLS 需要使用两张证书:签名证书(用于身份认证)和加密证书(用于密钥交换)。
#!/usr/bin/env python3
"""
generate_sm2_dual_cert.py
生成 SM2 双证书(签名证书 + 加密证书)
需要:pip install gmssl >= 3.2.0
"""
import os
import datetime
from gmssl import sm2, sm3, func
from gmssl.sm2 import CryptSM2
from cryptography import x509
from cryptography.x509.oid import NameOID, ExtensionOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, padding
from cryptography.hazmat.backends import default_backend
from cryptography.x509.oid import KeyUsageOID
import subprocess
def generate_sm2_key_pair():
"""使用 gmssl 生成 SM2 密钥对"""
# gmssl 3.2.x API:生成密钥对
sm2_crypt = CryptSM2(None)
private_key_bytes = sm2_crypt.generate_private_key()
public_key_bytes = sm2_crypt.generate_public_key(private_key_bytes)
return private_key_bytes, public_key_bytes
def create_sm2_self_signed_cert(private_key_bytes, public_key_bytes,
common_name, days_valid=365):
"""
创建 SM2 自签名证书
实际生产环境应从 CA 签发,此处演示证书结构
"""
# 使用 cryptography 生成 SM2 密钥对(兼容格式)
# 注意:cryptography 44.x 不支持 ec.SM2() 密钥生成
# 这里用 subprocess 调用 Tongsuo 命令行生成
cmd = [
'/opt/tongsuo/bin/openssl', 'ecparam', '-name', 'sm2',
'-genkey', '-noout', '-out', f'{common_name}_key.pem'
]
subprocess.run(cmd, check=True)
cmd = [
'/opt/tongsuo/bin/openssl', 'req', '-new', '-x509',
'-key', f'{common_name}_key.pem',
'-out', f'{common_name}_cert.pem',
'-days', str(days_valid),
'-subj', f'/CN={common_name}',
'-sm3'
]
subprocess.run(cmd, check=True)
print(f"已生成: {common_name}_cert.pem, {common_name}_key.pem")
def main():
"""生成签名证书和加密证书"""
print("=" * 60)
print("国密双证书生成工具")
print("=" * 60)
# 生成签名证书
print("\n[1/2] 生成签名证书...")
create_sm2_self_signed_cert(None, None, "sm2-sign", days_valid=365)
# 生成加密证书
print("\n[2/2] 生成加密证书...")
create_sm2_self_signed_cert(None, None, "sm2-enc", days_valid=365)
print("\n" + "=" * 60)
print("双证书生成完成!")
print(" - 签名证书: sm2-sign_cert.pem + sm2-sign_key.pem")
print(" - 加密证书: sm2-enc_cert.pem + sm2-enc_key.pem")
print("=" * 60)
if __name__ == '__main__':
main()环境说明:gmssl >= 3.2.0提供 Python 原生 SM2 支持,cryptography44.x 支持hashes.SM3()但不支持ec.SM2()密钥生成。需要 SM2 密钥时推荐用gmssl库。
3.3 Nginx 配置
# /etc/nginx/conf.d/gm-gateway.conf
# 国密加密服务网关配置
# 支持 TLS 1.3 + 国密密码套件 + 双证书
server {
listen 443 ssl;
server_name example.com;
# === 国密双证书配置 ===
# 签名证书(用于身份认证)
ssl_certificate /etc/nginx/certs/sm2-sign_cert.pem;
ssl_certificate_key /etc/nginx/certs/sm2-sign_key.pem;
# 加密证书(用于密钥交换)
# Tongsuo 支持在一张配置中同时加载两张证书
ssl_enc_certificate /etc/nginx/certs/sm2-enc_cert.pem;
ssl_enc_certificate_key /etc/nginx/certs/sm2-enc_key.pem;
# === 密码套件配置 ===
# 国密优先,回退国际套件
ssl_conf_command Ciphersuites ECC-SM2-SM4-GCM-SM3:ECDHE-SM2-SM4-GCM-SM3;
ssl_conf_command Options ServerPreference;
# === TLS 协议版本 ===
ssl_protocols TLSv1.2 TLSv1.3;
# === 证书链 ===
# 如果有 CA 中间证书,合并到证书文件中
# ssl_trusted_certificate /etc/nginx/certs/ca-chain.pem;
# === 会话缓存 ===
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# === 安全头部 ===
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Content-Type-Options nosniff always;
# === 代理到后端 ===
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 关键:告知后端原始请求是 HTTPS
proxy_set_header X-Forwarded-Ssl on;
}
}
# HTTP 自动跳转 HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}3.4 证书自动续期脚本
#!/usr/bin/env python3
"""
cert_autorenew.py
国密证书自动续期 + Nginx reload
配合 cron 每天运行:0 2 * * * python3 /opt/cert-gateway/cert_autorenew.py
"""
import subprocess
import os
import datetime
import logging
from pathlib import Path
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler('/var/log/cert-autorenew.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# 配置
CERT_DIR = Path('/etc/nginx/certs')
DOMAINS = ['example.com', 'www.example.com']
ACME_CLIENT = '/usr/local/bin/acme.sh'
DAYS_BEFORE_EXPIRE = 30 # 证书到期前 30 天续期
def check_cert_expiry(cert_path: str) -> int:
"""检查证书剩余天数"""
try:
cmd = [
'/opt/tongsuo/bin/openssl', 'x509', '-in', cert_path,
'-noout', '-enddate'
]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
# 输出格式:notAfter=Jun 15 00:00:00 2026 GMT
end_date_str = result.stdout.strip().split('=')[1]
end_date = datetime.datetime.strptime(end_date_str, '%b %d %H:%M:%S %Y GMT')
remaining = (end_date - datetime.datetime.utcnow()).days
return remaining
except Exception as e:
logger.error(f"检查证书有效期失败: {e}")
return -1
def renew_cert(domain: str):
"""使用 acme.sh 续期证书"""
logger.info(f"开始续期证书: {domain}")
try:
cmd = [
ACME_CLIENT, '--renew',
'-d', domain,
'--server', 'letsencrypt',
'--keylength', 'sm2',
'--output-cert', str(CERT_DIR / f'{domain}-cert.pem'),
'--output-key', str(CERT_DIR / f'{domain}-key.pem'),
'--reloadcmd', 'nginx -s reload'
]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
logger.info(f"续期成功: {domain}\n{result.stdout}")
return True
except subprocess.CalledProcessError as e:
logger.error(f"续期失败: {domain}\n{e.stderr}")
return False
def nginx_reload():
"""优雅重载 Nginx(不断开现有连接)"""
try:
# 先测试配置
subprocess.run(['nginx', '-t'], capture_output=True, check=True)
# 再 reload
subprocess.run(['nginx', '-s', 'reload'], capture_output=True, check=True)
logger.info("Nginx 已重新加载配置")
except subprocess.CalledProcessError as e:
logger.error(f"Nginx reload 失败: {e.stderr}")
def main():
logger.info("=" * 50)
logger.info("证书自动续期检查开始")
for domain in DOMAINS:
cert_path = str(CERT_DIR / f'{domain}-cert.pem')
if not os.path.exists(cert_path):
logger.warning(f"证书文件不存在: {cert_path}")
continue
remaining = check_cert_expiry(cert_path)
logger.info(f"{domain}: 剩余 {remaining} 天")
if remaining < DAYS_BEFORE_EXPIRE:
logger.info(f"{domain}: 证书即将到期,开始续期")
if renew_cert(domain):
nginx_reload()
else:
logger.info(f"{domain}: 证书有效期充足,无需续期")
logger.info("证书自动续期检查完成")
logger.info("=" * 50)
if __name__ == '__main__':
main()四、生产环境关键问题
4.1 双证书的正确使用
国密 TLS 的签名证书和加密证书有不同的用途:
- 签名证书:服务器在握手时用它证明自己的身份(替代 RSA/ECDSA 签名)
- 加密证书:用于密钥协商和数据传输加密
# 验证两张证书使用不同的公钥
from cryptography import x509
from cryptography.hazmat.primitives import serialization
def verify_dual_certs(sign_cert_path: str, enc_cert_path: str):
"""验证双证书使用不同的密钥对"""
with open(sign_cert_path, 'rb') as f:
sign_cert = x509.load_pem_x509_certificate(f.read())
with open(enc_cert_path, 'rb') as f:
enc_cert = x509.load_pem_x509_certificate(f.read())
sign_pub = sign_cert.public_key().public_bytes(
serialization.Encoding.X962,
serialization.PublicFormat.CompressedPoint
)
enc_pub = enc_cert.public_key().public_bytes(
serialization.Encoding.X962,
serialization.PublicFormat.CompressedPoint
)
if sign_pub == enc_pub:
raise ValueError("错误:签名证书和加密证书使用了相同的密钥对!")
print("✓ 双证书密钥对验证通过(不同密钥对)")4.2 浏览器兼容性
2026 年的浏览器国密支持情况:
| 浏览器 | SM2 支持 | SM3 支持 | SM4 支持 | 备注 |
|---|---|---|---|---|
| Chrome(Windows) | 通过操作系统 | 通过操作系统 | 通过操作系统 | 依赖 Windows 国密模块 |
| Edge(Windows) | 原生支持 | 原生支持 | 原生支持 | Windows 10+ |
| Firefox | 需安装 PKCS#11 模块 | 需配置 | 需配置 | 配置较复杂 |
| Safari | 不支持 | 不支持 | 不支持 | macOS 国密支持有限 |
# 智能密码套件选择
map $ssl_client_hello_suffix $ssl_suite {
default "ECDHE-RSA-AES256-GCM-SHA384:TLS_AES_256_GCM_SHA384";
"~SM2" "ECC-SM2-SM4-GCM-SM3:ECDHE-SM2-SM4-GCM-SM3";
}注意:$ssl_client_hello_suffix不是标准 Nginx 变量。实际实现中,应基于 ClientHello 扩展做判断,或使用 Tongsuo 提供的$ssl_client_ciphers变量。具体语法请参考对应版本的 Tongsuo 文档。
4.3 CDN 场景的解决方案
CDN 是国密改造中最难解决的问题。主流 CDN 服务商的边缘节点大多不支持国密密码套件。
方案 A:CDN 仅做流量转发,TLS 在网关终止
客户端 → CDN(TCP 转发)→ 国密网关(TLS 终止)→ 后端这种情况下,CDN 不参与 TLS 握手,只是做 TCP 层的负载均衡。国密 HTTPS 完全在网关层实现。
方案 B:双证书 + CDN 兼容
在 CDN 边缘节点使用国际证书(RSA/ECC),在源站网关使用国密证书。CDN 到源站之间使用国密 TLS。
客户端 → CDN(国际 TLS,RSA 证书)→ 源站网关(国密 TLS,SM2 证书)→ 后端五、从"证书管理"到"加密服务"的思维转变
5.1 两种范式的对比
| 维度 | 证书管理范式 | 加密服务范式 |
|---|---|---|
| 核心对象 | SSL/TLS 证书 | HTTPS 加密能力 |
| 管理目标 | 证书生命周期 | 加密基础设施 |
| 关注指标 | 证书到期时间、续期成功率 | HTTPS 可用性、握手成功率、延迟 |
| 运维主体 | 运维团队 + CLM 工具 | 加密网关 + 自动化平台 |
| 扩展性 | 每增加一个应用,重复一次证书部署 | 网关统一接入,后端零改造 |
| 密码演进 | 每个应用独立升级 | 网关层统一升级 |
5.2 什么时候该用哪种范式?
适合"证书管理"的场景:
- 应用数量少(< 10 个 TLS 端点)
- 所有应用都在可控的服务器上
- 服务器原生支持国密或有改造能力
- 运维团队有精力和技能管理证书
- 应用数量多(> 10 个 TLS 端点)
- 存在 CDN、WAF 等中间设备
- 部分应用部署在无法改造的环境中
- 需要统一管控密码算法的升级(如未来迁移到 PQC)
- 合规要求"统一入口、统一审计"
5.3 混合方案:渐进式改造
大多数企业的实际情况介于两者之间。推荐采用混合方案:
- 新系统:直接接入加密服务网关,零改造获得国密 HTTPS
- 可改造的旧系统:逐步部署国密支持,最终也接入网关
- 不可改造的系统:通过网关反向代理,网关做国密终止,后端保持 HTTP
# 网关路由配置示例(伪代码)
# /etc/nginx/conf.d/routes.conf
ROUTES = {
# 新系统:直接 HTTPS
"app1.example.com": {"scheme": "https", "backend": "http://internal-1:8080"},
# 可改造的旧系统:网关代理
"app2.example.com": {"scheme": "https", "backend": "http://internal-2:8080"},
# 不可改造的系统:网关做 TLS 终止
"legacy.example.com": {"scheme": "https", "backend": "http://legacy:80"},
# 需要国际兼容的入口:双套件
"public.example.com": {"scheme": "both", "backend": "http://public:8080"},
}六、总结
2026 年 7 月 1 日《电子认证服务使用密码管理办法》施行后,CA 许可制度将进一步规范化,证书的获取和管理将更加有序。但国密改造的真正瓶颈已经不在证书侧——而在整个加密生态的支持。
传统 CLM 的"申请-部署-监控"模式在国密场景下存在根本性局限,因为它假设了一个已经完备的加密生态。中国的国密生态还在建设中,这意味着我们需要一种不同的思路。
"加密服务网关"架构的核心洞察是:加密能力应该像电力一样集中供应,而不是每台机器自备发电机。 把国密支持集中到网关层,让后端应用专注于业务逻辑,这是国密改造规模化落地的可行路径。
关键要点回顾:
- 国密双证书必须使用不同的密钥对,签名证书和加密证书不可混用
- 加密服务网关把国密支持从应用层剥离到基础设施层,降低改造成本
- 浏览器兼容需要国密+国际双套件并行,通过 SNI 或 ALPN 智能选择
- CDN 场景需要在网关层终止国密 TLS,CDN 仅做 TCP 转发
- 证书续期仍然是基础能力,但只是加密服务的一个组件,而非全部