使用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,验证成功 })();
关键修改点
- 拆分Hex编解码逻辑:新增
strToHex/hexToStr处理字符串的UTF-8 Hex编解码,uint8ToHex/hexToUint8直接处理二进制数据的Hex编解码 - 签名生成时:直接将
crypto.subtle.sign返回的ArrayBuffer转成Uint8Array,再做Hex编码,跳过TextDecoder转字符串的步骤 - 签名验证时:将Hex编码的签名直接转回Uint8Array,传入
crypto.subtle.verify,跳过TextEncoder转字符串的步骤
内容的提问来源于stack exchange,提问作者Anurag Vohra
相关产品推荐
相关产品推荐

