☰
SM2 算法详解:椭圆曲线、签名 ID、密文格式与 OpenSSL 实战
2026/9/29 20:51:24 网站建设 项目流程

SM2 联调中,“同一条消息验签失败”和“另一种语言解不开密文”常常是参数或编码没对齐。本篇先拆开椭圆曲线、公钥加密和签名,再用 OpenSSL 3.5.6 做真实回环与错误 ID 实验。

SM2、SM3、SM4 分别做什么

SM2 是基于椭圆曲线的公钥密码算法体系,包含数字签名、公钥加密与密钥交换等不同方案。SM3 是摘要算法,SM4 是对称分组密码。三者不能只因名称接近就互换。本文实际运行 SM2 加解密和 SM2/SM3 签名,没有实现密钥交换协议。

OpenSSL 的 SM2 文档 说明其 EVP 实现支持签名和加密,并说明 SM2 密钥应采用指定的 SM2 曲线。拿到任意 EC 公钥,并不意味着它可以直接用于 SM2。

椭圆曲线上的“乘法”是什么

在素数域中,可以用 y²=x³+ax+b mod p 描述一类曲线。这里的点坐标是有限域元素,不是绘图软件里的连续实数曲线。给定基点 G 和私有整数 d,公钥点是 P=[d]G;方括号表示反复点加得到的标量乘法,不是把 x、y 各自乘 d。

已知 d 和 G 求 P 很容易;从 G、P 求 d 是椭圆曲线离散对数问题。RSA 使用大整数模幂,两者的数学结构、密钥位数含义与编码不同,不能把“256 位”直接与“2048 位”按数字大小比较强弱。本文不做算法速度排名,也没有进行两者同安全强度的性能测试。

加密里的 C1、C2、C3

把接收方公钥记为 P。概念上,发送方生成一次性随机 k,计算 C1=[k]G 和共享点 [k]P=(x2,y2),从坐标经 KDF 派生与消息等长的掩码,再与消息异或形成 C2;另计算摘要 C3,用于解密时校验。接收方用私钥 d 计算 [d]C1,得到相同共享点。

这是省略了点合法性、无穷远点、全零掩码及失败分支的原理说明,不是可自行上线的实现。完整算法应由经过维护的库执行。原理可对照作者提交的 SM2 说明草案第 7 节;该页面是已过期草案,不是 IETF 正式标准。

C1C3C2 和 C1C2C3 只是三部分的排列方式。还要问 C1 有没有未压缩点前缀 04,x/y 是否各补齐到固定宽度,以及外层有没有 ASN.1 DER 封装。OpenSSL 输出不是直接把三段十六进制拼起来。

本次用 asn1parse 实测得到:密文是一个 SEQUENCE,包含两个 INTEGER 和两个 OCTET STRING。OpenSSL 3.5 的实现源码 将它们定义为 C1x、C1y、C3、C2。ASN.1 INTEGER 可能为保持非负而出现前导 00,因此不能用固定字节偏移盲目切 DER。

签名为何还要一个 ID

SM2 签名把身份标识、曲线参数与公钥等信息形成绑定值 ZA,再与消息一起参与摘要。于是验签不只取决于“相同正文、相同公钥”,双方的 ID 字节也要一致。ID 不是秘密密码,不是可代替私钥的凭证,也不是让任何业务随意填一个昵称就自然完成身份认证。

开发者应在协议中明确 ID 内容及编码,并显式传入。示例采用 ASCII 字符串 1234567812345678,仅作为实验约定,不宣称所有业务都必须采用这个值。

SM2 签名也不是 ECDSA 换一个曲线名。不要把“先在业务层算一次 SM3”和“把原始消息交给 SM2/SM3 高层接口”混为一谈,后者通常负责 ZA 和消息摘要处理。重复预哈希会改变签名输入。

一份可运行的回环与失败实验

2026-09-28 实测环境:macOS 27.2 arm64,Python 3.13.13,PATH 中的 OpenSSL 3.5.6。先运行 openssl version 确认命令来源。保存下面代码为 sm2_demo.py,运行 python3 sm2_demo.py。Python 仅负责文件和进程组织,密码运算由 OpenSSL 完成,不依赖 gmssl 包。

临时目录内的密钥仅为合成实验生成,用完由 TemporaryDirectory 清理;不处理生产私钥。run 函数同时检查成功和预期失败,若错误 ID 意外通过,脚本会直接报错。

frompathlibimportPathfromtempfileimportTemporaryDirectoryimportsubprocessdefrun(*args,ok=True):p=subprocess.run(['openssl',*args],capture_output=True,text=True)assert(p.returncode==0)==ok,(args,p.returncode,p.stderr)returnpwithTemporaryDirectory(prefix='sm2-learning-')astmp:root=Path(tmp)sk,pk,msg,cipher,plain,sig=[str(root/x)forxin('private.pem','public.pem','msg.bin','cipher.der','plain.bin','sig.der')]Path(msg).write_bytes('小静 SM2 实验'.encode('utf-8'))run('genpkey','-algorithm','SM2','-out',sk)run('pkey','-in',sk,'-pubout','-out',pk)run('pkeyutl','-encrypt','-pubin','-inkey',pk,'-in',msg,'-out',cipher)first=Path(cipher).read_bytes()run('pkeyutl','-decrypt','-inkey',sk,'-in',cipher,'-out',plain)assertPath(plain).read_bytes()==Path(msg).read_bytes()run('pkeyutl','-encrypt','-pubin','-inkey',pk,'-in',msg,'-out',cipher)assertPath(cipher).read_bytes()!=firstprint('SM2 roundtrip=OK randomized=True')asn=run('asn1parse','-inform','DER','-in',cipher).stdoutassert'SEQUENCE'inasnandasn.count('INTEGER')==2andasn.count('OCTET STRING')==2print('ciphertext_DER=SEQUENCE(INTEGER, INTEGER, OCTET STRING, OCTET STRING)')run('pkeyutl','-sign','-rawin','-digest','sm3','-inkey',sk,'-in',msg,'-out',sig,'-pkeyopt','distid:1234567812345678')common=('pkeyutl','-verify','-rawin','-digest','sm3','-pubin','-inkey',pk,'-in',msg,'-sigfile',sig,'-pkeyopt')run(*common,'distid:1234567812345678')run(*common,'distid:wrong-id',ok=False)Path(msg).write_bytes(b'changed')run(*common,'distid:1234567812345678',ok=False)print('signature=OK wrong_ID=REJECT changed_message=REJECT')parsed=run('asn1parse','-inform','DER','-in',sig).stdoutassertparsed.count('INTEGER')==2print('signature_DER=SEQUENCE(INTEGER, INTEGER)')

本次实际输出:

SM2 roundtrip=OK randomized=True ciphertext_DER=SEQUENCE(INTEGER, INTEGER, OCTET STRING, OCTET STRING) signature=OK wrong_ID=REJECT changed_message=REJECT signature_DER=SEQUENCE(INTEGER, INTEGER)

本机实验确认了加解密回环、同明文不同密文、正确 ID 验签成功、错误 ID 与修改正文验签失败,以及密文/签名的 DER 结构。签名 DER 为两个 INTEGER 的 SEQUENCE;它不等于固定 64 字节的 r||s 原始拼接,两个格式需要明确转换规则。

命令参数对照 OpenSSL pkeyutl 文档:本例签名和验签都明确使用 -rawin -digest sm3,以及 -pkeyopt distid:…。升级版本后仍应核查默认值,不能因单机回环成功就推断跨库兼容。

真正的联调清单

与对方交换一份完整的测试协议:曲线和算法、输入字节、ID 字节、密文排列及外层格式、点前缀、签名编码、摘要处理层次,以及 Base64/十六进制规则。用非敏感固定消息和测试公钥验证;签名和密文含随机性,通常应比较解密/验签结果,而不是要求每次输出逐字节一样。

失败时先比较 UTF-8 字节长度和十六进制,再核对 ID。接着用 ASN.1 解析工具确认 DER 结构;对端若要求 r||s,使用可靠解析器读取整数并按协议固定宽度编码。不要靠删掉若干头字节“凑够长度”。

产品能否使用,还涉及证书信任、密钥保管、访问控制、协议与设备支持。运行一条 SM2 命令不代表整个系统完成安全改造或通过任何认证。本文只提供可复现的算法接口学习材料。

本文使用 AI 辅助整理与写作,代码和列出的输出已在文中环境实际运行;未进行生产环境、跨厂商或安全认证测试。

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

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

立即咨询