Metadata-Version: 2.4
Name: aihash
Version: 0.1.0
Summary: Доказуемый журнал работы ИИ-системы: сохранность и пломба
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/jacobb-del/aihash
Project-URL: Repository, https://github.com/jacobb-del/aihash
Project-URL: Issues, https://github.com/jacobb-del/aihash/issues
Project-URL: Documentation, https://github.com/jacobb-del/aihash/blob/main/FORMAT.md
Keywords: audit,audit-log,tamper-evident,provenance,rfc3161,timestamping,compliance,gdpr,llm,ai-agents
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Legal Industry
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: System :: Logging
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Operating System :: OS Independent
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Provides-Extra: ots
Requires-Dist: opentimestamps-client>=0.7; extra == "ots"
Provides-Extra: otel
Requires-Dist: opentelemetry-sdk>=1.20; extra == "otel"
Provides-Extra: s3
Requires-Dist: boto3>=1.28; extra == "s3"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: opentelemetry-sdk>=1.20; extra == "dev"
Requires-Dist: boto3>=1.28; extra == "dev"
Requires-Dist: moto[s3]>=5; extra == "dev"
Requires-Dist: setuptools>=77; extra == "dev"
Requires-Dist: wheel; extra == "dev"

# aihash

Доказуемый журнал работы ИИ-системы: записи о том, что сделал ИИ, сохраняются и не могут быть незаметно изменены задним числом.

![Правка одного символа ломает проверку](https://raw.githubusercontent.com/jacobb-del/aihash/main/demo/aihash.gif)

## Проверьте сами — две команды

В репозитории лежит настоящий журнал, запечатанный **настоящим** штампом времени [freetsa.org](https://freetsa.org). Не макет: те же файлы прогоняются в тестах.

```bash
pip install ./python
aihash verify ./demo/journal
```

```
Цепь сходится на всём протяжении.
Запечатано суток: 1 (2026-07-29)
  2026-07-29: rfc3161 — подпись службы штампов проверена (набор корней версии 1, корень freetsa)
```

Теперь **поменяйте один символ**: в `demo/journal/streams/voice-eu-3/segments/2026-07-29.jsonl` найдите `возврат придёт в течение 3 дней` и исправьте `3` на `5`. Та же команда:

```
НЕ СХОДИТСЯ

  поток voice-eu-3 seq=3: звено не совпадает с записанным
```

Код возврата `1`. Подробности, проверка через браузер и через `openssl` без нашего кода вообще — в [demo/](https://github.com/jacobb-del/aihash/blob/main/demo/README.md).

**Нашли дыру — заводите issue.** Формат в [FORMAT.md](https://github.com/jacobb-del/aihash/blob/main/FORMAT.md), векторы в [spec/](https://github.com/jacobb-del/aihash/blob/main/spec/), честный список того, что **не** доказывается, — [ниже](#что-доказано-и-что-нет).

---

Это не система наблюдаемости. Наблюдаемость отвечает на вопрос «что происходило». Здесь отвечают на вопрос «докажи».

```python
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 дня в году ничего не происходит. В день, когда прилетела претензия:

```bash
aihash seal    --root ./journal
aihash explain --root ./journal --stream voice-eu-3 --seq 3 --out ep.seal
```

Оппонент проверяет у себя — офлайн, без обращения к вам, без установки чего-либо:

```bash
aihash-verify ep.seal
```

```
Вывод: содержимое совпадает с отпечатком и входит в суточное дерево.
Пломба за эти сутки поставлена, но здесь не подтверждена — см. её
строку выше. Неизменность не доказана, пока проверка пломбы не
доведена до конца; отсутствием пломбы это не является.
Запись существовала не ранее 29.07.2026 08:45 UTC (drand:quicknet раунд 30837112)
и не позднее постановки пломбы.
```

Бинарь проверил всё, кроме подписи службы штампов: разбора CMS в нём пока нет. Подпись сверяет команда SDK — **без единого аргумента**, потому что корни известных служб вшиты в сам верификатор:

```bash
aihash verify ep.seal
```

```
  пломба rfc3161: проверена — подпись службы штампов проверена
                  (набор корней версии 1, корень freetsa)

Вывод: содержимое совпадает с отпечатком, отпечаток входит в
суточное дерево, суточная отметка запечатана.
Запись существовала не ранее 29.07.2026 08:45 UTC (drand:quicknet раунд 30837112)
и не позднее Jul 29 08:45:31 2026 GMT (rfc3161).
```

**Корень доверия едет с верификатором, а не с пакетом** — как у браузеров. Сертификат, приложенный к пакету, и корень, вложенный в сам штамп, доверия не создают никогда: иначе подделыватель приложил бы к поддельному штампу свой корень и заверил бы сам себя. Они годятся ровно на одно — подсказать, какой корень искать.

Службы, которой нет в наборе, вердикт не отвергает и не заверяет: «пломба стоит, но здесь не подтверждена», с указанием, какой именно сертификат нужен. Проверить её можно сразу, не дожидаясь нашего выпуска: `--tsa-ca <файл.pem>` главнее набора.

Разница между двумя выводами и есть продукт: «пломба стоит, подпись не сверена» — это не «пломбы нет», и верификатор обязан говорить именно то, что установил.

## Что доказано и что нет

| Доказываем | Не доказываем |
|---|---|
| содержимое не менялось после постановки пломбы | что записи **правдивы** |
| запись существовала не позже времени пломбы | что журнал **полон** |
| запись создана не раньше времени маяка | **авторство** — без опциональной подписи |
| записи идут в этом порядке, без вставок и изъятий | отсутствие обрезки хвоста **между** пломбами |

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

## Как это устроено

У каждого поля записи свой отпечаток, считаемый из имени, соли и значения. Листья собираются в дерево — его корень и есть отпечаток записи. Записи связаны в цепь, цепь за сутки сворачивается в дерево, и на суточный корень ставится **пломба** в трёх независимых местах сразу:

| Пломба | Для кого | Кто принуждает |
|---|---|---|
| RFC 3161 (eIDAS) | суд, страховщик | служба штампов времени |
| OpenTimestamps | технический скептик | цепь блоков Bitcoin |
| публичная лента | обнаружение раздвоения журнала | публичность |

Отказы этих трёх не коррелируют. Плюс публичный маяк [drand](https://drand.love) в начале каждого отрезка даёт **нижнюю** границу времени: значение раунда нельзя было знать заранее.

**Вычёркивание не ломает пломбу.** Удаляются значение и соль, лист дерева остаётся — отпечаток записи не меняется, цепь продолжает сходиться. Это решает конфликт удаления и неизменяемости **внутри журнала**; для копий, уже уложенных в неизменяемое хранилище, есть отдельная оговорка ниже.

## Состав

| Каталог | Что там |
|---|---|
| [FORMAT.md](https://github.com/jacobb-del/aihash/blob/main/FORMAT.md) | нормативная спецификация формата `aihash/1` |
| [spec/](https://github.com/jacobb-del/aihash/blob/main/spec/CONFORMANCE.md) | тестовые векторы и порядок портирования на новый язык |
| [python/](https://github.com/jacobb-del/aihash/blob/main/python/SDK.md) | SDK записи, командная строка, приёмники сохранности, экспортер OpenTelemetry |
| [typescript/](https://github.com/jacobb-del/aihash/blob/main/typescript/README.md) | SDK записи для Node, без зависимостей |
| [verifier/](https://github.com/jacobb-del/aihash/blob/main/verifier/README.md) | три независимых верификатора: бинарь на Go, страница для браузера, командная строка на Node |
| [PLAN.md](https://github.com/jacobb-del/aihash/blob/main/PLAN.md) | принятые решения и причины |
| [demo/](https://github.com/jacobb-del/aihash/blob/main/demo/README.md) | журнал с настоящим штампом, готовый пакет доказательства и верификатор — можно потрогать руками |

> **Продолжаете работу над проектом?** Начните с [STATE.md](https://github.com/jacobb-del/aihash/blob/main/STATE.md) — состояние,
> что проверено, открытые вопросы и первое действие. Потом [PLAN.md](https://github.com/jacobb-del/aihash/blob/main/PLAN.md):
> там записано, какие решения уже отвергнуты и почему.

## Проверка

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

Сначала собрать — набор атак отказывается проходить, пока собраны не все реализации, чтобы не проходить впустую:

```bash
pip install "./python[dev]"
python3 verifier/build.py                        # страница-верификатор
sh verifier/go/build.sh                          # бинарь
(cd typescript && npm install && npx tsc)        # SDK на TypeScript
```

Потом прогнать:

```bash
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](https://github.com/jacobb-del/aihash/blob/main/STATE.md).
- **Публичная лента защищает от раздвоения журнала только будучи опубликованной вовне.** Лента внутри того же каталога находится под контролем той же стороны, и верификатор говорит это прямым текстом.
- **Локализация расхождения возможна до записи, но не до поля.** Отпечаток считается по записи целиком.
- **На сборке 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` обнаруживает уже уложенные сутки, называет затронутые приёмники и возвращает ненулевой код, если удаление невозможно, — чтобы это выяснилось у вас, а не у проверяющего.

**Как выбирать.** Если персональные данные попадают в журнал и вы обязаны исполнять требования об удалении:

1. Не кладите поля с персональными данными в журнал вовсе — записывайте их идентификаторы, а содержимое держите в системе, где удаление возможно.
2. Либо укладывайте в архив в режиме `GOVERNANCE`, приняв, что его можно обойти привилегированным доступом.
3. Либо ограничьте срок хранения тем, что вы готовы удерживать при любом запросе.

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

## Лицензия

Apache-2.0, см. [LICENSE](https://github.com/jacobb-del/aihash/blob/main/LICENSE).

Открытый код и открытый формат выбраны не по идеологии, а прагматично: хранить чужие доказательства может только организация с репутацией, а опубликовать стандарт может кто угодно. Проверка обязана работать без нас — иначе доказательство стоит вдвое меньше, потому что оппонент скажет «проверено инфраструктурой ответчика».
