NodeJS crypto verify.verify验证ES256 P-256 JWT失败,RSA正常求助
问题描述
需要编写纯NodeJS函数验证JWT签名,部署环境无法使用第三方包。现有代码对RSA类密钥(如RS256)有效,但使用P-256曲线的ES256算法时,Node.js crypto模块的verify.verify始终返回false。已确认签名本身有效,疑问是否需要为verify.verify添加额外参数。
原始验证函数:
function verifyToken(token, publicKey) { const [headerEncoded, payloadEncoded, signatureEncoded] = token.split('.') const payload = JSON.parse(Buffer.from(payloadEncoded, 'base64').toString()) const verify = crypto.createVerify('sha256') verify.update(headerEncoded + '.' + payloadEncoded) const isValidSignature = verify.verify(publicKey, signatureEncoded, 'base64') if (!isValidSignature) { throw new Error('Invalid signature') } return payload }
测试对比代码:
const jwt = require('jsonwebtoken') const crypto = require('crypto') function verifyToken(token, publicKey) { const [headerEncoded, payloadEncoded, signatureEncoded] = token.split('.') const payload = JSON.parse(Buffer.from(payloadEncoded, 'base64').toString()) const verify = crypto.createVerify('sha256') verify.update(headerEncoded + '.' + payloadEncoded) const isValidSignature = verify.verify(publicKey, signatureEncoded, 'base64') if (!isValidSignature) { throw new Error('Invalid signature') } return payload } // RSA密钥示例 const privateKey1 = { "key": { "p": "<redacted>", "kty": "RSA", "q": "<redacted>", "d": "<redacted>", "e": "AQAB", "use": "sig", "kid": "<redacted>", "qi": "<redacted>", "dp": "<redacted>", "alg": "RS256", "dq": "<redacted>", "n": "<redacted>" }, "format": "jwk" } // ES256密钥示例(P-256曲线) const privateKey2 = { "key": { "kty": "EC", "d": "<redacted>", "use": "sig", "crv": "P-256", "kid": "<redacted>", "x": "<redacted>", "y": "<redacted>", "alg": "ES256" }, "format": "jwk" } // RSA签名测试(正常工作) var token1 = jwt.sign({ 'hello': 'world' }, privateKey1, { 'algorithm' : privateKey1.key.alg, expiresIn: 60 * 60, 'header': { 'typ': 'JWT', 'alg' : privateKey1.key.alg } }) console.log('json web token 1 = ', jwt.verify(token1, privateKey1)) // 正常 console.log('pure node token 1 = ', verifyToken(token1, privateKey1)) // 正常 // ES256签名测试(自研函数失败) var token2 = jwt.sign({ 'hello': 'world' }, privateKey2, { 'algorithm' : privateKey2.key.alg, expiresIn: 60 * 60, 'header': { 'typ': 'JWT', 'alg' : privateKey2.key.alg } }) console.log('json web token 2 = ', jwt.verify(token2, privateKey2)) // 正常 console.log('pure node token 2 = ', verifyToken(token2, privateKey2)) // 失败
解决方案
问题出在三个核心点:
- 签名格式不匹配:ES256的JWT签名采用IEEE P1363格式(直接拼接r和s的字节),而Node.js crypto默认期望ASN.1 DER编码格式的签名。
- 算法标识符错误:创建验证对象时,应使用ECDSA与SHA256组合的算法
'ecdsa-with-sha256',而非单纯的'sha256'。 - Base64URL解码不规范:JWT使用的是无padding、字符替换的Base64URL编码,需转换为标准Base64后再解码。
修改后的验证函数:
const crypto = require('crypto'); function verifyToken(token, publicKey) { const [headerEncoded, payloadEncoded, signatureEncoded] = token.split('.'); // 解析Payload const payload = JSON.parse( Buffer.from(base64UrlDecode(payloadEncoded)).toString() ); // 处理签名字符串:Base64URL转标准Base64并解码 const signatureBytes = base64UrlDecode(signatureEncoded); // 将P1363格式的ES256签名转换为ASN.1 DER格式(P-256的r/s长度均为32字节) const derSignature = convertP1363ToDer(signatureBytes, 32); // 创建ECDSA-SHA256验证对象 const verify = crypto.createVerify('ecdsa-with-sha256'); verify.update(`${headerEncoded}.${payloadEncoded}`); // 执行验证 const isValidSignature = verify.verify(publicKey, derSignature); if (!isValidSignature) { throw new Error('Invalid signature'); } return payload; } // 辅助函数:Base64URL解码 function base64UrlDecode(str) { str = str.replace(/-/g, '+').replace(/_/g, '/'); str += '='.repeat((4 - str.length % 4) % 4); return Buffer.from(str, 'base64'); } // 辅助函数:将P1363格式(r+s)的签名转换为ASN.1 DER格式 function convertP1363ToDer(signatureBytes, componentLength) { const r = signatureBytes.subarray(0, componentLength); const s = signatureBytes.subarray(componentLength); // 移除r和s开头的零字节(ASN.1整数编码要求) const trimLeadingZeros = (buf) => { let start = 0; while (start < buf.length && buf[start] === 0) start++; return start === buf.length ? Buffer.from([0]) : buf.subarray(start); }; const trimmedR = trimLeadingZeros(r); const trimmedS = trimLeadingZeros(s); // 构建ASN.1 DER结构:SEQUENCE(INTEGER(r), INTEGER(s)) const derSequence = Buffer.concat([ Buffer.from([0x30]), // SEQUENCE标签 Buffer.from([4 + trimmedR.length + trimmedS.length]), // 总长度 Buffer.from([0x02]), // INTEGER标签 Buffer.from([trimmedR.length]), // r的长度 trimmedR, Buffer.from([0x02]), // INTEGER标签 Buffer.from([trimmedS.length]), // s的长度 trimmedS ]); return derSequence; }
关键说明
- Base64URL处理:必须补全padding并替换特殊字符,否则会导致解码后的字节序列错误。
- 签名格式转换:P-256曲线的r和s各占32字节,拆分后需移除前导零以符合ASN.1整数编码规则,再组装成DER格式的SEQUENCE结构。
- 算法选择:
'ecdsa-with-sha256'明确指定了ECDSA结合SHA256的签名算法,完全匹配ES256的算法要求。
内容的提问来源于stack exchange,提问作者Neil Skilling
相关产品推荐
相关产品推荐

