RFC 8785 JSON 规范化方案 2020 年 6 月
Rundgren, et al. 资料性 [页]
流:
独立提交
RFC:
8785
类别:
资料性
发布:
ISSN:
2070-1721
作者:
A. Rundgren
Independent
B. Jordan
Broadcom
S. Erdtman
Spotify AB

RFC 8785

JSON 规范化方案 (JCS)

摘要

哈希和签名等加密操作需要将数据表示为一种不变格式, 以便这些操作能够可靠地重复执行。 解决这一问题的一种方式是创建数据的规范表示。 规范化还允许数据以其原始形式在“线路”上传输, 同时生产者和消费者端点对数据的规范化副本执行的 加密操作会生成一致的结果。

本文档描述 JSON 规范化方案 (JCS)。 本规范定义了如何通过基于 ECMAScript 为 JSON 原语定义的严格序列化方法、将 JSON 数据约束到 Internet JSON (I-JSON) 子集,以及使用确定性的属性 排序,来创建 JSON 数据的规范表示。

本备忘录状态

本文档不是互联网标准跟踪规范;它是为资料性目的而发布的。

这是对 RFC 系列的贡献,独立于任何其他 RFC 流。 RFC 编辑器已自行决定发布本文档,并且不对其实现或部署价值 作出任何声明。经 RFC 编辑器批准发布的文档不是任何级别 互联网标准的候选;见 RFC 7841 第 2 节。

关于本文档当前状态、任何勘误以及如何提供反馈的信息,可通过 https://www.rfc-editor.org/info/rfc8785 获得。

目录

1. 引言

本文档描述 JSON 规范化方案 (JCS)。 本规范定义了如何通过基于 ECMAScript [ECMA-262] 为 JSON 原语定义的严格序列化方法、将 JSON [RFC8259] 数据约束到 I-JSON [RFC7493] 子集,以及使用确定性的属性排序,来创建 JSON 数据的规范表示。 JCS 的输出是 JSON 数据的一种“可哈希”表示,可供加密方法使用。 后续段落概述主要设计考虑。

哈希和签名等加密操作需要将数据表示为一种不变格式, 以便这些操作能够可靠地重复执行。 实现这一点的一种方式是把数据转换为一种具有简单且固定表示的格式, 例如 base64url [RFC4648]。 JSON Web Signature (JWS) [RFC7515] 就是这样处理这一问题的。 另一种解决方案是创建数据的规范版本, 类似于 XML 签名 [XMLDSIG] 标准所采用的做法。

规范化方案的主要优势是数据可以保持其原始形式。 这是 JCS 背后的核心理由。 换句话说,使用规范化使 JSON 对象即使在签名之后仍能保持为 JSON 对象。 这可以简化系统设计、文档编写和日志记录。

为避免“重新发明轮子”,JCS 依赖 ECMAScript(又称 JavaScript) [ECMA-262] 从第 6 版开始定义的 JSON 原语 (字符串、数字和字面量)序列化。

经验丰富的 XML 开发人员可能还记得让 XML 签名通过验证时遇到的困难。 这通常是由于对相当复杂的 XML 规范化规则以及同样复杂的 Web Services 安全标准有不同解释造成的。 JCS 不应遭受类似问题的原因如下:

JCS 与一些依赖 JSON 规范化的现有系统兼容,例如 JSON Web Key (JWK) Thumbprint [RFC7638] 和 Keybase [KEYBASE]

关于加密之外的潜在用途,见 [JSONCOMP]

本文档的预期读者是 JSON 工具供应商以及基于 JSON 的加密解决方案的设计者。 假定读者了解 ECMAScript,包括 "JSON" 对象。

2. 术语

注意,本文档不属于 IETF 标准跟踪。然而,出于安全性和互操作性原因, 合规实现应当遵循所规定的行为。本文使用 BCP 14 来描述这种必要行为。

本文档中的关键词 "MUST"、"MUST NOT"、 "REQUIRED"、"SHALL"、"SHALL NOT"、"SHOULD"、"SHOULD NOT"、 "RECOMMENDED"、"NOT RECOMMENDED"、 "MAY" 和 "OPTIONAL",当且仅当它们以全大写形式出现时, 应按 BCP 14 [RFC2119] [RFC8174] 中的描述进行解释, 如这里所示。

3. 详细操作

本节描述与创建规范 JSON 表示相关的细节,以及 JCS 如何处理这些细节。

附录 F 描述了向现有 JSON 工具添加 JCS 支持的 RECOMMENDED 方式。

3.1. 输入数据的创建

要进行规范序列化的数据通常通过以下方式创建:

  • 解析先前生成的 JSON 数据。
  • 以编程方式创建数据。

无论使用哪种方法,要序列化的数据都 MUST 适配 I-JSON [RFC7493] 格式,这意味着以下要求:

  • JSON 对象 MUST NOT 呈现重复的属性名称。
  • JSON 字符串数据 MUST 能够表示为 Unicode [UNICODE]
  • JSON 数字数据 MUST 能够表示为 IEEE 754 [IEEE754] 双精度值。 对于需要比 IEEE 754 双精度所提供的精度更高或整数更长的应用, RECOMMENDED 将这类数字表示为 JSON 字符串; 关于如何以可互操作且可扩展的方式实现这一点,见附录 D

一个附加约束是,已解析的 JSON 字符串数据在后续序列化期间 MUST NOT 被更改。更多信息见附录 E

注意:尽管 Unicode 标准提供了重新排列某些字符序列的可能性, 称为“Unicode 规范化” [UCNORM], JCS 合规的字符串处理不会考虑这一点。也就是说,依赖 JCS 的方案中 涉及的所有组件 MUST “按原样”保留 Unicode 字符串数据。

3.2. 规范 JSON 数据的生成

以下小节描述创建上一节所详述数据的规范 JSON 表示所需的步骤。

附录 A 展示了基于 ECMAScript 的规范化器示例代码, 与 JCS 规范相匹配。

3.2.1. 空白

JSON 标记之间的空白 MUST NOT 被输出。

3.2.2. 原始数据类型的序列化

假定解析了以下 JSON 对象:

  {
    "numbers": [333333333.33333329, 1E30, 4.50,
                2e-3, 0.000000000000000000000000001],
    "string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/",
    "literals": [null, true, false]
  }

如果随后使用符合 ECMAScript "JSON.stringify()" 的序列化器 对解析后的数据进行序列化,结果(仅为显示目的添加了换行) 将与原始数据有很大差异:

  {"numbers":[333333333.3333333,1e+30,4.5,0.002,1e-27],"string":
  "€$\u000f\nA'B\"\\\\\"/","literals":[null,true,false]}

解析后的数据与其序列化对应物之间存在差异的原因在于, 输入数据具有较宽的容忍度(如 JSON [RFC8259] 所定义), 而输出数据(如 ECMAScript 所定义)具有固定表示。 如示例所示,数字也会受到舍入影响。

以下小节描述按照 JCS 对原始 JSON 数据类型进行序列化。 这一部分与 ECMAScript 的相应部分相同。 在未来某个 ECMAScript 版本使以下任何序列化方法失效的 (不太可能的)情况下,开发者社区将需要决定是坚持本规范, 还是创建新的规范。

3.2.2.1. 字面量的序列化

按照 JSON [RFC8259], 字面量 "null"、"true" 和 "false" MUST 分别被序列化为 null、true 和 false。

3.2.2.2. 字符串的序列化

对于 JSON 字符串数据(也包括 JSON 对象属性名称), 每个 Unicode 码点 MUST 按如下方式序列化 (见 [ECMA-262] 第 24.3.2.2 节):

  • 如果 Unicode 值落在传统 ASCII 控制字符范围内 (U+0000 到 U+001F),则除非它属于预定义 JSON 控制字符集合 U+0008、U+0009、U+000A、U+000C 或 U+000D,否则它 MUST 使用小写十六进制 Unicode 记法 (\uhhhh) 序列化;这些预定义控制字符 MUST 分别序列化为 \b、\t、\n、\f 和 \r。
  • 如果 Unicode 值位于 ASCII 控制字符范围之外, 则除非它等价于 U+005C (\) 或 U+0022 ("), 否则它 MUST “按原样”序列化; 这两个字符 MUST 分别序列化为 \\ 和 \"。

最后,所得 Unicode 码点序列 MUST 用双引号 (") 包围。

注意:由于无效 Unicode 数据(例如“孤立代理项”,如 U+DEAD) 可能导致互操作性问题,包括签名损坏,因此出现这类数据 MUST 使合规 JCS 实现以适当错误终止。

3.2.2.3. 数字的序列化

ECMAScript 基于 IEEE 754 [IEEE754] 双精度标准来表示 JSON 数字数据。这类数据 MUST[ECMA-262] 第 7.1.12.1 节进行序列化, 包括 "Note 2" 增强。

由于这一部分相对复杂,本文档不包含算法本身。 对于 JCS 合规数字序列化的实现者,Google 在 V8 [V8] 中的实现可作为参考。 另一个兼容的数字序列化参考实现是 Ryu [RYU], 它被附录 G 中提到的 JCS 开源 Java 实现使用。 附录 B 包含一组 IEEE 754 样例值及其对应的 JSON 序列化。

注意:由于 JSON 中不允许 Not a Number (NaN) 和 Infinity, 出现 NaN 或 Infinity MUST 使合规 JCS 实现以适当错误终止。

3.2.3. 对象属性的排序

尽管前一步规范化了原始 JSON 数据类型的表示, 但结果仍不符合“规范”要求,因为 JSON 对象属性并未按 字典序(字母顺序)排列。

将其应用到第 3.2.2 节中的样例,正确规范化后的版本应当 (仅为显示目的添加了换行)如下所示:

  {"literals":[null,true,false],"numbers":[333333333.3333333,
  1e+30,4.5,0.002,1e-27],"string":"€$\u000f\nA'B\"\\\\\"/"}

按照 JCS 对 JSON 对象属性进行字典序排序的规则如下:

  • JSON 对象属性 MUST 递归排序, 这意味着 JSON 子对象的属性也 MUST 排序。
  • JSON 数组数据也 MUST 扫描是否存在 JSON 对象(如果发现对象,则其属性 MUST 排序), 但数组元素顺序 MUST NOT 被更改。

当 JSON 对象即将对其属性排序时,MUST 遵守以下措施:

  • 排序过程应用于属性名称字符串的“原始”(未转义)形式。 也就是说,换行字符被视为 U+000A。
  • 要排序的属性名称字符串被格式化为 UTF-16 [UNICODE] 码元数组。 排序基于纯值比较,其中码元被视为无符号整数, 与区域设置无关。
  • 属性名称字符串要么在某个对两个字符串都有效的索引处具有不同值, 要么长度不同,或者两者兼有。 如果它们在一个或多个索引位置具有不同值, 令 k 为最小的此类索引;则根据使用 "<" 运算符确定的结果, 在位置 k 的值较小的字符串按字典序位于另一个字符串之前。 如果不存在它们不同的索引位置,则较短的字符串按字典序位于较长字符串之前。

    用通俗英语说,这意味着属性名称按如下升序排序:

            ""
            "a"
            "aa"
            "ab"
    

将排序算法基于 UTF-16 码元的理由是, 它直接映射到 ECMAScript(Web 浏览器和 Node.js 中的特性)、 Java 和 .NET 中的字符串类型。此外,JSON 仅支持表示为 UTF-16 码元的转义序列,因此了解和处理这类数据无论如何都是必要的。 使用另一种内部字符串数据表示的系统需要在排序之前将 JSON 属性名称字符串转换为 UTF-16 码元数组。 从 UTF-8 或 UTF-32 到 UTF-16 的转换由 Unicode [UNICODE] 标准定义。

以下 JSON 测试数据可用于验证 JCS 实现中排序方案的正确性:

  {
    "\u20ac": "Euro Sign",
    "\r": "Carriage Return",
    "\ufb33": "Hebrew Letter Dalet With Dagesh",
    "1": "One",
    "\ud83d\ude00": "Emoji: Grinning Face",
    "\u0080": "Control",
    "\u00f6": "Latin Small Letter O With Diaeresis"
  }

属性字符串排序后的预期参数顺序:

  "Carriage Return"
  "One"
  "Control"
  "Latin Small Letter O With Diaeresis"
  "Euro Sign"
  "Emoji: Grinning Face"
  "Hebrew Letter Dalet With Dagesh"

注意:为了获得确定性的属性顺序,对以 UTF-8 或 UTF-32 编码的数据进行排序也可行,但对于上述 JSON 数据,其结果会不同, 因而与本规范不兼容。 然而,在实践中,属性名称很少在 7 位 ASCII 之外定义, 因此可以在不转换为 UTF-16 的情况下以 UTF-8 或 UTF-32 格式排序字符串数据,并仍与 JCS 兼容。这是否可行取决于 使用 JCS 的环境。

3.2.4. UTF-8 生成

最后,为了创建平台无关的表示,前一步的结果 MUST 以 UTF-8 编码。

将其应用到第 3.2.3 节中的样例,应得到以下字节, 这里以十六进制记法显示:

  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

此数据旨在可用作加密方法的输入。

4. IANA 考虑事项

本文档没有 IANA 操作。

5. 安全考虑事项

对输入数据执行健全性检查至关重要,以避免缓冲区溢出以及可能影响系统完整性的类似问题。

当 JCS 应用于签名方案(例如附录 F 中描述的方案)时,应用在根据接收数据采取行动之前 MUST 执行以下操作:

  1. 解析 JSON 数据并验证其符合 I-JSON。
  2. 根据将要使用该数据的生态系统所定义的约定,验证数据的正确性。 这还包括定位保存签名数据的属性。
  3. 验证签名。

如果这些步骤中的任何一步失败,正在进行的操作 MUST 被中止。

6. 参考文献

6.1. 规范性参考文献

[ECMA-262]
ECMA International, "ECMAScript 2019 语言 规范", Standard ECMA-262 第 10 版, , <https://www.ecma-international.org/ecma-262/10.0/index.html>.
[IEEE754]
IEEE, "浮点运算 IEEE 标准", IEEE 754-2019, DOI 10.1109/IEEESTD.2019.8766229, <https://ieeexplore.ieee.org/document/8766229>.
[RFC2119]
Bradner, S., "RFC 中用于表示需求级别的关键词", BCP 14, RFC 2119, DOI 10.17487/RFC2119, , <https://www.rfc-editor.org/info/rfc2119>.
[RFC7493]
Bray, T., Ed., "I-JSON 消息 格式", RFC 7493, DOI 10.17487/RFC7493, , <https://www.rfc-editor.org/info/rfc7493>.
[RFC8174]
Leiba, B., "RFC 2119 关键词中大写与小写的 歧义", BCP 14, RFC 8174, DOI 10.17487/RFC8174, , <https://www.rfc-editor.org/info/rfc8174>.
[RFC8259]
Bray, T., Ed., "JavaScript 对象表示法 (JSON) 数据交换格式", STD 90, RFC 8259, DOI 10.17487/RFC8259, , <https://www.rfc-editor.org/info/rfc8259>.
[UCNORM]
The Unicode Consortium, "Unicode 规范化 形式", <https://www.unicode.org/reports/tr15/>.
[UNICODE]
The Unicode Consortium, "Unicode 标准", <https://www.unicode.org/versions/latest/>.

6.2. 资料性参考文献

[JSONCOMP]
Rundgren, A., ""Comparable" JSON (JSONCOMP)", 进行中的工作, Internet-Draft, draft-rundgren-comparable-json-04, , <https://tools.ietf.org/html/draft-rundgren-comparable-json-04>.
[KEYBASE]
Keybase, "JSON 和 Msgpack 的规范打包", <https://keybase.io/docs/api/1.0/canonical_packings>.
[NODEJS]
OpenJS Foundation, "Node.js", <https://nodejs.org>.
[OPENAPI]
OpenAPI Initiative, "OpenAPI 规范:一种用于描述现代 API 的广泛采用的行业标准", <https://www.openapis.org/>.
[RFC4648]
Josefsson, S., "Base16、Base32 和 Base64 数据编码", RFC 4648, DOI 10.17487/RFC4648, , <https://www.rfc-editor.org/info/rfc4648>.
[RFC7515]
Jones, M., Bradley, J., and N. Sakimura, "JSON Web Signature (JWS)", RFC 7515, DOI 10.17487/RFC7515, , <https://www.rfc-editor.org/info/rfc7515>.
[RFC7638]
Jones, M. and N. Sakimura, "JSON Web Key (JWK) Thumbprint", RFC 7638, DOI 10.17487/RFC7638, , <https://www.rfc-editor.org/info/rfc7638>.
[RYU]
"Ryu 浮点数序列化算法", commit 27d3c55, , <https://github.com/ulfjack/ryu>.
[V8]
Google LLC, "什么是 V8?", <https://v8.dev/>.
[XMLDSIG]
W3C, "XML 签名语法与处理 版本 1.1", W3C Recommendation, , <https://www.w3.org/TR/xmldsig-core1/>.

附录 A. ECMAScript 示例 规范化器

下面是一个用于基于 ECMAScript 系统的 JCS 规范化器示例:

  ////////////////////////////////////////////////////////////
  // Since the primary purpose of this code is highlighting //
  // the core of the JCS algorithm, error handling and      //
  // UTF-8 generation were not implemented.                 //
  ////////////////////////////////////////////////////////////
  var canonicalize = function(object) {

      var buffer = '';
      serialize(object);
      return buffer;

      function serialize(object) {
          if (object === null || typeof object !== 'object' ||
              object.toJSON != null) {
              /////////////////////////////////////////////////
              // Primitive type or toJSON, use "JSON"        //
              /////////////////////////////////////////////////
              buffer += JSON.stringify(object);

          } else if (Array.isArray(object)) {
              /////////////////////////////////////////////////
              // Array - Maintain element order              //
              /////////////////////////////////////////////////
              buffer += '[';
              let next = false;
              object.forEach((element) => {
                  if (next) {
                      buffer += ',';
                  }
                  next = true;
                  /////////////////////////////////////////
                  // Array element - Recursive expansion //
                  /////////////////////////////////////////
                  serialize(element);
              });
              buffer += ']';

          } else {
              /////////////////////////////////////////////////
              // Object - Sort properties before serializing //
              /////////////////////////////////////////////////
              buffer += '{';
              let next = false;
              Object.keys(object).sort().forEach((property) => {
                  if (next) {
                      buffer += ',';
                  }
                  next = true;
                  /////////////////////////////////////////////
                  // Property names are strings, use "JSON"  //
                  /////////////////////////////////////////////
                  buffer += JSON.stringify(property);
                  buffer += ':';
                  //////////////////////////////////////////
                  // Property value - Recursive expansion //
                  //////////////////////////////////////////
                  serialize(object[property]);
              });
              buffer += '}';
          }
      }
  };

附录 B. 数字序列化 样例

下表包含一组与 ECMAScript 兼容的数字序列化样例, 包括一些边界情况。"IEEE 754" 列指的是 "Number" 数据类型的 内部 ECMAScript 表示,该表示基于 IEEE 754 [IEEE754] 标准,使用 64 位(双精度)值,这里以十六进制表示。

表 1与 ECMAScript 兼容的 JSON 数字 序列化样例
IEEE 754 JSON 表示 注释
0000000000000000 0
8000000000000000 0 负零
0000000000000001 5e-324 最小正数
8000000000000001 -5e-324 最小负数
7fefffffffffffff 1.7976931348623157e+308 最大正数
ffefffffffffffff -1.7976931348623157e+308 最大负数
4340000000000000 9007199254740992 最大正 整数 (1)
c340000000000000 -9007199254740992 最大负 整数 (1)
4430000000000000 295147905179352830000 约 2**68 (2)
7fffffffffffffff NaN (3)
7ff0000000000000 Infinity (3)
44b52d02c7e14af5 9.999999999999997e+22
44b52d02c7e14af6 1e+23
44b52d02c7e14af7 1.0000000000000001e+23
444b1ae4d6e2ef4e 999999999999999700000
444b1ae4d6e2ef4f 999999999999999900000
444b1ae4d6e2ef50 1e+21
3eb0c6f7a0b5ed8c 9.999999999999997e-7
3eb0c6f7a0b5ed8d 0.000001
41b3de4355555553 333333333.3333332
41b3de4355555554 333333333.33333325
41b3de4355555555 333333333.3333333
41b3de4355555556 333333333.3333334
41b3de4355555557 333333333.33333343
becbf647612f3696 -0.0000033333333333333333
43143ff3c1cb0959 1424953923781206.2 舍入到偶数 (4)

注:

(1)
为了最大限度符合 ECMAScript "JSON" 对象, 要解释为真整数的值 SHOULD 位于 -9007199254740991 到 9007199254740991 范围内。 然而,数字在应用中的使用方式不会影响 JCS 算法。
(2)
尽管像 2**68 这样的一组特定整数可被视为具有扩展精度, 但 JCS/ECMAScript 数字序列化算法并不考虑这一点。
(3)
JSON 中不允许超出范围的值。 见第 3.2.2.3 节
(4)
该数字精确值为 1424953923781206.25,但在应用 第 3.2.2.3 节 中提到的 "Note 2" 规则后,将被截断并舍入到最接近的偶数值。

若要对 JCS 数字序列化器进行更详尽的验证,你可以测试 开发门户(见附录 I)中 (目前)可用的、包含大量样例值的文件。另一个选择是运行 V8 [V8] 作为实时参考,并配合一个生成大量随机 IEEE 754 值的程序。

附录 C. 作为“线路格式”的规范化 JSON

由于规范化过程(见第 3.2.4 节)的结果是完全有效的 JSON, 它也可以用作“线路格式”。然而,这只是一个选项,因为基于 JCS 的加密方案在大多数情况下并不依赖外部提供的 JSON 数据已经规范化。

实际上,使用 "JSON.stringify()" 序列化对象的 ECMAScript 标准方式 会产生一种更“合乎逻辑”的格式,其中属性保持其创建或接收时的顺序。 下面的示例展示了一个可能受益于 ECMAScript 标准序列化的地址记录:

  {
    "name": "John Doe",
    "address": "2000 Sunset Boulevard",
    "city": "Los Angeles",
    "zip": "90001",
    "state": "CA"
  }

使用规范化后,上述属性将按 "address"、"city"、"name"、 "state" 和 "zip" 的顺序输出,从人类(开发者或技术支持) 角度看,这会给数据增加模糊性。 规范化还会将 JSON 数据转换为单行文本,这对于调试和日志记录 可能并不理想。

附录 D. 处理大数

JSON 数字类型存在若干相关问题,这里通过以下示例对象进行说明:

  {
    "giantNumber": 1.4e+9999,
    "payMeThis": 26000.33,
    "int64Max": 9223372036854775807
  }

尽管上述样例符合 JSON [RFC8259], 应用通常会使用不同的原生数据类型来存储 "giantNumber" 和 "int64Max"。 此外,像 "payMeThis" 这样的货币数据大概不会依赖浮点数据类型, 因为十进制算术存在舍入问题。

处理 JSON 数字类型这种“重载”的既定方式(至少以可扩展方式而言) 是通过映射机制,根据属性名称指示解析器应如何处理不同属性。 然而,这大大限制了在 JSON 原始且多少受限的 JavaScript 上下文之外 使用 JSON 数字类型的价值。ECMAScript "JSON" 对象也不支持到 JSON 数字类型的映射。

因此,在当前 JSON 生态系统中没有自然位置的数字 MUST 使用 JSON 字符串类型进行包装。 这接近开放系统中的事实标准。这也适用于 JSON 中没有直接支持的 其他数据类型,例如附录 E 中描述的 "DateTime" 对象。

在使用 JSON 字符串类型的系统辅助下,无论是像下面这样以编程方式

  var obj = JSON.parse('{"giantNumber": "1.4e+9999"}');
  var biggie = new BigNumber(obj.giantNumber);

还是使用像 OpenAPI [OPENAPI] 这样的声明式方案,JCS 都不会对应用施加限制,包括使用 ECMAScript 时也是如此。

附录 E. 字符串子类型处理

由于 JSON 中的数据类型集合有限,JSON 字符串类型通常用于保存子类型。 这可能取决于 JSON 解析方法而导致互操作性问题, 面向更广泛受众的 JCS 合规应用 MUST 处理这些问题。

假设你想解析一个 JSON 对象,其中模式设计者指定属性 "big" 用于保存 "BigInt" 子类型,"time" 用于保存 "DateTime" 子类型, 而 "val" 应当是一个符合 JCS 的 JSON 数字。 以下示例展示了这样的对象:

  {
    "time": "2019-01-28T07:45:10Z",
    "big": "055",
    "val": 3.5
  }

可以通过以下 ECMAScript 语句解析该对象:

  var object = JSON.parse(JSON_object_featured_as_a_string);

解析后,可以提取实际数据;对于子类型, 这还涉及使用解析过程结果(一个 ECMAScript 对象)作为输入的转换步骤:

  ... = new Date(object.time); // Date object
  ... = BigInt(object.big);    // Big integer
  ... = object.val;            // JSON/JS number

注意,"BigInt" 数据类型目前仅由 V8 [V8] 原生支持。

使用附录 A中的示例代码对 "object" 进行规范化,将返回以下字符串:

  {"big":"055","time":"2019-01-28T07:45:10Z","val":3.5}

尽管这在 JCS 方面技术上是正确的,但还有另一种解析 JSON 数据的方式, 也可以与 ECMAScript 一起使用,如下所示:

  // "BigInt" requires the following code to become JSON serializable
  BigInt.prototype.toJSON = function() {
      return this.toString();
  };

  // JSON parsing using a "stream"-based method
  var object = JSON.parse(JSON_object_featured_as_a_string,
      (k,v) => k == 'time' ? new Date(v) : k == 'big' ? BigInt(v) : v
  );

如果现在将附录 A中的规范化器应用到 "object",将生成以下字符串:

  {"big":"55","time":"2019-01-28T07:45:10.000Z","val":3.5}

在这种情况下,"big" 和 "time" 的字符串参数相对于原始值发生了变化, 很可能会使依赖 JCS 的应用失败。

产生偏差的原因是,在基于流和基于模式的 JSON 解析器中, 原始字符串参数通常会即时被原生子类型替换,而该子类型在序列化时 可能表现出不同且依赖平台的模式。

也就是说,基于流和基于模式的解析 MUST 将子类型视为“纯”(不可变)JSON 字符串类型,并在后续步骤中 执行到指定原生类型的实际转换。 在 Go、Java 和 C# 等现代编程平台中,可以通过结合注解、getter 和 setter 以适度工作量实现这一点。下面是 C#/Json.NET 中的一个示例, 展示了可序列化为 JSON 对象的类的一部分:

  // The "pure" string solution uses a local
  // string variable for JSON serialization while
  // exposing another type to the application
  [JsonProperty("amount")]
  private string _amount;

  [JsonIgnore]
  public decimal Amount {
      get { return decimal.Parse(_amount); }
      set { _amount = value.ToString(); }
  }

在应用中,"Amount" 可以像任何其他属性一样访问, 而在 JSON 上下文中它实际上由带引号的字符串表示。

注意:上述示例还处理了 I-JSON 隐含的数字数据约束 (C# "decimal" 数据类型与 IEEE 754 双精度相比具有相当不同的特性)。

E.1. 数组中的子类型

由于 JSON 数组构造允许混合任意 JSON 数据类型, 可能仍然需要自定义解析和序列化代码来处理子类型。

附录 F. 实现指南

最优解决方案是在 JSON 序列化器中直接集成 JCS 支持 (解析器不需要改变)。 也就是说,规范化只是 JSON 序列化器的一个附加“模式”。 然而,目前情况并非如此。 幸运的是,可以通过外部提供的规范化器软件作为现有 JSON 序列化器的后处理器来引入 JCS 支持。 这种安排还使 JCS 实现者无需处理底层数据应如何表示为 JSON。

后处理器概念支持如下签名创建方案:

  1. 创建要签名的数据。
  2. 使用现有 JSON 工具序列化数据。
  3. 让外部规范化器处理序列化数据,并返回规范化后的结果数据。
  4. 对规范化数据进行签名。
  5. 通过指定的签名属性将生成的签名值添加到原始 JSON 数据中。
  6. 使用现有 JSON 工具序列化已完成的(现在已签名的)JSON 对象。

兼容的签名验证方案随后如下:

  1. 使用现有 JSON 工具解析已签名的 JSON 数据。
  2. 从指定的签名属性读取并保存签名值。
  3. 从解析后的 JSON 对象中移除签名属性。
  4. 使用现有 JSON 工具序列化剩余的 JSON 数据。
  5. 让外部规范化器处理序列化数据,并返回规范化后的结果数据。
  6. 使用创建签名时所用的算法和密钥,验证规范化数据是否与保存的签名值匹配。

上述规范化器实际上只是一个“过滤器”,可能可与多种相当不同的加密方案一起使用。

使用集成 JCS 支持的 JSON 序列化器时, 对于这两个过程,都可以消除规范化步骤之前执行的序列化。

附录 G. 开源 实现

以下开源实现已被验证与 JCS 兼容:

附录 H. 其他 JSON 规范化 工作

还有(并且曾经有)其他创建“Canonical JSON”的工作。 下面列出其中一些工作的 URL:

列出的工作都基于文本级 JSON 到 JSON 转换。 文本级规范化的主要特性是,它可以被设计为对所使用的 JSON 风格保持中立。 然而,这类方案也意味着需要对 JSON 解析过程进行重大改变, 这很可能成为采用障碍。 尽管需要付出某些 JSON 和应用约束的代价,JCS 仍被设计为 与现有 JSON 工具兼容。

附录 I. 开发门户

JCS 规范目前在以下位置开发: <https://github.com/cyberphone/ietf-json-canon>

JCS 源代码和大量测试数据可在以下位置获得: <https://github.com/cyberphone/json-canonicalization>

致谢

基于 ECMAScript 数字序列化这一做法最初由 James Manger 提出。 这最终促成了对 JSON 原语采用完整 ECMAScript 序列化方案。

其他为本规范贡献了宝贵意见的人包括 Scott AnanianTim BrayBen CampbellAdrian FarellRichard GibsonBron GondwanaJohn-Mark GurneyMike Jones, John LevineMark MillerMatthew MillerMark NottinghamMike SamuelJim SchaadRobert Tupelo-SchneckMichal Wadas

为进行真实世界的概念验证, Ulf AdamsTanner GoodingRemy Oudompheng 提供的软件以及对数字序列化的支持非常有帮助。

作者地址

Anders Rundgren
Independent
Montpellier
France
Bret Jordan
Broadcom
1320 Ridder Park Drive
San Jose, CA 95131
United States of America
Samuel Erdtman
Spotify AB
Birger Jarlsgatan 61, 4tr
SE-113 56 Stockholm
Sweden