同一枚 JWT 读得出内容,却验不过签名
一枚 RS256 令牌贴进解码页,头部与载荷照样解码,点「验证签名」却回「还没有输入密钥」。把模板整段 script 丢进 node:vm 跑一遍才明白:这页把「读」和「验」做成两套判定,验签侧窄到只有 13 个算法名进得了门,前六道闸门还不看你填的密钥。
被测的是 JWT 解析 那页,768 行、38463 字节,账目在 ALGS(L199–213)、pemToDer(L604)、doVerify(L648)三处;令牌与密钥全用 demo- 前缀自签。
13 格里只写了三类字段
Object.keys 数出 13 个键,即图 1 左列。hashByte 只在 HS 三档(32/48/64),salt 只在 PS 三档(32/48/64),curve 只在 ES 三档(P-256、P-384、P-521),余下 10 格三类字段全无,none 只有 family。
最容易上当的是 ES512:键里 512、曲线却是 P-521,差 9;坐标每段 66 字节,签名 132 是 2×66 而非 2×64。
图 1:字段决定家族,家族决定面板显隐。
签名段字节跟密钥跟哈希,不跟算法名
- HS 三档 32、48、64 字节,跟 hash,与那三格 hashByte 逐一对上。
- RS256@2048 与 RS384@2048 同为 256,RS512@4096 才 512,PS 三档同账。
- ES256 是 64、ES384 是 96、ES512 是 132,跟曲线。
换成 base64url 字符数就是图 2 末行十二档,与 ceil(bytes/3)*4 减补位在 12 行逐行相等。
图 2:两根同高柱子来自 2048 位的 RS256 与 RS384。
12 算法 × 4 入口,只有两种结果
每行令牌的 kid 写成 demo-key-1、载荷 lvl 写成 3,HS 密钥 demo-shared-key-2026-b22,非对称现生成 RSA-2048/4096 与三条 P 曲线的 PEM。四个入口只有两种结果:正确密钥与导出成 JWK 各 12/12 通过;lvl 从 3 改成 4 而签名段原样、或换一把同参数错钥,都是 12/12 不匹配。
密钥进门先看首字符
importKey 先 trim,再看首字符是不是 {:能解析走 jwk,否则 hmac 落 raw、其余落 spki。同一段密钥写成 raw 与写成 JWK,重算签名逐字节一致,32 字节、64 字符 hex。坑是 {"a":1}:解析得动、进 jwk 支抛 DataError,message 无中文,回落成「粘贴的密钥内容不完整,或与头部声明的算法不匹配」。
PEM 的三条拒绝加一次救回
BEGIN/END 不匹配报「没有识别到 PEM 的 BEGIN/END 标记」,裸 base64 也落这条;label 不是 PUBLIC KEY 就回显识别到的名字(CERTIFICATE、RSA PUBLIC KEY 各一条);BEGIN 与 END 不对称时反向引用失配,仍落第一条。body 先去空白、再把 - 换 +、_ 换 / 才进 atob,失败报「Base64 无法还原(长度 N)」,N 是去空白后的字符数。一次救回:RSA-2048 demo 公钥的 SPKI DER 294 字节编成 392 字符,19 枚 + 与 / 全换成 base64url 写法后两次输出逐字节一致;另一把 15 枚。字符表差别见 Base64。
七道闸门,六道走在读密钥之前
| 闸门 | 触发 | 回显 |
|---|---|---|
| L650 | 没有令牌 | 只 toast「先粘贴一个令牌」 |
| L651 | 5 段 JWE | 「无法验签」 |
| L654 | alg 为 none | 「没有签名可验」 |
| L658 | 家族不在四类 | 「不在本机可验证的范围内」 |
| L663 | 没有 WebCrypto | 浏览器口径,未实测 |
| L667 | 缺第 3 段 | 「缺少签名段」 |
| L675 | 密钥框空白 | 「还没有输入密钥」 |
七道都在密钥框留空下跑过,除最后一道外无一回显密钥文案,前六道在 L672 读 keyText 前返回。第四道喂了 6 个输入:HS224、ES256K、EdDSA、hs256、A128GCM 与头部不带 alg,全落这一条,查表大小写敏感。
两条分支失败时回吐的东西不一样
HMAC 是自己重算再比:sign 得到 expect 交给 equalBytes(先比 length,再定长 XOR-OR 折叠),失败时两段各印前 8 字节,恒为 16 个十六进制字符加一枚省略号。RS、PS、EC 走 verify,只回布尔值,「签名 N 字节」只在成功文案里;255 字节的 RS256 与 63 字节的 ES256 截短签名在 Node 侧返回 false,进「签名不匹配」而不报错。
salt 也不反映在长度上:PS256 的 saltLength 被写死成 32,同一把 RSA-PSS-2048 下 salt=20 签发的签名仍是 256 字节 342 字符,却验不过,面板也不提 salt。
口径补一句:除表中标注外全部为纯 Node 实测;DOM 像素、渲染定时器触发次数、非对称库对截短签名是否抛错,本批未开浏览器,记未实测。
三个面板开关各自听 family 的
jwt-hs-box 只在 hmac 开,jwt-pk-box 只在 rsa/pss/ec 开,按钮取两者的或。八种输入:没有 alg、未知串 XYZALG、none 三种三元素全隐藏;HS256 只开 HMAC 框;RS256、PS384、ES512 只开公钥框;塞一段长度除以 4 余 1 的坏签名,开关不动,只有那一行中段从「N 字节」换成「无法解码」。
常见问题
Q:同一个 JWT 粘进去,为什么这页读得出内容却验不过签名?
两套判定:解码只要求段能切开、字节能还原,验签要先过闸门——alg 得在 13 格里、家族在四类内、真有第 3 段。三段结构与 exp 的读法见站内那篇 旧文。
Q:签名段是 256 字节,就一定是 RS256 吗?
不一定,RS 与 PS 在 2048 位下都是 256 字节 342 字符;跟哈希的只有 HS 三档,跟曲线的只有 ES 三档。
Q:密钥填了,为什么还说没有输入密钥?
要么填错了框——显隐由 alg 决定,落在四类外或没有 alg 时连按钮一起隐藏;要么那一格只有一串空格。