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

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)) // 失败

解决方案

问题出在三个核心点:

  1. 签名格式不匹配:ES256的JWT签名采用IEEE P1363格式(直接拼接r和s的字节),而Node.js crypto默认期望ASN.1 DER编码格式的签名。
  2. 算法标识符错误:创建验证对象时,应使用ECDSA与SHA256组合的算法'ecdsa-with-sha256',而非单纯的'sha256'。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 10:17:40