使用Web Crypto API验证公钥失败的原因排查与解决
Node Crypto验证通过但Web Crypto API验证失败的原因及修复方案
我用Node Crypto可以正常完成ECDSA公钥验证并输出true,但切换到Web Crypto API(运行在不支持Node Crypto的Cloudflare Worker环境)时,即便参数完全相同,验证结果始终为false且无报错。
Node Crypto 可正常运行的代码
import crypto from "crypto"; const publicKeyPEM = `-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXjyD37iJ6K7dVWCANfrTEJkDFKZt dlaCMOGuTE3qsy4PF3FqUnDi0EZxty8n6Mb3W3Ahj0ASkF+GwNW8C/ztdQ== -----END PUBLIC KEY-----`; const messageToVerify = "hello world"; const signature = "MEUCIENzPHGDk+t1inhAvnqPX1OYLfSltYVIv1cipjW2F3CxAiEAzTVrj5CCHChsyeAif0qM6UvX3h0U7BDHhb+XmsXwO/c="; (() => { const verify = crypto.createVerify("SHA256"); verify.update(messageToVerify); const verified = verify.verify( publicKeyPEM, Buffer.from(signature, "base64") ); console.log(verified); // 输出 true })();
Web Crypto API 验证失败的代码
function str2ab(str) { const buf = new ArrayBuffer(str.length); const bufView = new Uint8Array(buf); for (let i = 0, strLen = str.length; i < strLen; i++) { bufView[i] = str.charCodeAt(i); } return buf; } async function importEcdsaKey(pem) { const pemHeader = "-----BEGIN PUBLIC KEY-----"; const pemFooter = "-----END PUBLIC KEY-----"; const pemContents = pem.substring( pemHeader.length, pem.length - pemFooter.length - 1 ); const binaryDerString = atob(pemContents); const binaryDer = str2ab(binaryDerString); return await crypto.subtle.importKey( "spki", binaryDer, { name: "ECDSA", namedCurve: "P-256", }, true, ["verify"] ); } function base64ToArrayBuffer(base64String) { const binaryString = atob(base64String); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes.buffer; } const publicKeyPEM = `-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXjyD37iJ6K7dVWCANfrTEJkDFKZt dlaCMOGuTE3qsy4PF3FqUnDi0EZxty8n6Mb3W3Ahj0ASkF+GwNW8C/ztdQ== -----END PUBLIC KEY-----`; const messageToVerify = "hello world"; const signature = "MEUCIENzPHGDk+t1inhAvnqPX1OYLfSltYVIv1cipjW2F3CxAiEAzTVrj5CCHChsyeAif0qM6UvX3h0U7BDHhb+XmsXwO/c="; (async () => { const publicKey = await importEcdsaKey(publicKeyPEM); const signatureArrayBuffer = base64ToArrayBuffer(signature); const data = new TextEncoder().encode(messageToVerify); const result = await crypto.subtle.verify( { name: "ECDSA", hash: { name: "SHA-256" }, }, publicKey, signatureArrayBuffer, data ); console.log(result); // 输出 false })();
问题原因
- 签名格式不兼容:Node Crypto默认支持ASN.1/DER格式的ECDSA签名,而Web Crypto API要求签名必须是原始的(r,s)字节对格式(IEEE P1363标准)。你提供的签名是ASN.1编码的,Web Crypto无法正确解析,导致验证失败。
- 密钥导入的小错误:原Web Crypto代码中截取PEM内容时多减了1个字符(
pem.length - pemFooter.length - 1),可能导致密钥数据不完整,不过这不是验证失败的核心原因,但仍需修正。
修复方案
1. 转换签名格式
编写函数将ASN.1/DER格式的签名转换为Web Crypto支持的IEEE P1363格式:
- ASN.1/DER格式的ECDSA签名结构是:
SEQUENCE(r, s),其中r和s都是可变长度的整数,需要解析后补全到32字节(P-256曲线的固定长度),再拼接成64字节的原始格式。
2. 修正密钥截取错误
将pem.length - pemFooter.length - 1改为pem.length - pemFooter.length,同时添加trim()去除多余的换行和空格,避免截断密钥内容。
修复后的Web Crypto代码
function str2ab(str) { const buf = new ArrayBuffer(str.length); const bufView = new Uint8Array(buf); for (let i = 0, strLen = str.length; i < strLen; i++) { bufView[i] = str.charCodeAt(i); } return buf; } async function importEcdsaKey(pem) { const pemHeader = "-----BEGIN PUBLIC KEY-----"; const pemFooter = "-----END PUBLIC KEY-----"; // 修正:移除多余的-1,添加trim()清理空白字符 const pemContents = pem.substring( pemHeader.length, pem.length - pemFooter.length ).trim(); const binaryDerString = atob(pemContents); const binaryDer = str2ab(binaryDerString); return await crypto.subtle.importKey( "spki", binaryDer, { name: "ECDSA", namedCurve: "P-256", }, true, ["verify"] ); } function base64ToArrayBuffer(base64String) { const binaryString = atob(base64String); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes.buffer; } // 新增:将ASN.1/DER格式签名转换为IEEE P1363格式 function derToIeeeP1363(signatureDer) { const view = new DataView(signatureDer); let offset = 0; // 跳过SEQUENCE标签和长度 if (view.getUint8(offset++) !== 0x30) throw new Error("Invalid signature format"); const length = view.getUint8(offset++); // 跳过r的标签和长度 if (view.getUint8(offset++) !== 0x02) throw new Error("Invalid r format"); const rLength = view.getUint8(offset++); const r = new Uint8Array(signatureDer.slice(offset, offset + rLength)); offset += rLength; // 跳过s的标签和长度 if (view.getUint8(offset++) !== 0x02) throw new Error("Invalid s format"); const sLength = view.getUint8(offset++); const s = new Uint8Array(signatureDer.slice(offset, offset + sLength)); // P-256曲线的r和s必须是32字节,不足则补前导0 const paddedR = new Uint8Array(32); paddedR.set(r, 32 - r.length); const paddedS = new Uint8Array(32); paddedS.set(s, 32 - s.length); // 拼接r和s得到64字节的原始签名 const result = new Uint8Array(64); result.set(paddedR); result.set(paddedS, 32); return result.buffer; } const publicKeyPEM = `-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXjyD37iJ6K7dVWCANfrTEJkDFKZt dlaCMOGuTE3qsy4PF3FqUnDi0EZxty8n6Mb3W3Ahj0ASkF+GwNW8C/ztdQ== -----END PUBLIC KEY-----`; const messageToVerify = "hello world"; const signature = "MEUCIENzPHGDk+t1inhAvnqPX1OYLfSltYVIv1cipjW2F3CxAiEAzTVrj5CCHChsyeAif0qM6UvX3h0U7BDHhb+XmsXwO/c="; (async () => { const publicKey = await importEcdsaKey(publicKeyPEM); const signatureDer = base64ToArrayBuffer(signature); // 转换签名格式 const signatureIeee = derToIeeeP1363(signatureDer); const data = new TextEncoder().encode(messageToVerify); const result = await crypto.subtle.verify( { name: "ECDSA", hash: { name: "SHA-256" }, }, publicKey, signatureIeee, data ); console.log(result); // 输出 true })();
内容的提问来源于stack exchange,提问作者James
相关产品推荐
相关产品推荐

