国密自签名证书实战:SM2双证书、Nginx/Tomcat配置与浏览器信任
1. 先搞清楚国密自签名证书到底解决什么问题折腾过内网系统的人大概都遇到过这种场景开发环境要跑 HTTPS但申请正式证书流程走不完或者系统对接方明确要求走 SM2 算法再或者测试一套国密改造后的网关手上却没有一张能用的国密证书。这时候国密自签名证书就是最省事的破局手段——它不需要任何第三方机构签发自己当自己的 CA用 OpenSSL 之类的工具在本地十分钟就能造出一条完整的证书链。这篇文章讲的就是这套东西从密钥生成、CA 自签、服务端证书签发到 Nginx、Tomcat 落地配置再到浏览器信任的完整链路。它适合三类人一是做内网系统联调的开发需要快速搭一套能跑通国密握手的测试环境二是做运维的朋友手上有一台已经编译好国密支持的 Nginx 或 Tomcat但卡在证书生成这一步三是刚接触 SM2、SM3、SM4 这些概念想找个能动手跑一遍的入口的同学。整条链路上最容易踩坑的地方不是命令本身而是“算法支持”和“TLS 套件支持”这两件事经常被混为一谈——很多教程只告诉你openssl req -x509就能生成结果证书生成了握手照样失败。1.1 自签名的适用边界什么时候该用什么时候别硬上先把最容易被误解的一点说清楚自签名证书和“没证书”不是一回事。自签名只是签发者是自己证书本身的结构、扩展项、有效期、密钥用法一应俱全用openssl x509 -text看出来的字段和正式证书没有任何区别。差别在于信任链的根——正式证书的根在操作系统和浏览器的内置信任库里而自签名的根只在你自己手里需要手动导入到客户端。所以自签名的适用场景非常明确纯内网、测试验证、内部系统之间互相调用、离线环境部署。反过来只要你的服务需要被公网用户用普通浏览器访问自签名就会把用户挡在“此连接不是私密连接”的警告页前——就算国密浏览器也不例外只要根证书没导进去。注意自签名不是“随便签”服务端证书的subjectAltName一定要写全。现代浏览器和很多 HTTP 客户端已经完全不看commonName了只认 SAN 里的域名和 IP漏掉这一项会直接判定证书无效而这个错误信息通常显示成“证书不受信任”很容易被误判成根证书没导入。1.2 SM2、SM3、SM4 在 TLS 里各管什么这三个算法经常被放在一起说但在一次 TLS 握手里它们的分工是完全不同的理清这一点后面所有配置错误你都能自己定位。SM2非对称算法基于 256 位椭圆曲线。它同时承担两件事——证书里存放的公钥是 SM2 公钥握手时的签名验签也用 SM2。可以简单类比成 ECDSA 的角色。SM3哈希摘要算法输出 256 位。证书签名的摘要、握手过程中的消息摘要、SM2 签名前的预处理都会用到它角色类比 SHA-256。SM4对称分组密码128 位分组、128 位密钥负责握手完成后应用数据的加密角色类比 AES。一次国密 TLS 1.2 握手大致是客户端和服务端协商出ECDHE_SM2_WITH_SM4_CBC_SM3这类套件用 SM2 做密钥交换的签名、用 SM4 加密应用数据、用 SM3 做完整性校验和签名摘要。三个算法缺一不可任何一个环节的版本不支持握手就停在 ClientHello 或 ServerHello 阶段。1.3 双证书体系国密 TLS 为什么非要两张证书这是国密改造里最有辨识度的一个设计也是自签名时最容易漏掉的一环。国际通用的 TLS 体系里一张服务端证书就够了它的公钥既用于身份签名验证也参与密钥交换。国密 TLS 把这两件事拆开了签名证书只负责身份认证加密证书专门用于密钥交换和密钥协商。为什么这么设计核心考虑是密钥管理。签名私钥一旦泄露攻击者可以伪造身份但历史流量解不开加密私钥泄露配合抓包数据就可能还原出历史通信内容。两张证书分离权限边界更清晰各自的生命周期和托管策略也能独立制定。代价就是配置复杂度翻倍——Nginx 要加载两组证书和私钥Tomcat 的 keystore 里要放两条链任何一组缺失都会导致握手在服务端直接报错。落实到自签名场景你需要签出的最少是四份材料CA 根证书、CA 根私钥、服务端签名证书含签名私钥、服务端加密证书含加密私钥。如果还要做双向认证再加一份客户端证书。这个规模不算大但每个文件对应的密钥用法不一样签错一处后面排查会很痛苦。2. 工具链选型OpenSSL、Tongsuo、GmSSL 到底用哪个工具选错后面全是白工。我在早期就干过这事用系统自带的 OpenSSL 1.1.1 生成了一整套 SM2 证书配置到 Nginx 上握手报“no shared cipher”查了两小时才发现系统里的 OpenSSL 压根没有国密 TLS 套件。所以这一节的重点不是罗列功能而是把“生成证书”和“跑国密 TLS”这两件事分清楚它们是两个能力。2.1 三个发行版的能力对照先给结论再解释原因。工具/分支SM2 密钥生成SM3 摘要自签名 SM2 证书TLS 1.2 国密套件TLS 1.3 国密套件OpenSSL 1.1.1 / 3.x 主线支持支持支持不支持部分版本不支持Tongsuo铜锁支持支持支持支持支持GmSSL 2.x / 3.x支持支持支持支持3.x 支持看这张表就能明白如果你的目标只是生成证书系统自带的 OpenSSL 就够用但只要你的目标包含让 Nginx 或 Tomcat 真正完成国密握手就必须换成支持国密套件的分支。OpenSSL 主线之所以不能跑国密 TLS是因为 TLS 的具体实现ssl/目录下的协议栈里没有注册国密的 CipherSuite 和对应的密钥交换逻辑。SM2 在主线里只是一个“椭圆曲线 签名算法”能被openssl dgst -sm3、openssl req这类命令行工具调用但不会被握手协议栈引用。Tongsuo 这类分支做的事情就是在协议栈层补齐ECDHE-SM2-WITH-SM4-CBC-SM3这些套件和双证书加载逻辑。2.2 一个常见的认知错误SM2 曲线名和支持国密是两码事很多人第一次接触国密会拿这条命令去验证openssl ecparam -list_curves | grep -i sm2看到输出SM2 : SM2 curve over a 256 bit prime field就以为“支持国密”。其实这只说明这个 OpenSSL 编译时带了 SM2 曲线定义属于数学层面的能力。真正决定能不能跑国密 TLS 的是这样验证openssl ciphers -v SM2 2/dev/null如果输出为空或者提示找不到匹配的密码套件那这个 OpenSSL 就没法用于国密 HTTPS 服务。Tongsuo 上执行这条命令会看到类似下面的结果ECDHE-SM2-WITH-SM4-CBC-SM3 TLSv1.2 KxECDH AuSM2 EncSM4-CBC MacSM3 ECC-SM2-WITH-SM4-CBC-SM3 TLSv1.2 KxECDH AuSM2 EncSM4-CBC MacSM3看到AuSM2和MacSM3这两列才算真的能用。2.3 我的选型建议与编译参数实测下来我个人在服务端场景优先选 Tongsuo原因是它同时提供国密 TLS 1.2 和 TLS 1.3 的支持而且和 Nginx、Java 生态的配合资料比较完整如果只是做教学演示、或者需要一个轻量的命令行工具来生成证书文件GmSSL 3.x 的gmssl子命令非常好用gmssl sm2keygen、gmssl certgen这类命令比 OpenSSL 的参数直观得多。编译 Tongsuo 的过程不复杂但有几个参数建议显式加上./config --prefix/opt/tongsuo \ --openssldir/opt/tongsuo/ssl \ --with-threads \ shared make -j$(nproc) sudo make installshared一定要加。后面编译 Nginx 的时候需要链接动态库如果只编译成静态库Nginx 的链接阶段很容易报一堆未定义符号。编译完成之后记得把共享库路径写进系统的动态链接配置否则运行时会提示“找不到 libssl.so.3”这类错误。提示不要用LD_LIBRARY_PATH长期顶替系统 OpenSSL改环境变量会影响机器上所有依赖 OpenSSL 的程序包括系统包管理器。正规做法是往/etc/ld.so.conf.d/里加一个配置文件然后执行ldconfig。3. 一步步生成 SM2 密钥对与自签名证书链接下来是真刀真枪的部分。我会按实际操作的顺序走一遍每一步都说明为什么这么写参数能省的和不能省的区别在哪里。以下命令中openssl均指已经安装好的国密分支版本。3.1 生成 SM2 私钥两种写法的实际差异生成 SM2 私钥有两条常见路径# 写法一老式的 ecparam openssl ecparam -genkey -name SM2 -out sm2.key -noout # 写法二新的 genpkey openssl genpkey -algorithm EC \ -pkeyopt ec_paramgen_curve:SM2 \ -pkeyopt ec_param_enc:named_curve \ -out sm2.key两种写法生成的密钥在数学上是等价的但输出格式有区别。写法一生成的是传统 EC 私钥结构SEC1写法二生成的是 PKCS8 结构。这个差异会在后面导出 PKCS12 和给 Java 使用时集中爆发某些 JDK 和 BouncyCastle 版本读 SEC1 格式的 SM2 私钥会报“Unknown key algorithm”而 PKCS8 的兼容性好得多。所以我的建议是默认用写法二。如果已经用写法一生成过了也不用重新来转换一下即可openssl pkcs8 -topk8 -nocrypt -in sm2.key -out sm2_pkcs8.key生成完先看一眼曲线对不对这一步别省openssl ec -in sm2_pkcs8.key -text -noout输出里ASN1 OID应该显示SM2NIST CURVE那一行不应该出现。如果显示的是prime256v1或者secp256r1说明参数没生效生成的是一张普通 ECC 密钥后面签出来的证书跟国密一点关系都没有。3.2 自签名根 CA 的配置文件写法与参数解读根 CA 是整条链的信任起点它的扩展项写法决定了后面所有签发出来的证书能不能被正确识别。我习惯用配置文件而不是一堆命令行参数原因是可复现、可版本管理而且扩展项不容易写漏。新建一个ca.cnf[req] distinguished_name dn x509_extensions v3_ca prompt no default_md sm3 [dn] C CN ST Beijing L Beijing O Lab Internal OU Security CN Lab Internal SM2 Root CA [v3_ca] basicConstraints critical, CA:TRUE keyUsage critical, digitalSignature, cRLSign, keyCertSign subjectKeyIdentifier hash authorityKeyIdentifier keyid:always几个关键点值得单独说明。default_md sm3保证签名摘要走 SM3不写的话某些版本会默认用 SHA-256签出来的证书虽然公钥是 SM2但签名算法是 ECDSA-SHA256——这种“半国密”证书在严格校验的环境里会被判定不合规。basicConstraints critical, CA:TRUE里的critical标记不能少它告诉校验方这个扩展必须被理解防止中间 CA 被当成终端证书使用。keyUsage里的keyCertSign是签发子证书的必需权限漏掉它后面用这张 CA 去签服务端证书时Chrome 系内核的客户端会直接拒绝这条链。执行签发openssl req -new -x509 \ -key ca.key \ -out ca.crt \ -days 7300 \ -config ca.cnf根 CA 的有效期可以给长一点十年八年都行。原因很实际根证书一旦换掉所有客户端都得重新导入内网环境里这个动作的成本远高于换一张服务端证书。服务端证书则建议控制在一年到两年留出轮换的余地。3.3 签发服务端签名证书与加密证书现在进入双证书环节。先各自生成密钥对openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:SM2 \ -pkeyopt ec_param_enc:named_curve -out sign.key openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:SM2 \ -pkeyopt ec_param_enc:named_curve -out enc.key注意签名和加密必须用两套独立的密钥对。我见过有人图省事两张证书共用同一个私钥配置能跑起来握手也能过但这等于把双证书的隔离价值抹掉了。真要做安全审计这一条是会被直接点名的。接着写服务端证书的扩展配置server.cnf[v3_sign] basicConstraints CA:FALSE keyUsage critical, digitalSignature, nonRepudiation extendedKeyUsage serverAuth, clientAuth subjectKeyIdentifier hash authorityKeyIdentifier keyid,issuer subjectAltName san [v3_enc] basicConstraints CA:FALSE keyUsage critical, keyEncipherment, dataEncipherment, keyAgreement extendedKeyUsage serverAuth subjectKeyIdentifier hash authorityKeyIdentifier keyid,issuer subjectAltName san [san] DNS.1 localhost DNS.2 sm2.lab.internal IP.1 127.0.0.1 IP.2 192.168.10.20签名证书的keyUsage只给digitalSignature加密证书给keyEncipherment和keyAgreement这个分工和前面讲的体系设计是一一对应的。subjectAltName里的 IP 地址要换成你实际访问的地址写多少条都不嫌多宁滥勿缺。生成 CSR 并签发# 签名证书 openssl req -new -key sign.key -out sign.csr \ -subj /CCN/OLab Internal/CNsm2.lab.internal openssl x509 -req -in sign.csr -CA ca.crt -CAkey ca.key \ -CAcreateserial -out sign.crt -days 730 -sm3 \ -extfile server.cnf -extensions v3_sign # 加密证书 openssl req -new -key enc.key -out enc.csr \ -subj /CCN/OLab Internal/CNsm2.lab.internal openssl x509 -req -in enc.csr -CA ca.crt -CAkey ca.key \ -CAcreateserial -out enc.crt -days 730 -sm3 \ -extfile server.cnf -extensions v3_enc-sm3这个参数别漏它指定用 SM3 作为签发摘要。有的版本里这个参数写作-md sm3两者等价。3.4 导出 PKCS12 与格式转换的坑Nginx 直接用 PEM 格式不需要转换。但 Tomcat 那边一般吃 PKCS12转换命令长这样openssl pkcs12 -export \ -inkey sign.key \ -in sign.crt \ -certfile ca.crt \ -name sm2sign \ -out sign.p12 \ -passout pass:changeit这里有个很隐蔽的坑。OpenSSL 3.x 默认用 PBES2 AES-256-CBC 来加密 PKCS12 里的私钥而部分 JDK 旧版本和某些 BouncyCastle 版本对这个组合的支持并不完整读取时会抛“failed to decrypt safe contents entry”之类的异常。这种情况有两个解法一是升级 BC 到较新版本二是导出时显式指定传统算法openssl pkcs12 -export \ -inkey sign.key -in sign.crt -certfile ca.crt \ -name sm2sign -out sign.p12 \ -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES \ -macalg sha1 \ -passout pass:changeit另外提醒一句keytool基本不认 SM2 私钥别指望用它来合并或者查看国密证书链。要查看 P12 内容用openssl pkcs12 -info -in sign.p12更靠谱。3.5 校验怎么确认证书真的是 SM2 加 SM3签完不校验等于闭着眼睛开车。我固定跑这三条# 看公钥算法和签名算法 openssl x509 -in sign.crt -noout -text | grep -E Signature Algorithm|Public Key Algorithm|ASN1 OID # 验证证书链 openssl verify -CAfile ca.crt sign.crt # 用签名证书对一段数据做 SM2 签名再验证 echo hello sm2 | openssl dgst -sm3 -sign sign.key -out sig.bin echo hello sm2 | openssl dgst -sm3 -verify (openssl x509 -in sign.crt -pubkey -noout) -signature sig.bin第一条命令的输出里Signature Algorithm应该出现sm3或者SM2-with-SM3字样Public Key Algorithm显示id-ecPublicKey或者SM2ASN1 OID是SM2。如果你看到 OID 是1.2.156.10197.1.301那就是国密算法标识符说明没问题。第二条如果输出OK链就是通的。提示SM2 签名有一个容易被忽略的细节——签名前要先计算 Z 值而 Z 值的计算依赖一个“用户标识”参数国密标准里默认值是十六进制31323334353637383132333435363738也就是 ASCII 字符串1234567812345678。不同的库对这个默认值的处理不一致跨语言验签比如 Java 生成的签名拿到 OpenSSL 里验失败时第一个要怀疑的就是这个参数。做自签名测试时尽量保证签名和验签用同一个库能省下大量排查时间。4. 落地到 Nginx国密 TLS 配置与双证书加载证书齐了接下来把它挂到服务上。Nginx 是国密改造里最常见的一环配置本身不复杂但双证书的加载方式和普通 HTTPS 有本质区别。4.1 编译带国密支持的 Nginx系统包管理器装的 Nginx 一定不支持国密必须自己编。思路很简单把 Nginx 的 SSL 模块指向前面装好的国密 OpenSSL 分支。./configure \ --prefix/opt/nginx \ --with-http_ssl_module \ --with-http_v2_module \ --with-openssl/opt/tongsuo-src \ --with-openssl-optshared \ --with-cc-opt-I/opt/tongsuo/include \ --with-ld-opt-L/opt/tongsuo/lib -Wl,-rpath,/opt/tongsuo/lib make -j$(nproc) sudo make install--with-openssl指向的是源码目录不是安装目录这一点经常被搞混。-Wl,-rpath建议加上把库路径直接写进二进制省得后面还要配动态库搜索路径迁移机器时尤其省事。另外需要一个前提Nginx 的主线 SSL 模块只懂标准的ssl_certificate指令双证书加载需要打过国密补丁的模块版本。Tongsuo 社区维护了配套的 Nginx 国密补丁不同分支的指令名可能略有差异配置前先看一眼补丁自带的示例配置和说明文件别照着一年前的博客硬抄。4.2 server 段配置逐行解读补丁打上之后配置大概长这样server { listen 443 ssl; server_name sm2.lab.internal; # 开启国密 TLS ssl_ntls on; # 第一组签名证书与签名私钥 ssl_certificate /opt/nginx/conf/certs/sign.crt; ssl_certificate_key /opt/nginx/conf/certs/sign.key; # 第二组加密证书与加密私钥 ssl_certificate /opt/nginx/conf/certs/enc.crt; ssl_certificate_key /opt/nginx/conf/certs/enc.key; # 协议版本 ssl_protocols TLSv1.2 TLSv1.3; # 国密套件优先国际套件兜底 ssl_ciphers ECDHE-SM2-WITH-SM4-CBC-SM3:ECC-SM2-WITH-SM4-CBC-SM3:ECDHE-SM2-WITH-SM4-GCM-SM3; # CA 链客户端校验时下发 # ssl_client_certificate /opt/nginx/conf/certs/ca.crt; # ssl_verify_client on; location / { root /opt/nginx/html; index index.html; } }几个地方值得展开说。ssl_ntls on是开关不开的话后面两组证书的加载指令会报语法错误或者被忽略。两组ssl_certificate和ssl_certificate_key的顺序不能乱先签名后加密顺序反了握手时服务端会报找不到匹配的证书。ssl_ciphers里我把 CBC 套件放在前面是因为兼容性更好一些客户端实现对 SM4-GCM 的支持还不完整。如果你的客户端全部可控且追求更好的性能GCM 版本可以提到前面。国际套件我没有放进列表这是刻意的——测试国密链路时如果留着国际套件客户端很可能悄悄协商到 ECDHE-RSA 上去你以为验证通过了其实根本没走国密。要确认的话抓包看 ServerHello 里的 CipherSuite 字段最直接。ssl_protocols里保留 TLSv1.3 是因为有些分支已经支持 TLS 1.3 的国密套件。早期版本的国密只覆盖 TLS 1.2那就要把 TLSv1.3 去掉不然握手可能协商到 TLS 1.3 后因为套件不匹配而失败。4.3 用 s_client 验证握手到底走没走国密配置 reload 之后别急着开浏览器先在服务端本地验证openssl s_client -connect 127.0.0.1:443 \ -cipher ECDHE-SM2-WITH-SM4-CBC-SM3 \ -tls1_2 -servername sm2.lab.internal注意这里的openssl必须是支持国密的那个版本用系统自带的会直接报“no cipher match”。握手成功的话输出里会有一行New, TLSv1.2, Cipher is ECDHE-SM2-WITH-SM4-CBC-SM3 Server public key is 256 bit看到ECDHE-SM2-WITH-SM4-CBC-SM3才算真的跑通了。如果显示的是别的套件名说明协商没按你的预期走。再补一条命令看服务端下发了哪些证书openssl s_client -connect 127.0.0.1:443 -showcerts /dev/null 2/dev/null | grep -E ^ *[0-9] s:正常应该能看到两张服务端证书有时还包括 CA 根证书。只看到一张说明加密证书没配进去或者顺序有问题。5. 落地到 Tomcat让 Java 说国密Tomcat 这边的难度比 Nginx 高一档问题不在配置而在 Java 的 SSL 实现本身。5.1 为什么原生 JSSE 不支持国密JDK 自带的 JSSESunJSSE实现里Signature 算法清单和 CipherSuite 清单是硬编码在源码里的国密不在其中。这意味着哪怕你的 JDK 支持 SM2 加密用 BouncyCastle 的 provider 能做TLS 握手层依然不会协商国密套件。你会在日志里看到类似“无共享密码套件”“no cipher suites in common”的报错。解决办法是换掉整个 TLS 实现用 BouncyCastle 提供的 BCJSSE。它是一套独立实现的 JSSE Provider注册进去之后可以接管SSLContext.getInstance(TLS)的创建过程从而提供国密套件支持。5.2 依赖与 Connector 配置Maven 依赖需要两个包dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk18on/artifactId version1.78.1/version /dependency dependency groupIdorg.bouncycastle/groupId artifactIdbctls-jdk18on/artifactId version1.78.1/version /dependency版本上建议用 1.7x 之后的版本国密套件的完整度和稳定性明显好于早期版本。两个包都放到 Tomcat 的lib目录下或者作为应用依赖打包进去。Connector 的配置核心在于指定 SSL 实现和套件Connector port8443 protocolorg.apache.coyote.http11.Http11NioProtocol maxThreads200 SSLEnabledtrue schemehttps securetrue SSLHostConfig sslProtocolTLSv1.2 ciphersECDHE_SM2_WITH_SM4_CBC_SM3,ECC_SM2_WITH_SM4_CBC_SM3 certificateVerificationnone Certificate certificateKeystoreFileconf/sm2-server.p12 certificateKeystoreTypePKCS12 certificateKeystorePasswordchangeit typeRSA / /SSLHostConfig /Connector有两处容易出错。第一套件名称里的分隔符是下划线不是 Nginx 里的连字符——ECDHE_SM2_WITH_SM4_CBC_SM3和ECDHE-SM2-WITH-SM4-CBC-SM3是同一件事的两种写法抄错格式会静默失效Tomcat 不会报错只是协商不上。第二typeRSA这个属性在国密场景下有些尴尬它是 Tomcat 早期为区分证书类型引入的对 SM2 证书并没有专门取值实测保留默认值影响不大但如果遇到证书解析异常可以试着去掉这个属性。PKCS12 文件的准备方式在前面 3.4 节已经讲过。这里补一点Tomcat 用的是同一个 keystore 文件如果要做双证书需要把签名证书和加密证书都放进同一个 P12并且保持链完整。合并的方式可以用 OpenSSL 先把两张证书拼成一个文件cat sign.crt enc.crt ca.crt bundle.crt openssl pkcs12 -export -inkey sign.key -in sign.crt \ -certfile bundle.crt -name sm2server -out sm2-server.p12具体哪个私钥作为主私钥、加密证书的私钥怎么关联不同版本的 BCJSSE 处理方式不完全一致遇到问题时优先去看 BC 官方文档里关于 GM 套件的说明和示例代码比在网上找博客靠谱。5.3 打印支持的套件确认配置真的生效Java 这边最实用的调试手段是在启动时把当前 SSLContext 支持的套件全打出来import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import java.security.Security; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; public class SuiteDump { public static void main(String[] args) throws Exception { Security.addProvider(new BouncyCastleProvider()); Security.addProvider(new BouncyCastleJsseProvider()); SSLContext ctx SSLContext.getInstance(TLS, BCJSSE); ctx.init(null, null, null); SSLSocketFactory f ctx.getSocketFactory(); for (String s : f.getSupportedCipherSuites()) { if (s.contains(SM) || s.contains(GM)) { System.out.println(s); } } } }跑出来如果能看到 SM2、SM4 相关的套件名说明 Provider 注册成功。如果输出为空通常是bctls包没进 classpath或者 Provider 注册顺序被别人覆盖了。把 Provider 注册放在所有 SSL 相关代码之前是最稳的做法。关于 Provider 优先级还有个补充说明。Tomcat 内部是通过SSLContext.getInstance(protocol)获取上下文的这个方法会按java.security里注册的顺序遍历所有 Provider第一个能提供 TLS 的实现被选中。所以想让 Tomcat 走 BCJSSE可以在java.security文件里把 BCJSSE 写在 SunJSSE 前面security.provider.1org.bouncycastle.jsse.provider.BouncyCastleJsseProvider security.provider.2org.bouncycastle.jce.provider.BouncyCastleProvider security.provider.3SUN security.provider.4SunJSSE但这么做是全局性的会影响这台 JVM 上所有网络连接。测试环境可以这么干生产环境建议通过自定义SSLContext的方式局部指定避免把别的服务带偏。6. 客户端信任浏览器插件与根证书导入服务端跑通了接下来是客户端。这一步最常见的现象是服务端日志显示握手成功浏览器却打不开或者干脆连不上。6.1 为什么普通浏览器连国密站点都打不开两个层面的原因。第一层是算法层面Chrome、Edge 这类浏览器底层用的加密库不支持 SM2 签名算法ClientHello 里根本不会带上国密套件服务端自然协商不出结果。第二层是信任层面即使客户端支持国密自签名证书的根不在系统信任库里一样会被拦。所以要访问国密站点客户端必须是支持国密的浏览器。市面上有几款专门做这个的浏览器安装完之后还要单独导入根证书。有些方案是浏览器插件形式通过在浏览器里注入一套加密和证书校验逻辑来支持国密安装插件之后同样需要把 CA 根证书导入到插件的信任区里。6.2 导入根证书的通用步骤与常见失败点不管用哪个客户端导入逻辑都差不多找到证书管理入口把ca.crt导入到“受信任的根证书颁发机构”存储区。具体路径一般在设置里的隐私与安全、证书管理或者高级设置里。导入之后有没有真正生效可以从这几个角度判断浏览器地址栏的锁标志是否正常点开看证书链是不是显示你自签的那张 CA。证书详情里的签名算法是不是 SM3公钥算法是不是 SM2。如果显示 SHA-256 或 RSA说明服务端下发的还是普通证书可能配置里的证书路径写错了。用openssl s_client从客户端机器上连一遍看返回的证书链和浏览器看到的是否一致。失败的情况里最常见的三个原因是导错了存储区导入到了“个人”或者“中间证书颁发机构”而不是“受信任的根证书颁发机构”服务端证书缺少subjectAltName以及访问的地址和证书里 SAN 写的地址对不上——比如证书里只写了域名你却在浏览器里输 IP这种情况浏览器一定会报错而且报错信息通常不会直说地址不匹配。6.3 双向认证场景的客户端证书准备如果服务端开了ssl_verify_client on客户端也要准备证书。签发方式和前面的服务端证书一样只是扩展项要改[v3_client] basicConstraints CA:FALSE keyUsage critical, digitalSignature extendedKeyUsage clientAuth subjectKeyIdentifier hash authorityKeyIdentifier keyid,issuer客户端证书的extendedKeyUsage必须是clientAuth写成serverAuth服务端会直接拒绝。签发命令openssl req -new -key client.key -out client.csr \ -subj /CCN/OLab Internal/CNtest-client openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \ -CAcreateserial -out client.crt -days 365 -sm3 \ -extfile client.cnf -extensions v3_client把client.crt和client.key合成 P12 之后导入到国密浏览器服务端再配上ssl_client_certificate ca.crt和ssl_verify_client on就完整了。注意双向认证调试时最容易卡在“服务端拒绝了客户端证书”上而这个错误在服务端日志里往往只有一行peer did not return a certificate。遇到这个先确认客户端的证书是不是真的被浏览器选中了——很多国密浏览器的证书选择页面不会主动弹出需要在设置里手动指定默认客户端证书。7. 常见问题速查与排查思路前面几节走完链路基本就通了。但实际环境里总会冒出各种奇怪的问题这里把我自己遇到过的和帮别人排查过的整理一遍按现象归类方便对号入座。7.1 握手阶段的失败这类问题的共同点是TCP 能连上TLS 握手阶段直接断服务端日志有记录但信息很模糊。排查时我习惯先做一件事——在两端分别用openssl s_client和openssl s_server对打把服务端软件Nginx、Tomcat从链路里摘出去。如果裸的 openssl 能握手成功说明证书和算法都没问题问题出在服务端软件的配置上如果裸的也握手失败那就是证书或者工具链的问题方向立刻清晰。常见原因里排第一的是套件名称格式写错。Nginx 用连字符Java 用下划线而且大小写敏感度在不同版本里不一样。排第二的是协议版本锁死比如服务端只开 TLSv1.3但客户端只支持 TLSv1.2 的国密套件两边根本碰不上。排第三的是双证书配置缺失只加载了签名证书服务端在处理 ClientKeyExchange 时找不到加密证书的私钥握手直接中止。7.2 证书自身的错误这类问题的典型现象是握手能走完但客户端报证书无效。排查重点是看服务端到底下发了什么用-showcerts参数把链拉出来逐张检查。现象大概率原因处理方式报“证书链不完整”只下发了服务端证书没带 CA配置里补上完整链文件报“证书与域名不匹配”SAN 缺失或没写访问用的地址重新签证书补齐 SAN报“证书已过期” / “尚未生效”客户端与服务端时间偏差过大校时或重签时放宽有效期报“密钥用法不允许”keyUsage 里缺少 digitalSignature重新签按用途区分扩展项报“签名算法不支持”证书实际用的是 SHA-256签发时加-sm3重新生成报“无法构建到受信任根的链”根证书没导入客户端信任区导入到受信任的根存储区这张表里的每一条我都至少踩过一次。其中“签名算法不支持”这条最坑因为证书的公钥确实是 SM2用工具看密钥算法完全正确只有看签名算法那一行才能发现问题。所以养成签发后立刻跑一遍openssl x509 -text的习惯比事后排查省时间。7.3 服务端软件层面的坑Nginx 侧的问题集中在库版本上。编译时使用了国密 OpenSSL运行时却链接到了系统自带的 OpenSSL表现就是配置里明明写着国密套件启动时却报“SSL_CTX_set_cipher_list 失败”或者字面上的“unknown cipher”。验证方式是nginx -V看编译参数再用ldd看一眼二进制实际链接的库路径。Tomcat 侧的问题更多出在类加载顺序上。bctls和bcprov两个包的版本必须匹配混用不同版本会出现NoSuchMethodError。另外如果应用自己打了 BC 的包和 Tomcat 的lib目录下的版本冲突也是类似的表现。排查时先确认-verbose:class输出里加载的是哪个路径下的类。还有一个容易被忽略的点把 Provider 注册放在应用代码里而 Tomcat 的 Connector 初始化在应用启动之前结果就是 Connector 初始化的时候 BCJSSE 还没注册上证书加载阶段就失败了。这种时序问题不会报明显的错只会表现成“配置看着都对就是起不来”。稳妥的做法是写一个ServletContextListener或者在setenv.sh里通过 JVM 参数提前注册让 Provider 在 Connector 初始化之前就位。最后分享一个我自己用了很久的小技巧把整套证书生成过程写成一个带参数的脚本把域名、IP、有效期、输出目录做成变量并且在脚本末尾自动跑一遍openssl verify和x509 -text的校验输出。内网环境里换域名、换机器、证书过期重签这些事发生得比想象中频繁有一份可重复执行的脚本比每次翻笔记重敲一遍要靠谱得多。而且脚本本身就是文档接手的人看一眼就知道这条链是怎么造出来的。

相关新闻

ponytail:让命令行任务编排与自动化配置更简单

ponytail:让命令行任务编排与自动化配置更简单

ponytail 这个名字初看有点随意,但你要是用过一次就会发现,它其实是一款非常顺手的命令行任务编排工具。简单说,它把那些反复敲的 Lint、测试、构建、发布、打标签等固定操作,统一封装成一个个可复用的“技能(skill&am…

2026/10/7 8:53:36 阅读更多 →
STM32F103驱动AT24C02 EEPROM实战:硬件接线、I²C时序与HAL库避坑指南

STM32F103驱动AT24C02 EEPROM实战:硬件接线、I²C时序与HAL库避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 8:52:33 阅读更多 →
HTTP/2帧解析实战:hyperframe逐字节拆解帧头与十种帧类型

HTTP/2帧解析实战:hyperframe逐字节拆解帧头与十种帧类型

1. 从一次"帧边界错乱"的排障说起:hyperframes 到底解决什么问题说到 hyperframes,圈内一般指的是 HTTP/2 里那一整套二进制帧(frame)的统称,而在 Python 生态里,它对应一个具体到不能再具体的库…

2026/10/7 8:52:33 阅读更多 →

最新新闻

补码符号位为何能参与运算?从模运算到负权重位讲透

补码符号位为何能参与运算?从模运算到负权重位讲透

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 9:21:14 阅读更多 →
SAP FICO固定资产管理:折旧范围配置实战与AFAB折旧运行解析

SAP FICO固定资产管理:折旧范围配置实战与AFAB折旧运行解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 9:21:14 阅读更多 →
题解:洛谷 P5741 【深基7.例10】旗鼓相当的对手 - 加强版

题解:洛谷 P5741 【深基7.例10】旗鼓相当的对手 - 加强版

本文分享的必刷题目是从蓝桥云课、洛谷、AcWing等知名刷题平台精心挑选而来,并结合各平台提供的算法标签和难度等级进行了系统分类。题目涵盖了从基础到进阶的多种算法和数据结构,旨在为不同阶段的编程学习者提供一条清晰、平稳的学习提升路径。 欢迎大家订阅我的专栏:算法…

2026/10/7 9:21:14 阅读更多 →
贪心题目:使绳子变成彩色的最短时间

贪心题目:使绳子变成彩色的最短时间

文章目录题目标题和出处难度题目描述要求示例数据范围解法思路和算法代码复杂度分析题目 标题和出处 标题:使绳子变成彩色的最短时间 出处:1578. 使绳子变成彩色的最短时间 难度 5 级 题目描述 要求 Alice 把 n\texttt{n}n 个气球排列在一根绳子…

2026/10/7 9:21:14 阅读更多 →
H3 六边形分层地理空间索引系统:核心机制、索引结构与实战入门指南

H3 六边形分层地理空间索引系统:核心机制、索引结构与实战入门指南

GIS 【免费下载链接】h3 Hexagonal hierarchical geospatial indexing system 项目地址: https://gitcode.com/gh_mirrors/h3/h3 点击查看 免费下载 H3 是一个把全球划分为六边形单元(cell)的开源地理空间索引系统,由 H3 Core Li…

2026/10/7 9:21:14 阅读更多 →
CST导出SPICE模型全攻略:txt转cir网表实战与常见坑

CST导出SPICE模型全攻略:txt转cir网表实战与常见坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 9:20:13 阅读更多 →

日新闻

ROS2机械臂仿真与运动控制:从URDF建模到Gazebo实战全解析

ROS2机械臂仿真与运动控制:从URDF建模到Gazebo实战全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:01:58 阅读更多 →
用浏览器直接改ESP32的WiFi密码:NVS键值配置工具设计与实现

用浏览器直接改ESP32的WiFi密码:NVS键值配置工具设计与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:02:00 阅读更多 →
芯片封装缺陷检测:扫描声学显微镜(SAT)原理与实操指南

芯片封装缺陷检测:扫描声学显微镜(SAT)原理与实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:02:00 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 7:15:40 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 5:29:09 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 6:26:51 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 8:21:32 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 4:21:51 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 1:18:13 阅读更多 →