Задумывались ли вы о том, как именно Go проверяет целостность ваших скаченных модулей? Как наш тулчейн проверяет, что зависимости которые вы видите в своем проекте, соответствуют зависимостям которые видят другие разработчики?
Я думаю, что нет, ибо работает простой принцип: работает — не трогай. Однако мне, по роду деятельности, пришлось залезть внутрь и прочитать (несколько раз) спеку и посмотреть реализацию. И для того, чтоб структурировать свои изыскания я пишу сей пост.
Все начинается с попытки получить модуль. После первого скачивания модуля (через вызов
go mod tidy или go mod download на проекте) в формате zip архива с прокси для модулей, Go делает следующее• Сортирует имена файлов в архиве и пробегает
sha256 по каждому файлу. Получившийся набор пар "hash filepath" он прогоняет через sha256 еще раз, кодирует его в base64, добавляет префикс h1: и запоминает результат. В общем виде это выглядит как вызов вот такой команды:
sha256sum $(find . -type f | sort) | sha256sum
• Далее он сравнивает получившийся хеш с записью о зависимости которая хранится в
go.sum внутри вашего проекта. Если её нет (или нет go.sum, т.е. вы только начали проект) то он идет в GOSUMDB и спрашивает хеш у него. В обоих случаях, после совпадения хешей, zip файл модуля пишется в локальные загрузки ($GOPATH/pkg/mod/cache/download/<url>) и распаковывается в локальный кеш ($GOPATH/pkg/mod/cache/<url>). Если не совпало, ничего не пишем и кидаем ошибку. Побочный вывод — нельзя называть модуль cache или sumdb, о чем ниже.- Если модуль приватный (выставлены переменные окружения
GOPRIVATE и/или GONOSUMDB) или вообще отключена связь с сервером хешсумм через GOSUMDB=off, то проверять хешсумму он будет исключительно с тем, что есть в go.sum внутри проекта. Если там такого модуля нет, то мы доверяем полученным данным и добавляем полученную хешсумму в go.sum. - Если нет прокси для модулей (например вы берёте файлы напрямую с гита) то Go сам создает zip архив и помещает его в скаченное.
- Подсчет хеша архива модуля недешевая операция в общем смысле, особенно если её постоянно вызывать. Поэтому, кроме zip файла модуля мы рядом храним полученный хеш в файле c суффиксом
.ziphash.• Во время сборки бинаря наш компилятор, чтобы не терять время, не проверяет целостность архива и распакованных данных, а только сравнивает, что строчки в
go.sum проекта совпадают с тем, что у нас лежит в ziphash файлах.И тут наступает вопрос: а как проверить, что никто не трогал наш кеш модулей? В дело вступает
go mod verify, который пробегаясь по всем модулям из go.mod делает следующее:• Сначала он читает хеш из
ziphash файла.• Затем он читает и хеширует распакованные файлы модуля, сравнивая их итоговый хеш с полученным на прошлом этапе. Затем тоже самое повторяется для zip архива модуля. Операция аналогична тому, что происходит при скачивании.
• Если не совпало, то
verify кидает ошибку на модуль и выходит.Обратите внимание, что здесь
go.sum проекта нигде не участвует. Проверяется целостность именно кеша.А что если мне подменили
ziphash вместе с файлами модуля и zip архивом? На это есть ряд ответов:• Во время скачивания предполагается, что между нами и БД хешсумм установленно защищенное соединение.
• Сама БД хешей, это даже не БД в общем смысле, а дерево, где каждый новый элемент зависит от прошлых вставок. Поэтому при попытке поменять запись развалятся записи о всех хешах модулей которые были вставлены после. Кому интересны подробности рекомендую вот эту статью https://research.swtch.com/tlog и погуглить
Merkle Tree.• Когда мы скачиваем хеш, мы получаем не отдельное значение, а некое «поддерево» хешей которое необходимо нам для валидации нашего модуля. Это дерево мы помещаем в
$GOPATH/pkg/mod/cache/download/sumdb/ (тот самый) и используем для проверки локального модуля. И тут происходит интересное: если удалить ziphash файл то даже без соединения с интернетом, go mod download сможет восстановить хеши из частей большого дерева лежащих в $GOPATH/pkg/mod/cache/download/sumdb/ и записать их обратно в ziphash.