无法用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
相关产品推荐
相关产品推荐

