Перейти к содержимому

Машинный перевод. Авторитетным источником является английская версия; проверка носителем языка ещё не выполнена.

audit

Аудиторские доказательства, которые проверяющий может проверить офлайн

Автор Olivares AI 11 мин чтения

Ваш аудитор требует доказательства того, что ваши агенты ИИ делали в прошлом квартале. Вы передаете им CSV-экспорт журнала аудита. Они задают один вопрос: «Можете ли вы доказать, что это не было отредактировано задним числом?» Большинство команд не могут. Журнал хранится в изменяемой базе данных, экспортируется той же системой, которая его создала. Аудитор должен принимать всю цепочку на веру, что как раз и делает её не доказательством.

Этот пост объясняет, как платформа формирует аудиторские доказательства, которые имеют силу без веры. Предыдущий пост освещал, почему важно иметь идентификацию каждого агента и журнал с обнаружением изменений для развертываний Claude Code и MCP. Этот пост погружается глубже в три свойства, которые делают доказательства независимо проверяемыми: хэш-цепочку, криптографическую подпись каждого события и экспорт в машинно-читаемый формат соответствия, который уже понимают инструменты аудитора.

Хэш-цепочка: каждое событие фиксирует все предыдущие события

Главная реестр является только для добавления и имеет хэш-цепочку для каждого арендатора. Каждое событие содержит хэш SHA-256 предыдущего события в своём поле prev_hash. Хэш цепочки события N вычисляется на основе канонического бинарного преобразования, которое включает: собственные поля события N (арендатор, номер последовательности, временная метка, актер, действие, объект, дайджест метаданных, хэш полезной нагрузки), соединённые с prev_hash из события N-1. При последовательности 1 prev_hash состоит из всех нулей — генезис-якорь.

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

Предобраз является фиксированным двоичным кодированием с префиксом длины и разделителем домена с версией — не JSON. Это осознанный выбор для защиты от трех векторов атак, которые оставил бы хеш на основе JSON:

  • Порядок ключей: порядок ключей в JSON-объекте не гарантируется большинством сериализаторов. Разный порядок ключей приводит к разному хешу даже для семантически одинаковых данных, что создаёт ложные разрывы цепочки — или, хуже того, позволяет злоумышленнику изменить порядок ключей, чтобы подделать соответствующий хеш.
  • Пробелы и форматирование чисел: {"seq": 1} и {"seq":1} и {"seq": 1.0} семантически эквивалентны в JSON, но дают разные SHA-256 дайджесты.
  • Фальсификация конкатенации: без префиксов длины два соседних коротких поля могут быть объединены для создания третьего длинного поля с тем же хешем. Использование префикса длины для каждого поля с указанием количества байт (4-байтовый big-endian) решает эту проблему.

Реализация в core/internal/store/canon/canon.go является единственным источником истины. И Append (запись), и Verify (чтение обратно) вызывают одну и ту же функцию EventHash. Второй реализации нет, чтобы происходило расхождение:

func EventHash(e Event) []byte {
    var buf []byte
    buf = lps(buf, domainEvent)       // "olivares.audit.v1"
    buf = lps(buf, e.TenantID)
    var seq [8]byte
    binary.BigEndian.PutUint64(seq[:], uint64(e.Seq))
    buf = append(buf, seq[:]...)
    buf = lps(buf, e.OccurredAt)
    buf = lps(buf, e.Actor)
    buf = lps(buf, e.ActorKind)
    buf = lps(buf, e.Action)
    buf = lps(buf, e.TargetKind)
    buf = lps(buf, e.TargetID)
    buf = append(buf, fixed(e.MetaDigest)...)
    buf = append(buf, fixed(e.PayloadHash)...)
    buf = append(buf, fixed(e.PrevHash)...)
    sum := sha256.Sum256(buf)
    return sum[:]
}

Разделитель домена "olivares.audit.v1" связывает хеш с его назначением и версией. Хеш из другого домена (контрольная точка, полезная нагрузка, дайджест метаданных) никогда не сможет совпасть с хешем события, даже если исходные байты случайно совпадают.

Подпись Ed25519: каждое событие является своей собственной якорной точкой

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

Подписи Ed25519 по каждому событию закрывают эту gap. Каждое событие, добавляемое в реестр, подписывается во время записи. Подпись охватывает разделённый по доменам предварительный образ арендатора, порядковый номер и хэш цепочки события:

domain ("olivares.audit.event.v1") || tenant || seq (8 bytes, big-endian) || hash

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

Внешний проверяющий, имеющий только открытый ключ, может проверять каждое событие отдельно: пересчитать цепной хэш из полей события, восстановить предварительное изображение и проверить подпись Ed25519. Если какое-либо событие было изменено после подписания, проверка подписи не пройдет для этого конкретного события — проверяющему не нужно доверять системе, которая создала доказательство.

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

Помимо подписей для каждого события, периодические контрольные точки нотариально подтверждают конец цепочки в рамках отдельного домена подписи (olivares.audit.checkpoint.v1). Для организаций, которым необходимо защищаться от компрометации на уровне хоста — а не только базы данных — контрольные точки могут подписываться внешним KMS/HSM ключом (AWS KMS, GCP Cloud KMS, Azure Key Vault), где приватный ключ никогда не находится на хосте. Подписи для каждого события справляются с атакующим, нацеленным только на базу данных; внешние контрольные точки защищают от атакующего, компрометирующего хост. Эти две модели угроз различаются; ни одна подпись по отдельности не покрывает обе.

Контракт с реестром: закреплено в одной и той же транзакции

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

Платформа обеспечивает более строгий контракт. И изменение состояния, и заверение в реестре происходят в одной и той же транзакции базы данных. Если заверение не удается, вся транзакция откатывается — изменение состояния никогда не фиксируется. Это не на основе максимально возможных усилий; это закрытое по отказу.

Журнал выполнения сессии иллюстрирует это. Когда сессия Claude Code меняет состояние (создана, запущена, останавливается, остановлена, с ошибкой), appendRunEvent фиксирует переход в двух местах атомарно:

  1. Глобальный аудиторский журнал с цепочкой хешей через sc.Audit().Append — защищенная от подделок цепочка, заякоренная PayloadHash.
  2. Журнал, доступный для запросов по каждой сессии — проекция только для добавления, связанная с глобальной цепочкой через audit_seq.

Оба записи происходят внутри транзакции Mutate вызывающего. Комментарий в коде runtime_ledger.go прямо указывает на замысел дизайна: «Журнал является системой учета, поэтому если печать не проходит, весь переход откатывается — это НЕ попытка сделать по возможности».

Сам PayloadHash фиксирует только канонические, не содержащие конфиденциальной информации факты переходов — ссылку на выполнение, последовательность, тип события, переход состояния и отметку времени. Он никогда не включает содержимое транскрипта, подсказки, значения окружения или секреты. Реестр доказывает что произошло; он не хранит что было сказано.

Аналогичная схема применяется к мутациям файлов рабочего пространства в workspace_ledger.go. Запись файла, создание каталога, перемещение или удаление фиксируются до выполнения операции с файловой системой. Если доказательство не может быть добавлено, мутация не выполняется. Печать содержит тип операции, путь и SHA-256 записанного содержимого — никогда сами байты содержимого.

Экспорт OSCAL: машиночитаемые доказательства, которые обрабатываются инструментами вашего аудитора

Журнал с защитой от подделки необходим, но недостаточен для аудитора. Если доказательства находятся в проприетарном формате, аудитор все равно зависит от ваших инструментов для их интерпретации. OSCAL — Open Security Controls Assessment Language, поддерживаемый NIST — это формат, который закрывает этот gap.

Платформа экспортирует запечатанные пакеты доказательств в виде связки OSCAL, содержащей три модели:

  • Определение компонента: возможности плоскости управления, выраженные как реализованные требования к соответствию стандартам (NIST SP 800-53, ISO 27001, EU AI Act и другие). Каждое реализованное требование содержит идентификатор контрола, ключи возможностей, которые это подтверждают, и фактический статус в виде пользовательского свойства.
  • Результаты оценки: результаты контроля с состоянием, соответствующим OSCAL. Цель каждой проверки имеет satisfied или not-satisfied с точным статусом продукта, сохранённым в поле причины.
  • Сопоставление контрольных мер: перекрёстная таблица от контрольных мер фреймворка к модели ссылочных возможностей платформы с использованием модели сопоставления контроля OSCAL версии 1.2.0. Связь всегда intersects-with — возможности покрывают часть контроля. Это никогда не утверждает соответствие; такое утверждение существует только в результатах оценки и основано на актуальных эксплуатационных данных.

Ограничение честности в экспорте OSCAL стоит указать явно. Перечисление статусов находки OSCAL имеет ровно два значения: satisfied и not-satisfied. Нет значений «partial» или «по замыслу». Контроль, который частично реализован, учтён по замыслу, имеет пробелы или unmapped, отображается как OSCAL not-satisfied, при этом реальный статус продукта находится в status.reason, а пользовательское свойство — в пространстве имён самой платформы (https://olivares.ai/ns/oscal). Экспорт никогда не превращает частично выполненный контроль в satisfied. Только контролям, подкреплённым актуальными оперативными доказательствами на момент закрепления, присваивается OSCAL satisfied.

Каждый документ OSCAL имеет свойства привязки к реестру: хэш манифеста, номер последовательности реестра на момент запечатывания, хэш реестра и результат проверки целостности. Это является связующим звеном между документом OSCAL, который аудитор просматривает в своем инструменте GRC, и базовой защищенной цепочкой, которую он может проверить независимо.

Активность для документирования процесса

Что конкретно означает оффлайн-проверка

“Оффлайн-проверка” — это не маркетинговый термин. Она описывает конкретную техническую процедуру: проверяющий берет экспортированные данные, запускает инструмент проверки на отключенном от сети устройстве и подтверждает целостность данных без какого-либо сетевого доступа к системе, которая их создала.

Архивный экспорт платформы записывает дерево каталогов из сегментов JSONL (по одной строке на событие, канонический JSON) плюс манифест для каждого сегмента. Манифест фиксирует диапазон последовательности сегмента, количество событий, первые и последние хэши цепочки, SHA-256 файла событий и последний хэш предыдущего сегмента для сквозной непрерывности между сегментами.

Оффлайн-верификатор (VerifyArchiveDir) затем выполняет следующее, полностью в константной памяти без сетевых запросов:

  1. Загрузите манифесты и сопоставьте их с файлами событий. Отдельный файл событий (без манифеста) или манифест, чей файл событий отсутствует, считается ошибкой. Единицей доказательства является пара.
  2. Поточно обрабатывайте каждую строку файла событий. Для каждого события повторно вычисляйте хэш цепочки из архивных полей, используя ту же функцию EventHash, что и живая система. Сравните его с сохранённым хэшем. Проверьте привязку prev_hash и отсутствие разрывов в последовательности.
  3. Проверьте каноничность. Снова маршализуйте каждую разобранную строку и убедитесь, что она даёт идентичный байтовый вывод с данными на диске. Это предотвращает скрытую передачу данных через неизвестные поля или атаки с дублированием ключей, которые могут пройти проверку хэша, но содержать скрытые данные.
  4. Проверьте подписи Ed25519 для каждого события. Для каждого события, не являющегося контрольной точкой, восстановите предобраз подписи и проверьте его относительно закреплённых публичных ключей.
  5. Проверьте подписи контрольных точек. Для каждого события контрольной точки проверьте подпись в домене контрольной точки по закреплённым ключам контрольной точки. Если закреплены только ключи событий (нет ключа контрольной точки), событие контрольной точки помечается как «непроверяемое» — закрытое отказом, нельзя пропускать.
  6. Проверьте непрерывность между сегментами. Убедитесь, что первая последовательность каждого сегмента следует за последней последовательностью предыдущего сегмента плюс один, и что последний хеш предыдущего сегмента совпадает с prev_segment_last_hash текущего сегмента.
  7. Проверьте дайджест файла событий. SHA-256 файла событий, вычисленный во время потоковой передачи, должен совпадать с events_sha256 из манифеста.

Проверяющее устройство сообщает о первой обнаруженной несоответствии с указанием конкретного номера последовательности и машины, читаемой причины: hash-mismatch, prev-mismatch, seq-gap, event-sig-invalid, event-sig-missing, checkpoint-sig-invalid, count-mismatch, events-sha256-mismatch или segment-link-mismatch.

Честное ограничение: офлайн-проверяющий подтверждает именно тот диапазон, который он проверил, и ничего за его пределами. Удалённый префикс или хвост невозможно обнаружить офлайн — каталог не указывает, где началась или закончилась цепочка. Отчёт проверки включает поле Ranges для каждого арендатора с флагом StartsMidChain, чтобы аудитору было точно известно, что именно подтверждено. Подписанные контрольные точки в цепочке работающей системы покрывают хвост; офлайн-экспорт охватывает архивный диапазон. Вместе они формируют полное подтверждение.

СлойЧто это доказываетЧто это не доказывает
Хеш-цепочкаВнутренняя согласованность; любое изменение разрушает цепочкуПроисхождение (кто записал события)
По каждому событию Ed25519Происхождение; каждое событие было подписано владельцем ключаЗащита от компрометации на уровне хоста
Контрольная точка вне устройстваСопротивление компрометации хоста (ключ KMS/HSM никогда не находится на хосте)Гранулярность по каждому событию (охватывает только контрольные точки)
Экспорт OSCALМашиночитаемые доказательства соблюдения стандартов, сопоставленные с фреймворкомЧто каждый контроль полностью выполнен (считается только актуальное доказательство)
Проверка архиваОфлайн повторное выведение всего вышеупомянутогоСобытия до или после экспортированного диапазона

Путь кода: от изменения состояния до заверенного доказательства

Последовательность от изменения состояния сессии Claude Code до проверяемого доказательства охватывает три уровня. В runtime_ledger.go функция appendRunEvent создает PayloadHash через SHA-256 хеширование канонических полей перехода с префиксом длины:

func runEventPayloadHash(runRef string, seq int64, event, from, to, detail, atTS string) [32]byte {
    h := sha256.New()
    for _, part := range []string{
        runRef, strconv.FormatInt(seq, 10), event, from, to, detail, atTS,
    } {
        _, _ = h.Write([]byte(strconv.Itoa(len(part))))
        _, _ = h.Write([]byte{':'})
        _, _ = h.Write([]byte(part))
    }
    var sum [32]byte
    copy(sum[:], h.Sum(nil))
    return sum
}

Этот хеш затем передается в sc.Audit().Append, который назначает следующий последовательный номер для каждого арендатора, связывает его с хешем предыдущего события, вычисляет цепной хеш этого события через EventHash, подписывает его с помощью Ed25519 и вставляет заверенное событие — все это в рамках транзакции вызывающего.

Результат: к моменту фиксации транзакции изменение состояния сеанса и его защищённая от подделки аудиторская запись либо оба сохраняются, либо оба откатываются. Нет промежутка, когда состояние изменилось, но доказательство не зафиксировано.

Ссылки

Похожие материалы

Часто задаваемые вопросы

Может ли злоумышленник, который взломал базу данных, повторно подписать подделанные события?

Подписи Ed25519 для каждого события защищают от компрометации только базы данных: украденных резервных копий, вставленных строк или реплики с ролью, обходящей RLS. Ключ подписи хранится в каталоге данных, а не в базе данных. В случае компрометации хоста платформа поддерживает подпись контрольных точек вне хоста через KMS/HSM (AWS KMS, GCP Cloud KMS, Azure Key Vault), при этом приватный ключ никогда не хранится на хосте. Подписи для каждого события останавливают атаки на уровне базы данных; контрольные точки вне хоста останавливают атаки на уровне хоста. Ни одно из решений по отдельности не покрывает оба сценария угроз.

Что произойдет, если экспорт OSCAL пометит контроль как satisfied, но оперативные данные затем изменятся?

Экспорт OSCAL отображает статус продукта в перечисление статусов обнаружения OSCAL в момент запечатывания. Только элементы управления, подтвержденные актуальными операционными данными на тот момент, получают OSCAL satisfied. Элемент управления со статусом by_design, partial, gap или unmapped отображается в OSCAL not-satisfied, при этом точный статус продукта сохраняется в поле причины и в пользовательском свойстве. Каждый пакет запечатанных доказательств является неизменяемым и помеченным отметкой времени. Если доказательства меняются, создается новый пакет, отражающий текущее состояние. Старый пакет остается нетронутым и повторно проверяемым, создавая временной ряд состояния соответствия, а не одну перезаписываемую запись.

Узнайте, до чего могут добраться ваши агенты

Olivares AI — открытая self-hosted платформа для управления вашим парком AI. Разверните её на собственной инфраструктуре и получите карту доступа, которую давно запрашивают ваши команды безопасности и платформ.