Проверка целостности материалов
Как убедиться, что статья на портале не менялась после публикации: манифест sha256, дерево Меркла и проверка одной командой
Каждый материал портала публикуется двумя файлами: страницей index.html, которую вы читаете, и её исходником index.md рядом. Вместе с сайтом выкладывается манифест /integrity.json — по записи на каждый материал и общий корень дерева Меркла.
Манифест нужен для одной вещи: вы можете проверить сами, что статья не менялась после публикации, не веря нам на слово.
Проверить одну статью
Возьмите адрес статьи, добавьте index.md и посчитайте хэш:
curl -s https://courses.digitable.life/post/logic/01-concepts-definitions-classification/index.md | sha256sum
Теперь найдите ту же статью в манифесте и сравните значения:
curl -s https://courses.digitable.life/integrity.json \
| jq -r '.articles[] | select(.path == "/post/logic/01-concepts-definitions-classification/index.md") | .sha256'
Строки совпали — файл ровно тот, который был заверен. Не совпали — статью правили после генерации манифеста, и это видно без нашего участия.
Проверить весь портал разом
curl -s https://courses.digitable.life/integrity.json > integrity.json
jq -r '.articles[] | "\(.sha256) .\(.path)"' integrity.json > SHA256SUMS
# выкачайте нужные index.md в ту же структуру каталогов и:
sha256sum -c SHA256SUMS
Что означает корень
Поле root — вершина дерева Меркла, построенного по всем материалам. Дерево, а не общий хэш от склейки, выбрано ради практического свойства: неизменность одной статьи доказывается путём длиной около десяти хэшей, без выкачивания остальных.
Правила счёта опубликованы прямо в манифесте, в поле merkle, чтобы корень мог пересчитать кто угодно:
- записи отсортированы по полю
path; - лист:
sha256("digitable-leaf\n" + path + "\n" + sha256(файл)); - узел:
sha256("digitable-node\n" + левый + правый); - нечётный элемент уровня поднимается выше без изменений.
Разные префиксы у листа и узла — не украшение: без них дерево уязвимо к подмене листа на пару узлов.
Поля revision и generated в корень не входят. Иначе он менялся бы при каждой пересборке сайта, даже когда ни один материал не тронут.
Проверить, что манифест наш
Всё, что выше, отвечает на вопрос «файл не менялся после того, как манифест сгенерировали». Отдельный вопрос — «манифест сгенерировали мы». Тот, кто подменил бы портал целиком, перегенерировал бы и манифест, и все хэши у него сошлись бы.
Поэтому корень подписан. Рядом с манифестом лежит отделённая подпись /integrity.json.minisig — формат minisign, алгоритм Ed25519. Ключ проверки — одна строка из 56 символов, начинающаяся на RW; где её взять, сказано в следующем разделе.
curl -sO https://courses.digitable.life/integrity.json \
-O https://courses.digitable.life/integrity.json.minisig
minisign -Vm integrity.json -P 'RWQ7b6INTcxAb8R71YnYVPSOVCjrCwtYLyqMJt13GAdlWMeY5xcOwkdU'
Ответ Signature and comment signature verified означает две вещи сразу: манифест подписан владельцем этого ключа, и с момента подписи в нём не изменился ни один байт. Строка Trusted comment покажет заверенный корень, число материалов и дату подписи — она тоже накрыта подписью, переписать её незаметно нельзя.
Ответ Signature verification failed означает, что проверять хэши статей дальше уже незачем: манифест не тот, которым мы его подписали.
Сам minisign — 39 килобайт, отдельная от нас программа: apt install minisign, brew install minisign, pacman -S minisign. То, что подпись проверяется чужим кодом, а не нашим, здесь не мелочь, а суть: наш собственный скрипт проверки, каким бы открытым он ни был, в худшем случае просто напечатал бы «всё хорошо».
Если /integrity.json.minisig отдаёт 404 — эта выкладка ещё не подписана: ключ не опубликован, точки из следующего раздела пусты, и проверить пока можно только хэши из разделов выше.
Ключ заведён 4 августа 2026 года, и подпись ставится автоматически в основном пути выкладки — том, что идёт из хранилища секретов. Резервный путь (scripts/deploy-manual.sh) подписи не ставит намеренно: закрытый ключ живёт секретом репозитория, и на машине, с которой идёт ручная выкладка, его нет. Поэтому выкладка, сделанная резервным путём, уезжает неподписанной, и признак этого — тот самый 404 из предыдущего абзаца.
Об этом ключе честно стоит сказать одну вещь. Он сгенерирован не владельцем вручную, а автоматизированным помощником по его прямому распоряжению, и секретная часть прошла через рабочую сессию этого помощника, прежде чем попасть в хранилище секретов. Это слабее, чем ключ, созданный на машине владельца и никуда не уезжавший. Практическое следствие: если однажды выяснится, что сессия была скомпрометирована, подписи этим ключом теряют доказательную силу задним числом. Поэтому ключ подлежит замене на созданный вручную, и до тех пор его назначение — не юридическое доказательство, а обнаружение подмены при обычной эксплуатации.
Пока этого не произошло, работает всё, что описано выше: хэш каждой статьи, корень дерева Меркла и возможность пересчитать его самому. Не работает ровно одно — доказательство того, что манифест наш.
Где взять ключ и почему не у нас
Ключ, взятый с портала, не доказывает ничего. Тот, у кого хватило доступа подменить /integrity.json, положит рядом свою подпись и свой ключ, и они прекрасно сойдутся друг с другом. Именно поэтому копии ключа на портале и нет: адрес /integrity.pub не публикуется ни одним путём выкладки, и проверкой такая копия всё равно не была бы.
Смысл появляется, когда ключ приходит другим путём:
- Запись в DNS.
dig +short TXT _minisign.digitable.life— отдают серверы имён, а не веб-сервер портала. - Профиль на GitHub. github.com/digitable-lol — площадка, которой не управляем ни мы, ни наш хостер.
- Архив интернета. Снимок этой страницы в web.archive.org, сделанный до интересующей вас даты: команда выше напечатана в нём вместе с ключом, и задним числом мы её не перепишем.
Это не «доверенная третья сторона»: все четыре точки, кроме последней, ведут к одному владельцу. Они дают другое — независимость каналов. Чтобы подсунуть вам чужой ключ, придётся захватить одновременно портал, зону DNS, второй сервер и учётную запись на GitHub; если захвачено что-то одно, источники разойдутся между собой, и это заметно без всякого доверия к нам.
И главное: ключ живёт годами, а 56 символов легко сохранить. Сверив их один раз, вы получаете возможность заметить любую будущую подмену — включая подмену самого ключа.
Чего эта проверка не даёт
Подпись отвечает на вопрос «кто заверил», но не защищает от кражи ключа: если бы ключ утёк, подписи, сделанные после утечки, ничего не стоили бы. Поэтому дата подписи стоит внутри самой подписи, а замена ключа объявляется во всех четырёх точках публикации сразу — старая строка не исчезает молча.
Доказательства даты публикации манифест не даёт. Если оно понадобится, добавится якорение корня во времени — 32 байта на весь портал. Сами материалы в блокчейн не попадут никогда: неизменяемая запись противоречит обязанности удалять персональные данные по требованию, а корень эту обязанность не нарушает, потому что восстановить из него текст нельзя.
Если хэши не сошлись
Напишите на support@digitable.ru и приложите адрес статьи и обе строки. Расхождение — это либо наша ошибка в выкладке, либо подмена; в обоих случаях мы хотим о нём знать.