
最近在做信创环境适配和密评改造项目时被问得最多的一个问题就是Nginx到底能不能把国密HTTPS跑起来答案是可以但有一个前提——你得把Nginx底下的SSL库从OpenSSL换成TongSuo并且按照SM2双证书的体系来配置证书。这篇文章是我近期从零编译TongSuo、生成SM2签名/加密双证书、再接入Nginx的完整记录包含每一步的命令、参数含义和踩坑记录适合正在做国密改造、等保密评适配或者信创项目交付的同学参考。国密HTTPS并不是单独换一个证书那么简单它涉及算法、协议、证书体系的整体切换。如果只停留在“装个国密版OpenSSL”的层面后面会遇到一堆兼容性和双证书配置问题。所以我会先讲清楚SM2双证书和TLCP协议到底是怎么回事再落到具体的编译和部署操作这样遇到问题的时候你也能自己排查而不是只会抄配置。1. 国密HTTPS到底在解决什么问题1.1 SM2/SM3/SM4与TLCP双证书体系传统HTTPS用的是RSA/ECDSA做身份认证和密钥协商用AES做数据加密用SHA系列做摘要。国密HTTPS换成了另一套算法组合SM2是非对称算法负责签名和密钥交换SM3是密码杂凑算法负责摘要SM4是对称分组算法负责数据加密。算法替换只是表层真正麻烦的是证书体系。国密SSL协议TLCP参考GMT 0024-2014规定了一套双证书机制每个站点需要同时具备签名证书和加密证书两个证书对应两对独立的SM2密钥。签名证书的作用类似身份证用来验证服务器身份客户端用它确认“我连的确实是这个站点”。加密证书的作用类似保险柜钥匙用于握手阶段协商出后续数据加密的会话密钥。两个证书虽然都是SM2算法但私钥不同、用途不同不能互相替代。为什么非要做成双证书而不是像国际TLS那样一个证书搞定核心原因是合规要求签名密钥用于身份认证加密密钥用于数据保护两者分离之后即使加密私钥泄露攻击者也无法伪造服务器身份安全边界更清晰。这在实际密评中是一个硬性指标所以双证书不是可选功能而是国密HTTPS的基本形态。1.2 为什么默认Nginx和OpenSSL搞不定国密很多人一开始以为给Nginx装上证书就行结果折腾半天发现根本跑不起来。原因有两个层面。第一OpenSSL主线版本长期没有默认启用国密算法套件。虽然OpenSSL 1.1.1开始提供了SM2/SM3/SM4的基础算法接口但默认配置下并不包含国密TLCP协议所需的密钥交换套件比如ECC-SM2-SM4-CBC-SM3、ECC-SM2-SM4-GCM-SM3这些套件在OpenSSL里没有完整实现。第二Nginx原版的SSL模块是为标准TLS设计的。它只认识ssl_certificate和ssl_certificate_key这两个指令没有双证书体系里的加密证书概念也没有根据客户端能力自动选择国密套件的能力。哪怕你强行在配置文件里写上国密cipherNginx也会在握手阶段直接报错。要打破这个局面常用的做法是引入TongSuo铜锁。TongSuo是一个基于OpenSSL维护的国密增强分支可以把它理解成“带国密能力的OpenSSL”它在不改变OpenSSL基本接口的前提下补全了国密TLCP协议、SM2双证书、国密套件等能力。配合打了补丁的Nginx增强分支就能让Nginx正常完成国密握手和双证书加载。2. 环境准备TongSuo源码编译2.1 基础依赖与工具链准备我这次是在一套CentOS 7.9环境上做的编译CPU是x86_64后续又在一台ARM64的机器上重复了一遍流程结论是步骤基本一致只要把依赖包装对就行。如果你用的是openEuler、统信UOS或者银河麒麟命令的包管理部分需要稍微调整但整体思路不变。编译TongSuo之前先把基础编译工具和依赖包装上yum install -y gcc gcc-c make perl pcre-devel zlib-devel如果你是Ubuntu/Debian系对应的是apt install -y build-essential libpcre3-dev zlib1g-dev perl这里有个容易忽略的点Perl是编译OpenSSL系项目时必需的因为OpenSSL的Configure脚本依赖Perl来生成Makefile和头文件。缺Perl的话后面执行./config会直接报Cant locate strict.pm之类的错误排查起来还挺误导人的。如果你的环境是内网隔离的离线环境就需要提前把gcc、make、perl、pcre-devel、zlib-devel这些rpm包和TongSuo源码包一起拷进去用yum localinstall或者rpm -ivh逐个安装。离线编译最忌讳缺依赖我建议先在一台联网机器上把依赖关系用yum deplist捋清楚否则现场装到一半发现缺包会非常被动。2.2 编译安装TongSuoTongSuo的源码在GitHub上有官方仓库搜索Tongsuo-Project/Tongsuo就能找到建议直接下载最新的release源码包而不是拉master分支因为release版本经过测试稳定性有保证。下载解压后进入源码目录执行配置wget https://github.com/Tongsuo-Project/Tongsuo/archive/refs/tags/8.4.0.tar.gz tar zxf 8.4.0.tar.gz cd Tongsuo-8.4.0 ./config --prefix/usr/local/tongsuo --enable-ntls make -j8 make install重点看--enable-ntls这个参数。ntls在TongSuo项目里代表国密安全传输层协议支持也就是TLCP。如果不加这个参数编译出来的库跟普通OpenSSL没什么区别后续Nginx加载国密套件一样会失败。编译耗时取决于机器性能一般几分钟到十几分钟不等。装完之后检查一下产物ls /usr/local/tongsuo/bin/正常情况下会看到openssl这个可执行文件以及lib目录下的libcrypto.so和libssl.so。可以用下面这一行确认国密支持是否真的编进去了/usr/local/tongsuo/bin/openssl ciphers -s | grep SM2如果输出里有ECC-SM2-SM4-CBC-SM3、ECC-SM2-SM4-GCM-SM3这类套件名就说明国密支持已经编译进去。这一步是我每次编译完必做的验证因为有时候--enable-ntls拼写错了或者被忽略编译不报错但功能没生效后面排查握手失败会很痛苦。2.3 编译带国密能力的NginxTongSuo本身只是个SSL库要让Nginx用上它得重新编译Nginx并在编译时把TongSuo以OpenSSL替代品的方式传给Nginx的构建系统。下载Nginx源码后执行wget https://nginx.org/download/nginx-1.24.0.tar.gz tar zxf nginx-1.24.0.tar.gz cd nginx-1.24.0 ./configure \ --prefix/usr/local/nginx \ --with-http_ssl_module \ --with-http_v2_module \ --with-openssl/usr/local/tongsuo \ --with-openssl-optenable-ntls \ --with-pcre \ --with-zlib \ --with-stream \ --with-stream_ssl_module make -j8 make install解释几个关键参数。--with-openssl/usr/local/tongsuo是让Nginx在编译时直接使用TongSuo源码目录而不是系统自带的OpenSSL。这里要注意Nginx的构建系统对OpenSSL系库采用的是“源码集成编译”模式也就是指定路径后Nginx会自己重新编译一份OpenSSL/TongSuo进去所以这个路径最好是TongSuo的源码目录而非安装目录。--with-openssl-optenable-ntls会把enable-ntls透传给TongSuo的Configure脚本确保Nginx内部集成的TongSuo带回国密支持。这一步漏掉的话Nginx虽然能编过但运行时不认识国密cipher。最终编译出来的Nginx是静态链接TongSuo的这种方式的好处是运行时不需要额外设置LD_LIBRARY_PATH也不会跟系统里其他OpenSSL库打架。我强烈建议生产环境用这种静态编译方式不要图省事去动态替换系统的libssl.so那样很容易把sshd、yum这些系统工具搞挂。编译完成后验证一下/usr/local/nginx/sbin/nginx -V输出里应该能看到--with-openssl/usr/local/tongsuo和--with-openssl-optenable-ntls这两行并且能看到built with OpenSSL相关的版本信息。如果你在配置里需要用到国密双证书相关指令还可以编译Nginx的TongSuo增强分支或自行打补丁这一步在后续配置章节会讲到先用标准Nginx源码搭配TongSuo也能满足大部分需求。3. SM2双证书的生成与申请3.1 生成SM2签名证书私钥与CSR双证书体系里签名证书和加密证书是两套独立的SM2密钥对所以第一步是分别生成两个私钥。签名证书的私钥和CSR生成命令如下cd /etc/nginx/certs # 生成SM2签名私钥 /usr/local/tongsuo/bin/openssl genpkey \ -algorithm EC \ -pkeyopt ec_paramgen_curve:SM2 \ -out sign.key # 生成签名证书CSR /usr/local/tongsuo/bin/openssl req \ -new \ -sm2 \ -key sign.key \ -subj /CNexample.com \ -addext subjectAltNameDNS:example.com,DNS:www.example.com \ -out sign.csr这里有一个非常关键的细节-sm2参数必须加上。它会在CSR里标记该密钥对使用SM2算法同时影响后续证书签名时使用的摘要算法。如果不加生成的CSR可能在算法标识上不完整CA那边会直接拒收。另一个细节是subjectAltName扩展。国密证书和普通证书一样如果域名不在SAN里Chrome等浏览器会报证书名称不匹配。虽然国密浏览器对SAN的校验略宽松但建议从生成CSR这一步就把SAN配好避免后面返工。私钥文件生成之后立刻执行chmod 600 sign.key把权限收紧。SM2私钥一旦泄露等于整个国密身份体系被攻破这点和RSA私钥是同等重要的。3.2 生成加密证书与解密私钥加密证书需要一对独立的SM2密钥但它的CSR有一个特殊要求加密证书的CSR要用签名证书的私钥来签名而不是用加密证书自己的私钥签名。这个设计是为了在申请阶段让CA确认签名证书和加密证书的申请主体是同一个实体。步骤拆解如下# 生成SM2加密私钥 /usr/local/tongsuo/bin/openssl genpkey \ -algorithm EC \ -pkeyopt ec_paramgen_curve:SM2 \ -out enc.key # 生成加密证书CSR注意-key用的是sign.key /usr/local/tongsuo/bin/openssl req \ -new \ -sm2 \ -key sign.key \ -subj /CNexample.com \ -addext subjectAltNameDNS:example.com \ -out enc.csr注意第二段命令里-key参数填的是sign.key也就是签名私钥而不是enc.key。很多第一次做双证书的人在这里会搞反直接导致CA提示证书主体不一致或者签发出来的加密证书无法和签名证书配对使用。如果只是本地测试不需要走外部CA可以直接用签名私钥自签这两张证书/usr/local/tongsuo/bin/openssl x509 \ -req \ -days 365 \ -in sign.csr \ -signkey sign.key \ -out sign.crt \ -sm2 /usr/local/tongsuo/bin/openssl x509 \ -req \ -days 365 \ -in enc.csr \ -signkey sign.key \ -out enc.crt \ -sm2自签证书只能用来验证配置流程不能用于真实验收。生产环境的做法一般是把sign.csr和enc.csr提交给支持国密算法的CA机构由CA完成签名和加密证书的签发最后会返回两个crt文件以及配套的私钥。整个流程里签名私钥始终留在你手里绝对不能发给任何人包括CA。3.3 证书链拼接与格式检查很多国密证书不是直接由根CA签发的中间会有一层甚至多层中间CA。Nginx加载证书时需要把服务器证书和中间证书拼在一起顺序是服务器证书在前中间证书在后根证书一般不放在证书链里。拼接命令cat sign.crt intermediate.crt sign_chain.crt cat enc.crt intermediate.crt enc_chain.crt拼接完成后用openssl检查证书链是否正常/usr/local/tongsuo/bin/openssl crl2pkcs7 -nocrl -certfile sign_chain.crt | \ /usr/local/tongsuo/bin/openssl pkcs7 -print_certs -noout如果输出里能依次看到服务器证书和中间证书的subject信息说明拼接OK。常见的错误是把根证书也拼进去了这样会导致证书链过长部分客户端校验失败。用openssl x509 -text看一眼证书里的算法标识确认Signature Algorithm是SM3WithSM2Public Key Algorithm是SM2 Public Key这就对了。如果显示的是RSA或者ecdsa-with-SHA256说明证书根本不是国密证书后面配置了也是白搭。4. Nginx国密HTTPS配置实战4.1 server块最小配置与双证书指令拿到签名证书、加密证书和私钥之后就可以写Nginx配置了。这里我以TongSuo增强分支的Nginx为例标准Nginx源码配合TongSuo库时如果编译的模块支持双证书指令配置方式是一样的。一个最小可用的server块如下server { listen 443 ssl; server_name example.com; ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; ssl_ciphers ECC-SM2-SM4-CBC-SM3:ECC-SM2-SM4-GCM-SM3:\ ECDHE-SM2-SM4-CBC-SM3:ECDHE-SM2-SM4-GCM-SM3; ssl_certificate /etc/nginx/certs/sign_chain.crt; ssl_certificate_key /etc/nginx/certs/sign.key; ssl_enc_certificate /etc/nginx/certs/enc_chain.crt; ssl_enc_certificate_key /etc/nginx/certs/enc.key; location / { root /var/www/html; index index.html; } }这段配置里有几个点需要重点理解。ssl_certificate和ssl_certificate_key在国密模式下加载的是签名证书和签名私钥。ssl_enc_certificate和ssl_enc_certificate_key是TongSuo增强模块新加的指令专门用于加载加密证书和加密私钥。如果你的Nginx加载配置文件时报unknown directive ssl_enc_certificate说明这个Nginx不是国密增强版或者编译时没有把TongSuo的国密模块带进去需要回到第二章重新编译。ssl_ciphers里列的是国密握手会用到的套件核心是ECC-SM2-SM4-CBC-SM3和ECC-SM2-SM4-GCM-SM3这两个套件对应国密TLCP协议的标准加密套件。用户端如果发来国密cipherNginx会从中挑选一个双方都支持的套件完成握手。注意不要在这里同时写一堆RSA套件那会干扰国密套件的协商优先级。ssl_protocols我写的是TLSv1到TLSv1.3全开这是为了兼容不同年代的国密浏览器。早期国密浏览器基于TLSv1.0/1.1实现只支持这两个版本新版浏览器则支持TLSv1.2甚至TLSv1.3。如果你直接照抄常规配置只开TLSv1.2和TLSv1.3部分老的国密客户端会直接握手失败。当然从安全角度讲TLSv1.0和TLSv1.1确实弱一些但国密改造项目里兼容性优先安全基线一般会单独评估。4.2 标准HTTPS与国密自动协商降级实际生产环境里有个现实问题用户的浏览器并不都是国密浏览器。Chrome、Firefox、Edge这些国际浏览器默认不支持SM2/SM4套件如果站点只配置国密证书普通用户访问就会失败。解决思路是在同一台Nginx上同时配置两套证书一套是国际标准RSA/ECDSA证书一套是国密SM2双证书。TongSuo增强版Nginx会根据客户端握手中携带的cipher列表自动选择使用哪套证书。配置示例server { listen 443 ssl; server_name example.com; ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # 国际标准证书 ssl_certificate /etc/nginx/certs/standard.crt; ssl_certificate_key /etc/nginx/certs/standard.key; # 国密签名证书 ssl_sign_certificate /etc/nginx/certs/sign_chain.crt; ssl_sign_certificate_key /etc/nginx/certs/sign.key; # 国密加密证书 ssl_enc_certificate /etc/nginx/certs/enc_chain.crt; ssl_enc_certificate_key /etc/nginx/certs/enc.key; }注意这里我把国际证书作为ssl_certificate国密签名证书放到ssl_sign_certificate指令里。不同版本的TongSuo Nginx指令名可能略有差异有的版本用ssl_certificate作为签名证书国密增强指令只负责加密证书有的版本则像我这样用ssl_sign_certificate区分。写法不同验收标准是一致的普通浏览器走国际证书国密浏览器走SM2双证书两者互不干扰。判断当前Nginx分支指令名的办法很简单编译完成后执行/usr/local/nginx/sbin/nginx -V 21 | grep -i tongsuo看有没有tongsuo相关模块标识再看源码补丁里的README通常都会列出支持的指令。不清楚的时候宁可先在一台测试机上试一把也别直接上生产。4.3 性能与安全参数调整国密HTTPS上线后性能调优的思路和传统HTTPS类似但有几个参数需要单独注意。以下是我在项目中常用的调优参数ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_buffer_size 4k; ssl_session_tickets off;ssl_session_cache对国密握手尤其重要。国密TLCP握手本身比RSA握手略显复杂如果每次新建连接都走完整握手CPU压力和延迟都很明显。开了session cache之后客户端和服务器之间可以在一定时间内复用会话握手开销大幅降低。10m的共享缓存大约能存几万个会话对于大多数业务站点足够了。ssl_buffer_size设置为4k可以让动态小页面响应更快减少发送缓冲区的等待时间。但如果你的站点主要输出大文件4k反而会增加系统调用次数需要按业务场景调整。关于ssl_session_tickets我建议明确关闭。Session Ticket在实现国密TLCP协议时部分分支的兼容性并不好而且ticket key管理和轮换也是额外的工作量。用共享缓存的方式做会话复用已经能满足业务需求还能少踩一些坑。HTTP/2方面国密浏览器对HTTP/2的支持参差不齐建议先在测试环境验证目标浏览器能否正常协商h2再决定是否开启listen 443 ssl http2。我在项目里遇到过一个情况某国密浏览器打开HTTP/2后图片加载异常关掉HTTP/2就正常了。所以国密场景里HTTP/2不是必须项稳定优先。5. 常见问题与排查技巧实录5.1 编译安装期问题编译阶段最容易出问题的地方是依赖缺失和参数拼写错误我整理了一张排查表错误现象可能原因解决办法Cant locate strict.pm in INC缺少Perl或Perl模块安装perl确认perl -v可用cryptography.h: No such file or directory系统OpenSSL开发头文件与TongSuo冲突不要混用系统OpenSSL头文件用--with-openssl指定TongSuo源码unknown option --enable-ntls版本过旧或拼写错误确认TongSuo版本支持国密开关检查参数名make: command not found缺少gcc/make工具链安装gcc gcc-c make编译Nginx时提示找不到PCRE缺少PCRE库安装pcre-devel或用--with-pcre指定源码目录有一个容易忽略的坑是系统里同时装了多个OpenSSL版本。Nginx在编译时会尝试寻找系统OpenSSL头文件如果你用--with-openssl/usr/local/tongsuo指定了TongSuo源码但源码目录本身不完整Nginx可能回退去用系统头文件结果编译出一个混合体。这种问题很难从编译输出里直接发现建议编译完成后用ldd /usr/local/nginx/sbin/nginx检查依赖再结合nginx -V确认openssl路径。5.2 配置启动与证书加载问题Nginx配置写好后nginx -t是必须做的一步。如果没有通过大部分情况是以下几个原因。第一类是unknown directive ssl_sign_certificate或unknown directive ssl_enc_certificate。这是最典型的国密Nginx编译不完整问题。原因基本是Nginx源码没有打上TongSuo的双证书补丁或者编译时没有把对应的国密模块编进去。解决方法是换用TongSuo官方提供的Nginx增强分支源码重新编译或者确认在configure阶段加了--with-http_ssl_module且补丁生效。第二类是证书文件加载失败。错误信息类似cannot load certificate key原因通常是私钥文件权限不对或者私钥与证书不匹配。Nginx的worker进程一般以nobody用户运行如果私钥文件是700权限且属于rootworker就读不了。我的习惯是私钥统一chmod 600证书文件chmod 644并确保Nginx进程用户对证书目录有读权限。第三类是不小心把加密证书配到签名证书的位置。这种情况nginx -t往往能通过因为格式上都是合法的X.509证书但握手时客户端会报签名验证失败。排查方法是用openssl比对证书里的公钥和私钥/usr/local/tongsuo/bin/openssl x509 -in sign_chain.crt -noout -pubkey /usr/local/tongsuo/bin/openssl pkey -in sign.key -pubout两个输出一致才说明证书和私钥配对成功。5.3 握手失败与浏览器访问问题配置全对证书也加载成功但浏览器就是打不开。这类问题通常发生在握手阶段排查手段主要是openssl s_client。用TongSuo自带的openssl模拟国密握手/usr/local/tongsuo/bin/openssl s_client \ -connect 127.0.0.1:443 \ -servername example.com \ -cipher ECC-SM2-SM4-CBC-SM3如果握手成功输出里能看到Cipher is ECC-SM2-SM4-CBC-SM3和Verify return code等信息。如果握手失败常见错误和对策如下错误现象可能原因解决办法no cipher matchNginx没有配置国密套件或客户端cipher与服务端不匹配检查ssl_ciphers是否包含ECC-SM2-SM4-*系列套件handshake failure客户端不支持TLCP协议或证书选择错误确认客户端是否为国密浏览器确认双证书指令配置正确unknown protocol客户端尝试用标准TLS访问但服务端只启用TLSv1.1以下检查ssl_protocols是否覆盖客户端支持的协议版本self signed certificate自签证书导致的告警测试环境可忽略生产必须替换为CA签发证书浏览器侧的问题则比较直接。普通Chrome/Firefox打开国密站点会显示不安全的连接这不代表服务端有问题而是浏览器不支持国密套件。要完整验证国密HTTPS效果得用支持国密的浏览器比如红莲花安全浏览器、奇安信可信浏览器或者360安全浏览器的国密版本。5.4 双证书使用中的几个经验踩过几次坑之后我总结了几条双证书使用中的个人经验。第一加密证书的私钥一定要单独备份且备份方式要比签名证书更严格。签名证书丢了可以吊销重签加密证书如果私钥丢失理论上可以通过签名证书重新申请但流程上要重新走CA的审核时间长且麻烦。所以在做证书归档的时候我会把签名私钥和加密私钥分别放在两个加密压缩包里并记录好私钥的SHA256指纹防止备份介质损坏后无法判断哪个文件对应哪张证书。第二证书快到期时续期不要想当然地只换签名证书。国密双证书要求签名证书和加密证书的期限和主体保持一致如果只换其中一张客户端在验签时很可能因为证书有效期错配而报错。我在项目里设定了一个提醒机制离到期前30天就把两张证书一起重签避免出现“签名证书合法但加密证书过期”的诡异状态。第三负载均衡场景下要注意多台Nginx的证书一致性。如果前面挂了多台Nginx做集群签名证书、加密证书、私钥必须在所有节点上保持一致。实务上我会用同一套证书文件分发到所有节点而不是每台机器各签一张。否则客户端在会话复用或负载均衡切换时会遇到证书不匹配导致的握手失败。第四国密HTTPS的日志排查比普通HTTPS更依赖协议层面的信息。Nginx的access log里ssl_protocol和ssl_cipher两个字段建议显式打开。配置方式是在http块中加入log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $ssl_protocol $ssl_cipher; access_log logs/access.log main;这样每次请求都会记录客户端实际使用的协议版本和套件排查兼容性问题时可以快速确认用户到底走的是国密还是标准TLS通道。最后再说一个线上应急的小技巧如果发现国密证书配置导致普通用户无法访问而国密浏览器又是验收的硬性要求可以临时把国密证书和加密证书的配置注释掉只保留标准证书让站点先恢复可用性再根据报错日志定位国密模块的问题。毕竟对一个正在跑业务的服务来说先保证可用再谈合规。这个顺序在项目里反复验证过比在线上硬扛着问题调试要稳妥得多。