跳到主要内容
ecdsa.com

库互操作速查表

在库 A 中签名,在库 B 中验证——哪里会出问题?选择两端,即可得到一份具体不匹配项的检查清单:签名编码、high-S 处理、公钥格式和确定性 nonce,每一项都附有修复它的那行代码。

静态参考数据——无需输入,没有网络请求,也没有任何数据需要发送

openssl dgst / openssl pkeyutl

浏览器、Deno、Node 的 webcrypto

签名方
验证方
库
OpenSSL CLI
WebCrypto(crypto.subtle)
签名
ASN.1 DER
原始 r‖s(P1363)
low-S
两者都接受 · 签名可能为任一种
两者都接受 · 签名可能为任一种
RFC 6979
需手动启用
否——随机 nonce
公钥
SPKI PEM
JWK
曲线
P-256, P-384, P-521, secp256k1
P-256, P-384, P-521

在 OpenSSL CLI 中签名 → 在 WebCrypto(crypto.subtle) 中验证

需修复 2 处问题才能正常工作
会出错

签名格式不匹配:输出 ASN.1 DER,需要 原始 r‖s

OpenSSL CLI 默认生成 ASN.1 DER,而 WebCrypto(crypto.subtle) 要求 原始 r‖s。密钥和数学运算都没有问题——只是验证方读不了这些字节:变长的 DER 数据块对定宽解析器来说长度不对。

在任意一端修复都可以——两种编码承载的信息完全相同,因此转换是无损的,也不涉及任何密码运算。

签名方一侧让 OpenSSL CLI 输出 原始 r‖s

没有内置选项

CLI 无法以 P1363 格式输出 ECDSA 签名。从 DER 结构中取出两个 INTEGER,分别在左侧填充到曲线长度(P-256 上为 32 字节)——`openssl asn1parse -inform DER -in sig.der` 会打印出 r 和 s,方便你用肉眼核对转换结果。

验证方一侧让 WebCrypto(crypto.subtle) 接受 ASN.1 DER

没有内置选项

没有 DER 模式。把定长输出从中间一分为二,分别去掉每一半的前导零,再把这两个整数封装进 DER SEQUENCE,然后交给 OpenSSL 风格的验证方。

† 行为因版本或构建而异——请在你使用的版本中验证。

另请参阅: DER ⇄ 原始格式转换器 · ECDSA 签名格式:DER 与原始格式

需要检查

公钥格式不匹配:SPKI PEM 与 JWK

OpenSSL CLI 把公钥导出为 SPKI PEM,而 WebCrypto(crypto.subtle) 要求 JWK。这确实是不同的容器:SPKI 在点的外层包上曲线标识符,JWK 以 base64url 字段分别写出 x 和 y 坐标,而 SEC1 点只有坐标字节。三者底层的密钥材料完全相同。

OpenSSL CLI:`openssl ec -pubin -in pub.pem -pubout -outform DER` 会把同一个密钥输出为原始 SPKI DER;`openssl ec -pubin -in pub.pem -text -noout` 以十六进制打印 SEC1 未压缩点。

WebCrypto(crypto.subtle):importKey("spki", derBytes, …) 接受的是 SPKI *DER*:先去掉 PEM 的头部和尾部并做 base64 解码,因为 PEM 文本本身会被拒绝。

另请参阅: ECDSA 签名验证工具

值得了解

只使用 P-256 / P-384 / P-521——双方都支持的曲线

两端支持的曲线集合不一致:secp256k1 仅 OpenSSL CLI 支持。选用双方共同支持的曲线上的密钥,就不会遇到这个问题。

OpenSSL CLI:构建时启用的全部曲线:NIST 素数曲线、secp256k1、Brainpool 等——你手上的二进制文件支持哪些,以 `openssl ecparam -list_curves` 的输出为准。

WebCrypto(crypto.subtle):仅支持三条 NIST 曲线。secp256k1 不在规范之内,因此比特币和以太坊密钥需要改用 JavaScript 库。

值得了解

除非你手动启用 RFC 6979,否则 OpenSSL CLI 使用随机 nonce

常规的签名路径使用随机 nonce。从 3.2 版本起,可以用签名参数 `nonce-type:1` 选择确定性 ECDSA(RFC 6979),同时显式指定摘要算法:`openssl dgst -sha256 -sign key.pem -sigopt nonce-type:1`,或 `pkeyutl -pkeyopt nonce-type:1 -pkeyopt digest:sha256`。

这是正确的行为,不是 bug——但这意味着相同的密钥和消息每次运行都会产生不同的字节,因此逐字节比较签名的测试会失败。应改为比较验证结果。

† 行为因版本或构建而异——请在你使用的版本中验证。

所有库并排对比

仅列出默认行为——每一行都可以通过某个标志或改用其他调用来改变。横向滚动可查看全部列。

库默认签名格式如何切换low-S 策略RFC 6979默认公钥格式
OpenSSL CLI
openssl dgst / openssl pkeyutl
签名方
ASN.1 DER
ASN.1 DER——由两个 INTEGER 组成的 SEQUENCE,因此 P-256 签名通常为 70~72 字节,而且长度随签名而变化。
CLI 没有原始 r‖s 模式——需要在外部转换 DER 输出。†
两者都接受
签名和验证两个方向都没有 low-S 策略:大约一半的签名中 s 位于上半区间,验证时也从不检查这一点。
需手动启用
常规的签名路径使用随机 nonce。从 3.2 版本起,可以用签名参数 `nonce-type:1` 选择确定性 ECDSA(RFC 6979),同时显式指定摘要算法:`openssl dgst -sha256 -sign key.pem -sigopt nonce-type:1`,或 `pkeyutl -pkeyopt nonce-type:1 -pkeyopt digest:sha256`。†
SPKI PEM
SPKI PEM:由 `openssl ec -pubout` 写出的 `-----BEGIN PUBLIC KEY-----` 块。注意,并不存在 `-----BEGIN EC PUBLIC KEY-----` 这种格式——带 EC 标签的块是私钥。
Node.js node:crypto
crypto.sign / crypto.verify / KeyObject
ASN.1 DER
ASN.1 DER,除非你明确要求使用另一种编码。
在密钥对象上设置 `dsaEncoding: "ieee-p1363"`——签名时*和*验证时都要设置。
两者都接受
上下两半的 s 都接受,也不做任何规范化——底层就是 OpenSSL。
否——随机 nonce
随机 nonce:相同的密钥和消息每次调用都会产生不同的签名字节。
SPKI PEM
`publicKey.export({ type: "spki", format: "pem" })` 是常见形式;改用 `export({ format: "jwk" })` 只需一次调用即可得到 JWK。
WebCrypto(crypto.subtle)
浏览器、Deno、Node 的 webcrypto
验证方
原始 r‖s(P1363)
只支持原始 r‖s,长度固定:P-256 上为 64 字节,P-384 上为 96 字节,P-521 上为 132 字节。规范中没有定义其他编码。
无可切换——WebCrypto 从不使用 DER。请在 API 之外转换。
两者都接受
签名和验证时都没有 low-S 规则。
否——随机 nonce
随机 nonce,而且 API 不允许你自行提供 nonce。
JWK
导入和导出时总要指定格式:"jwk"、"spki"(DER 字节)或 "raw"(SEC1 未压缩点)。大多数 Web 代码传递的是 JWK 形式。
Go crypto/ecdsa
SignASN1 / VerifyASN1,或 big.Int 数对
ASN.1 DER
`SignASN1`(`crypto.Signer` 走的就是这条路径)返回 ASN.1 DER。更底层的 `Sign` 以 `*big.Int` 形式返回 r 和 s,在你编码之前,它不属于任何一种传输格式。
DER 用 `SignASN1`/`VerifyASN1`;定宽原始格式用 `Sign`/`Verify` 加 `FillBytes`。
两者都接受
完全不处理 low-S:验证时上下两半区间的 s 都接受。
否——随机 nonce
与 RFC 6979 不兼容。较新的版本会同时根据密钥、消息和新生成的随机数派生 nonce,因此每次运行得到的签名字节都不同,无法在测试中固定。†
SPKI DER
`x509.MarshalPKIXPublicKey` 生成 SPKI DER;再调用一次 `pem.EncodeToMemory` 就能得到 PEM 块。
Python cryptography(pyca)
hazmat.primitives.asymmetric.ec
ASN.1 DER
DER——`private_key.sign(...)` 返回 ASN.1 SEQUENCE。
`decode_dss_signature` / `encode_dss_signature` 在 DER 与整数对之间转换;定宽填充需要你自己添加。
两者都接受
上下两半的 s 都接受;没有任何环节会规范化 s。
需手动启用
默认使用随机 nonce。较新的版本允许在 ECDSA 对象上设置确定性签名标志,这条路径要求底层是较新的 OpenSSL——依赖它之前,先确认你的 wheel 是基于哪个版本构建的。†
SPKI PEM
`public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)`——与 OpenSSL 写出的 PEM 块相同。
Python ecdsa(纯 Python)
SigningKey / VerifyingKey,sigencode= 与 sigdecode=
原始 r‖s(P1363)
原始 r‖s:`sign()` 返回拼接在一起的两个值,并用零填充到曲线长度。
每次调用签名和验证时,都由 `sigencode=` 和 `sigdecode=` 参数选择编码。
两者都接受
上下两半的 s 都接受。该库还提供了规范化编码器,强制把 s 放到下半区间——它们与普通编码器并存,必须显式选择。†
需手动启用
RFC 6979 确定性签名以单独的签名方法提供;普通的 sign() 调用使用随机数。
SEC1 点
密钥以裸点形式传递:`VerifyingKey.to_string()` 默认返回不带 0x04 前缀的 x‖y,基于 SPKI 的技术栈无法直接接受。†
Java JCA(SunEC)
java.security.Signature
ASN.1 DER
`SHA256withECDSA` 生成 ASN.1 DER。
在算法名称后加上 `inP1363Format`——同一个类,不同的传输格式。
两者都接受
签名和验证两个方向都没有 low-S 策略。
否——随机 nonce
SunEC 使用随机 nonce 签名。添加 Bouncy Castle 等第三方提供者即可使用确定性 ECDSA。†
SPKI DER
`publicKey.getEncoded()` 返回 X.509 SPKI DER。JDK 不支持 PEM——base64 封装需要你自己完成。
Rust p256 / k256(RustCrypto)
ecdsa::Signature、SigningKey、VerifyingKey
原始 r‖s(P1363)
默认的 `Signature` 类型就是定宽数对——在 P-256 和 secp256k1 上为 64 字节。DER 需要单独、显式地转换。†
定宽数对是默认类型;与 DER 之间的双向转换都需要显式进行。†
两者都接受
secp256k1 的 crate 在签名时会把 s 规范化到下半区间,与比特币的约定一致;NIST 曲线的 crate 则不会。验证时两种都接受,因此对端要求严格时,应在签名端做规范化。†
是,默认启用
默认使用 RFC 6979 确定性 nonce——相同的密钥和消息总是产生相同的字节。随机化变体需要手动启用。
SEC1 点
`VerifyingKey` 序列化为 SEC1 编码点——压缩格式 33 字节,未压缩格式 65 字节。SPKI DER 和 PEM 通过配套的 SPKI/PKCS#8 trait 提供。†
libsecp256k1(Bitcoin Core)
secp256k1_ecdsa_sign / _verify
由你选择
序列化之前两者都不是:签名存放在一个不透明的内部结构体中,输出时再选择 compact(原始 r‖s)或 DER。
序列化为 compact 得到原始 r‖s,序列化为 DER 得到比特币的传输格式——每次调用单独选择。
强制 low-S
最严格的一个。签名时总是输出 low-S,验证时直接拒绝 high-S 签名——该库专门提供了规范化调用,就是为了让你修正来自通用签名方的签名。
是,默认启用
默认使用 RFC 6979;如有需要,可以传入其他 nonce 函数。
SEC1 点
SEC1 点:压缩格式 33 字节,未压缩格式 65 字节。API 中完全没有 SPKI、PEM 或 JWK。
PHP openssl_*
openssl_sign / openssl_verify
ASN.1 DER
DER——`openssl_sign()` 原样返回 OpenSSL 生成的结果。
完全没有原始格式模式:在自己的代码中转换,或使用已经实现了这一步的 JOSE 库。
两者都接受
没有 low-S 策略——原样继承 OpenSSL 的行为。
否——随机 nonce
随机 nonce;该扩展没有提供确定性模式。
SPKI PEM
输入和输出都是 PEM 字符串:`openssl_pkey_get_public()` 接受 `-----BEGIN PUBLIC KEY-----` 块或整张证书。
Ruby OpenSSL
OpenSSL::PKey::EC
ASN.1 DER
DER——`key.sign(digest, data)` 返回 ASN.1 SEQUENCE。
没有内置的原始格式模式:通过 OpenSSL::ASN1 手动转换,或者交给 JOSE gem 处理。
两者都接受
没有 low-S 策略——行为与 OpenSSL 完全一致。
否——随机 nonce
随机 nonce;没有提供确定性选项。
SPKI PEM
SPKI PEM。较新的版本提供了专门导出公钥的方法——值得使用,因为对含有私钥部分的密钥直接调用 `to_pem`,写出的是私钥。†

† 行为因版本或构建而异——请在你使用的版本中验证。

进一步了解

How it works

A signature that verifies in one library and fails in another is almost never wrong mathematically: the two sides simply disagree about packaging. An ECDSA signature is a pair of integers (r, s), and each ecosystem picked its own way to serialize them — ASN.1 DER in the OpenSSL and X.509 lineage, fixed-width r‖s in WebCrypto, JWS and most blockchain stacks. On top of that sit two more sources of disagreement: whether s must be in the lower half of the range (the low-S rule that Bitcoin enforces and general-purpose libraries ignore), and how the public key itself is wrapped — an SPKI structure, a JWK object, or the bare SEC1 point.

This page compares those defaults for a pair of libraries and lists only the differences that actually cause a failure, together with the switch or conversion that removes each one. Every row describes what a library does when you call the obvious API without extra arguments — not what it is capable of. Nearly all of it is configurable, which is exactly why two teams reading the same documentation end up incompatible, and why the fix is usually one option rather than new code.