./research / verifiable-audit-layer

Проверяемый слой аудита для ИИ.

Защищённый от подмены слой аудита для инференса моделей и исполнения агентов: каноническая сериализация, хеширование с разделением доменов, подписи DSSE, Merkle-DAG на каждый запуск и межзапусковый Merkle Mountain Range, доказывающий, что ни один запуск не был удалён. Каждый примитив выбран по прямому бенчмарку против реальных альтернатив, с зафиксированными сырыми данными. Эта заметка и есть проект и стоящие за ним эксперименты.

аудиткриптографиябенчмаркиshipped

Модель угроз

Мы предполагаем противника, который контролирует хранилище и может переписывать записи задним числом, включая самого оператора, который их создал. Цель в том, чтобы любое изменение запечатанного запуска было обнаружимо, чтобы запись оставалась проверяемой в течение срока хранения, измеряемого годами, и чтобы проверка не раскрывала никаких приватных входов. Два свойства всё время держатся раздельно: целостность одного запуска и полнота множества запусков. Квитанция на запуск даёт первое и ничего не говорит о втором, поэтому оба конструируются по отдельности.

Каждый выбор ниже сделан одним и тем же образом. Мы не утверждаем, что примитив наилучший; мы сравниваем его с достоверными альтернативами на той же машине и фиксируем сырые данные, так что любая цифра здесь воспроизводится одной командой. Тайминги — самый быстрый из нескольких прогонов после отбрасывания разогрева, со средним и стандартным отклонением, чтобы нестабильное измерение отмечалось, а не скрывалось. Числа получены на одном хосте AMD64 на CPython 3.14 и осмысленны как отношения, а не как абсолютные величины.

Эксперименты и итоги

Каждая веха была прямым тестом против реальных альтернатив, а не единичным измерением. Обзор ниже; подробные результаты по каждой — в следующих разделах.

Веха Проверено против Итог
Каноническая форма наивный JSON, CBOR JCS, побайтово идентично, ~3x стоимости
Хеширование SHA-256, BLAKE3, keccak-256 keccak нативно 17x, те же байты
Нативное ядро бэкенды pip vs Rust запечатывание 6x, побайтово идентичный корень
Подпись ECDSA, BLS, ML-DSA Ed25519, 64 B, детерминированная
Постквантовая ML-DSA 44 / 65 / 87 поэтапно, проверка наравне, 38x размера
След на запуск 8 атак подделки, накладные все пойманы, ~121 us / событие
Межзапусковый лог простая пересборка Merkle MMR, плоское добавление, 483x

Каноническая сериализация

Капсула должна хешироваться в одни и те же байты в нашем ядре на Python, в нашем клиенте на TypeScript и в собственном инструменте аудитора, иначе действительная запись не пройдёт проверку. Мы используем RFC 8785 (JSON Canonicalization Scheme), который фиксирует форматирование чисел, порядок ключей по кодовой единице UTF-16 и отклонение неконечных значений. Мы тестировали его против JSON с наивной сортировкой ключей и против CBOR.

Подход Python vs Node Стоимость
Наивный сортированный JSON расходится на Unicode и числах базовая линия
RFC 8785 JCS побайтово идентично около 3x при сериализации
CBOR бинарный, для чтения нужен декодер компактный

Наивный JSON расходится между рантаймами в порядке Unicode и в форматировании чисел, поэтому молча ломает межъязыковую проверку. JCS побайтово идентичен в обоих, при примерно 3x времени сериализации, оплачиваемого один раз на хеш и пренебрежимого рядом с хешированием и сетевой работой вокруг. Мы держим ровно одну его реализацию. Когда наш нативный модуль принёс собственный сериализатор JSON, мы его убрали, потому что второй канонизатор, совпадающий на обычных входах и расходящийся на граничных случаях, которые может выбрать противник, хуже, чем никакой.

Хеширование и нативное ядро

Листья хешируются BLAKE3, а узлы Merkle — keccak-256, оба с разделением доменов по RFC 6962 (разные префиксы для листьев и внутренних узлов), чтобы закрыть класс второго прообраза, где внутренний узел выдаётся за лист. keccak-256 — это не SHA3-256; они делят перестановку, но различаются одним байтом набивки, а он-чейн-верификатор говорит на keccak, поэтому молчаливая подмена заставила бы каждый корень падать он-чейн. Поэтому слой хеширования вычисляет запрошенный алгоритм или бросает исключение, никогда не двойника, и записывает алгоритм в каждой капсуле, чтобы верификатор воспроизводил точную функцию, а не выводил её из того, что установлено локально.

Примитив, вход 64 B Пропускная способность Примечание
SHA-256 (stdlib) 1,50M ops/s базовая линия, всегда есть
BLAKE3 (pip) 1,44M ops/s наравне с SHA-256
keccak-256 (pip) 99k ops/s в 15x медленнее, узкое место
keccak-256 (ядро на Rust) 1,69M ops/s в 17x быстрее pip

У keccak нет реализации в стандартной библиотеке, что и сделало нативный модуль стоящим постройки. Разрыв наибольший на входах размером с событие, потому что затраты pycryptodome там — это аллокация объектов на вызов, а не хеширование, ровно тот размер, который аудиторский след хеширует чаще всего; сквозь весь путь нативное ядро поднимает запечатывание запуска на 1000 событий с 78 до 491 запечатывания в секунду, около 6x. Что важно: включение ускорителя не может изменить результат: запечатывание одного и того же запуска с крейтом и без него даёт побитово идентичный корень, набор тестов соответствия сверяет оба пути с векторами, опубликованными авторами алгоритма, а CI собирает одно колесо со стабильным ABI (abi3) и проверяет этот единственный артефакт на каждом Python от 3.10 до 3.14. Установка меняет скорость и ничего больше.

Подписи для записи длиной в десятилетие

Подпись на аудиторской записи должна держаться столько, сколько запись может оспариваться, а обязательства по логированию ИИ уже тянутся к десятилетию. Если схема ломается, пока запись ещё в своём окне хранения, противник может подделать её и датировать задним числом, так что весь архив падает разом. Мы сравнили Ed25519 против ECDSA, BLS и постквантового семейства NIST ML-DSA по пропускной способности и размерам.

Схема Проверка (ops/s) Размер подписи
ECDSA P-256 16 887 ~72 B
ML-DSA-44 (постквантовая) 10 725 2420 B
Ed25519 10 375 64 B
BLS12-381 только референс-реализация 96 B, агрегирует

Два результата опровергли предположения, которые мы принесли с собой, и оба мы держим в записи. Ed25519 не самый быстрый для проверки: ECDSA P-256 измерился примерно в 1,6x быстрее, на оптимизированном ассемблере библиотеки. Ed25519 остаётся по умолчанию по размеру подписи и по детерминизму (RFC 8032), который устраняет отказ ECDSA от повторного использования nonce, утекающий приватный ключ, пока подписи ещё проверяются. И постквантовые подписи не медленны на проверке: ML-DSA-44 чуть обошёл Ed25519, его цена — в подписании (примерно в 13x медленнее) и в размере (примерно в 38x байт). Поскольку след подписывается один раз и проверяется годами, дорогая сторона — та, что мы делаем меньше всего. BLS отклонён по требованию, а не по числу: агрегация схлопывает многие подписи в 96 байт, но агрегат проверяется по принципу всё-или-ничего и нуждается в каждом ключе и сообщении, что несовместимо с раскрытием одной ветви решения без раскрытия остального.

Переосмысление, которое важно: подписи не конфиденциальны, поэтому «собери сейчас, расшифруй потом» неприменимо, а реальная угроза — ретроактивная подделка внутри окна хранения. Привязка корня к публичному реестру доказывает, когда запись существовала, независимо от схемы подписи, так что сломанный ключ стоит способности доказать, кто запечатал запись, но не когда и не что. Поэтому привязка лежит на критическом пути, а ML-DSA выстроен за ней поэтапно, при этом конверт DSSE остаётся гибким по алгоритму, а алгоритм записывается на каждую капсулу.

След аудита на каждый запуск

Каждый запуск — это только-добавляемая, связанная хешем последовательность событий (вызовы LLM, вызовы инструментов, извлечения, порождения субагентов, человеческие одобрения, решения ограждений), с корнем Merkle над всеми ними и подписью DSSE над корнем. Хранятся только хеши входов и выходов, никогда сами полезные нагрузки, поэтому след по умолчанию сохраняет приватность. Верификатор не просто возвращает «действительно» или «недействительно»; он называет сломавшуюся гарантию.

Операция (запуск на 102 события) Стоимость Масштабирование
Записать одно событие ~121 us плоско, ниже 0,01% шага
Запечатать, построить корень 0,15 ms линейно по событиям
Проверить, пересчитать все листья 9,2 ms линейно, около 60x запечатывания

Запись — менее сотой доли процента типичного шага агента, в котором доминирует круговой обход к модели, так что накладные не повод сэмплировать, и след остаётся полным, а не статистическим. Проверка гораздо тяжелее запечатывания, потому что пересчитывает каждый лист из содержимого, тогда как запечатывание лишь строит дерево над уже присутствующими хешами. Эта асимметрия верна для аудиторского следа, где запись постоянна, а проверка редка, и она отмечает массовую перепроверку как место, где нативное ядро и параллелизм окупаются далее. Чтобы сделать гарантии конкретными, демо атакует запечатанный запуск восемью способами, каждый отклонён с конкретной причиной:

Атака Отклонена как
Переписать вывод модели несовпадение хеша листа
Удалить или вставить событие несовпадение числа событий
Переставить события разрыв цепочки
Обрезать запуск несовпадение головы цепочки
Понизить алгоритм хеша несовпадение хеша листа
Предъявить под неверным ключом подпись не проверяется

Межзапусковый журнал прозрачности

Действительная капсула на запуск не может доказать, что множество запусков полно. Оператор, отбросивший неудобный запуск, держит коллекцию по отдельности безупречных капсул и историю с дырой. Мы закрываем это единым только-добавляемым журналом корней капсул, поддерживающим доказательства включения (запуск есть в журнале) и доказательства согласованности (журнал прежнего размера — точный префикс журнала теперь). Согласованность ловит цензуру: пересобранный журнал с удалённым запуском внутренне корректен, и каждая уцелевшая капсула всё ещё проверяется, но он не проходит проверку согласованности против ранее опубликованного корня, и та же проверка отклоняет вставку задним числом. Это работает лишь против корня, зафиксированного до подмены, поэтому тот корень должен жить где-то, что оператор не может пересмотреть, например в публичном якоре.

Мы используем Merkle Mountain Range, а не простое дерево Merkle, потому что добавление лишь прибавляет узлы и никогда не переписывает ни один, что позволяет журналу жить на write-once- или объектном хранилище и держит старые доказательства включения действительными по мере роста журнала. Это свойство близко к оптимальному, а не просто удобно: ePrint 2025/234 доказывает, что любое сжатое только-добавляемое обязательство должно порождать суперлинейное число обновлений свидетелей, а близкий вариант MMR по существу достигает границы.

Размер журнала Сборка MMR Простая пересборка
500 1,3 ms 158 ms (120x)
1000 2,6 ms 618 ms (237x)
2000 5,2 ms 2491 ms (483x)

Добавление остаётся плоским по мере роста журнала (около 390k добавлений в секунду и при 100, и при 10 000 записях), тогда как пересборка простого дерева падает с 8176 до 84 корней в секунду на том же диапазоне — квадратичная-против-линеарифмической подпись. Хранилище устаканивается на 2,00 сохранённых хеша на запись, а доказательства включения растут лишь с 7 до 17 шагов между 100 и 100 000 записями. Где мы проигрываем: наши доказательства согласованности имеют O(log^2 n), неся по одному пути на пик старого журнала вместо достижимого O(log n), несколько килобайт при 100 000 записях и осознанный размен ради конструкции, которую рецензент может проверить чтением.

Границы, которые мы не переступаем

Хеширование даёт доказуемость подмены, а не защиту от неё. Противник, пересобравший запуск и пересчитавший его корень, производит самосогласованную запись, потому что корень Merkle доказывает, что события соответствуют корню, а не то, что корень — именно опубликованный. Подпись ловит это, а публичный якорь доказывает время. Журнал прозрачности в большом опирается на то же предположение: он обнаруживает удалённый запуск только относительно корня, зафиксированного до удаления. Мы заявляем эти границы в продукте и его демо, и тест утверждает, что демо подделки продолжает показывать атаку, которую мы не останавливаем, чтобы честный случай не мог тихо исчезнуть.

Воспроизводимость

Каждая цифра здесь взята из зафиксированного бенчмарка, перезапускаемого одной командой, рядом с тестами, наборами соответствия между языками и между реализациями, и CI-матрицей, прогоняющей Python с 3.10 до 3.14 с нативным ядром и без него. Методология, сырой JSON и заметки о решениях (включая измерения, где каждый выбор проигрывает) — в репозитории, потому что слой, делающий ИИ проверяемым, сам обязан быть проверяемым.