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

使用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
})();

问题原因

  1. 签名格式不兼容:Node Crypto默认支持ASN.1/DER格式的ECDSA签名,而Web Crypto API要求签名必须是原始的(r,s)字节对格式(IEEE P1363标准)。你提供的签名是ASN.1编码的,Web Crypto无法正确解析,导致验证失败。
  2. 密钥导入的小错误:原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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 08:41:05