Готов поспорить, что проблема в круге JSON.parse → JSON.stringify, а не в кодировке, которую вы передаёте в update(). Поэтому её смена ничего и не дала.
HMAC считается от тех самых байтов, которые подписал партнёр. Когда вы парсите и заново сериализуете, вы подписываете свою сериализацию данных, а не их. Для обычных ASCII-payload'ов с компактным форматированием результат часто получается байт в байт одинаковым, так что evt_2051 и evt_1001 проходят по везению. В evt_2044 есть не-ASCII символы, и именно там сериализаторы расходятся. Если партнёр отдаёт Zo\u00eb Ram\u00edrez (с экранированием, как по умолчанию делают некоторые энкодеры в Java/PHP/Python), то JSON.stringify выдаст вам буквально Zoë Ramírez, и дайджест изменится. Бывает и наоборот. Порядок ключей, пробелы и формат чисел тоже могут отличаться, но диакритика лучше всего объясняет ваш симптом.
Решение: хешируйте delivery.raw напрямую, не трогая.
const expected = crypto
.createHmac("sha256", secret)
.update(delivery.raw) // string or Buffer, exactly as received
.digest("hex");
JSON.parse вызывайте только после того, как подпись проверена. Если вы сохраняли доставки на диск, убедитесь также, что то, что их писало, не перекодировало и не нормализовало текст. Безопаснее всего хранить сырой Buffer.
Раз уж вы всё равно там копаетесь: сравнивайте дайджесты через crypto.timingSafeEqual, а не через ===. Доки: https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b
Надеюсь, поможет. Напишите, починила ли 2044 версия с raw.
Переведено с английского·Показать оригиналI'd bet the problem is the JSON.parse → JSON.stringify round trip, not the encoding passed to update(). That's why changing it did nothing.
HMAC is over the exact bytes the partner signed. Once you parse and re-stringify, you're signing your serialization of the data, not theirs. For plain ASCII payloads with compact formatting, the two often come out byte-identical, so evt_2051 and evt_1001 pass by luck. evt_2044 has non-ASCII characters, and that's where serializers diverge. If the partner emits Zo\u00eb Ram\u00edrez (escaped, as some Java/PHP/Python encoders do by default), JSON.stringify will give you the literal Zoë Ramírez instead, and the digest changes. It can go the other way too. Key order, whitespace and number formatting can also differ, but the accents fit your symptom best.
Fix: hash delivery.raw directly, untouched.
const expected = crypto
.createHmac("sha256", secret)
.update(delivery.raw) // string or Buffer, exactly as received
.digest("hex");
Only call JSON.parse after the signature checks out. If you saved the deliveries to disk, also make sure whatever wrote them didn't re-encode or normalize the text. Storing the raw Buffer is safest.
While you're in there, compare digests with crypto.timingSafeEqual rather than ===. Docs: https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b
Hope that helps — and let us know if the raw version fixes 2044.