☰
jose JWS 验签选项(VerifyOptions)完全指南:algorithms 与 crit 的深度解析与实战用法
2026/9/28 6:59:59 网站建设 项目流程
  • 网络安全
  • 认证鉴权
  • 后端

【免费下载链接】jose

JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes

项目地址:https://gitcode.com/gh_mirrors/jo/jose
点击查看免费下载

导读

在 jose 库中,VerifyOptions是所有 JWS(JSON Web Signature)验签操作共用的配置接口,负责约束"验签时允许哪些签名算法"以及"如何对待 JWS 头部中标记为 Critical 的扩展参数"。本文将围绕 VerifyOptions 接口文档 展开,结合 src/types.d.ts 的类型定义与 src/lib/options.ts 的底层校验实现,逐一拆解algorithms与crit两个选项的含义、默认行为、错误语义与安全边界,并通过 Compact、Flattened、General 三种序列化以及 JWT 验签场景给出可复制、可运行的实战示例。读完本文,你将能精准配置 jose 的验签白名单、正确处理crit扩展头,并理解这些选项在源码层面的真实执行流程。

一、VerifyOptions 是什么:一处定义、处处生效

1.1 接口定义与继承关系

VerifyOptions并不是一个孤立的接口,它在类型体系中是"JWS 验签选项"与"JWT 验签选项"的共同基座:

// src/types.d.ts /** JWS Verification options. */ export interface VerifyOptions extends CritOption { algorithms?: JWSAlgorithm[] }
  • algorithms属性直接声明在本接口上;
  • crit属性来自被继承的CritOption接口(见 src/types.d.ts#L601-L619),该接口被 Sign、Verify、Encrypt、Decrypt 全部操作共享。

由此形成两条重要的应用链路:

  1. JWS 验签链路:compactVerify、flattenedVerify、generalVerify三个函数都以options?: VerifyOptions为第三个参数,详见 src/jws/compact/verify.ts#L51-L54、src/jws/flattened/verify.ts 与 src/jws/general/verify.ts;
  2. JWT 验签链路:jwtVerify使用扩展接口JWTVerifyOptions extends types.VerifyOptions, types.JWTClaimVerificationOptions(见 src/jwt/verify.ts#L13-L14),因此algorithms、crit同样可直接传给jwtVerify。

此外,VerifyOptions还通过 src/index.ts#L125 从主入口导出,是 jose 公共 API 的一部分。

1.2 在验签流程中的位置

从源码看,验签函数的第一步就是通过prepareVerify(options)将这两个选项"编译"成验签共享状态(见 src/lib/jws_verify.ts#L75-L77):

export function prepareVerify(options?: types.VerifyOptions): VerifyShared { return [options && validateAlgorithms('algorithms', options.algorithms), options?.crit] }

返回的三元组VerifyShared(算法集合、crit 映射、可选的非 b64 载荷缓存)会贯穿整个签名校验过程。也就是说:VerifyOptions的生效点在"任何密钥解析、任何签名验证"之前,属于验签流程的第一道闸门。

二、algorithms:验签算法白名单

2.1 语义与默认行为

按 VerifyOptions.md 的定义:

  • algorithms?: string[]是"被接受的 JWSalg(Algorithm)头部参数取值列表";
  • 默认行为:不传该选项时,凡是"适用于当前所用密钥/秘密"的算法一律放行;
  • 重要限制:未受保护的 JWT(即{ "alg": "none" })永远不会被本 API 接受——即使algorithms里写了'none'也不会生效,因为none根本不在可校验的算法体系内。

2.2 底层实现与参数校验

algorithms的解析发生在 src/lib/options.ts#L16-L29:

export function validateAlgorithms(option: string, algorithms?: string[]): Set<string> | undefined { if ( algorithms !== undefined && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s !== 'string')) ) { throw new TypeError(`"${option}" option must be an array of strings`) } if (!algorithms) { return undefined } return new Set(algorithms) }

可以提炼出三个实现事实:

  1. 类型约束严格:必须是字符串数组。混入非字符串元素(如[null]、[42])会直接抛出TypeError: "algorithms" option must be an array of strings,这一点被 test/jwt/verify.test.ts#L115-L123 的用例锁定;
  2. 转为 Set 提速:合法输入会被转换为Set,使后续"alg 是否在白名单内"的判断成为 O(1) 查找;
  3. 不传则放行:undefined时返回undefined,语义就是"使用默认白名单"。

2.3 白名单判定与报错路径

算法判定发生在validateJwsHeaders(见 src/lib/jws_verify.ts#L88-L105):

const alg = joseHeader.alg if (typeof alg !== 'string' || !alg) { throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid') } if (shared[0] && !shared[0].has(alg)) { throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed') }

顺序很关键:先校验头部缺省/非法,再做白名单过滤。若 token 头部的alg不在algorithms白名单内,会抛出错误码为ERR_JOSE_ALG_NOT_ALLOWED的JOSEAlgNotAllowed(见 src/util/errors.ts)。该错误在 test/jwt/verify.test.ts#L101-L114 中有直接验证:对HS256签发的 JWT 传algorithms: ['PS256'],验签即抛"alg" (Algorithm) Header Parameter value not allowed。

2.4 实战示例:Compact JWS 验签收紧算法白名单

import { compactVerify } from 'jose' const jws = 'eyJhbGciOiJFUzI1NiJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9vci4.kkAs_gPPxWMI3rHuVlxHaTPfDWDoqdI8jSvuSmqV-8IHIWXg9mcAeC9ggV-45ZHRbiRJ3obUIFo1rHphPA5URg' // 只接受 ES256,其余算法一律拒绝 const { payload, protectedHeader } = await compactVerify(jws, publicKey, { algorithms: ['ES256'], }) console.log(protectedHeader) // { alg: 'ES256' } console.log(new TextDecoder().decode(payload))

适用场景与建议:

  • 多密钥/多算法环境下,用algorithms固定白名单可显著缩小攻击面,防止"算法混淆/降级攻击"(如把 RSA 签名的 JWT 硬解释为 HMAC);
  • 在jwtVerify中同样适用,且可与其 JWT Claims 校验选项(issuer、audience、clockTolerance等)叠加使用;
  • 与动态密钥解析函数(getKey)配合时同样生效,因为prepareVerify在密钥解析之前就已执行。

三、crit:Critical 扩展头参数的处理策略

3.1 语义:一个"声明 + 检查"映射表

按 VerifyOptions.md 的定义:

  • crit?: { [propName: string]: boolean }是一个对象,键是"被识别的crit头参数名称";
  • 值为true表示该参数必须被完整性保护(即必须出现在 Protected Header 中);
  • 值为false表示该参数是否受保护无关紧要(可以出现在 Unprotected Header);
  • 内置特例:JWS 扩展头参数b64永远被识别并正确处理,不需要在crit中声明;除此之外,目前没有任何注册的头参数享受这种内置待遇。

⚠️ 重要警告(文档原文强调):crit选项只检查头参数在提供时是否语法正确、以及(可选地)是否被完整性保护。它不会处理该头参数本身,也不会在参数缺失时拒绝操作。你必须在验签成功之后,自行验证该参数确实存在,并按照协议规范完成后续的校验步骤。

3.2 底层校验逻辑全流程

crit的完整校验实现在 src/lib/options.ts#L46-L96 的validateCrit中,按序执行:

  1. 未受保护的 crit 即拒绝:若 JOSE Header 中出现crit但 Protected Header 中没有,抛JWSInvalid(错误码ERR_JWS_INVALID),消息为"crit" (Critical) Header Parameter MUST be integrity protected(对应 test/jws/crit.test.ts#L8-L17);
  2. 结构合法性检查:crit必须是非空字符串数组,否则抛"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present(见 test/jws/crit.test.ts#L18-L27);
  3. 识别性检查:crit中列出的每个参数必须出现在"识别集合"里(该集合 = 选项中的crit映射合并内置的JWS_RECOGNIZED,内置集合目前只有{ b64: true },见 src/lib/options.ts#L10-L14),否则抛JOSENotSupported(ERR_JOSE_NOT_SUPPORTED):Extension Header Parameter "..." is not recognized;
  4. 存在性检查:每个列出的参数必须实际存在于 JOSE Header 中(用Object.hasOwn做自有属性判断,见 test/jws/crit.test.ts#L112-L142,constructor、toString、__proto__等继承属性都不算数);
  5. 完整性保护检查:若该参数在crit映射中标记为true,则它必须同时出现在 Protected Header 中,否则抛Extension Header Parameter "..." MUST be integrity protected。

此外还有两个值得注意的实现细节:

  • 识别集合的合并顺序:{ __proto__: null, ...recognizedOption, ...recognizedDefault }——用户选项在前、内置默认在后,因此内置的b64: true永远生效,用户无法用crit: { b64: false }覆盖;
  • 生产端与消费端的差异(见 src/lib/options.ts#L31-L44):crit数组中出现重复值时,生产端(签名)会抛"crit" (Critical) Header Parameter MUST NOT contain duplicate values,而消费端(验签)容忍重复——因为 RFC 7515 只禁止生产方列重复名,接收方仅"可以"认为其无效。这一差异被 test/jws/crit.test.ts#L93-L158 的两组用例分别覆盖。

3.3 实战示例:声明并校验自定义扩展头

假设你的协议在 JWS 中加入自定义扩展头urn:example:foo,并要求它必须被完整性保护:

import { FlattenedSign, flattenedVerify } from 'jose' const key = new Uint8Array(32) // 对称密钥,仅用于演示 // 生产端:把扩展头放进 Protected Header,并在 crit 中声明 const jws = await new FlattenedSign(new TextEncoder().encode('payload')) .setProtectedHeader({ alg: 'HS256', crit: ['urn:example:foo'], 'urn:example:foo': 'bar' }) .sign(key) // 消费端:声明识别该参数,并要求它受完整性保护 const { payload } = await flattenedVerify(jws, key, { crit: { 'urn:example:foo': true }, }) // ⚠️ 验签成功后,仍需自行确认该头参数存在并按协议处理 console.log(new TextDecoder().decode(payload))

对应的失败场景(可在 test/jws/crit.test.ts 中找到等价断言):

  • 把crit放进 Unprotected Header →ERR_JWS_INVALID;
  • crit列出的参数名未在选项中声明 →ERR_JOSE_NOT_SUPPORTED;
  • 参数声明为true但只出现在 Unprotected Header →ERR_JWS_INVALID;
  • 参数声明为false时放在 Unprotected Header 则可正常通过(见 test/jws/crit.test.ts#L70-L75)。

3.4 内置 b64 扩展头:一个开箱即用的特例

b64(base64url 编码载荷开关)是 jose 唯一内置识别的 JWS 扩展头。即使crit选项完全为空,只要 JWS 头中声明crit: ['b64'],验签端也会自动完成:

  • 通过validateB64(src/lib/options.ts#L98-L113)强制b64必须是布尔值,否则抛The "b64" (base64url-encode payload) Header Parameter must be a boolean;
  • 依据b64: false切换到"非编码载荷"验证路径:Compact 序列化要求载荷必须是纯 ASCII(见 src/lib/jws_verify.ts#L147-L153),Flattened 序列化则接受字符串或Uint8Array(src/lib/jws_verify.ts#L211-L217)。

值得提醒:jwtVerify会直接拒绝b64: false的 token(见 src/jwt/verify.ts#L179-L181:JWTs MUST NOT use unencoded payload),因为 JWT 规范强制要求载荷采用 base64url 编码。普通 JWS 验签则无此限制。

四、VerifyOptions 在三种 JWS 序列化与 JWT 验签中的统一语义

VerifyOptions不区分序列化形态,Compact、Flattened、General 三种验签函数共享同一套选项语义,只是底层执行路径略有差异:

验签函数序列化选项参数共享校验内核
compactVerifyCompact(三段式字符串)options?: VerifyOptionsverifyCompact
flattenedVerifyFlattened JSON(单签名)options?: VerifyOptionsverifySignature
generalVerifyGeneral JSON(多签名)options?: VerifyOptionsverifySignature(逐签名)
jwtVerifyCompact JWToptions?: JWTVerifyOptionsverifyCompact+ Claims 校验

关键实现事实(见 src/lib/jws_verify.ts#L226-L249):Compact 路径先按.拆分为三段,解析出 Protected Header 后立刻执行validateJwsHeaders——也就是说algorithms白名单在密钥解析之前就已生效。而 Flattened/General 路径(src/lib/jws_verify.ts#L196-L223)则先合并 Protected 与 Unprotected 头(要求两者名称互不相交,否则抛JWS Protected and JWS Unprotected Header Parameter names must be disjoint),再统一执行算法白名单与 crit 校验。

另一个值得注意的细节:Compact 验签的verifyCompact会在密钥解析前对 token 做快照(test/jws/compact.verify.test.ts#L79-L94),防止getKey回调中篡改 token 组件影响校验输入——这保证了你传入的VerifyOptions所校验的头与最终验签的头是同一份。

五、错误速查表

VerifyOptions相关校验可能触发的错误(均定义于 src/util/errors.ts,实际报错消息以 src/lib/options.ts 与 src/lib/jws_verify.ts 为准):

错误类型错误码触发条件对应用例
TypeError—algorithms不是纯字符串数组test/jwt/verify.test.ts#L115-L123
JOSEAlgNotAllowedERR_JOSE_ALG_NOT_ALLOWEDtoken 的alg不在algorithms白名单test/jwt/verify.test.ts#L106-L114
JWSInvalidERR_JWS_INVALIDcrit未受完整性保护 / 结构非法 / 所列参数缺失或未受保护test/jws/crit.test.ts#L7-L37
JOSENotSupportedERR_JOSE_NOT_SUPPORTEDcrit列出未在选项中声明的扩展参数test/jws/crit.test.ts#L28-L36

六、实践建议与安全边界总结

  1. 生产环境务必显式传入algorithms白名单。默认"按密钥放行"虽方便,但在多算法互通场景下可能意外接受你并不想支持的算法;固定白名单是防算法混淆的第一道防线。
  2. crit只负责"语法与完整性"检查,不负责业务语义。文档与源码反复强调:验签成功后仍需自行确认扩展头存在并按协议处理。将crit视为"协议合规性钩子",而非"业务校验器"。
  3. 不要把b64写进自定义crit映射。它是内置特例,写与不写效果相同;且用户映射无法覆盖内置的b64: true。
  4. 在 JWT 场景叠加使用。jwtVerify接受VerifyOptions与 Claims 校验选项的组合,可在一次调用内同时完成"算法白名单 + crit 合规 + iss/aud/exp 校验"。
  5. 阅读源码加深理解:选项解析见 src/lib/options.ts,验签主流程见 src/lib/jws_verify.ts,完整类型定义见 src/types.d.ts#L710-L720,行为测试见 test/jws/crit.test.ts 与 test/jwt/verify.test.ts。
  • 网络安全
  • 认证鉴权
  • 后端

【免费下载链接】jose

JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes

项目地址:https://gitcode.com/gh_mirrors/jo/jose
点击查看免费下载
上一篇:秒懂Flink:深入理解Flink四大基石之时间和水位线
下一篇:告别记事本!3步搞定Notepads文件关联,让编辑效率提升300%

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询