crypto.subtle 是 Web 平台的标准密码学接口。同样的调用——generateKey、importKey、sign、verify、exportKey——在 Chromium、Firefox、WebKit 以及 Node.js 等服务端运行时中都能使用,因此对于必须在两端运行的代码,它是自然的选择。它只存在于安全上下文中:在普通 http 页面上,crypto.subtle 是 undefined。
规范把 ECDSA 签名格式固定为原始 r‖s,在 P-256 上为 64 字节,检查在全部四个引擎中得到的正是这种格式。它与 JWS 和 JWT 一致,因此 ES256 令牌无需转换;但它与 OpenSSL、Java 的 SHA256withECDSA、Go 的 SignASN1 和 node:crypto 都不一致,这些使用的都是 ASN.1 DER。把 DER 签名传给 verify() 时,它只返回 false 而不报错,因此这种不匹配看起来就像伪造的签名;可以用 DER ⇄ 原始格式转换器转换,或在签名验证工具中检查两种形式。
密钥是另一个常见的麻烦来源。importKey("spki") 接收 DER 字节,从不接收 PEM 文本;"raw" 接收 SEC1 点:Node.js、Chromium 和 Firefox 接受压缩形式,WebKit 则拒绝,因此可移植的代码应发送未压缩点。私钥只能以 PKCS#8 DER 或 JWK 格式导入——OpenSSL 默认写出的 SEC1 EC PRIVATE KEY 文件必须先转换为 PKCS#8,例如使用 JWK 转换器。
曲线列表只有 P-256、P-384 和 P-521:secp256k1 和 Brainpool 在所有引擎中都会抛出 NotSupportedError,因此比特币和以太坊密钥需要 @noble/curves 这样的库。Ed25519 在全部四个引擎中都能使用;Ed448 只能在 Node.js 中使用。后量子支持参差不齐:Node.js 24.22 实现了 ML-DSA 和 ML-KEM,但会给出实验性警告;Chromium 156 接受 ML-DSA 和 ML-KEM-768/1024,但不接受 ML-KEM-512;Firefox 157 和 WebKit 27.2 两者都不支持——依赖它们之前要先做特性检测,并可以在后量子签名工具中试用这些算法。
示例展示了可移植的写法:在算法对象中指明哈希算法,导入外部的 PEM 密钥时先把它解码为 DER,并记住 64 字节的输出必须先包装成 DER,OpenSSL 风格的验证方才能读取。