相同字符串的 SHA256 不同值

SHA256 different values for same String

我正在生成以下字符串的 SHA256

{
    "billerid": "MAHA00000MUM01",
    "authenticators": 
    [
        {
            "parameter_name": "CA Number",
            "value": "210000336768"
        }
    ],
    "customer": 
    {
        "firstname": "ABC",
        "lastname": "XYZ",
        "mobile": "9344895862",
        "mobile_alt": "9859585525",
        "email": "abc@billdesk.com",
        "email_alt": "abc2@billdesk.com",
        "pan": "BZABC1234L",
        "aadhaar": "123123123123"
    },
    "metadata": 
    {
        "agent": 
        {
            "agentid": "DC01DC31MOB528199558"
        },
        "device": 
        {
            "init_channel": "Mobile",
            "ip": "124.124.1.1",
            "imei": "490154203237518",
            "os": "Android",
            "app": "AGENTAPP"
        }
    },
    "risk":
    [
        {
          "score_provider": "DC31",
          "score_value": "030",
          "score_type": "TXNRISK"
        },
        {
          "score_provider": "BBPS",
          "score_value": "030",
          "score_type": "TXNRISK"
        }
    ]
}

我从不同来源得到不同的 SHA256 输出。 本网站:https://www.freeformatter.com/sha256-generator.html#ad-output 计算上述字符串的 SHA256:053353867b8171a8949065500d7313c69fe7517c9d69eaff11164c35fcb14457

本网站(https://emn178.github.io/online-tools/sha256.html)给出的SHA256为eae5c26759881d48a194a6b82a9d542485d6b6ce96297275c136b1fa6712f253

我在 Javascript 中使用 CryptoJs 库来计算 SHA256,它也给出了 eae5c26759881d48a194a6b82a9d542485d6b6ce96297275c136b1fa6712f253 这个结果。

我希望 SHA256 计算为:053353867b8171a8949065500d7313c69fe7517c9d69eaff11164c35fcb14457

为什么不同地方的SHA256计算会有差异?

您遇到的问题是由于 编码 差异造成的。相同字符串的编码可能产生不同结果的原因有多种:

  • 不同的行结尾(CR/LF 代表 Windows,LF 代表 Linux,CR 代表经典 MacOS);
  • 空格的其他差异(制表符或空格,行尾的空格);
  • 不同的字符编码(Windows-1252、UTF-8 和 UTF-16 或 内部 语言实现中的字符表示);
  • 存在元信息(存在字节顺序标记);
  • 在编码中处理特殊字符的不同方式(一个字符后跟组合波浪线和带有组合波浪线的字符,请参阅 Unicode equivalence);

还有可能产生不同结果的不可见错误:

  • 存在不可打印的字符/控制代码(字符串末尾的空值 0x00 可能是最好的例子);

除了任何(结构化)文本可能存在的所有这些差异之外,JSON 数据结构也可能具有等效值。最好的例子可能是数字前的前导 + 字符。这完全是虚假的,但仍会导致不同的文本表示,但数字值相同。


如果字符串的编码不同,则哈希算法的二进制输入也不同,对于普通加密哈希,您将得到大约 50% 的位不同的结果。产生相同输入的方法称为 canonicalization(或 C14N,因为在规范化的 C 和 N 之间有 14 个字符)。

对于 XML,canonical form has been defined long ago. For JSON this is not the case, even though canonicalization of JSON would be much easier. JSON has a much less convoluted set of rules after all. There are attempts to canonicalize JSON, see e.g. this draft RFC 明确提到加密哈希:

For example when a cryptographic hash is applied over a JSON document, a single physical representation allows the hash to represent the logical content of the document by removing variation in how that content is encoded in JSON.

This draft RFC 顺便说一句,看起来更彻底一些。


现在您可以保留其中一个 RFC 草案。如果您想保留换行符,那么您可以使用这些明确定义的规则序列化 JSON 和将其用作哈希函数的输入,同时保持 JSON 本身不变。以不同的方式 formatted JSON 仍然会生成相同的散列。

[Input JSON] -> (parse) -> (canonicalize & serialize) -> (hash) -> [hash value]
[Input JSON'] -> (parse) -> (canonicalize & serialize) -> (hash) -> [hash value']

如果 Input JSONInput JSON' 在结构上/语义上相同,这里的哈希输出将是相同的,因为规范化会消除差异。


请注意,JSON Web 签名 (JWS) 会解决此问题。毕竟签名在内部使用哈希。签名位于包含的有效负载之上,并且仅使用该有效负载的编码。只要中间系统不重新编码 JSON,就可以了。签名不必完全相同,只需验证数据即可。

不幸的是,哈希不是这种情况。但是,实际上,您可以将 JSON 定义为文件并使用相同的推理。缺点当然是 如果 你得到一个差异,你将不得不执行二进制比较来找到差异,然后追溯引入变化的地方。工作系统可能会破坏哈希,而语义仍然相同(例如,在替换或更新 JSON 库时)。