基于OpenSSL实现SM2国密算法:从原理到落地的完整复盘
2026/9/3 19:35:30 网站建设 项目流程

简介:面向密码学与国密算法学习者,基于OpenSSL库实现的国密SM2算法资源,核心覆盖数字签名与密钥交换两大功能,公钥加密未包含。代码使用SM2推荐椭圆曲线,密钥派生函数采用标准HASH算法而非SM3,便于与作者其他SM3实现对照改造。压缩包仅9KB,共6个文件,包括2个C源文件、2个头文件及VC工程文件(dsw/dsp),整体轻量,适合快速阅读算法流程。目前已有8282人学习浏览。演示代码完整展示签名验签、密钥交换的关键步骤,并附带SM2建议曲线测试用例,可帮助读者理解SM2数学原理与OpenSSL接口调用方式。作者明确说明仅供算法过程演示,不适用工程应用,学习者可在其基础上补充公钥加密、替换KDF为SM3,或优化错误处理以加深掌握。压缩包小巧但知识密度较高,适合具备一定密码学基础、希望结合代码研读国密规范的人群。 前阵子给一个金融客户做国密改造,对方上来就问了一句:你们的OpenSSL支持SM2吗?我随口回了一句:支持啊,1.1.1之后原生支持。结果到了现场才发现,“支持”和“能用”之间起码隔着一个项目周期的距离:证书链不认、签名算法标识不对、加密证书和签名证书混用、压测工具不认国密套件,一个接一个冒出来。这篇文章就基于OpenSSL落地SM2算法的完整过程做个复盘,从算法原理、命令实操、代码封装一直讲到问题排查,适合正在推进国密改造的研发和运维同学,也适合刚接触国密体系、想把SM2真正跑通一次的读者。

1. 国密改造到底改的是什么:先搞清楚需求再动手

1.1 为什么突然要碰SM2

很多刚接触这个领域的同事,一听到“国密改造”四个字,第一反应就是“把RSA换成SM2”。但真做起来你会发现,国密算法不是单个算法,而是一整套密码体系的替换:SM2负责非对称运算(签名、加密、密钥协商),SM3负责摘要,SM4负责对称加密,ZUC是序列密码,主要在移动通信场景出现,一般业务系统用不到。四者组合在一起,才能形成完整的密码应用闭环。

单从OpenSSL角度来看,从1.1.1版本开始,OpenSSL确实原生支持SM2、SM3、SM4,这解决了一大半问题。但1.1.1之前的老版本不支持国密算法,想用只能靠GmSSL这类分支或者自己打补丁。所以你会发现很多老系统在国密改造时第一个动作就是升级OpenSSL,这事本身不是为了让版本号好看,而是为了拿到国密算法族的底层能力。而升级OpenSSL又牵扯系统库、编译环境、应用链接方式,一步没弄好,整条业务链都可能挂,这个后面专门讲。

1.2 双证书体系:签名和加密必须分开

国密体系里最容易让人懵的就是双证书。RSA体系一张证书全搞定,国密体系必须两对密钥:一对签名密钥,一对加密密钥。很多人不理解为什么非要弄两对,增加工作量。实际上这是安全边界的必然要求。

签名私钥的作用是证明“你是你”,一旦泄露,别人就能冒充你的身份去签名;加密私钥的作用是解开别人发给你的密文,一旦泄露,历史加密数据全部暴露。两种密钥泄露后的风险截然不同,隔离存放是合理的。更关键的是工程上的差异:加密私钥通常允许托管备份(不然用户忘了口令,谁也救不回数据),签名私钥则严格禁止托管。这个差异直接导致证书申请流程、密钥存储方案、应用代码结构全都跟着变。

可以把两者对比着看:

维度RSA体系国密SM2体系
密钥对数1对,公私钥各自一个2对,签名对+加密对
私钥用途签名、解密共用签名私钥只签名,加密私钥只解密
私钥托管一般不强制区分加密私钥可托管,签名私钥禁止托管
证书链路一张证书即可签名证书和加密证书分别签发
应用改动改动小所有密钥存储、调用的代码都要区分

这套双证书逻辑贯穿国密改造的每个环节,后面实操部分还会涉及。

2. SM2算法核心原理:不用啃完数学,但要懂这几件事

2.1 椭圆曲线密码的基本逻辑

SM2本质上是椭圆曲线公钥密码算法,跑在素域F(p)上一条256位曲线上。它的安全基础是椭圆曲线离散对数问题:给你一个基点G和计算结果kG,想反推出k是多少,目前没有任何多项式时间算法。用生活类比就是,给你一张地图上的A点和B点,告诉你“从A出发沿着某个方向走了多少步到B”这条信息很容易验证,但只给你A和B,让你反推出走了多少步,几乎不可能。

SM2的256位密钥安全强度大约相当于128位对称密钥,对标RSA要做到3072位才有同等水平。所以SM2的密钥短、计算快、证书体积小,这也是为什么国密体系在性能上并不吃亏。OpenSSL里把SM2归入EC算法体系,但曲线参数是国密标准专门定义的,内部叫SM2曲线,和普通的prime256v1不是一回事。后面生成密钥时,曲线名选错会导致整套体系都识别为“非国密”,这个坑必须提前避开。

2.2 签名、加密、密钥交换的差异

SM2不是一个算法,而是一族算法,分三种用途,实现时千万不能混:

  • 签名:对消息先做SM3摘要,再用私钥对摘要签名。签名值由r和s两个256位整数拼接而成,长度约64字节。验签时用公钥,并结合用户ID参与计算。
  • 加密:过程稍微复杂,会先临时生成一对密钥做密钥协商,派生出对称密钥来加密消息,最终密文结构是C1||C3||C2。其中C1是临时公钥点,C3是SM3杂凑值,C2才是真正的密文。
  • 密钥交换:类似ECDH,但双方身份信息会参与运算,能防中间人攻击,适合TLS握手这类场景。

这三种用途在OpenSSL里的接口完全不同。签名走dgst系列,加密走pkeyutl系列,密钥交换要结合TLS套件用。如果拿着加密的功能去当签名用,或者反过来,都会在对接时出现莫名其妙的错误。

2.3 用户ID和设备参数:默认值也是坑

SM2签名过程会把“用户ID”混入杂凑计算,这是它和RSA签名的一个明显区别。国密标准里定义了一个默认用户ID:“1234567812345678”,16字节的ASCII字符串。如果通信双方一个用默认ID,另一个自定义了ID,验签必然失败,而且报错信息往往很隐晦,一会儿是verify error,一会儿是bad signature,根本猜不到是ID不匹配。

在OpenSSL的EVP接口里,可以通过EVP_PKEY_CTX_set1_id来设置用户ID。很多刚接触SM2的开发者根本没意识到还有这个参数,直接用默认值跑通了本地测试,一到和其他厂商系统对接就挂,最后排查半天才发现是ID不一致。记住一句话:SM2联调时,用户ID必须和对端完全一致,包括长度和值。

3. 基于OpenSSL实现SM2的完整实操

3.1 环境准备:OpenSSL版本怎么选

优先选OpenSSL 1.1.1或3.x LTS版本。1.1.1原生支持SM2、SM3、SM4;3.x版本也支持,但要注意3.x如果开启FIPS模式,默认不开放国密算法,必须跑在非FIPS模式下才能正常使用。拿到新环境先执行两条命令确认:

openssl version -a openssl list -public-key-algorithms | grep -i sm2 openssl list -digest-algorithms | grep -i sm3

第一条命令看版本和编译配置,第二条如果输出SM2相关算法名,说明当前版本支持国密。如果输出为空,说明版本早于1.1.1,或者编译时没带相关模块。

编译安装时,强烈建议用独立prefix,不要覆盖系统自带的OpenSSL:

./config --prefix=/usr/local/openssl shared zlib make -j"$(nproc)" make install

这一步在CentOS 7.6上尤其重要。系统自带的1.0.2k被yum、curl等工具依赖,贸然覆盖会导致系统级工具直接崩溃。装在独立目录后,通过环境变量引用:

export PATH=/usr/local/openssl/bin:$PATH export LD_LIBRARY_PATH=/usr/local/openssl/lib64:$LD_LIBRARY_PATH

3.2 密钥对生成与查看

密钥生成是第一步,也是后面所有操作的基础。直接给命令:

# 生成SM2私钥 openssl ecparam -genkey -name SM2 -out sm2_private.pem # 从私钥导出公钥 openssl ec -in sm2_private.pem -pubout -out sm2_public.pem # 转成PKCS#8格式(很多应用只认这个格式) openssl pkcs8 -topk8 -in sm2_private.pem -out sm2_private_pkcs8.pem -nocrypt # 查看私钥明细 openssl ec -in sm2_private.pem -text -noout

为什么生成SM2密钥要用ecparam而不是genrsa?因为SM2本质是椭圆曲线密钥,OpenSSL把它归入EC算法,但曲线必须是国密标准定义的SM2曲线。-name SM2这个参数就是在告诉OpenSSL“用SM2这条标准曲线”。如果误用其他曲线,比如prime256v1,后面签名、证书也能生成,但整个体系都会被识别为非国密,等于白做。

另外,PKCS#8格式转换很值得养成习惯。很多密码机、签名服务器、Java的KeyStore都只认PKCS#8,提前转好能省很多对接时间。

3.3 证书请求与自签名

有了密钥对,下一步就是生成证书。测试环境可以用自签名,生产环境要把CSR提交给CA签发:

# 生成SM2证书请求 openssl req -new -key sm2_private.pem -out server.csr -sm2 -subj "/CN=sm2.example.com" # 自签名证书(仅测试用) openssl x509 -req -in server.csr -signkey sm2_private.pem -out server.crt -days 365 -sm2 # 查看证书详情 openssl x509 -in server.crt -text -noout

这里的-sm2参数很容易漏。漏掉之后,生成的CSR或证书签名算法会变成错误标识,证书虽然能生成,但对端一验就能发现签名算法不是国密,握手失败。查看证书时,注意Signature Algorithm应该是SM3-SM2。

双证书场景下,签名证书和加密证书必须分别用两套密钥各生成一个CSR,再提交给CA。不能拿一套密钥同时签两份证书。很多第一次做国密改造的团队,以为“双证书”就是拿一份密钥签两次,这个理解是错的。

3.4 签名、验签、加解密实操

命令层面的签名验签和加解密是理解SM2工作原理最快的方式:

# SM3摘要 openssl dgst -sm3 data.txt # SM2签名(内部走SM3摘要) openssl dgst -sm3 -sign sm2_private.pem -out data.sig data.txt # 验签 openssl dgst -sm3 -verify sm2_public.pem -signature data.sig data.txt # SM2加密(公钥加密) openssl pkeyutl -encrypt -pubin -inkey sm2_public.pem -in plain.txt -out cipher.bin # SM2解密(私钥解密) openssl pkeyutl -decrypt -inkey sm2_private.pem -in cipher.bin -out plain_restored.txt

注意,pkeyutl -encrypt是SM2公钥直接加密,适合加密少量数据,通常就几十字节。实际业务中不建议直接拿SM2加密大文件,正确做法是混合加密:随机生成一个SM4密钥,用SM4加密业务数据,再用SM2加密这个SM4密钥。这样既保证了SM2的国密合规性,又有SM4的对称加密性能。

3.5 代码封装:EVP接口调用要点

命令能跑通后,要嵌入业务程序还得用OpenSSL的EVP接口。用C语言做SM2签名,核心代码大致是这样一个流程:

EVP_MD_CTX *mdctx = EVP_MD_CTX_new(); EVP_DigestSignInit(mdctx, NULL, EVP_sm3(), NULL, pkey); // 关键:设置SM2用户ID,必须与对端一致 EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_from_pkey(NULL, pkey, NULL); unsigned char uid[] = "1234567812345678"; EVP_PKEY_CTX_set1_id(pctx, uid, sizeof(uid) - 1); EVP_DigestSignUpdate(mdctx, data, data_len); size_t sig_len = 0; EVP_DigestSignFinal(mdctx, NULL, &sig_len); unsigned char *sig = malloc(sig_len); EVP_DigestSignFinal(mdctx, sig, &sig_len);

这里有几个容易出错的地方。摘要算法必须显式写EVP_sm3(),不能默认用SHA256,否则签名算法就变成了SM2+SHA256,对端会拒绝。用户ID一定要通过EVP_PKEY_CTX_set1_id设置,并且和对端保持一致。OpenSSL 1.1.1里SM2相关算法默认可用;3.x版本则通过default provider加载,代码层面EVP接口基本不用改,但配置上要留意FIPS开关。

4. 踩坑实录:SM2+OpenSSL常见问题排查

4.1 开发环境问题

老CentOS上做国密改造,第一关往往是编译OpenSSL。系统自带的Perl版本过旧或者缺少模块,会直接报“Perl is needed by openssl”。解决方法是先补齐依赖:

yum install -y perl-core perl-IPC-Cmd

编译完成后,最常碰到的诡异问题是:明明安装了新版OpenSSL,程序运行起来却还是走老版本路径。表现就是程序链接时报“header version mismatch”,或者运行时报SSL routines::unexpected eof while reading。原因基本都是系统里同时存在多套OpenSSL,动态库搜索路径没配对。用一句话确认:ldd 你的程序 | grep crypto,看实际链接的libcrypto.so是哪个路径下的。

Windows环境下装OpenSSL也有类似的坑。PATH环境变量里如果同时存在多个OpenSSL版本,命令行里openssl version显示的是一个版本,程序实际加载的DLL可能又是另一个版本。解决思路和Linux一致:只保留一个版本在PATH里,或者直接用完整路径调用。

4.2 运行时报错排查

把几个月来遇到的高频报错整理成一张速查表,基本覆盖了SM2+OpenSSL的常见问题:

报错信息可能原因排查方向
ssl routines::unexpected eof while readingTLS握手时对端直接断开,国密套件协商失败或证书不匹配抓包看ServerHello,确认套件和证书链
sm2 sign error / no id set签名时未设置用户ID或双方ID不一致两端统一用户ID
bad signature / verify failure验签时公钥不匹配,或用户ID不一致确认公钥对、ID、摘要算法
invalid ec parameters / wrong curve密钥不是SM2曲线用ecparam -name SM2重新生成
expecting: ANY PRIVATE KEY私钥格式不对或文件损坏检查PEM格式,必要时转PKCS#8
RSA_padding_check_PKCS1_type_1:invalid padding把RSA的加解密逻辑套到了SM2上确认接口类型,SM2加密要用pkeyutl,不能套RSA代码
git报curl 56 OpenSSL SSL_read错误国密改造后中间设备断开TLS连接检查代理、负载均衡是否支持国密套件
Certificate signature algorithm is not SM2证书链中混入了RSA证书全链路证书都要是国密签发

其中unexpected eof while reading这个报错特别有迷惑性。第一次遇到时我以为是网络问题,后来抓包才发现是负载均衡器不支持国密套件,直接把TLS握手请求断掉了。国密改造排查这类问题,Wireshark看ServerHello里协商的套件名称是最快的手段。

4.3 对接联调阶段的实践建议

真正进入联调阶段后,最磨人的往往不是算法本身,而是“你以为全链路都是SM2了,其实某个中间环节还在用RSA”。比如网关转发了请求但没开国密套件,比如压测工具(LoadRunner这类)没有加载国密算法库导致压测全是握手失败,再比如CMS格式里的SignedAndEnvelopedData没有按国密标准生成。这些问题不跑真实业务报文根本发现不了。

我的建议是,在动代码之前先做一次全链路算法扫描:把TLS证书、签名验签、数据加解密、密钥管理各个环节全部列出来,逐个确认用的是哪套算法。然后搭一个最小闭环——OpenSSL生成双证书、自签名、签名验签、加密解密全部跑通——再往业务代码里嵌EVP接口。这条路径能帮你快速区分“算法问题”和“业务代码问题”,省掉大量无效联调时间。

最后再提一个容易忽略的点:国密证书链是全链路的,不只是服务器证书。如果只换了服务器证书,根证书和中间证书还是RSA的,TLS握手一样会失败。而且国密握手对客户端也有要求,客户端必须支持国密套件,否则再好的服务端配置也白搭。测试时建议客户端和服务端都用国密算法库,一步步验证,别急着端到端一把梭。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询