Доказуемый журнал работы ИИ-системы: записи о том, что сделал ИИ, сохраняются и не могут быть незаметно изменены задним числом.
В репозитории лежит настоящий журнал, запечатанный настоящим штампом времени freetsa.org. Не макет: те же файлы прогоняются в тестах.
pip install ./python
aihash verify ./demo/journalThe chain matches end to end.
Days sealed: 1 (2026-07-29)
2026-07-29: rfc3161 — timestamp authority signature verified (root store version 1, root freetsa)
Теперь поменяйте один символ: в demo/journal/streams/voice-eu-3/segments/2026-07-29.jsonl найдите возврат придёт в течение 3 дней и исправьте 3 на 5. Та же команда:
DOES NOT MATCH
stream voice-eu-3 seq=3: the link does not match the recorded one
Код возврата 1. Подробности, проверка через браузер и через openssl без нашего кода вообще — в demo/.
Нашли дыру — заводите issue. Формат в FORMAT.md, векторы в spec/, честный список того, что не доказывается, — ниже.
Это не система наблюдаемости. Наблюдаемость отвечает на вопрос «что происходило». Здесь отвечают на вопрос «докажи».
import aihash
log = aihash.open("./journal", realm="romashka-prod", stream="voice-eu-3")
log.record({"actor": "assistant",
"output.text": "возврат придёт в течение 5 дней",
"config.model": "gpt-4o-2026-02-11"})Дальше 364 дня в году ничего не происходит. В день, когда прилетела претензия:
aihash seal --root ./journal
aihash explain --root ./journal --stream voice-eu-3 --seq 3 --out ep.sealОппонент проверяет у себя — офлайн, без обращения к вам, без установки чего-либо:
aihash-verify ep.sealVerdict: the content matches its fingerprint and is in the daily tree.
A seal was placed for these days but is not confirmed here — see
its line above. Integrity is not proven until verifying the seal
is finished; this is not the same as having no seal.
The record existed no earlier than 29.07.2026 08:45 UTC (drand:quicknet round 30837112)
and no later than the seal.
Бинарь проверил всё, кроме подписи службы штампов: разбора CMS в нём пока нет. Подпись сверяет команда SDK — без единого аргумента, потому что корни известных служб вшиты в сам верификатор:
aihash verify ep.seal seal rfc3161: verified — timestamp authority signature verified
(root store version 1, root freetsa)
Verdict: the content matches its fingerprint, the fingerprint is
in the daily tree, and the daily checkpoint is sealed.
The record existed no earlier than 29.07.2026 08:45 UTC (drand:quicknet round 30837112)
and no later than Jul 29 08:45:31 2026 GMT (rfc3161).
Корень доверия едет с верификатором, а не с пакетом — как у браузеров. Сертификат, приложенный к пакету, и корень, вложенный в сам штамп, доверия не создают никогда: иначе подделыватель приложил бы к поддельному штампу свой корень и заверил бы сам себя. Они годятся ровно на одно — подсказать, какой корень искать.
Службы, которой нет в наборе, вердикт не отвергает и не заверяет: «пломба стоит, но здесь не подтверждена», с указанием, какой именно сертификат нужен. Проверить её можно сразу, не дожидаясь нашего выпуска: --tsa-ca <файл.pem> главнее набора.
Разница между двумя выводами и есть продукт: «пломба стоит, подпись не сверена» — это не «пломбы нет», и верификатор обязан говорить именно то, что установил.
| Доказываем | Не доказываем |
|---|---|
| содержимое не менялось после постановки пломбы | что записи правдивы |
| запись существовала не позже времени пломбы | что журнал полон |
| запись создана не раньше времени маяка | авторство — без опциональной подписи |
| записи идут в этом порядке, без вставок и изъятий | отсутствие обрезки хвоста между пломбами |
Пломба не доказывает, что запись правдивая. Она доказывает, что запись старая — сделана тогда, когда ещё не было известно, что окажется выгодным. Практически всё жульничество с логами делается задним числом; убрав возможность править задним числом, мы убираем почти всю проблему.
У каждого поля записи свой отпечаток, считаемый из имени, соли и значения. Листья собираются в дерево — его корень и есть отпечаток записи. Записи связаны в цепь, цепь за сутки сворачивается в дерево, и на суточный корень ставится пломба в трёх независимых местах сразу:
| Пломба | Для кого | Кто принуждает |
|---|---|---|
| RFC 3161 (eIDAS) | суд, страховщик | служба штампов времени |
| OpenTimestamps | технический скептик | цепь блоков Bitcoin |
| публичная лента | обнаружение раздвоения журнала | публичность |
Отказы этих трёх не коррелируют. Плюс публичный маяк drand в начале каждого отрезка даёт нижнюю границу времени: значение раунда нельзя было знать заранее.
Вычёркивание не ломает пломбу. Удаляются значение и соль, лист дерева остаётся — отпечаток записи не меняется, цепь продолжает сходиться. Это решает конфликт удаления и неизменяемости внутри журнала; для копий, уже уложенных в неизменяемое хранилище, есть отдельная оговорка ниже.
| Каталог | Что там |
|---|---|
| FORMAT.md | нормативная спецификация формата aihash/1 |
| spec/ | тестовые векторы и порядок портирования на новый язык |
| python/ | SDK записи, командная строка, приёмники сохранности, экспортер OpenTelemetry |
| typescript/ | SDK записи для Node, без зависимостей |
| verifier/ | три независимых верификатора: бинарь на Go, страница для браузера, командная строка на Node |
| PLAN.md | принятые решения и причины |
| demo/ | журнал с настоящим штампом, готовый пакет доказательства и верификатор — можно потрогать руками |
Продолжаете работу над проектом? Начните с STATE.md — состояние, что проверено, открытые вопросы и первое действие. Потом PLAN.md: там записано, какие решения уже отвергнуты и почему.
Формат один, реализаций четыре. Каждая сверяется с общими векторами, и сверх того они проверяются друг против друга: журнал, написанный одной, обязаны принять остальные.
Сначала собрать — набор атак отказывается проходить, пока собраны не все реализации, чтобы не проходить впустую:
pip install "./python[dev]"
python3 verifier/build.py # страница-верификатор
sh verifier/go/build.sh # бинарь
(cd typescript && npm install && npx tsc) # SDK на TypeScriptПотом прогнать:
python3 spec/tools/check.py # 111 проверок векторов формата
go test -C verifier/go ./... # бинарь
node verifier/tests/conformance.js # 104 проверки браузерного ядра
cd python && python -m pytest -q # SDK, восемь атак, сквозные сценарии
cd typescript && npm test # SDK и сверка четырёх реализацийПроверено на чистом клоне в контейнере: Debian и Alpine, glibc и musl, amd64 и arm64, Python 3.9, Node 18/20/24, Go 1.22/1.24. Отпечатки совпадают побайтово с теми, что получаются на macOS/arm64.
- Подпись штампа сверяет только SDK. Корни известных служб вшиты в него, поэтому аргументов не требуется. Бинарь на Go, страница для браузера и верификатор на Node разбора CMS не содержат: они проверяют, что штамп относится именно к этой отметке, и честно сообщают, что подпись не сверена. Служба вне набора — то же самое сообщение с указанием, какой корень нужен;
--tsa-caперекрывает набор. - Сертификаты живут меньше, чем срок хранения журнала. Когда сертификат службы истечёт, подпись перестанет сверяться, хотя штамп останется тем же. Верификатор в этом случае говорит «пломба стоит, не подтверждена», а не «подделка». Разбор и варианты решения — в STATE.md, раздел 9.
- Публичная лента защищает от раздвоения журнала только будучи опубликованной вовне. Лента внутри того же каталога находится под контролем той же стороны, и верификатор говорит это прямым текстом.
- Локализация расхождения возможна до записи, но не до поля. Отпечаток считается по записи целиком.
- На сборке python.org для macOS отсутствуют корневые сертификаты — маяк и штамп времени не получатся, пока не выставлен
SSL_CERT_FILEили не выполненInstall Certificates.command. Отказ громкий и виден в выводеseal. - Принуждение S3 Object Lock обеспечивает AWS, а не эта библиотека. Наша часть — запросить режим
COMPLIANCEс верной датой и отказаться писать в корзину без включённой блокировки.
Две возможности продукта конфликтуют, и об этом надо знать до внедрения, а не после запроса на удаление.
Вычёркивание правит журнал, но не копию, уже уложенную в архив. Что можно сделать:
| Приёмник | Достигает ли вычёркивание архива |
|---|---|
local: |
да, после aihash archive --date … --force |
s3:// в режиме GOVERNANCE |
да, привилегированным доступом |
s3:// в режиме COMPLIANCE |
нет. Прежнюю версию не удалит никто до истечения срока хранения |
Это не недоработка, а прямое следствие того, ради чего режим COMPLIANCE и выбирается: его нельзя обойти, в том числе владельцу. Команда aihash redact обнаруживает уже уложенные сутки, называет затронутые приёмники и возвращает ненулевой код, если удаление невозможно, — чтобы это выяснилось у вас, а не у проверяющего.
Как выбирать. Если персональные данные попадают в журнал и вы обязаны исполнять требования об удалении:
- Не кладите поля с персональными данными в журнал вовсе — записывайте их идентификаторы, а содержимое держите в системе, где удаление возможно.
- Либо укладывайте в архив в режиме
GOVERNANCE, приняв, что его можно обойти привилегированным доступом. - Либо ограничьте срок хранения тем, что вы готовы удерживать при любом запросе.
Правильное решение на уровне формата — шифрование значений полей ключами вне архива: удаление ключа делает значение невосстановимым даже в неизменяемом хранилище. Это изменение формата, и в версии 1 его нет.
Apache-2.0, см. LICENSE.
Открытый код и открытый формат выбраны не по идеологии, а прагматично: хранить чужие доказательства может только организация с репутацией, а опубликовать стандарт может кто угодно. Проверка обязана работать без нас — иначе доказательство стоит вдвое меньше, потому что оппонент скажет «проверено инфраструктурой ответчика».
