1. 为什么要在编译层面折腾国密支持
很多人第一次接触国密改造,第一反应是找个现成的二进制包直接替换,或者装个模块了事。但真到了生产环境,尤其是金融、政务、能源这类对传输安全有硬性要求的场景,你会发现光靠动态加载模块往往不够——要么版本对不上,要么编译参数缺斤少两,要么运行起来握手直接失败。所以“编译配置”这四个字,才是国密改造里真正见功力的地方。
这篇是系列的第二篇,上一篇我们聊了国密算法的基础概念和整体改造思路,这一篇直接进入硬核环节:从源码编译入手,把 Nginx 改造成一个能同时跑国际算法和国密算法的双栈服务。说白了,就是让同一台服务器既能接普通浏览器的 HTTPS 请求,也能接国密客户端的加密连接,两边互不打架。
这篇文章适合谁看?如果你已经会基本的 Linux 编译操作,懂 Nginx 配置文件的大致结构,但一碰到国密就不知道从哪下手,那这篇就是写给你的。我会把每一步为什么这么做、参数怎么算、坑在哪里,全部摊开讲。全程不依赖任何特定厂商的闭源包,纯靠开源组件和标准编译流程走通。
先明确一个核心认知:Nginx 本身并不原生支持国密算法,它依赖 OpenSSL 提供的密码学能力。所以“编译 Nginx 支持国密”这件事,本质上拆成两步——第一步是编译一个支持国密的 OpenSSL,第二步是让 Nginx 链接这个 OpenSSL 重新编译。两步缺一不可,顺序也不能反。
注意:国密改造涉及密码学组件的替换,任何一步编译参数出错都可能导致握手失败或性能骤降。建议在测试环境完整走通后再上生产。
2. 编译前的整体设计与组件选型
2.1 为什么选源码编译而不是包管理安装
系统自带的 OpenSSL 和 Nginx 包,编译时用的都是默认参数,默认参数里不包含国密算法套件。你就算把配置文件写上天,底层没有对应的算法实现,照样握手失败。包管理安装的另一个问题是版本锁定——你想换 OpenSSL 版本,系统包管理器会告诉你依赖冲突,动不了。
源码编译的好处是彻底可控。OpenSSL 编译时可以指定启用国密算法相关的编译开关,Nginx 编译时可以指定链接我们自己编译出来的 OpenSSL 路径。整条链路从底层到上层都是自己掌控的,出了问题也好排查。
代价就是编译过程比较长,OpenSSL 全量编译大概十几分钟,Nginx 快一些,几分钟。但这是一次性的工作,编译好之后可以打包分发到同架构的其他机器上,不用每台都重新编。
2.2 组件版本选择与兼容性考量
版本选择这块我踩过不少坑,直接说结论。OpenSSL 建议选 1.1.1 系列的一个较新小版本,这个系列对国密算法的支持比较成熟,社区补丁也全。太老的版本缺少必要的算法接口,太新的 3.x 系列虽然也支持,但部分国密相关的编译开关和 API 有变动,和某些 Nginx 版本的配合需要额外打补丁,新手容易卡住。
Nginx 建议选 1.20 以上的稳定版。1.20 之后的版本对 TLS 1.3 和双证书配置的支持更完善,而国密双栈恰恰需要双证书机制——一张国际算法证书、一张国密证书,根据客户端支持的算法自动选择。
还有一个容易被忽略的组件是国密算法引擎或补丁。OpenSSL 官方源码里对 SM2、SM3、SM4 的支持是有的,但默认编译不一定全部启用。你需要确认编译配置里开启了对应的开关。有些场景还需要额外的国密引擎来提供硬件加速或特定实现,这个看具体需求。
| 组件 | 推荐版本区间 | 选择理由 | 注意事项 |
|---|---|---|---|
| OpenSSL | 1.1.1 系列较新小版本 | 国密支持成熟,API 稳定 | 避免过老版本缺接口 |
| Nginx | 1.20 以上稳定版 | 双证书和 TLS 1.3 支持完善 | 确认与 OpenSSL 兼容 |
| 国密引擎 | 按需 | 硬件加速或特定实现 | 非必需,视场景而定 |
2.3 双栈架构的核心思路
双栈的意思是同一套 Nginx 同时支持国际算法和国密算法的 TLS 握手。实现方式不是跑两个 Nginx 实例,而是在一个 server 块里配置两张证书,Nginx 根据客户端 ClientHello 里支持的算法套件自动选择用哪张证书完成握手。
这个机制的底层依赖是 OpenSSL 的多证书支持能力。当客户端发起连接时,ClientHello 里会带上自己支持的密码套件列表。如果列表里有国密套件,Nginx 就走国密证书和国密算法;如果只有国际套件,就走国际证书。整个过程对用户透明,不需要手动切换端口或域名。
这种设计的好处是迁移平滑。你不需要一次性把所有客户端都换成国密,可以国际和国密并行跑一段时间,逐步切换。对于面向公众的服务,这一点尤其重要,因为你不确定访问者用的是什么浏览器或客户端。
3. 编译 OpenSSL 的关键参数与实操
3.1 环境准备与依赖检查
动手之前先把编译工具链装齐。不同发行版的包名不一样,但核心就是编译器、make、perl 这几样。Perl 是必须的,OpenSSL 的编译配置脚本是 Perl 写的,没有它连 configure 都跑不起来。
# 以常见发行版为例,安装编译依赖 # 编译器与构建工具 gcc make perl # 如果系统里已有旧版 OpenSSL 开发包,建议先确认路径 which openssl openssl version这里有个细节要注意:系统里通常已经装了一个 OpenSSL,编译新版本时不要覆盖系统自带的,否则可能影响系统里其他依赖 OpenSSL 的服务。正确做法是把新版本编译安装到一个独立目录,比如/usr/local/openssl-gm,然后通过编译参数让 Nginx 去链接这个目录。
提示:编译前用
ldd --version确认一下系统的基础库版本,太老的系统可能需要先升级基础库,否则编译过程中会出现奇怪的链接错误。
3.2 configure 参数逐项拆解
OpenSSL 的 configure 参数决定了最终产物包含哪些能力。国密支持相关的关键参数需要重点关注。
./config \ --prefix=/usr/local/openssl-gm \ --openssldir=/usr/local/openssl-gm/ssl \ --shared \ enable-sm2 enable-sm3 enable-sm4 \ no-weak-ssl-ciphers逐项解释一下。--prefix指定安装目录,独立目录避免污染系统。--openssldir指定配置文件目录。--shared生成动态链接库,Nginx 链接时用动态库比静态库灵活,后续升级 OpenSSL 不用重新编译 Nginx。
enable-sm2 enable-sm3 enable-sm4这三个是国密算法的核心开关。SM2 是非对称算法,用于证书和密钥交换;SM3 是哈希算法,用于摘要和签名;SM4 是对称算法,用于数据加密。三个都要开,缺一个国密套件就跑不起来。
no-weak-ssl-ciphers是安全加固选项,禁用已知的弱加密套件。这个不是国密必需的,但生产环境建议加上,减少攻击面。
参数写完之后执行 config 脚本,它会检查环境并生成 Makefile。如果这一步报错,通常是缺依赖或者参数拼写错误,仔细看报错信息,一般都能定位到具体原因。
3.3 编译安装与验证
configure 通过之后就是标准的编译安装流程。
make -j$(nproc) make install-j$(nproc)是并行编译,用满 CPU 核心数加快速度。OpenSSL 编译比较吃 CPU,并行能省不少时间。编译过程中如果出现警告不用太紧张,只要不是 error 就继续。但如果出现 error,一定要停下来解决,不要强行跳过。
安装完成后验证一下国密算法是否可用。
/usr/local/openssl-gm/bin/openssl version /usr/local/openssl-gm/bin/openssl ecparam -list_curves | grep SM2第一条命令看版本号,确认是新编译的版本。第二条命令列出支持的椭圆曲线,如果能看到 SM2,说明国密非对称算法已经编译进去了。再验证一下 SM3 和 SM4。
/usr/local/openssl-gm/bin/openssl dgst -sm3 /dev/null /usr/local/openssl-gm/bin/openssl enc -sm4-cbc -helpSM3 那条命令会对空输入做一次摘要,能输出结果就说明算法可用。SM4 那条会打印帮助信息,能看到 sm4-cbc 选项就说明对称算法也正常。
注意:验证时一定要用新编译的 openssl 完整路径,不要直接敲
openssl,因为系统 PATH 里可能还是旧版本,验证结果会误导你。
4. 编译 Nginx 并链接国密 OpenSSL
4.1 Nginx 编译参数配置
Nginx 的编译配置比 OpenSSL 简单一些,核心就是告诉它去哪里找我们刚编译好的 OpenSSL。
./configure \ --prefix=/usr/local/nginx-gm \ --with-http_ssl_module \ --with-http_v2_module \ --with-openssl=/path/to/openssl-source \ --with-openssl-opt="enable-sm2 enable-sm3 enable-sm4" \ --with-cc-opt="-I/usr/local/openssl-gm/include" \ --with-ld-opt="-L/usr/local/openssl-gm/lib -Wl,-rpath,/usr/local/openssl-gm/lib"这里有几个关键点。--with-http_ssl_module是必须的,没有它 Nginx 根本不支持 HTTPS。--with-http_v2_module建议加上,HTTP/2 在现代服务里基本是标配。
--with-openssl指向 OpenSSL 源码目录,注意是源码目录不是安装目录。Nginx 编译时会自己再去编译一遍 OpenSSL,所以需要源码。--with-openssl-opt把国密开关传给 OpenSSL 的编译过程,确保 Nginx 链接的那个 OpenSSL 也带国密能力。
--with-cc-opt和--with-ld-opt分别指定头文件路径和库文件路径。-Wl,-rpath这个参数很重要,它把库路径写进可执行文件的运行时搜索路径,这样 Nginx 启动时才能找到我们自定义目录下的 OpenSSL 库,而不是系统默认的。
4.2 编译过程中的常见报错处理
编译 Nginx 时最常见的报错是找不到 OpenSSL 头文件或者链接不到库。如果报openssl/ssl.h: No such file or directory,说明--with-cc-opt里的 include 路径不对,检查一下/usr/local/openssl-gm/include下是不是真的有 ssl.h。
如果报链接错误,类似undefined reference to SSL_CTX_set_cipher_list,通常是--with-ld-opt的库路径不对,或者库文件没生成。回去检查 OpenSSL 编译安装那一步是否成功。
还有一种情况是编译能过,但 Nginx 启动时报error while loading shared libraries。这就是 rpath 没设对,Nginx 运行时找不到动态库。解决办法要么重新编译加上正确的 rpath,要么把库路径加到系统库配置里。
# 临时验证可以用这个方式指定库路径 LD_LIBRARY_PATH=/usr/local/openssl-gm/lib /usr/local/nginx-gm/sbin/nginx -t但生产环境不建议依赖环境变量,还是编译时把 rpath 写死更可靠。
4.3 验证 Nginx 的国密能力
编译安装完成后,先别急着配证书,先确认 Nginx 链接的 OpenSSL 版本对不对。
/usr/local/nginx-gm/sbin/nginx -V输出里会显示编译参数和 OpenSSL 版本。确认版本号和你编译的一致,并且编译参数里能看到国密相关的选项。
再进一步验证,可以用 Nginx 自带的测试命令配合国密套件。
/usr/local/openssl-gm/bin/openssl ciphers -v | grep -i sm这条命令列出所有包含 SM 的密码套件。如果能看到类似ECDHE-SM2-SM4-CBC-SM3这样的套件,说明整条链路都通了。这个套件的含义是:密钥交换用 SM2,数据加密用 SM4-CBC,完整性校验用 SM3。这是国密 TLS 的典型套件组合。
5. 双栈证书配置与握手流程解析
5.1 证书准备与格式要求
国密证书和国际证书在格式上有区别。国际证书通常是 RSA 或 ECC 密钥,国密证书用的是 SM2 密钥。生成国密证书需要用到支持 SM2 的工具链。
# 生成 SM2 私钥 /usr/local/openssl-gm/bin/openssl ecparam -genkey -name SM2 -out sm2.key # 生成证书签名请求 /usr/local/openssl-gm/bin/openssl req -new -key sm2.key -out sm2.csr -sm3 # 自签名生成证书(测试用,生产环境应走正规 CA) /usr/local/openssl-gm/bin/openssl x509 -req -in sm2.csr -signkey sm2.key -out sm2.crt -sm3 -days 365注意签名时指定-sm3,表示用 SM3 做摘要。生产环境的国密证书需要向支持国密的 CA 机构申请,自签名证书只适合测试。
国际证书的生成流程不变,用常规的 RSA 或 ECC 流程即可。两张证书准备好之后,放到 Nginx 能读取的目录,权限设置成只有 Nginx 工作进程用户可读。
5.2 Nginx 双证书配置写法
双栈配置的核心是在 server 块里同时指定两张证书。Nginx 从某个版本开始支持ssl_certificate指令出现多次,分别对应不同的算法类型。
server { listen 443 ssl; server_name example.local; # 国际算法证书 ssl_certificate /etc/nginx/certs/intl.crt; ssl_certificate_key /etc/nginx/certs/intl.key; # 国密算法证书 ssl_certificate /etc/nginx/certs/sm2.crt; ssl_certificate_key /etc/nginx/certs/sm2.key; # 密码套件配置,同时包含国际和国密套件 ssl_ciphers 'ECDHE-SM2-SM4-CBC-SM3:ECDHE-SM2-SM4-GCM-SM3:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384'; ssl_prefer_server_ciphers on; ssl_protocols TLSv1.2 TLSv1.3; location / { root /usr/share/nginx/html; index index.html; } }ssl_ciphers里把国密套件放在前面,国际套件放在后面。ssl_prefer_server_ciphers on表示优先使用服务端指定的顺序。这样配置之后,支持国密的客户端会优先协商到国密套件,不支持的客户端会自动落到国际套件。
提示:国密套件的名称在不同 OpenSSL 版本里可能有细微差异,配置前先用
openssl ciphers -v确认实际支持的套件名称,直接复制命令输出里的名称最稳妥。
5.3 握手过程与算法协商机制
客户端发起连接时,ClientHello 里带着自己支持的密码套件列表。Nginx 收到后,结合服务端配置的套件列表,选出一个双方都支持的最优套件。
如果客户端支持国密套件,且服务端配置了国密证书,协商结果就是国密套件,后续的密钥交换、数据加密、摘要校验全部走国密算法。如果客户端不支持国密,协商结果就是国际套件,走常规的 RSA 或 ECC 流程。
这个协商过程完全由 TLS 协议层自动完成,应用层无感知。你可以用抓包工具看握手过程,ClientHello 和 ServerHello 里的 CipherSuite 字段能清楚看到最终选了什么套件。
有一个细节值得注意:国密 TLS 的握手流程和国际 TLS 略有不同。国密标准里定义了双证书体系,签名证书和加密证书是分开的。但在实际部署中,很多场景用的是单证书简化模式。具体用哪种模式,取决于你的客户端要求和服务端实现。这一点在配置前要和客户端侧确认清楚,否则可能出现服务端配好了但客户端不认的情况。
6. 常见问题排查与性能调优实录
6.1 握手失败的典型原因与定位方法
握手失败是国密改造里最常见的问题,表现是客户端连不上或者连接被重置。排查的第一步是看 Nginx 错误日志。
tail -f /usr/local/nginx-gm/logs/error.log如果日志里出现no shared cipher,说明客户端和服务端没有共同支持的密码套件。原因可能是服务端没配国密套件,或者客户端不支持国密,或者套件名称写错了。
如果出现certificate verify failed,说明证书验证没过。可能是证书链不完整,或者客户端不信任服务端的 CA。国密证书的信任链和国际证书是两套体系,客户端需要预置对应的根证书。
如果出现unsupported protocol,检查ssl_protocols配置,确认协议版本双方都支持。
用 openssl 命令行工具可以模拟握手,快速定位问题。
# 模拟国密握手 /usr/local/openssl-gm/bin/openssl s_client -connect example.local:443 -cipher 'ECDHE-SM2-SM4-CBC-SM3' # 模拟国际握手 openssl s_client -connect example.local:443第一条命令如果握手成功,会打印出证书信息和协商的套件。失败的话会给出具体错误,根据错误信息对症下药。
6.2 性能影响评估与优化建议
国密算法的性能和国际算法有差异。SM2 在密钥交换阶段的计算量比 RSA 小,但比 ECC 略大。SM4 的加解密速度和 AES 接近,在支持硬件加速的平台上差距更小。SM3 的哈希速度和 SHA256 相当。
整体来看,国密 TLS 的性能损耗在可接受范围内,但有几个优化点值得注意。
第一,开启会话复用。TLS 握手是性能开销的大头,会话复用能避免重复的完整握手。配置ssl_session_cache和ssl_session_timeout即可。
ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m;第二,如果平台支持国密硬件加速,编译 OpenSSL 时启用对应的引擎。硬件加速能把 SM2 和 SM4 的计算卸载到专用硬件,性能提升明显。但硬件加速依赖特定平台,通用服务器上不一定有。
第三,合理设置ssl_buffer_size。默认值对大多数场景够用,但如果传输大量小文件,适当调小可以减少延迟。
| 优化项 | 配置指令 | 预期效果 | 适用场景 |
|---|---|---|---|
| 会话复用 | ssl_session_cache | 减少完整握手次数 | 所有场景 |
| 硬件加速 | 编译时启用引擎 | 提升国密计算速度 | 支持加速的平台 |
| 缓冲区调优 | ssl_buffer_size | 降低小文件延迟 | 小文件传输多 |
6.3 独家避坑经验汇总
说几个我在实际操作中踩过的坑,都是文档里不会写的。
第一个坑是 OpenSSL 编译时的enable-sm2开关。有些小版本的 OpenSSL 里,SM2 的支持是默认开启的,不需要显式指定。但如果你显式指定了而该版本又不认这个参数,configure 会直接报错。解决办法是先跑一次./config --help看看支持哪些参数,按实际支持的来写。
第二个坑是 Nginx 的--with-openssl参数。这个参数指向的是 OpenSSL 源码目录,Nginx 编译时会自己再编译一遍 OpenSSL。但如果你之前已经单独编译安装过 OpenSSL,Nginx 编译时可能会混淆,用了错误的版本。稳妥的做法是 Nginx 编译时用一个干净的 OpenSSL 源码副本,不要和之前编译过的目录混用。
第三个坑是证书链的顺序。国密证书的证书链里,根证书和中间证书的顺序有讲究。顺序错了,某些客户端会验证失败。一般来说,服务端证书在前,中间证书在后,根证书通常不需要放在服务端。具体顺序参考 CA 机构提供的说明。
第四个坑是双栈配置下的套件优先级。如果你把国际套件放在国密套件前面,支持国密的客户端也会优先协商到国际套件,国密能力就浪费了。所以国密套件一定要放在前面,配合ssl_prefer_server_ciphers on使用。
第五个坑是日志级别。排查握手问题时,把 Nginx 日志级别调到info或debug,能看到详细的握手过程。但生产环境不要长期开 debug,日志量太大,影响性能。排查完记得调回去。
error_log /usr/local/nginx-gm/logs/error.log info;这个配置在排查阶段很有用,能看到每一步握手协商的细节。调完之后改回error或warn级别。
7. 编译产物的打包与迁移部署
7.1 制作可分发的编译产物
编译一次不容易,如果有多台同架构的机器要部署,没必要每台都重新编译。把编译产物打包,直接分发过去就行。
需要打包的内容包括:OpenSSL 安装目录、Nginx 安装目录、以及必要的配置文件模板。打包前先确认目标机器的架构和基础库版本一致,否则动态库可能不兼容。
# 打包 OpenSSL 和 Nginx 安装目录 tar czf nginx-gm-package.tar.gz /usr/local/openssl-gm /usr/local/nginx-gm分发到目标机器后解压到相同路径,然后验证动态库依赖是否满足。
ldd /usr/local/nginx-gm/sbin/nginx | grep "not found"如果有not found的库,说明目标机器缺依赖,需要先补上。常见的是 libpcre、zlib 这些基础库。
7.2 迁移后的配置调整与验证
迁移到新机器后,证书路径、日志路径、用户权限这些可能需要调整。检查 Nginx 配置文件里的路径是否和新环境一致。
启动前先用nginx -t测试配置语法。
/usr/local/nginx-gm/sbin/nginx -t测试通过后再启动。启动后立即验证国密握手是否正常,用前面提到的 openssl s_client 命令测一遍。确认无误后再把流量切过来。
注意:迁移部署时最容易忽略的是文件权限。Nginx 工作进程通常以非 root 用户运行,如果证书文件权限设置过严,工作进程读不到证书,握手会失败。确保证书文件对 Nginx 工作进程用户可读。
7.3 版本升级与回滚策略
国密组件后续可能需要升级,比如 OpenSSL 出了安全补丁,或者 Nginx 有新版本。升级策略建议是先在测试环境验证,确认没问题再上生产。
回滚方案也要提前准备好。最简单的回滚就是保留旧版本的安装目录,出问题时把软链接或启动脚本指回旧版本。所以升级时不要直接覆盖旧目录,而是新装一个目录,通过切换启动路径来实现版本切换。
# 假设旧版本在 /usr/local/nginx-gm-old # 新版本在 /usr/local/nginx-gm-new # 通过软链接切换 ln -sfn /usr/local/nginx-gm-new /usr/local/nginx-gm这样切换和回滚都只是一条命令的事,风险可控。
我个人在实际操作中的体会是,国密改造这件事,编译环节虽然繁琐,但恰恰是最值得投入时间的地方。把编译参数吃透,把依赖关系理顺,后面配置和运维阶段会省很多事。反过来,如果编译阶段图省事用了来路不明的二进制包,后面出问题排查起来会非常痛苦,因为你根本不知道那个包是用什么参数编出来的。所以宁可多花半小时自己编译,也不要在这件事上走捷径。