如何对JSON对象进行加密哈希处理?

63
以下问题看起来比它本身更加复杂。
假设我有一个任意的JSON对象,它可能包含任意数量的数据,包括其他嵌套的JSON对象。我想要的是JSON数据的加密哈希/摘要,而不考虑实际的JSON格式本身(例如:忽略JSON标记之间的换行和空格差异)。
最后一部分是必需的,因为JSON将由许多不同平台上的各种(反)序列化器生成/读取。我知道至少有一个Java的JSON库在反序列化期间完全删除格式。因此它会破坏哈希。
上述任意数据条款还使事情变得复杂,因为它防止我按给定顺序获取已知字段并在散列之前连接它们(大致思考Java的非加密hashCode()方法如何工作)。
最后,对JSON字符串进行整体散列处理(在反序列化之前)也不可取,因为在计算哈希时应忽略JSON中的某些字段。
我不确定是否有解决此问题的好方法,但欢迎任何方法或想法=)

1
你看过XML DSig了吗?他们有同样的问题,并且有一个相当复杂的“规范化”规范。 - mtraut
5
我注意到你的名字与问题非常贴切。 - Nandeep Mali
6
正在进行标准化。请参阅JSON Web Signature(JWS)草案RFC。http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-17 - user239558
1
该RFC仅指定了一种JSON格式来存储有效载荷+签名+一些标头,没有提到JSON规范化。 - Roman Plášil
@RomanPlášil,Go / Node.js / Python 中已经有现成的实现,你可以使用它们并为你进行规范化。 - Natim
1
您也可以查看此RFC草案:http://dpaste-bkero.paas.allizom.org/MtkA/raw - Natim
9个回答

56
这个问题在计算允许灵活性的任何数据格式的哈希时都很常见。要解决这个问题,您需要使表示规范化。
例如,Twitter和其他服务用于身份验证的OAuth1.0a协议需要请求消息的安全哈希。为了计算哈希,OAuth1.0a说您需要首先按字母顺序排列字段,用换行符分隔它们,删除字段名称(这些名称是众所周知的),并使用空行表示空值。签名或哈希是在规范化结果上计算的。
XML DSIG也是如此 - 您需要在签名之前对XML进行规范化。由于这是签名的基本要求,因此有一个提议的W3标准涵盖此问题。有些人称之为c14n。
我不知道json的规范化标准。值得研究一下。
如果没有这样的标准,您肯定可以为您特定的应用程序使用建立一个约定。一个合理的开始可能是:
  • 按名称字典顺序排序属性
  • 所有名称使用双引号
  • 所有字符串值都使用双引号
  • 名称和冒号之间以及冒号和值之间没有空格或只有一个空格
  • 值和逗号之间没有空格
  • 所有其他空格都被折叠为单个空格或无空格 - 选择一种方式
  • 排除任何不想签名的属性(例如,保存签名本身的属性)
  • 使用所选算法对结果进行签名

您可能还想考虑如何在JSON对象中传递该签名 - 可能会建立一个众所周知的属性名称,例如“nichols-hmac”,该名称获取哈希的Base64编码版本。此属性必须明确地被哈希算法排除。然后,JSON的任何接收者都可以检查哈希。

规范化表示不需要是应用程序中传递的表示形式。它只需要在给定任意JSON对象时易于生成。


4
规范化还必须考虑字符表示方式的差异: "A""\u0041""é""\u00e9" 以及 "\u00E9"。数字也存在同样的问题:10.1e1 - dolmen
1
规范化也必须考虑数字。ECMAscript定义了JSON.stringify,它指定如果数字在[1e-6, 1e21)范围内,则不使用指数格式化数字;否则,在小数点前使用1位数字进行格式化。 - Dave

9

如果你不想自己发明JSON规范化/规范化方法,可以考虑使用bencode。从语义上讲,它与JSON相同(由数字、字符串、列表和字典组成),但具有加密哈希所必需的明确编码属性。

bencode被用作种子文件格式,每个BitTorrent客户端都包含一个实现。


JSON非常受欢迎,因为几乎每种编程语言都有可用于对象(反)序列化的库。 - Jason Nichols
6
我指的是仅在哈希之前使用bencode作为规范化步骤。除了您的哈希算法外,一切都保持JSON格式。 - Nikita Nemkin
bencode非常棒且易于实现。规范JSON也无法使用标准JSON解析器进行解析。对于此应用程序,两者都不需要被解析,因为它只需要哈希函数输入。 - joeforker
1
我在一个名为Learning Registry的OSS项目上工作,它是一个分布式JSON数据库。我们必须在每个JSON文档进入数据库之前对其进行签名。为了实现这一点,我们(除其他事项外)将JSON转换为Bencode进行签名,因为Bencode是可靠的语义表示,而JSON则不是(根据我们的经验)。 - Steve Midgley
3
bencoding 仅编码字节字符串,而 JSON 则编码 Unicode 字符串。因此,您需要在 bencode 的基础上设计一个针对 JSON 字符串的规范化方法。而 bencode 不会编码 JSON 所拥有的浮点数值。 - dolmen

8
这与S/MIME签名和XML签名存在的问题相同。也就是说,要签名的数据有多个等效的表示方式。
例如,在JSON中:
{  "Name1": "Value1", "Name2": "Value2" }

对比。

{
    "Name1": "Value\u0031",
    "Name2": "Value\u0032"
}

或者,根据您的应用程序,这甚至可能是等效的:
{
    "Name1": "Value\u0031",
    "Name2": "Value\u0032",
    "Optional": null
}

规范化可以解决这个问题,但实际上你根本不需要面对这个问题。

如果您能够控制规范,简单的解决方法是将对象包装在某种容器中,以保护它免于转换为"等效"但不同的表示形式。

也就是说,不要签署"逻辑"对象,而是签署特定的序列化表示形式。例如,JSON对象--> UTF-8文本-->字节。将字节视为字节进行签名,然后通过base64编码将其传输为字节。由于您正在签署字节,因此像空格之类的差异是签署的一部分。

不要尝试这样做:

{  
   "JSONContent": {  "Name1": "Value1", "Name2": "Value2" },
   "Signature": "asdflkajsdrliuejadceaageaetge="
}

只需执行以下操作:

{
   "Base64JSONContent": "eyAgIk5hbWUxIjogIlZhbHVlMSIsICJOYW1lMiI6ICJWYWx1ZTIiIH0s",
   "Signature": "asdflkajsdrliuejadceaageaetge="

}

不要对JSON进行签名,而是签名编码后的JSON字节。是的,这意味着签名不再透明。

优点:这样做可以减少属性之间的耦合,正如您的“Optional”对象所示。小缺点:标准API工具无法理解此封装。不过,为它们生成哈希值也并非易事。 - LexH
4
多年来我一直关注这个问题,但如果我今天必须实现哈希,我会采取以下方法。 - Jason Nichols

3

JSON-LD可以进行规范化操作。

您需要定义上下文。


2

1
理想情况下,JavaScript本身应该为JavaScript对象定义一个正式的哈希过程。
然而,我们确实有RFC-8785 JSON规范化方案(JCS),希望它可以在大多数JSON库和特别是流行的JavaScript JSON对象中实现。通过进行这种规范化,只需应用您首选的哈希算法即可。
如果浏览器和其他工具和库中都提供了JCS,那么可以合理地期望大多数传输中的JSON数据都以这种常见的规范化形式存在。对于像这样的标准的普遍一致应用和验证,可以在一定程度上抵御一些微不足道的安全威胁。

0

我会按照给定的顺序(例如按字母顺序)处理所有字段。为什么任意数据会有所不同?您可以通过属性迭代(反射)来解决。

或者,我会尝试将原始的JSON字符串转换为某种明确定义的规范形式(删除所有多余的格式)-并对其进行哈希处理。


0

我们在使用哈希JSON编码负载时遇到了一个简单的问题。在我们的情况下,我们使用以下方法:

  1. 将数据转换为JSON对象;
  2. 对JSON负载进行base64编码
  3. 对生成的base64负载进行消息摘要(HMAC)。
  4. 传输base64负载。

使用此解决方案的优点:

  1. Base64将为给定的负载产生相同的输出。
  2. 由于生成的签名将直接从base64编码的负载派生而来,并且由于base64负载将在端点之间交换,因此我们可以确保签名和负载将得到维护。
  3. 此解决方案解决了由于特殊字符编码差异而引起的问题。

缺点

  1. 负载的编码/解码可能会增加开销
  2. Base64编码的数据通常比原始负载大30%以上。

Base64编码的数据通常比原始负载大约**30%**。 - Deezzle LuBimkii
你难道不需要先对对象键进行排序(例如按字母顺序),因为JSON不能保证对象键的顺序,所以相同的数据可能有不同的键顺序,从而产生不同的哈希值吗? - pythonjsgeo
不需要对原始字符串进行排序,因为您将对b64有效载荷进行哈希处理,而不是对原始JSON字符串本身进行哈希处理。 - Deezzle LuBimkii
@DeezzleLuBimkii “载荷”指什么?具体而言,如果我有对象 {"a":1,"b":2},当你说“载荷”时,你是指仅值 12,还是指整个序列化字符串 '{"a":1,"b":2}'?如果是后者,则键的顺序绝对很重要。顺序的更改将更改base64编码。如果您指的是前者,那么这并没有完全解决问题。 - Michael Matthew Toomim
2
@DeezzleLuBimkii,你在“将序列化数据转换为base64”方面表现得很含糊——关键是在你检查之前必须将数据序列化为JSON字符串,因此,如果字段的顺序发生了变化——即使实际上是相同的数据——检查也会失败。因此,如果您要检查在可能进行反序列化和重新序列化后数据是否相同,则顺序很重要。 - taxilian
显示剩余3条评论

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接