You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SubtleCrypto验签失败但Node.js Crypto验签成功,如何解决?

修复SubtleCrypto ECDSA验签失败问题

我在Node环境中实现WebAuthN服务端功能时遇到一个问题:Node原生crypto模块的验签结果符合预期(返回true),但SubtleCrypto的验签结果始终为false。我需要使用SubtleCrypto,因为最终要部署到无服务器平台。

以下是我的测试代码:

import nCrypto from "crypto";
const base64UrlToUintArray = (src) => {
  const raw = atob(src.replaceAll("-", "+").replaceAll("_", "/"));
  return Uint8Array.from(raw, (char) => char.charCodeAt(0));
};

const jwk = {
  kty: "EC",
  alg: "ES256",
  crv: "P-256",
  x: "uymD4lAdTz26QJV30-DRkEt1d9WTazEKPfuWN3Fyd9U",
  y: "BZvD93pXLYfqXkp9sqylz_UN88OLapiV8drZQHdUCZE",
};
const input = Uint8Array.from([
  177, 105, 230, 129, 73, 100, 147, 50, 55, 183, 116, 206, 222, 74, 197, 72,
  177, 173, 14, 226, 46, 148, 229, 253, 77, 144, 96, 248, 233, 108, 69, 171, 5,
  101, 81, 31, 94, 138, 188, 129, 159, 72, 206, 19, 189, 169, 103, 118, 178, 17,
  79, 84, 126, 190, 133, 144, 77, 172, 161, 115, 67, 100, 154, 222, 235, 62, 5,
  6, 187,
]);
const signature =
  "MEQCIAE8ZT7CC9uQFaOIzPaEtLwnIGgjJLDYeZTyOc9hzxmkAiAphf71sZD5zxfAu0ELOOcn6yAfzu-Jbm-XT3SLe9dQgw";

const key = await nCrypto.subtle.importKey(
  "jwk",
  jwk,
  { name: "ECDSA", namedCurve: "P-256" },
  false,
  ["verify"]
);
const verified = await nCrypto.subtle.verify(
  {
    name: "ECDSA",
    hash: "SHA-256",
  },
  key,
  base64UrlToUintArray(signature),
  input
);
const verified2 = nCrypto
  .createVerify("sha256")
  .update(Buffer.from(input))
  .verify(
    nCrypto.createPublicKey({ key: jwk, format: "jwk" }),
    Buffer.from(base64UrlToUintArray(signature))
  );
console.log("web crypto verification", verified);
console.log("node crypto verification", verified2);

问题原因

Node原生crypto模块会自动识别并处理ASN.1/DER格式的ECDSA签名,而SubtleCrypto要求签名必须是原始的R/S格式(IEEE P1363标准)——也就是将签名拆分为等长的R和S两部分,拼接成连续的字节数组。你当前的签名是ASN.1/DER格式(开头的MEQC是Base64编码的ASN.1头),这就是SubtleCrypto验签失败的核心原因。

修复步骤

  1. 添加一个转换函数,将ASN.1/DER格式的签名转换为SubtleCrypto需要的原始R/S格式:
// 将ASN.1/DER格式的ECDSA签名转换为原始R/S格式(P-256适用,64字节总长度)
function derToRawSignature(derSignature) {
  const buffer = Buffer.from(derSignature);
  // ASN.1结构:0x30 (SEQUENCE) + length + 0x02 (INTEGER) + rLength + r + 0x02 (INTEGER) + sLength + s
  let offset = 0;
  if (buffer[offset] !== 0x30) throw new Error("Invalid signature format");
  offset += 1;
  // 处理长度字段(简单长度,这里忽略多字节长度的情况,因为P-256签名不会超过127字节)
  const length = buffer[offset];
  offset += 1;
  // 读取R部分
  if (buffer[offset] !== 0x02) throw new Error("Invalid R field");
  offset += 1;
  const rLength = buffer[offset];
  offset += 1;
  const r = buffer.slice(offset, offset + rLength);
  offset += rLength;
  // 读取S部分
  if (buffer[offset] !== 0x02) throw new Error("Invalid S field");
  offset += 1;
  const sLength = buffer[offset];
  offset += 1;
  const s = buffer.slice(offset, offset + sLength);
  // 补零到32字节(P-256的每个分量是32字节)
  const paddedR = Buffer.concat([Buffer.alloc(32 - r.length, 0), r]);
  const paddedS = Buffer.concat([Buffer.alloc(32 - s.length, 0), s]);
  // 拼接R和S得到原始格式签名
  return Uint8Array.from(Buffer.concat([paddedR, paddedS]));
}
  1. 修改验签代码,使用转换后的签名:
// 原签名转换为Uint8Array后,先转成原始R/S格式
const derSignature = base64UrlToUintArray(signature);
const rawSignature = derToRawSignature(derSignature);

const verified = await nCrypto.subtle.verify(
  {
    name: "ECDSA",
    hash: "SHA-256",
  },
  key,
  rawSignature, // 这里用转换后的原始签名
  input
);

验证结果

修改后重新运行代码,SubtleCrypto的验签结果会和Node原生crypto一致,返回true。

内容的提问来源于stack exchange,提问作者KTibow

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 07:45:00