拓冰建站拓冰建站
首页 / 资讯中心 / 正文

JCS JSON 规范化(RFC 8785)深入解析:inngest 中的实现与应用实践

JCS JSON 规范化RFC 8785深入解析inngest 中的实现与应用实践【免费下载链接】inngestThe leading workflow orchestration platform. Run stateful step functions and AI workflows on serverless, servers, or the edge.项目地址: https://gitcode.com/GitHub_Trending/in/inngestJSON Canonicalization SchemeJCSRFC 8785是保证同一份 JSON 数据在任何平台、任何语言下都能序列化成完全相同的字节序列的规范化方案。本指南以 inngest 仓库内 vendor 的github.com/gowebpki/jcs库为核心结合其源码与 inngest 中的真实调用场景讲解 JCS 的三大核心规则ECMAScript 兼容的原始类型序列化、I-JSON 子集约束、递归字典序属性排序并展示它如何被用于请求签名HMAC与函数配置哈希等对字节一致性敏感的领域。读完本文你将理解 JCS 的完整规范化流程、掌握jcs.Transform的正确用法并能在自己的签名/哈希方案中复用这套实践。JCS 解决什么问题哈希与签名等密码学操作有一个隐含前提目标数据在序列化、传输、解析的过程中不允许发生任何变化。但 JSON 恰恰是一种宽容的格式——同一份逻辑数据可以写成无数种合法形态对象属性顺序可以任意排列字符串转义可以等价互换如\u0042与B、\n与真实换行数字可以有多种等价写法4.50与4.5、1E30与1e30、2e-3与0.002空白符可以随意增删。如果签名方按{b:1,a:2}计算摘要而验签方收到的是{a:2,b:1}即便逻辑内容完全一致签名校验也会失败。JCS 正是为了解决这个问题它把 JSON 数据约束为一种唯一合法序列化形态从而让先规范化、再哈希/签名成为跨平台可复现的稳定流程。JCS 是公开标准RFC 8785其规范建立在三块基石之上JSON 原始类型的序列化方式兼容 ECMAScript 的JSON.stringify()见 RFC 8259 定义的 JSON 格式与 ECMAScript 规范将 JSON 数据约束到I-JSONRFC 7493子集排除非互操作的数字与字符形态提供一套平台无关的对象属性排序方案。一句话概括其核心思想README 原话对 JSON 原始类型使用与 ECMAScriptJSON.stringify()兼容的方法序列化对 JSON 对象的属性进行递归的字典序排序JSON 数组同样参与规范化但元素顺序保持不变。JCS 在 inngest 中的落点inngest 将github.com/gowebpki/jcs v1.0.0见 go.mod用于三处对字节一致性极其敏感的环节场景位置用途SDK 请求签名vendor/github.com/inngest/inngestgo/signature.go对请求体做 JCS 规范化后再计算 HMAC-SHA256 签名Connect Worker 分组哈希pkg/connect/state/group.go对函数配置做 JCS 规范化后取 SHA-256用于识别 Worker 分组执行器请求体预处理pkg/execution/driver/driver.go发给 SDK 的请求体先经 JCS 规范化这些调用点将在后文逐一展开它们共同体现了 JCS 的典型价值让不同语言、不同 SDK 实现产出的字节流严格一致从而保证跨端签名验证与哈希判等成立。快速上手Transform 的核心用法该库的公开 API 只有一个核心函数定义在 vendor/github.com/gowebpki/jcs/jcs.go// Transform converts raw JSON data from a []byte array into a // canonicalized version according RFC 8785 func Transform(jsonData []byte) ([]byte, error)基本用法package main import ( fmt github.com/gowebpki/jcs ) func main() { raw : []byte({ b : 1, a : [ 2, 1 ] }) canonical, err : jcs.Transform(raw) if err ! nil { panic(err) } // 输出{a:[2,1],b:1} fmt.Println(string(canonical)) }Transform的输入是原始 UTF-8 编码的 JSON 字节数组输出为规范化后的字节数组。从源码看它的执行分三步判空jsonData nil时返回No JSON data provided错误调用递归下降解析器parseElement解析根元素对象 / 数组 / 字符串 / 简单类型解析完成后剩余的字节只允许是空白符0x20空格、0x0a换行、0x0d回车、0x09制表符否则返回Improperly terminated JSON object错误。值得注意的细节Transform的返回值是所有内部空白全部被移除的紧凑 JSON——这也是规范化的一部分因为空白符不是逻辑内容却会影响字节级比较。规范化规则详解源码级下面结合jcs.go与es6numfmt.go的实现逐条拆解 JCS 对每一类 JSON 数据的处理。对象递归字典序排序对象的处理位于parseObjectjcs.go流程是逐个读取name : value成员对将属性名转换为UTF-16 code unit 序列作为排序键sortKey : utf16.Encode([]rune(rawUTF8))按排序键做字典序lexicographic插入排序逐位比较 UTF-16 code unit 的数值较小者排前若前缀完全相同较短者排前如a排在aa前若两个属性名排序键完全相同返回Duplicate key: ...错误——重复键在规范化中是不合法的由于parseElement是递归的对象的嵌套对象、嵌套数组同样会递归规范化这正是 README 所说recursive process的含义。排序依据是 UTF-16 code unit 而非 Unicode 码点这是为了与 ECMAScript 的行为完全一致——JSON.stringify在内部正是按 UTF-16 code unit 处理字符串的。源码注释也点明了这一点Sort keys on UTF-16 code units. Since UTF-8 doesnt have endianness this is just a value transformation. In the Go case the transformation is UTF-8 UTF-32 UTF-16。数组元素顺序保持不变parseArrayjcs.go逐元素调用parseElement并重新拼装[...]。数组的每个元素都参与规范化对象元素会排序、数字会重写但元素之间的相对顺序绝不改变——因为数组天然是有序集合调整顺序会破坏语义。字符串最小的必要转义字符串处理由parseQuotedString解析与decorateString重写两段组成jcs.go输入侧解析\、\\、\b、\f、\n、\r、\t七种标准转义\/被识别为良性但无用的转义直接还原为/\uXXXX则解析为 UTF-16 code unit——若遇到代理对surrogate pair会校验其后的\u转义并调用utf16.DecodeRune解码为完整码点缺失代理对时报Missing surrogate错误。输出侧字符串以最小转义集重新序列化——只有\、\\、\b、\f、\n、\r、\t会被转义其余 ASCII 控制字符 0x20统一写成\u00xx小写十六进制如\u000f非 ASCII 字符则以原始 UTF-8 字节直接输出。特别地/不需要转义。也就是说输入中各种等价写法\u0042vsB、\nvs 真实换行、\/vs/在输出中都被收敛为同一种唯一形态。数字ES6 风格的最短往返表示这是 JCS 最微妙也最体现互操作性的部分。parseSimpleType先识别字面量true、false、null其余按 I-JSON 数字处理调用strconv.ParseFloat(value, 64)解析为 IEEE-754 双精度浮点数再用NumberToJSONvendor/github.com/gowebpki/jcs/es6numfmt.go序列化。NumberToJSON的规则依次是拒绝 NaN / InfinityIEEE-754 的指数位全 1掩码0x7ff0000000000000即为非法 JSON 数字返回null与错误消除-0-0与0一律输出为0ES6-JSON/JCS 规范要求符号单独处理负数先取绝对值再在前面加-选择 ES6 的 g 格式当数值满足1e-6 x 1e21时用f定点格式否则用e科学计数法格式——这正是 ECMAScript 最短往返表示shortest round-trip的规则用strconv.FormatFloat(..., -1, 64)产出最短且能还原回同一浮点数的十进制表示修正指数写法Go 会输出1e09需重写为 JCS 要求的1e9去掉指数前导零。这一套规则保证4.50→4.51E30→1e302e-3→0.002333333333.33333329→333333333.3333333浮点精度内的最短表示。空白与整体结构isWhiteSpace仅认四种字节空格、换行、回车、制表符scan跳过它们读取下一个有效字符Transform收尾时再次确认尾部无多余非空白内容。因此最终输出必然是无空白、结构紧凑的 JSON。官方样例验证README 给出了权威的输入输出对照可用来验证任何 JCS 实现的正确性。输入{ numbers: [333333333.33333329, 1E30, 4.50, 2e-3, 0.000000000000000000000000001], string: \u20ac$\u000F\u000aA\u0042\u0022\u005c\\\\/, literals: [null, true, false] }期望输出{literals:[null,true,false],numbers:[333333333.3333333,1e30,4.5,0.002,1e-27],string:€$\u000f\nAB\\\\\\/}对照前文规则逐项验证属性按字典序排序literals→numbers→string数字全部收敛为最短表示333333333.3333333IEEE-754 双精度下的最短往返值、1e30、4.5、0.002、1e-27小于1e-6走科学计数法且指数无前导零字符串转义收敛\u20ac输出为 UTF-8 字节€控制字符\u000F写成\u000f\u000a收敛为\n\u0042还原为B\u0022收敛为\\u005c与\\收敛为\\\保持\\/还原为/数组元素顺序原样保留字面量null、true、false保持不变。互操作的关键统一到 UTF-8README 特别提醒为了平台间互操作输出必须统一转换为UTF-8。以十六进制表示上述输出的字节序列为7b 22 6c 69 74 65 72 61 6c 73 22 3a 5b 6e 75 6c 6c 2c 74 72 75 65 2c 66 61 6c 73 65 5d 2c 22 6e 75 6d 62 65 72 73 22 3a 5b 33 33 33 33 33 33 33 33 33 2e 33 33 33 33 33 33 33 2c 31 65 2b 33 30 2c 34 2e 35 2c 30 2e 30 30 32 2c 31 65 2d 32 37 5d 2c 22 73 74 72 69 6e 67 22 3a 22 e2 82 ac 24 5c 75 30 30 30 66 5c 6e 41 27 42 5c 22 5c 5c 5c 5c 5c 22 2f 22 7d可以看到€U20AC以 UTF-8 三字节e2 82 ac输出控制字符与转义则以 ASCII 字节输出。任何语言的 JCS 实现只要遵守 RFC 8785对这个输入都应产出完全相同的字节序列——这正是规范化的终极含义。实战inngest 如何用 JCS 保障签名与哈希场景一SDK 请求的 HMAC 签名inngest 的 Go SDK 在签名环节直接使用 JCSvendor/github.com/inngest/inngestgo/signature.gofunc Sign(ctx context.Context, at time.Time, key, body []byte) (string, error) { key normalizeKey(key) var err error if len(body) 0 { body, err jcs.Transform(body) if err ! nil { logger.Default().Warn(failed to canonicalize body, error, err) } } ts : at.Unix() if at.IsZero() { ts time.Now().Unix() } mac : hmac.New(sha256.New, key) _, _ mac.Write(body) // Write the timestamp as a unix timestamp to the hmac to prevent timing attacks. _, _ fmt.Fprintf(mac, %d, ts) sig : hex.EncodeToString(mac.Sum(nil)) return fmt.Sprintf(t%ds%s, ts, sig), nil }关键点请求体先经jcs.Transform规范化再进入 HMAC-SHA256 计算。这意味着任何 SDK 语言实现只要遵守 JCS对同一逻辑请求都会算出相同的签名时间戳ts被写进 HMAC 输入防止重放/时序攻击签名格式为tunix时间戳shex摘要验签侧validateRequestSignature会对收到的 body 再次执行jcs.Transform后重新计算签名并比对同时校验时间戳在signatureTimeDeltaMax 5 * time.Minute窗口内防止过期请求签名密钥通过normalizeKey去除signkey-xxx-前缀响应签名走signWithoutJCS——刻意不做规范化仅去掉 Go 编码器附加的尾部换行。这说明是否规范化是签名协议的设计决策请求体来自各语言 SDK必须规范化才能跨语言一致响应体由服务端统一生成字节流本就可控。场景二Connect Worker 分组哈希inngest 的 Connect 网关需要判断多个 Worker 是否属于同一分组拥有完全一致的函数配置实现于 pkg/connect/state/group.gofunc functionConfigHash(appConfig *connect.AppConfiguration) ([]byte, error) { var functionHash []byte b, err : jcs.Transform(appConfig.Functions) if err ! nil { return nil, fmt.Errorf(could not canonicalize function config: %w, err) } res : sha256.Sum256(b) functionHash res[:] return functionHash, nil }该哈希随后与账号、环境、SDK 语言/版本、平台、应用版本一起拼成字符串再做一次 SHA-256 得到最终分组哈希workerGroupHashFromConnRequest。这里 JCS 的价值是不同 SDK、不同序列化顺序产出的函数配置经规范化后哈希值一致从而能可靠地将配置相同的 Worker 归入同一分组。场景三执行器请求体预处理执行器在将请求发给 SDK 前也先用 JCS 规范化pkg/execution/driver/driver.goj, err : json.Marshal(req) if err ! nil { return nil, fmt.Errorf(error marshalling request to JSON: %w, err) } // ... 检查请求体是否超过 consts.MaxSDKRequestBodySize 限制 ... b, err : jcs.Transform(j) if err ! nil { return nil, fmt.Errorf(error transforming request with JCS: %w, err) } return b, nil请求体先json.Marshal再jcs.Transform最终以规范化字节流交付给 SDK保证后续任何基于请求体的签名/校验如场景一的验签逻辑都能拿到稳定的字节输入。与其他方案的边界与取舍JWSRFC 7515组合JCS 常与 JWS 结合使用典型模式是Detached JWS JCS——先规范化 JSON 载荷再签名实现无复制detached的签名方案其他规范化努力社区还存在多种 JSON 规范化提案如 draft-staykov-hu-json-canonical-form、Canonical JSON 等但RFC 8785 / JCS 是当前互操作生态含 ES6 行为对齐中应用最广的标准其独特之处在于数字格式与字符串转义都严格对齐 ECMAScript 语义ECMAScript 侧的演进社区亦在推进JSON.canonify()提案试图将规范化能力原生引入 JS 标准库进一步说明规范化正在成为跨语言互操作的基础设施能力。项目背景与许可本仓库中的实现位于 vendor/github.com/gowebpki/jcs最初由 Anders Rundgrencyberphone创作json-canonicalization项目后由 Bret Jordan 与 Benedikt Thoma 在原作者许可下 fork 并清理了 Go 版本以 Apache 2.0 许可发布见 LICENSE。仓库共三个文件核心解析器 jcs.go约 460 行手写递归下降解析器、ES6 数字格式化器 es6numfmt.go、以及本文依据的 README.md。小结JCSRFC 8785用三句话即可概括原始类型按 ECMAScript 语义序列化、对象属性递归字典序排序、数组元素顺序不动。本文通过gowebpki/jcs的源码拆解了它的完整实现——从 UTF-16 code unit 排序键、最小转义字符串、ES6 最短往返数字格式到-0消除、指数前导零修正、代理对校验——并展示了 inngest 在 SDK 签名signature.go、Worker 分组哈希group.go与执行器请求预处理driver.go三处的落地实践。无论你是在设计跨语言签名协议、需要稳定哈希函数配置还是想让自己的 API 具备字节级可复现的请求体JCS 都是一套标准、成熟、可直接复用的方案先jcs.Transform再哈希或签名。【免费下载链接】inngestThe leading workflow orchestration platform. Run stateful step functions and AI workflows on serverless, servers, or the edge.项目地址: https://gitcode.com/GitHub_Trending/in/inngest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门