Сертификат, хеш и граница доверия
Оглавление серии «Реестр» · Урок 1 из 10.
Создадим небольшой учебный реестр сертификатов. Он будет хранить хеш документа, адрес издателя и признак отзыва. Веб-страница позволит посчитать хеш локального файла и позднее обратиться к контракту. Начнём с модели доверия: какие сведения такая запись действительно подтверждает, а каких в ней нет. В конце первого урока браузер должен вычислять SHA-256 выбранного файла, не отправляя документ на сервер. Блокчейн пока не понадобится: сначала зафиксируем смысл данных, которые собираемся туда записать.
Курс предполагает знание JavaScript, обработчиков DOM и async/await. Solidity будем разбирать постепенно. Все дальнейшие операции относятся к локальной учебной EVM на вашем компьютере. При подготовке уроков не запускались компиляция, сеть, кошелёк или транзакции. Поэтому рассматриваем исходники и ожидаемые наблюдения, а не результаты выполненного развёртывания. В архиве нет настоящих приватных ключей, документов учащихся или адреса уже опубликованного контракта.
Запись и документ решают разные задачи
Сам сертификат может быть PDF-файлом с названием курса, именем учащегося и подписью организации. Реестр не будет хранить этот файл. Он получит короткий идентификатор, вычисленный из его байтов. Такой идентификатор позволяет позднее сравнить конкретный файл с тем, для которого издатель создал запись. Совпадение относится к содержимому байтов, а не к внешнему виду документа: два PDF, выглядящие одинаково, могут иметь разные внутренние данные и разные хеши.
Например, редактор PDF изменил метаданные или сохранил документ с другой внутренней структурой. Изображение на странице осталось прежним, но последовательность байтов изменилась. Хеш тоже ожидается другим. Реестр тогда не найдёт прежнюю запись по новому значению. Это не обязательно свидетельство подделки: файл мог быть заново экспортирован. Именно поэтому издателю нужно определить канонический выдаваемый файл, а получателю — сохранять оригинальные байты документа, а не только его скриншот.
Файл остаётся вне цепочки. Адрес издателя не раскрывает автоматически название школы или принадлежность кошелька человеку. Внешняя договорённость связывает известную организацию с доверенным публичным адресом. Контракт лишь проверяет, разрешено ли этому адресу создавать записи. Если неизвестный адрес выдал запись о произвольном файле, наличие записи не подтверждает качество обучения. Нужно отдельно знать, почему именно этот издатель заслуживает доверия в конкретной системе.
Хеш фиксированной длины
SHA-256 принимает байты произвольного размера и возвращает 256 бит, то есть 32 байта. В веб-интерфейсе покажем их как шестнадцатеричную строку с префиксом 0x. Это удобное текстовое представление bytes32, которое позднее передадим контракту. Префикс не является частью исходного файла и не меняет вычисление хеша. Каждая пара символов после префикса представляет один байт результата; для 32 байтов потребуется 64 шестнадцатеричных символа.
Solidity часто использует keccak256, но это другой алгоритм. В нашем протоколе хеш файла вычисляется SHA-256 в браузере и передаётся как готовое значение. Контракт не должен заново применить Keccak к тексту строки и считать это тем же идентификатором. Нужно различать байты файла, байты уже рассчитанного хеша и текстовую запись хеша. Ошибка на этой границе даст другую запись, даже если все отдельные функции будут технически работать исправно.
Полный модуль вычисления
В hash.js проверим наличие файла и ограничим размер пятью мебибайтами. Затем прочитаем его через arrayBuffer и передадим байты в Web Crypto. Весь файл оказывается в памяти, поэтому такой лимит важен для поведения небольшой лаборатории. Он не является универсальной рекомендацией для любых документов. Для больших ресурсов понадобится потоковая стратегия с подходящей реализацией хеша и другой моделью потребления памяти, которую здесь не скрываем за этим коротким примером.
export async function fileDigest(file) {
if(!file || file.size>5*1024*1024)throw Error('Выберите файл до 5 МиБ');
const buffer=await file.arrayBuffer();const digest=await crypto.subtle.digest('SHA-256',buffer);
return '0x'+Array.from(new Uint8Array(digest),byte=>byte.toString(16).padStart(2,'0')).join('');
}
crypto.subtle.digest работает асинхронно. Результат тоже является буфером байтов, поэтому преобразуем его в Uint8Array. Для каждого байта записываем два шестнадцатеричных символа. padStart сохраняет ведущий ноль, иначе некоторые байты получат один символ и итоговая строка станет неправильной длины. Именно форматирование обеспечивает правильное текстовое представление, а не свойство toString(16) само по себе. Хеш уже вычислен до этого преобразования; мы лишь подготавливаем способ его передачи.
Браузерная граница доступа
Посетитель сам выбирает файл через обычное поле input type="file". Страница получает доступ к выбранному объекту File, а не ко всем файлам компьютера. Имя файла можно показывать человеку, но в алгоритм хеша мы его не включаем. Переименование оригинального файла не изменит его идентификатор, если байты остались прежними. Напротив, изменение одного байта содержимого должно привести к другому результату. Такой контракт удобно объяснять получателям сертификатов до подключения любых сетевых технологий.
Web Crypto доступен в подходящем защищённом контексте. Для лаборатории используем локальный сервер на loopback, который поддерживается современными браузерами как доверенный локальный контекст. Простое открытие файла HTML и произвольный незашифрованный удалённый адрес не являются обещанным профилем курса. Если crypto.subtle недоступен, проверьте контекст и поддержку браузера, а не заменяйте алгоритм на случайный «короткий хеш» ради исчезновения ошибки. Параметры протокола должны оставаться неизменными.
Что будет храниться в контракте
В следующем состоянии запись получит три поля: публичный адрес издателя, время выдачи по блоку и признак отзыва. Ключом станет рассчитанный bytes32. Отсутствующая запись будет определяться нулевым адресом издателя. Имена, электронная почта и файл в контракт не попадут. Тем не менее публичный хеш не следует автоматически считать анонимными данными: если набор возможных документов мал или они уже известны, третья сторона может рассчитать значения и сопоставить их.
Время блока не заменяет официальное время подписания документа. Оно относится к моменту регистрации записи в данной цепочке. Между выпуском PDF и записью может пройти интервал. В реальном продукте необходимо назвать эти события точно и выбрать правила учёта времени. Наш пример намеренно хранит небольшой набор сведений, чтобы понять механизм записи и отзыва. Он не содержит внешней аккредитации, юридической проверки или доказательства того, что человек действительно прошёл курс.
Самостоятельный опыт с байтами
Сохраните небольшой текстовый файл и выберите его на странице. После действия «Вычислить SHA-256» ожидается строка длиной 66 символов с учётом префикса. Переименуйте файл без изменения содержимого и вычислите результат снова: ожидается то же значение. Затем добавьте перевод строки и сохраните файл: ожидается другое значение. Этот опыт показывает, что идентификатор относится к байтам. При написании урока вычисление не запускалось, поэтому конкретное значение хеша здесь не выдумывается.
API описан в SubtleCrypto.digest, а выбор файлов — в File API. Зафиксировали модель: оригинальный документ остаётся у получателя, реестр хранит ограниченный идентификатор и статус доверенного издателя. Дальше перенесём эту модель в Solidity и разберём, как постоянное хранилище отличается от временных переменных функции. Это позволит читать запись без отправки новой транзакции и явно различать найденный сертификат и отсутствие данных.
Исходники этого шага
Архив серии содержит полную папку lesson-01. Возьмите именно её; переносить файлы из соседних папок не требуется. Версии и самостоятельная подготовка описаны в README. Пример, установка зависимостей и его выполнение при написании не запускались; результаты в тексте ожидаются по разбору исходников. Материал сохранён локально как Markdown-черновик.