☰
jose 库 GeneralJWSInput 输入结构详解:通用 JSON 序列化 JWS 的 payload 与 signatures 规范
2026/9/28 3:55:55 网站建设 项目流程
  • 网络安全
  • 认证鉴权
  • 后端

【免费下载链接】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
点击查看免费下载

导读

GeneralJWSInput是 jose 库中描述General JWS(通用 JSON 序列化 JWS)校验函数输入结构的核心 TypeScript 接口,它定义了多签名 JWS 文档中payload与signatures两个成员的字段类型与语义约束,并被generalVerify等校验函数直接消费。读完本文,你将掌握 General JWS 的数据结构规范、payload在 RFC 7797 未编码载荷(b64: false)与分离签名(detached signature)场景下的特殊形态、signatures数组与扁平化结构(Flattened JWS)的对应关系,以及该输入结构在 jose 源码(src/jws/general/verify.ts、src/types.d.ts)中的实际校验流程。

General JWS 与 GeneralJWSInput 的定位

在 JWS(JSON Web Signature,RFC 7515)标准中,存在三种序列化形态:Compact(紧凑)序列化、Flattened(扁平)JSON 序列化与 General(通用)JSON 序列化。其中General 序列化是最完整的 JSON 表达形式:它在一个 JSON 对象中同时承载一份载荷(payload)与一个或多个签名/MAC(MAC,Message Authentication Code,即基于共享密钥的认证码)条目,每个条目可以拥有各自独立的算法、密钥和头部参数。

GeneralJWSInput正是 jose 库为这一形态的校验(verify)输入而定义的接口。从源码注释可见其定位(src/types.d.ts):

/** * General JWS definition for verify function inputs, allows payload as {@link !Uint8Array} for * detached signature validation. */ export interface GeneralJWSInput { payload: string | Uint8Array signatures: Omit<FlattenedJWSInput, 'payload'>[] }

它明确了两点关键设计:

  1. 该接口面向校验输入,即由签名方产出、交给校验方解析的 JWS 文档;
  2. payload除了标准的 BASE64URL 字符串外,还允许Uint8Array,这是为分离签名(detached signature)场景预留的能力——签名内容不随文档传输时,校验方可将原始载荷以二进制形式补齐。

GeneralJWSInput 的成员结构

GeneralJWSInput仅有payload与signatures两个必填成员,其完整定义记录于接口文档 docs/types/interfaces/GeneralJWSInput.md。

payload:JWS 载荷的两种合法形态

payload的类型为string | Uint8Array,其语义按文档原文为:

The "payload" member MUST be present and contain the value BASE64URL(JWS Payload).

也就是说,按照 JWS 标准,"payload" 成员必须存在,且其值是对 JWS 原始载荷做 BASE64URL 编码后的字符串。例如:

const jws = { payload: 'SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9vci4', signatures: [ { signature: 'FVVOXwj6kD3DqdfD9yYqfT2W9jv-Nop4kOehp_DeDGNB5dQNSPRvntBY6xH3uxlCxE8na9d_kyhYOcanpDJ0EA', protected: 'eyJhbGciOiJFUzI1NiJ9', }, ], }

上述payload值是字符串It’s a dangerous business, Frodo, going out your door.的 BASE64URL 编码,这也与测试用例 test/jws/general.test.ts 中GeneralSign签名输出的 payload 值完全一致。

RFC 7797 未编码载荷(b64: false)时的例外:当 JWS 头部声明了"b64": false(即启用 RFC 7797 的 Unencoded Payload Option,将b64列入crit扩展参数)时,载荷不再做 BASE64URL 编码,"payload" 成员的含义随之改变。此时文档允许调用方直接传入Uint8Array形式的原始载荷。这一点同时落在类型层面:payload: string | Uint8Array的联合类型正是为这两种模式设计的。

signatures:签名条目数组

signatures的类型为:

signatures: Omit<FlattenedJWSInput, 'payload'>[]

其语义按文档原文为:

The "signatures" member value MUST be an array of JSON objects. Each object represents a signature or MAC over the JWS Payload and the JWS Protected Header.

即signatures成员的值必须是一个 JSON 对象数组,其中每个对象代表一个对「JWS Payload + JWS Protected Header」签名或计算 MAC 的条目。TypeScript 工具类型Omit<FlattenedJWSInput, 'payload'>表示:每个条目在结构上等价于扁平化输入接口FlattenedJWSInput(见 docs/types/interfaces/FlattenedJWSInput.md)去掉payload成员后的剩余部分。

signatures 条目与 FlattenedJWSInput 的对应关系

要理解signatures条目的内部字段,必须先理解FlattenedJWSInput。一个 General JWS 本质上可以视为「多个 Flattened JWS 共享同一 payload」的组合体,因此其条目结构与扁平化结构高度一致。FlattenedJWSInput在源码中的定义(src/types.d.ts)包含四个成员:

成员类型必填性语义
payloadstring \| Uint8Array必填被 Omit 移除,因为载荷上浮到 GeneralJWSInput 顶层共享
signaturestring必填BASE64URL(JWS Signature),即该条目的签名值或 MAC 值
headerJWSHeaderParameters可选JWS未保护头部(Unprotected Header),以未编码的 JSON 对象呈现而非字符串,其中的头部参数值不受完整性保护
protectedstring可选BASE64URL(UTF8(JWS Protected Header)),即受保护头部的 BASE64URL 编码,其中的参数值受完整性保护

因此,signatures数组中的每个元素结构如下:

{ signature: string, // 必填,BASE64URL 编码的签名/MAC protected?: string, // 可选,BASE64URL(UTF8(受保护头部)) header?: JWSHeaderParameters, // 可选,未编码的未保护头部 JSON 对象 }

这里有两个值得注意的标准约束:

  • protected 与 header 的缺席规则:当 JWS Protected Header 非空时protected成员必须存在;为空时则必须缺席。未保护头部同理——非空时header必须存在,否则必须缺席;
  • 完整性保护差异:protected中的头部参数被纳入签名计算,因此任何篡改都会导致签名校验失败;而header中的参数不参与签名计算,属于「告知性质」,校验方不应仅凭未保护头部做出安全决策。

从输入到校验:generalVerify 如何消费 GeneralJWSInput

GeneralJWSInput是generalVerify函数的第一个参数类型(见 docs/jws/general/verify/functions/generalVerify.md)。其签名如下:

export function generalVerify( jws: types.GeneralJWSInput, key: types.KeyInput, options?: types.VerifyOptions, ): Promise<types.GeneralVerifyResult>

generalVerify的实现(src/jws/general/verify.ts)完整体现了GeneralJWSInput各成员的校验逻辑:

  1. 结构校验:首先要求jws必须是对象(否则抛出JWSInvalid,错误码ERR_JWS_INVALID);随后校验signatures必须为数组且每个元素都是对象,任何一条不合规都会抛错(src/jws/general/verify.ts);
  2. 快照与解析:对每个签名条目调用snapshotJws做快照,解析protected头部的 BASE64URL,并依据crit中是否包含b64、b64是否为布尔值,将校验模式归类为 0/1/2 三种之一(src/jws/general/verify.ts);
  3. RFC 7797 一致性检查:用位运算modes |= mode统计各条目的 b64 模式,一旦同时出现 b64: true 与 b64: false 的混合使用(modes === 3),立即抛出JWSInvalid('inconsistent use of JWS Unencoded Payload (RFC7797)')(src/jws/general/verify.ts);
  4. 逐条验签:遍历signatures,对每条执行verifySignature,返回第一个验证成功的条目的结果;若全部失败则抛出JWSSignatureVerificationFailed(src/jws/general/verify.ts)。

需要特别指出的是:由于signatures数组共享同一个顶层payload,验证失败与格式错误的边界被刻意模糊——当payload缺失或prepareVerify解析失败时,实现会统一抛出JWSSignatureVerificationFailed而非内部错误,以避免向调用方泄露「是文档格式错误还是签名不匹配」的区分信息(src/jws/general/verify.ts)。

校验成功后的返回结构为GeneralVerifyResult(见 docs/types/interfaces/GeneralVerifyResult.md),仅包含验证成功的那一个签名条目的解码结果:

成员类型说明
payloadUint8Array解码后的 JWS 载荷
protectedHeader?JWSHeaderParameters该条目的受保护头部
unprotectedHeader?JWSHeaderParameters该条目的未保护头部

其余签名条目的头部仅用于 RFC 7797 一致性检查,不会出现在结果中——校验方应当只信任返回值中携带的数据。

输入侧的对称产物:GeneralJWS 与 GeneralSign

与校验输入GeneralJWSInput对应的输出侧接口是GeneralJWS(见 docs/types/interfaces/GeneralJWS.md),两者结构几乎一致,差异仅在payload类型:

接口payload 类型用途
GeneralJWSInputstring \| Uint8Array校验函数输入,Uint8Array用于分离签名验证
GeneralJWSstring签名函数输出;RFC 7797 b64: false 时返回空字符串

两者在源码中相邻定义(src/types.d.ts),GeneralJWS.signatures同样为Omit<FlattenedJWSInput, 'payload'>[]。

在签名侧,GeneralSign 类 与 Signature 接口 负责构造满足GeneralJWSInput形态的文档。其典型用法(文档内置示例):

const jws = await new jose.GeneralSign( new TextEncoder().encode('It’s a dangerous business, Frodo, going out your door.'), ) .addSignature(ecPrivateKey) .setProtectedHeader({ alg: 'ES256' }) .addSignature(rsaPrivateKey) .setProtectedHeader({ alg: 'PS256' }) .sign() console.log(jws)

GeneralSign.sign()的实现(src/jws/general/sign.ts)会逐条调用createSignature生成签名,并约束:

  • 至少添加一个签名(否则抛JWSInvalid('at least one signature must be added'));
  • 所有条目的 b64 模式必须一致,否则抛出与校验侧相同的inconsistent use of JWS Unencoded Payload (RFC7797);
  • 当 b64: false 时,输出的GeneralJWS.payload为空字符串(因为载荷不再编码进文档,而是交由调用方通过分离签名机制传递)。

这一输出与输入结构的对称关系,被测试用例 test/jws/general.test.ts 与 test/jws/general.test.ts 直接验证:常规模式产出 BASE64URL payload,b64: false 模式产出空 payload,且两种模式的signatures.length均为 2。

实战:分离签名(detached signature)验证

GeneralJWSInput允许payload为Uint8Array的核心价值在于分离签名场景:当载荷体积大、不宜随签名文档重复传输时,发送方只传送signatures数组,接收方用自己持有的原始载荷补全payload后再校验。

import { generalVerify } from 'jose' // 假设载荷已在传输中单独送达(未做 BASE64URL 编码) const originalPayload = new TextEncoder().encode('It’s a dangerous business, Frodo, going out your door.') // 收到的 JWS 文档仅含签名条目,payload 以 Uint8Array 补齐 const jws = { payload: originalPayload, // 分离签名:直接传入原始字节 signatures: [ { signature: 'FVVOXwj6kD3DqdfD9yYqfT2W9jv-Nop4kOehp_DeDGNB5dQNSPRvntBY6xH3uxlCxE8na9d_kyhYOcanpDJ0EA', protected: 'eyJhbGciOiJFUzI1NiJ9', }, ], } const { payload, protectedHeader } = await generalVerify(jws, publicKey) console.log(protectedHeader) console.log(new TextDecoder().decode(payload))

对于 RFC 7797 b64: false 的分离签名,文档同样要求传入Uint8Array载荷。校验侧的实现对此有专门处理:inputPayload instanceof Uint8Array ? new Uint8Array(inputPayload) : inputPayload,并在验签前通过encodeJsonUnencodedPayload将未编码载荷重新组织进签名输入(src/jws/general/verify.ts)。

常见校验错误与排查

基于generalVerify源码与测试(test/jws/general.test.ts),以下输入形态会被拒绝:

错误输入形态异常类型触发位置
jws不是对象(如 null、字符串)JWSInvalidsrc/jws/general/verify.ts
signatures不是数组,或数组元素不是对象JWSInvalidsrc/jws/general/verify.ts
各签名条目的 b64 模式混合(true 与 false 并存)JWSInvalidsrc/jws/general/verify.ts
所有签名均验证失败 / payload 缺失JWSSignatureVerificationFailedsrc/jws/general/verify.ts

此外,签名侧对头部还有「只能设置一次」的约束:对同一个Signature实例重复调用setProtectedHeader或setUnprotectedHeader会抛出TypeError(消息分别为setProtectedHeader can only be called once与setUnprotectedHeader can only be called once),且头部值必须是纯对象(null、字符串、数组、Date、数字、布尔值均会触发ERR_JWS_INVALID)——测试用例 test/jws/general.test.ts 逐项覆盖了这些边界。

小结

GeneralJWSInput是 jose 通用 JSON 序列化 JWS 校验链路的类型基石:payload承载 BASE64URL 编码的载荷并在 RFC 7797 / 分离签名场景下退化为Uint8Array;signatures由去除payload的FlattenedJWSInput条目构成数组,每个条目包含必填的signature与可选的protected、header。它既是被generalVerify直接消费的输入契约,又与签名侧GeneralSign产出的GeneralJWS形成对称闭环。理解该结构,是正确构造、传输与校验多签名 JWS 文档、以及实现分离签名方案的前提。

进一步阅读:接口定义 docs/types/interfaces/GeneralJWSInput.md、docs/types/interfaces/FlattenedJWSInput.md、docs/types/interfaces/GeneralJWS.md;校验实现 src/jws/general/verify.ts;签名实现 src/jws/general/sign.ts;测试用例 test/jws/general.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
点击查看免费下载
上一篇:lm-evaluation-harness 中的 NaijaRC 尼日利亚语言多选阅读理解评测任务:配置、提示模板与实现详解
下一篇:Cilium Ingress 路径类型(pathType)实战指南:Exact、Prefix 与 ImplementationSpecific 的匹配行为与 Envoy 实现原理

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

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

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

立即咨询