node:crypto 是随每个 Node.js 版本发布的密码学模块。它的一次性函数 crypto.sign() 和 crypto.verify() 以及 KeyObject 类支持 ECDSA、Ed25519、Ed448、RSA,从 Node.js 24.22 起还支持 ML-DSA、SLH-DSA 和 ML-KEM。一切都运行在 Node.js 自带的 OpenSSL 之上(Node.js 24.22.0 中为 3.5.8),因此大部分行为与 OpenSSL 命令行一致——而与同一进程以 crypto.subtle 提供的 WebCrypto API 不同。
你最可能遇到的互操作问题是签名编码。crypto.sign() 返回 ASN.1 DER,而 WebCrypto、JWS/JWT、.NET 和大多数硬件钱包使用定长的原始 r‖s 格式。另一种编码的签名不会抛出异常:crypto.verify() 只是返回 false,看上去和伪造的签名完全一样。在签名端和验证端都为密钥对象设置 dsaEncoding: "ieee-p1363",或者用 DER ⇄ 原始格式转换器转换字节。
当对端要求严格时,另外两个不那么显眼的默认行为也很关键。签名从不规范化为 low-S,因此大约一半的签名会被强制要求 low-S 的验证方拒绝,例如 libsecp256k1 和 @noble/curves 2.x。此外,nonce 是随机的,同一密钥和同一消息每次调用都会得到不同的字节——测试中应比较验证结果,而不要比较签名字节。如果某个签名者重复使用了 nonce,nonce 重用检测工具就能恢复出私钥,所以随机 nonce 需要可靠的随机源。
把算法设为 null 时,无论是哪条曲线,EC 密钥都会悄悄地使用 SHA-256 签名;这样一来,P-384 密钥的签名在任何要求 SHA-384 的对端都会验证失败。每次调用都要指明哈希算法;用 crypto.createPublicKey() 或 crypto.createPrivateKey() 只加载一次密钥——它们能读取 PEM、DER、JWK,甚至裸 SEC1 点——并保留得到的 KeyObject,而不是反复解析字符串。
要检查在其他地方生成的签名,把它和密钥一起粘贴到签名验证工具中;要查看哪些默认行为会与另一套技术栈冲突,在库互操作矩阵中选择两端。