| RFC 8785 | JSON 规范化方案 | 2020 年 6 月 |
| Rundgren, et al. | 资料性 | [页] |
哈希和签名等加密操作需要将数据表示为一种不变格式, 以便这些操作能够可靠地重复执行。 解决这一问题的一种方式是创建数据的规范表示。 规范化还允许数据以其原始形式在“线路”上传输, 同时生产者和消费者端点对数据的规范化副本执行的 加密操作会生成一致的结果。¶
本文档描述 JSON 规范化方案 (JCS)。 本规范定义了如何通过基于 ECMAScript 为 JSON 原语定义的严格序列化方法、将 JSON 数据约束到 Internet JSON (I-JSON) 子集,以及使用确定性的属性 排序,来创建 JSON 数据的规范表示。¶
本文档不是互联网标准跟踪规范;它是为资料性目的而发布的。¶
这是对 RFC 系列的贡献,独立于任何其他 RFC 流。 RFC 编辑器已自行决定发布本文档,并且不对其实现或部署价值 作出任何声明。经 RFC 编辑器批准发布的文档不是任何级别 互联网标准的候选;见 RFC 7841 第 2 节。¶
关于本文档当前状态、任何勘误以及如何提供反馈的信息,可通过 https://www.rfc-editor.org/info/rfc8785 获得。¶
版权所有 (c) 2020 IETF Trust 和列为本文档作者的人员。 保留所有权利。¶
本文档受 BCP 78 以及在本文档发布之日生效的 IETF Trust 与 IETF 文档相关的法律条款 (https://trustee.ietf.org/license-info) 约束。请仔细阅读这些文档,因为它们描述了你关于本文档的 权利与限制。¶
本文档描述 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]。¶
本文档的预期读者是 JSON 工具供应商以及基于 JSON 的加密解决方案的设计者。 假定读者了解 ECMAScript,包括 "JSON" 对象。¶
注意,本文档不属于 IETF 标准跟踪。然而,出于安全性和互操作性原因, 合规实现应当遵循所规定的行为。本文使用 BCP 14 来描述这种必要行为。¶
本文档中的关键词 "MUST"、"MUST NOT"、 "REQUIRED"、"SHALL"、"SHALL NOT"、"SHOULD"、"SHOULD NOT"、 "RECOMMENDED"、"NOT RECOMMENDED"、 "MAY" 和 "OPTIONAL",当且仅当它们以全大写形式出现时, 应按 BCP 14 [RFC2119] [RFC8174] 中的描述进行解释, 如这里所示。¶
本节描述与创建规范 JSON 表示相关的细节,以及 JCS 如何处理这些细节。¶
附录 F 描述了向现有 JSON 工具添加 JCS 支持的 RECOMMENDED 方式。¶
要进行规范序列化的数据通常通过以下方式创建:¶
无论使用哪种方法,要序列化的数据都 MUST 适配 I-JSON [RFC7493] 格式,这意味着以下要求:¶
一个附加约束是,已解析的 JSON 字符串数据在后续序列化期间 MUST NOT 被更改。更多信息见附录 E。¶
注意:尽管 Unicode 标准提供了重新排列某些字符序列的可能性, 称为“Unicode 规范化” [UCNORM], JCS 合规的字符串处理不会考虑这一点。也就是说,依赖 JCS 的方案中 涉及的所有组件 MUST “按原样”保留 Unicode 字符串数据。¶
以下小节描述创建上一节所详述数据的规范 JSON 表示所需的步骤。¶
附录 A 展示了基于 ECMAScript 的规范化器示例代码, 与 JCS 规范相匹配。¶
假定解析了以下 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 版本使以下任何序列化方法失效的 (不太可能的)情况下,开发者社区将需要决定是坚持本规范, 还是创建新的规范。¶
对于 JSON 字符串数据(也包括 JSON 对象属性名称), 每个 Unicode 码点 MUST 按如下方式序列化 (见 [ECMA-262] 第 24.3.2.2 节):¶
最后,所得 Unicode 码点序列 MUST 用双引号 (") 包围。¶
注意:由于无效 Unicode 数据(例如“孤立代理项”,如 U+DEAD) 可能导致互操作性问题,包括签名损坏,因此出现这类数据 MUST 使合规 JCS 实现以适当错误终止。¶
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 实现以适当错误终止。¶
尽管前一步规范化了原始 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 遵守以下措施:¶
属性名称字符串要么在某个对两个字符串都有效的索引处具有不同值, 要么长度不同,或者两者兼有。 如果它们在一个或多个索引位置具有不同值, 令 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 的环境。¶
最后,为了创建平台无关的表示,前一步的结果 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¶
此数据旨在可用作加密方法的输入。¶
对输入数据执行健全性检查至关重要,以避免缓冲区溢出以及可能影响系统完整性的类似问题。¶
当 JCS 应用于签名方案(例如附录 F 中描述的方案)时,应用在根据接收数据采取行动之前 MUST 执行以下操作:¶
如果这些步骤中的任何一步失败,正在进行的操作 MUST 被中止。¶
下面是一个用于基于 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 += '}';
}
}
};
¶
下表包含一组与 ECMAScript 兼容的数字序列化样例, 包括一些边界情况。"IEEE 754" 列指的是 "Number" 数据类型的 内部 ECMAScript 表示,该表示基于 IEEE 754 [IEEE754] 标准,使用 64 位(双精度)值,这里以十六进制表示。¶
| 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)
|
注:¶
若要对 JCS 数字序列化器进行更详尽的验证,你可以测试 开发门户(见附录 I)中 (目前)可用的、包含大量样例值的文件。另一个选择是运行 V8 [V8] 作为实时参考,并配合一个生成大量随机 IEEE 754 值的程序。¶
由于规范化过程(见第 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 数据转换为单行文本,这对于调试和日志记录 可能并不理想。¶
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 时也是如此。¶
由于 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 双精度相比具有相当不同的特性)。¶
最优解决方案是在 JSON 序列化器中直接集成 JCS 支持 (解析器不需要改变)。 也就是说,规范化只是 JSON 序列化器的一个附加“模式”。 然而,目前情况并非如此。 幸运的是,可以通过外部提供的规范化器软件作为现有 JSON 序列化器的后处理器来引入 JCS 支持。 这种安排还使 JCS 实现者无需处理底层数据应如何表示为 JSON。¶
后处理器概念支持如下签名创建方案:¶
兼容的签名验证方案随后如下:¶
上述规范化器实际上只是一个“过滤器”,可能可与多种相当不同的加密方案一起使用。¶
使用集成 JCS 支持的 JSON 序列化器时, 对于这两个过程,都可以消除规范化步骤之前执行的序列化。¶
以下开源实现已被验证与 JCS 兼容:¶
还有(并且曾经有)其他创建“Canonical JSON”的工作。 下面列出其中一些工作的 URL:¶
列出的工作都基于文本级 JSON 到 JSON 转换。 文本级规范化的主要特性是,它可以被设计为对所使用的 JSON 风格保持中立。 然而,这类方案也意味着需要对 JSON 解析过程进行重大改变, 这很可能成为采用障碍。 尽管需要付出某些 JSON 和应用约束的代价,JCS 仍被设计为 与现有 JSON 工具兼容。¶
JCS 规范目前在以下位置开发: <https://github.com/cyberphone/ietf-json-canon>。¶
JCS 源代码和大量测试数据可在以下位置获得: <https://github.com/cyberphone/json-canonicalization>。¶
基于 ECMAScript 数字序列化这一做法最初由 James Manger 提出。 这最终促成了对 JSON 原语采用完整 ECMAScript 序列化方案。¶
其他为本规范贡献了宝贵意见的人包括 Scott Ananian、 Tim Bray、 Ben Campbell、 Adrian Farell、 Richard Gibson、 Bron Gondwana、 John-Mark Gurney、 Mike Jones, John Levine、 Mark Miller、 Matthew Miller、 Mark Nottingham、 Mike Samuel、 Jim Schaad、 Robert Tupelo-Schneck 和 Michal Wadas。¶
为进行真实世界的概念验证, Ulf Adams、 Tanner Gooding 和 Remy Oudompheng 提供的软件以及对数字序列化的支持非常有帮助。¶