Web3: проверка хеша манифеста в браузере
Оглавление курса «Web3-интерфейс: реестр в браузере» · Урок 7 из 30.
Реестр хранит отпечаток выпуска, а посетитель загружает файл с учебными материалами. Эти два действия следует соединить точным правилом. В этом уроке определим, какие байты хешируются, как сравнить результат с contentHash и почему совпадение отпечатка не заменяет оценку качества текста.
В карточке курса 17 уже есть contentHash из согласованного снимка. Базовый контракт принимает любые ненулевые 32 байта; он не навязывает алгоритм их получения. Наш веб-проект отдельно выбирает правило: SHA-256 точных байтов файла манифеста выпуска. Это соглашение приложения нужно соблюдать и при публикации, и при чтении. Программы не запускались, а конкретный отпечаток настоящего выпуска здесь не объявляется.
Что именно считается выпуском
Манифест перечисляет файлы курса, их размеры и собственные отпечатки. Его байты служат небольшим входом для проверки выпуска целиком. Если издатель перепишет порядок полей JSON или заменит окончания строк, байты изменятся и общий SHA-256 тоже изменится, даже когда человек считает содержание одинаковым. Поэтому хешируем сохранённый файл, а не результат произвольного повторного JSON.stringify в браузере.
Такой подход не требует универсальной канонизации JSON для нашего первого примера. Издатель публикует конкретный файл, а читатель сравнивает конкретно полученные байты. Если продукту нужно сравнивать семантически одинаковые документы, вводится отдельная каноническая схема и фиксируется её версия. Нельзя незаметно заменить правило посреди курса и считать старые отпечатки совместимыми.
Внешний маршрут загрузки не входит в базовый CourseCatalog. Страница получает контролируемый URL манифеста из конфигурации каталога. Нельзя выводить адрес загрузки непосредственно из bytes32: SHA-256 не содержит местоположения файла. Контракт также не обещает, что файл будет доступен через год; хранение и резервирование относятся к отдельному процессу.
Хеширование полученных байтов
Ниже самостоятельный модуль браузерной проверки manifest-digest.js. Web Crypto использует безопасный контекст браузера. Для локального окружения его доступность следует учитывать при подготовке страницы; библиотека не является подменой проверки origin или HTTPS.
export async function sha256Hex(bytes) {
const digest = await crypto.subtle.digest('SHA-256', bytes);
return '0x' + Array.from(new Uint8Array(digest),
b => b.toString(16).padStart(2, '0')).join('');
}
export async function compareManifest(url, expectedHash) {
const response = await fetch(url, { credentials: 'omit' });
if (!response.ok) throw new Error('Манифест недоступен');
const bytes = await response.arrayBuffer();
const actualHash = await sha256Hex(bytes);
return {
matches: actualHash.toLowerCase() === expectedHash.toLowerCase(),
actualHash,
bytes,
};
}
Мы хешируем ArrayBuffer, а не уже разобранный JavaScript-объект. credentials: 'omit' подчёркивает, что публичный манифест не получает cookie личного кабинета. Междоменные ограничения и политика endpoint всё равно действуют: наличие этого параметра не разрешает браузеру читать любой чужой ресурс. В настоящем загрузчике также ограничивают размер документа и время ожидания.
response.ok сообщает об успешном HTTP-статусе, но не о правильном файле. Статус 200 может вернуть страницу ошибки вместо JSON. Поэтому сначала сравнивается отпечаток, затем проверяется ожидаемая схема. Если отпечаток не совпал, интерфейс не должен продолжать отображение документа как проверенного выпуска.
Результаты сравнения
Разделим три состояния. При совпадении полученные байты соответствуют отпечатку выбранного снимка. При несовпадении байты отличаются от заявленного выпуска. При невозможности загрузки сравнение не выполнено. Фраза «проверка не пройдена» для всех трёх случаев слишком расплывчата: пользователю непонятно, нужно ли повторить сеть или отвергнуть содержимое.
Выпуск мог обновиться между чтением реестра и загрузкой файла. Если URL всегда показывает последний манифест, старый снимок A будет сравниваться с новым документом B. Такой результат не обязательно означает атаку. Лучше иметь неизменяемый маршрут каждого выпуска и отдельно указатель на текущий. Если маршрут всё же изменяемый, страница показывает оба отпечатка и предлагает получить новый снимок.
Совпадение с отпечатком из неактивной записи означает только соответствие сохранённым байтам. Оно не возвращает активности курсу и не объясняет причину деактивации. Пользователь видит две независимые характеристики: «байты совпали» и «выпуск неактивен». Одна зелёная галочка для обеих характеристик скрыла бы важную часть состояния.
Разбор манифеста после проверки
Предположим, манифест содержит номер курса, версию схемы и массив материалов. После совпадения отпечатка всё равно проверяем поля, поскольку издатель мог опубликовать ошибочный документ. Хеш подтверждает связь байтов, а не правильность структуры. Номер должен совпасть с курсом 17, версия схемы — поддерживаться клиентом, количество элементов — укладываться в ограничение интерфейса.
Ссылки из манифеста не превращаются в HTML без проверки. Название урока выводится через textContent, а маршрут допускается только по политике приложения. Даже файл от известного издателя может содержать случайную небезопасную разметку. Проверка отпечатка не делает innerHTML безопасным и не отменяет обычную защиту веб-приложения.
Если манифест содержит отпечатки отдельных Markdown-файлов, они проверяются по такому же явному правилу байтов. Проверенный список файлов не означает, что каждый файл уже загружен и проверен. Интерфейс показывает состояние проверки рядом с конкретным материалом и не переносит результат одного файла на всё содержимое без выполнения нужных сравнений.
Границы доверия
Мы доверяем источнику конфигурации экземпляра и результатам чтения RPC. Если эти источники подменены, злоумышленник может представить свой реестр со своим отпечатком. Поэтому проверка байтов является частью общей цепочки происхождения, а не самостоятельной магической защитой. Запись развёртывания, домен сайта и выбранный издатель остаются значимыми.
Для упражнения измените мысленно один пробел в JSON и отдельно поменяйте только имя файла на сервере. В первом случае SHA-256 меняется, во втором — нет, если байты сохранились. Затем рассмотрите недоступный файл и активный реестр: блокчейн не восстановит содержимое самостоятельно. Ожидаемый итог — точное понимание обещания отпечатка. Браузерный API описан в SubtleCrypto.digest, а хранение по содержимому развивается в курсе IPFS.
Сжатие транспорта и исходные байты
HTTP может передавать файл с транспортным сжатием. Браузер предоставляет приложению распакованные байты ответа, поэтому правило нашего выпуска относится к байтам документа после транспортного декодирования. Оно не сравнивает произвольный архив сетевого пакета. Издателю следует сохранить точные байты самого манифеста и не путать их с gzip-файлом резервной копии.
Если сервер преобразует документ при выдаче, например меняет окончания строк или добавляет разметку, совпадение нарушится. Это полезный сигнал: пользователь получил другой набор байтов. Исправление состоит в неизменяемой выдаче опубликованного артефакта, а не в ослаблении сравнения до «почти одинакового текста». Разбор содержания и проверка байтов отвечают на разные вопросы.