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

无法用JS验证Go生成的ECDSA P-256签名问题求助

问题解决:Go ECDSA P-256签名在JS端验证失败

核心差异点

Go的crypto/ecdsa与JS的webcrypto/jsrsasign在ECDSA处理上的核心差异集中在签名格式、哈希编码、公钥格式三个方面,其中签名格式不匹配是最常见的失败原因。

排查与解决步骤

1. 对齐签名格式

Go后端生成签名通常有两种格式:

  • ASN.1 DER格式:通过ecdsa.SignASN1()生成,长度约70-72字节,是X.509标准签名格式。
  • P1363格式:手动拼接r和s的字节(各32字节,不足补前导零),固定64字节,部分Go项目会用这种紧凑格式。

JS端默认期望ASN.1 DER格式,若Go用的是P1363格式,需先转换:

// 将64字节P1363格式签名转为ASN.1 DER格式
function p1363ToAsn1(signatureBytes) {
  const r = signatureBytes.slice(0, 32);
  const s = signatureBytes.slice(32, 64);

  // 字节数组转BigInt(去除前导零)
  const bytesToBigInt = (bytes) => {
    let hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
    return BigInt('0x' + hex);
  };

  // ASN.1 INTEGER编码处理(避免解析为负数)
  const encodeInteger = (n) => {
    let hex = n.toString(16);
    if (hex.length % 2 !== 0) hex = '0' + hex;
    let bytes = Uint8Array.from(hex.match(/.{2}/g).map(b => parseInt(b, 16)));
    if (bytes[0] >= 0x80) bytes = new Uint8Array([0, ...bytes]);
    return new Uint8Array([0x02, bytes.length, ...bytes]);
  };

  const rEncoded = encodeInteger(bytesToBigInt(r));
  const sEncoded = encodeInteger(bytesToBigInt(s));
  const seqLength = rEncoded.length + sEncoded.length;

  return new Uint8Array([0x30, seqLength, ...rEncoded, ...sEncoded]);
}

2. 确保哈希值完全一致

JS端必须使用与移动端、后端完全相同的原始哈希字节,不能直接用hex字符串或错误编码:

// 将哈希的hex字符串转为Uint8Array
function hexToBytes(hex) {
  return Uint8Array.from(hex.match(/.{2}/g).map(byte => parseInt(byte, 16)));
}

3. 公钥格式正确导入

Go后端需导出SPKI格式的公钥(而非PKCS#8),JS端才能正确导入:

  • Go导出公钥代码:
import (
  "crypto/ecdsa"
  "crypto/x509"
  "encoding/pem"
)

func ExportPublicKeyPEM(pub *ecdsa.PublicKey) ([]byte, error) {
  pubBytes, err := x509.MarshalPKIXPublicKey(pub)
  if err != nil {
    return nil, err
  }
  return pem.EncodeToMemory(&pem.Block{
    Type:  "PUBLIC KEY",
    Bytes: pubBytes,
  }), nil
}
  • JS导入公钥(webcrypto):
async function importPublicKey(pem) {
  const header = "-----BEGIN PUBLIC KEY-----";
  const footer = "-----END PUBLIC KEY-----";
  const rawPem = pem.replace(header, "").replace(footer, "").replace(/\s/g, "");
  const derBytes = Uint8Array.from(atob(rawPem), c => c.charCodeAt(0));
  
  return window.crypto.subtle.importKey(
    "spki",
    derBytes,
    { name: "ECDSA", namedCurve: "P-256" },
    true,
    ["verify"]
  );
}

4. 完整验证示例(webcrypto)

async function verifyFileIntegrity(pubKeyPem, hashHex, signatureBase64) {
  const pubKey = await importPublicKey(pubKeyPem);
  const hashBytes = hexToBytes(hashHex);
  const signatureBytes = Uint8Array.from(atob(signatureBase64), c => c.charCodeAt(0));
  
  // 若签名是P1363格式,先转ASN.1
  const asn1Sig = p1363ToAsn1(signatureBytes);
  
  return window.crypto.subtle.verify(
    { name: "ECDSA", hash: { name: "SHA-256" } },
    pubKey,
    asn1Sig,
    hashBytes
  );
}

// 调用示例
verifyFileIntegrity(
  "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----",
  "abcdef123456...", // 移动端生成的SHA256哈希hex
  "base64EncodedSignature..."
).then(isValid => {
  console.log("验证结果:", isValid);
});

关键检查项

  • 打印Go生成的签名长度:64字节是P1363,70+字节是ASN.1。
  • 对比JS和Flutter中使用的哈希字节数组,确保完全一致。
  • 用在线ASN.1解析工具验证签名格式是否符合预期。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 14:15:27