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

使用Web Crypto签名序列化后验证失败问题排查

Web Crypto HMAC签名验证失败(Hex编码场景)

问题描述

使用JavaScript的Web Crypto API实现字符串签名、序列化及反序列化验证功能,自定义了createHexTokenFromStrClaim和verifyHexTokenAndGetStrClaim函数。因Base64包含特殊字符影响其他模块,采用Hex编码,但验证步骤始终失败——在verifyHexTokenAndGetStrClaim中,将Hex解码后的签名重新编码后,与原始签名完全不一致。

问题根源

核心错误在于二进制签名数据被当作UTF-8文本处理:

  • 签名是crypto.subtle.sign返回的二进制ArrayBuffer,包含大量非UTF-8合法字符的字节
  • 用TextDecoder将二进制签名转字符串时,无效字节会被替换为�(Unicode替换字符),导致原始字节信息丢失
  • 后续再通过TextEncoder将字符串转回Uint8Array时,得到的是替换后的字节数据,和原始签名完全不符

修复方案

直接对二进制签名数据(Uint8Array)进行Hex编解码,跳过文本转译步骤。同时优化自定义的Hex编解码函数,支持直接处理Uint8Array和ArrayBuffer,避免字符串转译带来的数据损坏。

修复后的完整代码

const keyUsages = ["sign","verify"];
const algo = {
    name: "HMAC",
    hash: {name: "SHA-512"}
};

class Util{
    static text_encoder=new TextEncoder();
    static text_decoder=new TextDecoder();

    // 字符串转Hex编码(处理UTF-8)
    static strToHex(str) {
        const uint8 = this.text_encoder.encode(str);
        return this.uint8ToHex(uint8);
    }

    // Hex编码转字符串(还原UTF-8)
    static hexToStr(hex) {
        const uint8 = this.hexToUint8(hex);
        return this.text_decoder.decode(uint8);
    }

    // Uint8Array转Hex编码
    static uint8ToHex(uint8) {
        return Array.from(uint8)
            .map(byte => byte.toString(16).padStart(2, '0'))
            .join('');
    }

    // Hex编码转Uint8Array
    static hexToUint8(hex) {
        const uint8 = new Uint8Array(hex.length / 2);
        for (let i = 0; i < hex.length; i += 2) {
            uint8[i / 2] = parseInt(hex.substr(i, 2), 16);
        }
        return uint8;
    }
    
    static async createHexTokenFromStrClaim(algo, str_claim, crypto_key){
        // 对原始字符串做Hex编码
        const hex_str_claim = Util.strToHex(str_claim);
        // 把Hex编码后的字符串转成Uint8Array用于签名
        const dataToSign = Util.text_encoder.encode(hex_str_claim);

        // 生成签名(ArrayBuffer)
        const signatureBuffer = await crypto.subtle.sign(algo, crypto_key, dataToSign);
        // 直接将签名的二进制转Hex编码
        const hex_sign = Util.uint8ToHex(new Uint8Array(signatureBuffer));

        return `${hex_str_claim}.${hex_sign}`;
    }
    
    static async verifyHexTokenAndGetStrClaim(algo, hex_token, crypto_key){
        const [hex_str_claim, hex_sign] = hex_token.split(".");
        
        if(!hex_str_claim || !hex_sign) return;

        // 还原待验证的数据
        const dataToVerify = Util.text_encoder.encode(hex_str_claim);
        // 把Hex签名转成原始的Uint8Array(二进制)
        const signatureUint8 = Util.hexToUint8(hex_sign);

        // 执行验证
        const isValid = await crypto.subtle.verify(
            algo, 
            crypto_key, 
            signatureUint8.buffer, 
            dataToVerify.buffer
        );
        
        if(isValid){
            // 还原原始字符串
            return Util.hexToStr(hex_str_claim);
        }

        return;
    }
}

(async()=>{
    const crypto_key = await crypto.subtle.generateKey(algo, true, keyUsages);
    const message = "Hello world こんにちは、元気ですか ईश्वरेण सह एकतां प्राप्तुं एषः महान् दिवसः अस्ति";
    const token  = await Util.createHexTokenFromStrClaim(algo, message, crypto_key);
    console.log(token);

    const claim = await Util.verifyHexTokenAndGetStrClaim(algo, token, crypto_key);
    console.log(claim); // 输出原始message,验证成功
})();

关键修改点

  1. 拆分Hex编解码逻辑:新增strToHex/hexToStr处理字符串的UTF-8 Hex编解码,uint8ToHex/hexToUint8直接处理二进制数据的Hex编解码
  2. 签名生成时:直接将crypto.subtle.sign返回的ArrayBuffer转成Uint8Array,再做Hex编码,跳过TextDecoder转字符串的步骤
  3. 签名验证时:将Hex编码的签名直接转回Uint8Array,传入crypto.subtle.verify,跳过TextEncoder转字符串的步骤

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 13:05:25