跳到主要内容
ecdsa.com
Node.js

Node.js node:crypto

Node.js 内置的 crypto 模块:基于每个 Node.js 版本自带的 OpenSSL,提供 ECDSA、EdDSA、RSA 和后量子签名——默认使用 DER 签名和随机 nonce,且没有 low-S 策略。
许可证
MIT
检查的版本
24.22.0 ·
源代码
github.com/nodejs/node
文档
nodejs.org/api/crypto.html

兼容性概览

ECDSAEdDSA后量子

ECDSA 曲线
P-224P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1
EdDSA
Ed25519Ed448
后量子算法
ML-KEMML-DSASLH-DSA
公钥格式
spki-pemspki-derx509jwksec1-uncompressedsec1-compressed
私钥格式
pkcs8-pempkcs8-derencrypted-pkcs8sec1-pemsec1-derjwkraw-scalar

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,而不是反复解析字符串。

要检查在其他地方生成的签名,把它和密钥一起粘贴到签名验证工具中;要查看哪些默认行为会与另一套技术栈冲突,在库互操作矩阵中选择两端。

事实及其依据的检查

以下内容均非抄自文档。检查脚本 content/libraries/verify/node-crypto/run.sh 于2026年10月20日在容器镜像 node:24.22.0-bookworm-slim 中运行了 Node.js node:crypto 24.22.0(运行时为 OpenSSL 3.5.8);本页内容必须与它的输出一致,每条事实都列出了对应的输出行。

  • ECDSA 签名编码

    der

    crypto.sign() 返回 ASN.1 DER SEQUENCE,在 P-256 上通常为 70 到 72 字节。在密钥对象中设置 dsaEncoding: "ieee-p1363" 后,它改为返回定长 64 字节的 r‖s 格式;签名端和验证端都必须设置该选项。

    检查记录
    fact ecdsa-signature-encoding = der
    evidence ecdsa-signature-encoding: default output parses as DER: true; ieee-p1363 output length 64
  • 签名时的 low-S

    either

    签名没有经过规范化:检查中生成的 64 个签名里,至少有一个的 s 落在取值范围的上半部分,这符合随机 nonce 的预期。

    检查记录
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: high-S seen among 64 signatures
  • 验证时的 high-S

    accepts-high-s

    对于 s 被替换为 n − s 的签名,无论是 DER 格式还是原始格式,crypto.verify() 都接受。只有严格要求 low-S 的对端才会拒绝。

    检查记录
    fact ecdsa-low-s-verify = accepts-high-s
  • ECDSA 的 nonce

    random

    随机 nonce:用同一密钥对同一消息签名两次得到了不同的字节,用 RFC 6979 的测试密钥也没有复现该 RFC 中的签名。Node.js 不提供确定性 ECDSA 选项。

    检查记录
    fact ecdsa-nonce = random
    evidence ecdsa-nonce: same input twice gives identical bytes: false; r equals RFC 6979 A.2.5: false
  • 签名调用对哪些内容做哈希

    hashes-message

    数据会用你指定的算法计算哈希——在独立实现中,该签名针对消息的 SHA-256 验证通过。没有可以对预先计算的摘要签名的调用,因此要传入消息本身。

    检查记录
    fact ecdsa-prehash = hashes-message
    evidence ecdsa-prehash: signature verifies against SHA-256(message) in @noble/curves: true
  • 默认哈希

    sha256

    算法为 null 或 undefined 时,无论曲线是什么,EC 密钥都使用 SHA-256 签名。对 P-384 和 P-521 密钥要显式指定哈希算法。

    检查记录
    fact ecdsa-default-hash = sha256
  • 格式错误的 ECDSA 签名

    returns-false

    在需要 DER 的地方传入原始 r‖s 签名、在需要原始格式的地方传入 DER,或者传入纯粹的垃圾数据,都会让 crypto.verify() 返回 false;不会抛出任何异常,因此格式问题看起来就像签名错误。

    检查记录
    fact ecdsa-malformed-signature = returns-false
  • ECDSA 曲线

    P-224P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1

    P-224、P-256、P-384、P-521、secp256k1 和 Brainpool r1 曲线都能生成密钥,并完成签名与验证的往返测试。crypto.getCurves() 列出的是 OpenSSL 名称,例如 P-256 对应 prime256v1。

    检查记录
    fact ecdsa-curves = P-224, P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1, brainpoolP512r1
  • EdDSA

    Ed25519Ed448

    Ed25519 和 Ed448 通过 crypto.sign(null, data, key) 签名;Ed448 还接受 context 选项。

    检查记录
    fact eddsa = Ed25519, Ed448
  • 后量子算法

    ML-KEMML-DSASLH-DSA

    在自带 OpenSSL 3.5.8 的 Node.js 24.22.0 中,ML-KEM-512/768/1024(通过 crypto.encapsulate() 和 crypto.decapsulate())、ML-DSA-44/65/87 和 SLH-DSA 均可正常使用。

    检查记录
    fact pqc = ML-KEM, ML-DSA, SLH-DSA
  • 公钥格式

    spki-pemspki-derx509jwksec1-uncompressedsec1-compressed

    crypto.createPublicKey() 能读取 PEM 和 DER 格式的 SPKI、PEM 格式的 X.509 证书、JWK,以及通过 format: "raw-public" 读取裸 SEC1 点(未压缩或压缩形式)。不带 0x04 前缀的 64 字节 x‖y 被拒绝。

    检查记录
    fact public-key-formats = spki-pem, spki-der, x509, jwk, sec1-uncompressed, sec1-compressed
  • 私钥格式

    pkcs8-pempkcs8-derencrypted-pkcs8sec1-pemsec1-derjwkraw-scalar

    crypto.createPrivateKey() 能读取 PEM 和 DER 格式的 PKCS#8、用口令加密的 PKCS#8、PEM 和 DER 格式的 SEC1 EC PRIVATE KEY、带 d 的 JWK,以及通过 format: "raw-private" 读取 32 字节的裸标量。

    检查记录
    fact private-key-formats = pkcs8-pem, pkcs8-der, encrypted-pkcs8, sec1-pem, sec1-der, jwk, raw-scalar

用法

用 P-256 签名并验证,在两端都指明哈希算法和签名编码——这里使用原始 r‖s,以便与 WebCrypto 或 JOSE 对端互通。
import { generateKeyPairSync, sign, verify } from "node:crypto";

const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
const message = Buffer.from("order #1042 approved");

// Name the hash and the encoding explicitly, on both sides:
// "der" for OpenSSL, Java and Go peers, "ieee-p1363" (raw r||s) for WebCrypto and JOSE.
const signature = sign("sha256", message, { key: privateKey, dsaEncoding: "ieee-p1363" });
const ok = verify("sha256", message, { key: publicKey, dsaEncoding: "ieee-p1363" }, signature);

console.log("signature bytes:", signature.length);
console.log("verified:", ok);

代码片段测试工具会在容器镜像 node:24.22.0-bookworm-slim 中原样运行每个示例,只要输出不匹配,运行就会失败。

命令
node usage.mjs
输出必须匹配(正则表达式)
signature bytes: 64\nverified: true

库