Вопрос или проблема
У меня есть два хеша sha256 в виде шестнадцатеричных строк
HASH1=b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
HASH2=7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730
Теперь я хочу создать новый SHA256, который будет зависеть от этих двух хешей (для структуры меркл-дерева). Любые две шестнадцатеричных репрезентации одних и тех же хешей всегда должны приводить к репрезентации одного и того же бинарного хеша (например, если я опираюсь на строковые представления, то это должно быть какой-то канонической формой, такой как закодированная в ASCII строчная)
Я знаю, что мне нужно использовать инъективную функцию, но какой лучший способ?
Мне следует использовать строковые значения и какой-то разделитель, а затем хешировать бинарное представление этой строки (и полагаться на bash, использующий кодировку ASCII для этого)?
NEW_HASH=$(echo -n "${HASH1,,},${HASH2,,}" | openssl dgst -sha256 | sed 's/(stdin)= //')
Или лучше сначала конвертировать шестнадцатеричные представления в бинарный вид, а затем просто конкатенировать бинарные представления (поскольку оба хеша имеют фиксированную длину, результат будет однозначным)?
BIN1=$(echo -n "$HASH1" | xxd -r -p -)
BIN2=$(echo -n "$HASH2" | xxd -r -p -)
NEW_HASH=$(echo -n "$BIN1$BIN2" | openssl dgst -sha256 | sed 's/(stdin)= //')
Какой лучший подход и почему?
Криптофункции работают с сырыми байтами. Поэтому, если заданы строки в шестнадцатеричном виде, в общем, хорошей идеей будет сначала декодировать эти строки, а затем передать содержащийся массив байтов в криптофункцию.
В вашем случае вам нужно объединить два массива байтов, закодированных в шестнадцатеричном формате, затем взять SHA256 хеш результата. Существует два способа сделать это:
a) Объедините ваши две шестнадцатеричные строки, затем декодируйте объединенную строку в массив байтов, затем возьмите SHA256 хеш массива байтов.
b) Декодируйте каждую шестнадцатеричную строку в массив байтов, затем объедините два массива байтов вместе, затем возьмите SHA256 хеш массива байтов.
Оба вышеуказанных метода дают одинаковые результаты.
Для (a) вы можете сделать:
{ echo -n "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"; echo -n "7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730"; } | xxd -p -r | sha256sum
Что дает:
fc87504c81f99f46407174938c24d0d0b6c65179565a2ac0fd6bd12c2016cbe8
Для (b) вы можете сделать:
{ echo -n "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c" | xxd -p -r; echo -n "7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730" | xxd -p -r; } | sha256sum
Что (как ожидалось) также дает:
fc87504c81f99f46407174938c24d0d0b6c65179565a2ac0fd6bd12c2016cbe8
Проблема со стандартным меркл-деревом в том, что оно может обеспечить атаку второго преображения или худшие подделки подписей.
Для простоты предположим, что родителем узлов (e1,e2)
является H(h1||h2)
с h1=H(e1)
и h2=H(e2)
. Теперь злоумышленник может использовать это для поиска второго преображения для списка; h1||h2
, и это будет хешироваться в H(h1||h2)
. Теперь злоумышленник может заменить (e1,e2)
единичным узлом h1||h2
, и это будет действительным меркл-деревом.
Худшее может произойти, если подпись включает узел меркл H(h1||h2)
, тогда мы можем подделать подпись для h1||h2
, даже если это не имеет смысла.
Поэтому меркл-дерево нуждается в контрмеры против этих. Решение заключается в добавлении 0
для левого узла и 1
для правого узла, а также добавлении уровня (самый низкий уровень 0
). H(left||right||leaf)
, если дочерний узел является листом, а иначе H(left||right||left,right)
. Это обеспечивает разделение домена.
Для более подробной информации о меркл-дереве, смотрите ответ Скуамен Рассказчицы в Cryptography.SE
Кодирование не так важно с точки зрения безопасности, если нет атак. Помните, что атака коллизии префиксов на SHA-1 использует свойства формата файла, такие как PDF. Просто отметим, что SHA-1 уже до этого атаковали намного ранее.
Формат может повлиять на скорость, бинарное кодирование может быть быстрее, если размер сообщения превышает 224 бита. В данном случае, если вы представите его в шестнадцатеричном формате, то SHA256 потребует двойного вызова сжатия. Для более высокой скорости вы можете рассмотреть BLAKE2s и его параллельную версию BLAKE3. Если вам нужна хеш-функция NIST, то используйте SHA-512, поскольку она предназначена для 64-битных процессоров. Если вам требуется только 256-битный вывод, то уже существует и такая версия; SHA-512/256, которая защищает от атак на расширение длины, как BLAKE2, так и серии SHA3.
Хеш – это набор битов. Если вы действительно хотите работать с хешами, вам следует использовать их бинарное представление, а не текстовое представление.
Например, “b5” в вашем примере означает, что первый байт вашего хеша – “10110101”. Но если вы рассматриваете это как текст, это значит, что каждый символ будет учитываться отдельно, поэтому “b5” будет представлен как “0110001000110101” (“b” -> 98 -> “01100010”, “5” -> 53 -> “00110101”).
Ответ или решение
Как правильно вычислить SHA256-хеш из двух существующих SHA256-хешей?
Ваша задача заключается в том, чтобы совместить два SHA256 хеша, представленных в виде шестнадцатичных строк, и вычислить новый SHA256 хеш, который будет уникальным для этих двух значений. Данная ситуация часто встречается при реализации структур данных, таких как деревья Меркле.
Исходные данные
Вы представили два SHA256 хеша:
HASH1=b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
HASH2=7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730
Возможные подходы
Вы правильно упомянули, что важно использовать однозначную (инъективную) функцию при комбинировании хешей. Обсуждая подходы, можно выделить два основных способа:
- Конкатенация шестнадцатичных строк и их кодирование в байты.
- Конвертация шестнадцатичных строк в байты, а затем конкатенация двоичных представлений.
Подход 1: Конкатенация строк
В этом методе Вы можете просто объединить два хеша в одну шестнадцатичную строку, а затем преобразовать эту строку в байтовый массив:
NEW_HASH=$(echo -n "${HASH1,,}${HASH2,,}" | xxd -r -p | openssl dgst -sha256 | sed 's/(stdin)= //')
Подход 2: Байтовая конкатенация
В альтернативном варианте, лучше всего произвести декодирование каждой шестнадцатичной строки в байты и затем объединить их:
BIN1=$(echo -n "$HASH1" | xxd -r -p)
BIN2=$(echo -n "$HASH2" | xxd -r -p)
NEW_HASH=$(echo -n "$BIN1$BIN2" | openssl dgst -sha256 | sed 's/(stdin)= //')
Какой способ лучше?
Оба метода в конечном итоге приводят к одному и тому же результату, но второй способ считается предпочтительным. Он использует истинное двоичное представление хешей, что исключает возможные ошибки, возникающие при работе со строками.
Важно помнить, что криптографические функции работают с бинарными данными. Таким образом, при декодировании шестнадцатичных строк и использовании байтов в хеш-функции можно избежать потенциальных проблем, связанных с кодировкой.
Безопасность и производительность
Использование бинарного представления хешей не только повышает безопасность, исключая риски, связанных с возможными конфликта этой информации, но и может повысить производительность, так как обработка байтов обычно быстрее.
Дополнительные рекомендации
Если ваша система или приложение требует высокой производительности при хешировании, вы могли бы рассмотреть возможность использования более современных альтернатив, таких как BLAKE2 или BLAKE3. Эти алгоритмы более эффективны и обеспечивают аналогичные (если не более высокие) уровни безопасности, особенно при больших объемах данных.
Заключение
Наиболее безопасным и рекомендуемым подходом к вычислению SHA256 хеша из двух других хеша является использование их бинарных представлений. Такой метод не только предотвращает потенциальные ошибки, но и гарантирует, что результат будет единственным и однозначным. Использование проверенных и быстродействующих хеш-функций, таких как открытый стандарт SHA-256 или современные альтернативы, позволяет обеспечить надежность и производительность ваших криптографических операций.