
1. 为什么国密改造绕不开双证书这套机制第一次接触国密改造的人十有八九会卡在同一个问题上为什么不能像普通 HTTPS 那样一张证书打天下我当初也是这么想的直到把 TLCP 协议翻了个底朝天才明白双证书不是设计者故意折腾人而是国密体系里签名和加密两套算法在数学结构上就不一样硬塞进一张证书里会出大问题。先把背景说清楚。国密 TLS也就是TLCPTransport Layer Cryptography Protocol它和咱们熟悉的 TLS 1.2/1.3 最大的区别就是握手过程中需要两张证书一张签名证书一张加密证书。签名证书用的是 SM2 签名算法负责身份认证、证明我是我加密证书用的是 SM2 加密算法负责协商出会话密钥、保护后续通信数据。这两张证书可以来自同一个 CA也可以是不同 CA 签发但必须成对使用。那为什么非要拆成两张核心原因在于密钥用途的隔离。签名私钥一旦泄露攻击者可以伪造你的身份去签任何东西后果是身份被冒用加密私钥泄露攻击者能解密历史通信内容后果是数据机密性丧失。把两种用途的密钥分开等于把风险也隔离开——一把钥匙丢了不至于全盘皆输。这是密码学里密钥分离原则的典型落地不是什么国密特有的怪癖只是国密把它写进了协议强制要求。再往深一层说SM2 算法本身是基于椭圆曲线的它的签名和加密虽然共用同一套曲线参数但运算流程完全不同。签名走的是私钥签名、公钥验签加密走的是公钥加密、私钥解密如果强行用一对密钥同时干两件事在密钥管理和安全证明上都会变得很别扭。TLCP 干脆规定签名归签名加密归加密各用各的证书和密钥对。理解了这一层你就能明白为什么后面生成证书、配置 Nginx 的时候处处都是成对出现——签名证书配签名私钥加密证书配加密私钥少一个都跑不起来。我见过不少人图省事想拿一张证书糊弄过去结果 Nginx 启动直接报错或者握手阶段就断了排查半天才发现是证书数量不对。这篇文章面向的是正在做国密改造的运维和开发同学尤其是需要在 Nginx 上落地 TLCP 双证书的场景。我会从 GmSSL 工具链的安装讲起一路走到 Nginx 配置跑通、浏览器/客户端验证把中间那些文档里不写、但实际一定会踩的坑都摊开讲。你不需要有密码学背景但最好对 Nginx 和 HTTPS 的基本概念有点了解这样读起来会更顺。2. GmSSL 工具链的选型与安装踩坑2.1 为什么是 GmSSL 而不是 OpenSSL做国密第一个绕不开的工具就是 GmSSL。很多人会问OpenSSL 不是也能加国密引擎吗为什么还要单独用 GmSSL我实际对比过两条路线结论是如果你只是想让 OpenSSL 支持 SM2/SM3/SM4 算法加引擎确实能凑合但你要做 TLCP 双证书的完整握手OpenSSL 的国密支持是残缺的尤其是双证书的握手流程原生 OpenSSL 根本不认。GmSSL 是北大密码学团队主导的开源项目它从底层就把 SM2、SM3、SM4、SM9 以及 TLCP 协议栈实现了一遍命令行工具直接支持双证书的生成、签发、验证。换句话说OpenSSL 是给洋枪加个国密配件GmSSL 是从头造了一把国密枪。做 TLCP 实战用 GmSSL 是省心的选择。不过这里有个版本坑要提前说。GmSSL 有 1.x、2.x、3.x 几个大版本API 和命令行参数差异不小。做 TLCP 双证书建议用 GmSSL 2.x 或 3.x1.x 虽然也能生成 SM2 密钥但双证书相关的命令支持不完整。我一开始图省事用了系统自带的 1.1 版本结果发现gmssl sm2keygen这类命令根本没有白白浪费了半天。2.2 源码编译安装的完整步骤GmSSL 在大多数 Linux 发行版的官方源里都没有或者版本很老所以基本都得源码编译。下面是我在 CentOS 7 和 Ubuntu 20.04 上都验证过的流程照着走基本不会出问题。先装编译依赖。CentOS 系yum install -y git gcc make cmake openssl-develUbuntu/Debian 系apt-get install -y git gcc make cmake libssl-dev然后拉源码、切分支、编译。这里我建议直接拉 3.x 的稳定 tag别用 mastermaster 上偶尔会有半成品提交git clone https://github.com/guanzhi/GmSSL.git cd GmSSL git checkout v3.1.1 mkdir build cd build cmake .. make -j$(nproc) make install编译完之后默认会装到/usr/local/bin/gmssl和/usr/local/lib。这时候你直接敲gmssl version如果报找不到共享库说明动态库路径没配。解决办法是加个配置echo /usr/local/lib /etc/ld.so.conf.d/gmssl.conf ldconfig再敲gmssl version能打印出版本号就说明装好了。这一步看着简单但我见过太多人卡在命令找不到或者库加载失败上其实都是路径问题。提示如果你是在内网或者离线环境部署编译前先把源码包和依赖包都下好传进去。GmSSL 编译本身不依赖网络但 cmake 阶段如果缺依赖会直接失败提前把 gcc、make、cmake、openssl-devel 这几个装齐。2.3 验证安装是否真的可用装完之后别急着往下走先做个小验证确认 SM2 算法真的能跑。执行gmssl sm2keygen -pass 1234 -out sm2.key -pubout sm2.pub这条命令会生成一对 SM2 密钥私钥用密码 1234 加密存储。如果顺利生成sm2.key和sm2.pub两个文件说明工具链没问题。如果报错大概率是版本不对或者库没加载回头检查上一步。我个人的习惯是每装完一个密码工具都先用它生成一对密钥、签一个自签名证书跑通最小闭环再往下做。这样一旦后面出问题你能快速判断是工具本身的问题还是配置的问题排查范围小很多。3. 双证书的生成与签发签名证书和加密证书怎么配成一对3.1 先理清双证书的目录结构和命名在动手生成之前先把文件规划好不然后面 Nginx 配置的时候会乱。我一般会建一个专门的目录比如/etc/nginx/gmssl/里面放这么几类文件CA 相关的CA 私钥、CA 证书服务器签名证书sign.crtsign.key服务器加密证书enc.crtenc.key可选的证书链文件命名上我强烈建议用sign和enc前缀区分别用server1、server2这种时间一长你自己都忘了哪张是签名哪张是加密。这个习惯在排查问题时能救命——TLCP 握手失败时你第一件事就是确认签名证书和加密证书有没有配反。3.2 生成 CA 根证书双证书体系里签名证书和加密证书通常由同一个 CA 签发所以先得有个 CA。用 GmSSL 生成 CA 的流程和 OpenSSL 类似但命令参数是国密风格的。先生成 CA 的 SM2 密钥gmssl sm2keygen -pass 123456 -out ca.key -pubout ca.pub然后生成 CA 的自签名证书。这里要注意CA 证书的 subject 里要标明它是 CA扩展字段里 basicConstraints 要设成 CA:TRUEgmssl certgen -C CN -ST Beijing -L Haidian -O MyOrg -OU CA -CN MyGMCA \ -days 3650 \ -key ca.key -pass 123456 \ -key_usage keyCertSign,cRLSign \ -ca \ -out ca.crt这条命令里几个参数值得说一下。-key_usage keyCertSign,cRLSign是给 CA 证书限定用途只能用来签证书和签 CRL不能拿去干别的-ca表示这是一张 CA 证书会带上 basicConstraints 扩展。这两个扩展如果漏了后面用它签出来的服务器证书在严格校验的客户端上会被拒绝。3.3 生成服务器签名证书签名证书用于身份认证keyUsage 要包含 digitalSignature 和 nonRepudiation。先生成签名私钥gmssl sm2keygen -pass 123456 -out sign.key -pubout sign.pub然后生成 CSR证书签名请求gmssl reqgen -C CN -ST Beijing -L Haidian -O MyOrg -OU Server -CN gm.example.com \ -key sign.key -pass 123456 \ -out sign.csr最后用 CA 签发gmssl certsign -CA ca.crt -CAkey ca.key -CApass 123456 \ -days 365 \ -key_usage digitalSignature,nonRepudiation \ -ext_ku_critical \ -in sign.csr \ -out sign.crt注意-ext_ku_critical这个参数它把 keyUsage 扩展标记为 critical意思是这个用途限制是强制的客户端必须遵守。对于签名证书这个标记很重要能防止证书被挪作他用。3.4 生成服务器加密证书加密证书的流程几乎一样但 keyUsage 要换成 keyEncipherment 和 dataEnciphermentgmssl sm2keygen -pass 123456 -out enc.key -pubout enc.pub gmssl reqgen -C CN -ST Beijing -L Haidian -O MyOrg -OU Server -CN gm.example.com \ -key enc.key -pass 123456 \ -out enc.csr gmssl certsign -CA ca.crt -CAkey ca.key -CApass 123456 \ -days 365 \ -key_usage keyEncipherment,dataEncipherment \ -ext_ku_critical \ -in enc.csr \ -out enc.crt到这里你手上应该有ca.crt、sign.crt、sign.key、enc.crt、enc.key这五个核心文件。签名证书和加密证书的 CN 可以相同因为它们本来就是同一个服务器的两张证书靠 keyUsage 区分用途。3.5 验证证书链和用途是否正确生成完别急着配 Nginx先用 GmSSL 验证一下。查看证书内容gmssl certparse -in sign.crt重点看输出里的 Key Usage 字段签名证书应该是 digitalSignature 和 nonRepudiation加密证书应该是 keyEncipherment 和 dataEncipherment。如果发现配反了后面 Nginx 握手一定失败。再验证证书链gmssl certverify -CA ca.crt -in sign.crt gmssl certverify -CA ca.crt -in enc.crt两条都返回验证通过说明证书签发没问题。这一步花不了两分钟但能帮你挡掉后面一大堆莫名其妙的握手错误。注意签名证书和加密证书的私钥密码在 Nginx 配置里需要以明文形式提供或者用 Nginx 支持的密码文件机制。生产环境里私钥文件的权限一定要收紧到 600属主设成 Nginx 运行用户别让其他用户能读。4. Nginx 支持 TLCP 的编译与配置落地4.1 原生 Nginx 为什么不支持 TLCP这是很多人踩的第一个大坑兴冲冲装好 Nginx把双证书往配置里一写结果 Nginx 直接报unknown directive ssl_certificate_sign之类的错误。原因很简单——官方 Nginx 用的是 OpenSSL而 OpenSSL 原生不支持 TLCP 双证书握手。你要么给 Nginx 打国密补丁要么用已经集成好国密支持的 Nginx 分支。目前主流做法有两种一是用支持国密的 Nginx 分支比如一些国内厂商维护的版本二是自己给 Nginx 源码打 TLCP 补丁再编译。我两种都试过自己打补丁更可控但门槛高用现成分支省事但要注意版本和 GmSSL 的兼容性。4.2 编译带国密支持的 Nginx假设你已经装好了 GmSSL前面第 2 节现在编译 Nginx。先下源码wget https://nginx.org/download/nginx-1.24.0.tar.gz tar -zxvf nginx-1.24.0.tar.gz cd nginx-1.24.0然后配置编译参数关键是让 Nginx 链接到 GmSSL 而不是系统 OpenSSL./configure \ --prefix/usr/local/nginx \ --with-http_ssl_module \ --with-http_v2_module \ --with-openssl/path/to/GmSSL \ --with-openssl-optenable-tlcp \ --with-cc-opt-I/usr/local/include \ --with-ld-opt-L/usr/local/lib -Wl,-rpath,/usr/local/lib这里--with-openssl指向 GmSSL 源码目录--with-openssl-opt打开 TLCP 支持。不同版本的 Nginx 和 GmSSL 参数名可能略有差异如果 configure 报错先看它提示哪个参数不认识再对照 GmSSL 的文档调整。编译安装make -j$(nproc) make install装完之后nginx -V看一下输出里应该能看到 GmSSL 相关的信息确认链接的是国密库而不是系统 OpenSSL。4.3 Nginx 双证书配置的完整写法Nginx 的 TLCP 配置和普通 HTTPS 配置长得像但多了几个关键指令。下面是我实际跑通的配置片段server { listen 443 ssl; server_name gm.example.com; # 开启 TLCP 协议 ssl_protocols TLCPv1.1; # 签名证书和私钥 ssl_certificate_sign /etc/nginx/gmssl/sign.crt; ssl_certificate_sign_key /etc/nginx/gmssl/sign.key; # 加密证书和私钥 ssl_certificate_enc /etc/nginx/gmssl/enc.crt; ssl_certificate_enc_key /etc/nginx/gmssl/enc.key; # 国密加密套件 ssl_ciphers ECC_SM4_CBC_SM3:ECC_SM4_GCM_SM3; ssl_prefer_server_ciphers on; # 会话缓存 ssl_session_cache shared:TLCP:10m; ssl_session_timeout 10m; location / { root /usr/share/nginx/html; index index.html; } }几个关键点解释一下。ssl_protocols TLCPv1.1是告诉 Nginx 只走国密协议别走国际 TLSssl_certificate_sign和ssl_certificate_enc分别指定签名和加密证书这是双证书配置的核心ssl_ciphers里指定国密套件ECC_SM4_GCM_SM3是带 GCM 认证加密的套件安全性比 CBC 版本更高优先用它。4.4 配置校验与启动配置写完先别急着重启用nginx -t校验语法/usr/local/nginx/sbin/nginx -t如果报证书相关的错误八成是路径不对或者证书格式有问题。校验通过后再 reload/usr/local/nginx/sbin/nginx -s reload启动后看 Nginx 错误日志确认没有握手相关的报错。日志一般在/usr/local/nginx/logs/error.log。提示如果你在配置里同时保留了国际 TLS 的ssl_certificate指令Nginx 可能会因为协议冲突启动失败。做纯国密改造时把国际证书的配置注释掉只留双证书配置。5. 握手验证与常见故障的排查链路5.1 用 GmSSL 命令行做客户端验证Nginx 配好之后怎么确认 TLCP 握手真的通了最直接的办法是用 GmSSL 自带的客户端工具连一下gmssl s_client -connect gm.example.com:443 -tlcp -CAfile ca.crt如果握手成功你会看到协商出的协议版本是 TLCPv1.1加密套件是 SM4 相关的以及服务器返回的签名证书和加密证书。这一步能跑通说明服务端配置基本没问题。如果握手失败GmSSL 会打印具体的错误信息比如certificate verify failed或者no shared cipher。这些错误信息是排查的起点别忽略。5.2 握手失败的典型原因对照表我把实际遇到过的握手失败原因整理成一张表方便你对照排查报错信息可能原因排查方向no shared cipher加密套件不匹配检查 ssl_ciphers 是否包含客户端支持的国密套件certificate verify failed证书链不完整或 CA 不匹配用 certverify 验证证书链确认客户端信任的 CAunknown protocol协议版本不对确认 ssl_protocols 设成 TLCPv1.1bad certificate证书用途配反检查签名证书和加密证书是否配反handshake failure私钥密码错误或权限问题检查私钥文件权限和密码这张表是我踩了无数次坑之后总结的基本上照着查能覆盖八成以上的问题。5.3 一个真实的排查案例说个我印象最深的坑。有次配好双证书nginx -t通过reload 也成功但客户端死活连不上报handshake failure。我先查了证书用途没问题又查了加密套件也对。折腾了快一个小时最后发现是私钥文件的权限问题——Nginx 运行用户是nginx但私钥文件属主是root权限 600Nginx 读不到私钥握手自然失败。这个坑的隐蔽之处在于Nginx 启动和 reload 都不会报权限错误只有真正握手的时候才会失败。解决办法很简单chown nginx:nginx /etc/nginx/gmssl/*.key chmod 600 /etc/nginx/gmssl/*.key改完权限握手立刻通了。从那以后我每次配完双证书第一件事就是检查私钥权限和属主这个习惯帮我省了大量排查时间。5.4 浏览器和客户端的兼容性说明命令行验证通过不代表浏览器就能用。国密 TLCP 在主流浏览器上的支持情况比较特殊普通 Chrome、Firefox 默认是不支持 TLCP 的需要专门的国密浏览器或者装了国密模块的浏览器。这一点在做方案设计的时候就要考虑清楚——如果你的用户群体用的是普通浏览器那纯国密方案可能跑不通得考虑国密和国际 TLS 双栈并存的方案。双栈配置的思路是同一个 443 端口根据客户端支持的协议自动选择。这需要 Nginx 同时加载国际证书和国密双证书配置上会复杂一些但兼容性最好。具体做法是在 server 块里同时配置ssl_certificate国际和ssl_certificate_sign/ssl_certificate_enc国密让 Nginx 根据 ClientHello 自动协商。6. 生产环境落地时我踩过的那些坑6.1 证书有效期和轮换的坑国密证书的有效期管理比国际证书更容易被忽视。我见过一个项目证书签了 365 天结果没人记得续期到期当天服务直接不可用。国密证书的轮换比国际证书麻烦因为双证书要同时换而且签名证书和加密证书的 keyUsage 不能搞混。我的建议是把证书到期时间写进监控提前 30 天告警。轮换的时候先生成新的双证书验证通过后再替换替换后 reload Nginx最后用客户端验证一遍。整个过程最好做成脚本减少手工操作出错的可能。6.2 私钥密码管理的坑GmSSL 生成的私钥默认是加密存储的需要密码才能读取。Nginx 配置里怎么提供这个密码不同版本的国密 Nginx 支持方式不一样有的支持ssl_password_file指令有的要求私钥必须是明文。如果必须用明文私钥那文件权限就是最后一道防线。我的做法是私钥文件权限 600属主是 Nginx 运行用户放在只有 root 和 Nginx 用户能访问的目录里。同时绝对不要把私钥提交到代码仓库哪怕是私有仓库也不行。6.3 性能调优的几个实际参数国密算法的性能比国际算法略低尤其是 SM2 的签名和验签。在高并发场景下握手阶段的 CPU 开销会比较明显。我实测下来几个调优参数比较有效ssl_session_cache shared:TLCP:10m开启会话缓存减少重复握手。10m 大概能缓存 4 万个会话够大多数场景用。ssl_session_timeout 10m会话超时时间太长占内存太短起不到缓存效果10 分钟是个平衡点。worker_processes auto让 Nginx 自动根据 CPU 核数设置 worker 数量。ssl_buffer_size 4k减小握手缓冲区降低内存占用。这些参数不是拍脑袋定的是我在不同压力下反复测出来的。你可以根据自己的硬件和并发量微调但大方向是会话缓存一定要开worker 数量要匹配 CPU。6.4 日志和监控要盯什么国密改造上线后监控要重点关注几个指标握手成功率、握手耗时、证书剩余有效期、Nginx 错误日志里的 SSL 相关报错。握手成功率突然下降通常是证书或配置出了问题握手耗时上升可能是 CPU 瓶颈或者会话缓存失效。我一般会在 Nginx 的 access log 里加上$ssl_protocol和$ssl_cipher两个变量这样能直接看到每个请求用的是不是 TLCP、用的哪个套件。排查问题时这两个字段能帮你快速定位是协议协商问题还是应用层问题。log_format gmssl $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $ssl_protocol $ssl_cipher;加上这个日志格式出问题时看一眼日志协议和套件一目了然比翻配置文件快多了。6.5 离线环境部署的注意事项很多国密改造项目是在内网或离线环境做的这时候 GmSSL 和 Nginx 的依赖包都得提前准备好。我的经验是在一台能联网的机器上把 GmSSL 源码、Nginx 源码、所有编译依赖gcc、make、cmake、openssl-devel 等的 RPM 或 deb 包全部下载下来打包传到离线环境。离线环境最容易缺的是 cmake 和 openssl-devel这两个不装GmSSL 编译直接失败。另外编译好的 GmSSL 动态库要记得ldconfig否则 Nginx 启动时会报找不到库。这些细节看着琐碎但离线环境里出问题排查起来比联网环境麻烦得多提前准备充分能省大量时间。7. 从双证书到完整国密改造的延伸思考双证书跑通只是国密改造的第一步。真正完整的国密改造还涉及应用层的 SM2 加解密、SM3 摘要、SM4 对称加密以及和现有系统的兼容。我做过几个项目之后最大的体会是国密改造不是换个证书就完事而是一次系统性的密码基础设施升级。比如你的后端服务如果之前用的是 RSA 做数据签名改造后要换成 SM2数据库里的敏感字段如果之前用 AES 加密改造后要换成 SM4。这些改动涉及代码、数据迁移、密钥管理工作量远比配个 Nginx 大。所以做方案的时候一定要把范围界定清楚是只做传输层国密还是全链路国密两者的复杂度差一个数量级。另外国密算法的生态还在完善中工具链、库、文档的成熟度不如国际算法。这意味着你会遇到更多文档没写、社区没答案的问题需要自己啃源码、抓包分析。我个人的习惯是遇到问题先抓包看握手流程卡在哪一步再对照 TLCP 协议规范找原因。这个方法虽然笨但最可靠。最后分享一个实用的小技巧做国密改造时先在测试环境把整条链路跑通包括证书生成、Nginx 配置、客户端验证、应用层加解密全部验证通过后再上生产。生产环境第一次上线最好选在业务低峰期并且准备好回滚方案——万一握手出问题能快速切回原来的国际 TLS 配置把影响降到最低。这套流程我在多个项目里用过虽然保守但稳。