Смотрите видео урок: Защита AI-агентов с помощью криптографических квитанций
(Видео урок и миниатюра будут добавлены командой контента Microsoft после слияния, в соответствии с шаблоном уроков 14 / 15.)
В этом уроке рассматриваются:
После завершения урока вы сможете:
Представьте, что вы развернули AI-агента для Contoso Travel. Агент читает запросы клиентов, вызывает API авиакомпаний для поиска вариантов и бронирует места от имени клиента. За прошлый квартал агент обработал 50 000 бронирований.
Сегодня пришел аудитор. Он задает простой вопрос: «Покажите, что сделал ваш агент».
Вы передаете файлы журналов. Аудитор смотрит их и задает более сложный вопрос: «Как я могу быть уверен, что эти логи не были отредактированы?»
Это проблема журнала аудита. Большинство нынешних развертываний агентов опираются на:
Ни один из этих вариантов не позволяет аудитору ответить на вопрос без необходимости доверять кому-то (вам, вашему облачному провайдеру, продавцу базы данных). Для внутреннего использования это часто приемлемо. Для регулируемых нагрузок (финансы, здравоохранение, всё, подчинённое EU AI Act) — нет.
Криптографические квитанции решают эту проблему, делая каждое действие агента независимо проверяемым. Аудитору не нужно доверять вам. Ему нужен только ваш публичный ключ и сама квитанция.
Квитанция — это JSON-объект, который фиксирует, что агент сделал, подписанный цифровой подписью.
flowchart LR
A[Агент вызывает инструмент] --> B[Формирование полезной нагрузки квитанции]
B --> C[Канонизация JSON RFC 8785]
C --> E[Подпись Ed25519 канонических байтов]
E --> F[Квитанция с подписью]
F --> G[Аудитор проверяет офлайн]
G --> H{Подпись действительна?}
H -- yes --> I[Доказательство защиты от подделки]
H -- no --> J[Квитанция отклонена]
Минимальная квитанция выглядит так:
{
"type": "agent.tool_call.v1",
"agent_id": "contoso-travel-bot",
"tool_name": "lookup_flights",
"tool_args_hash": "sha256:a3f9c1...",
"result_hash": "sha256:7b2e1d...",
"policy_id": "contoso-travel-policy-v3",
"timestamp": "2026-04-25T14:30:00Z",
"sequence": 47,
"previous_receipt_hash": "sha256:9d4e6a...",
"signature": {
"alg": "EdDSA",
"sig": "c5af83...",
"public_key": "8f3b2c..."
}
}
За защиту отвечают три свойства:
Подпись. Квитанция подписана шлюзом агента с помощью приватного ключа Ed25519. Любой с соответствующим публичным ключом может проверить подпись офлайн. Подлог в любом поле делает подпись недействительной.
Каноническое кодирование. Перед подписанием квитанция сериализуется с использованием схемы канонического JSON (JCS, RFC 8785). Это гарантирует, что две реализации, производящие одну и ту же логическую квитанцию, создают байт-в-байт идентичный вывод. Без канониализации разные сериализаторы JSON давали бы разные подписи за один и тот же контент.
Хеш-цепочка. Поле previous_receipt_hash связывает каждую квитанцию с предыдущей. Удаление или изменение порядка ломают все последующие квитанции. Подлог становится заметен на уровне цепочки даже при обходе отдельных подписей.
Вместе эти свойства дают три гарантии:
Для создания квитанции не нужна специальная библиотека. Криптографические примитивы широко доступны, а логика занимает несколько десятков строк Python.
Практические упражнения в code_samples/18-signed-receipts.ipynb подробно проходят весь процесс. Кратко:
import json
import hashlib
import base64
from nacl import signing
from jcs import canonicalize # Стандартный JSON по RFC 8785
def b64url_nopad(data: bytes) -> str:
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
def sha256_canonical(obj) -> str:
"""SHA-256 of a Python object's JCS-canonical JSON form."""
return f"sha256:{hashlib.sha256(canonicalize(obj)).hexdigest()}"
# Сгенерировать или загрузить ключ подписи (в производстве хранить в хранилище ключей)
signing_key = signing.SigningKey.generate()
verify_key = signing_key.verify_key
# Построить полезную нагрузку квитанции (пока без подписи)
tool_args = {"origin": "SYD", "destination": "LAX"}
tool_result = [{"flight": "QF11", "price": 1850, "stops": 0}]
payload = {
"type": "agent.tool_call.v1",
"agent_id": "contoso-travel-bot",
"tool_name": "lookup_flights",
"tool_args_hash": sha256_canonical(tool_args),
"result_hash": sha256_canonical(tool_result),
"policy_id": "contoso-travel-policy-v3",
"timestamp": "2026-04-25T14:30:00Z",
"sequence": 0,
"previous_receipt_hash": None,
}
# Канонизировать и подписать байты JCS напрямую. PureEdDSA выполняет хеширование внутренне.
canonical_bytes = canonicalize(payload)
signature_bytes = signing_key.sign(canonical_bytes).signature
# Прикрепить структурированный объект подписи.
receipt = {
**payload,
"signature": {
"alg": "EdDSA",
"sig": b64url_nopad(signature_bytes),
"public_key": b64url_nopad(bytes(verify_key)),
},
}
Это весь пайплайн подписи. Упражнения в блокноте объясняют каждый шаг.
Проверка — это обратная операция:
import base64
import hashlib
from nacl import signing
from nacl.exceptions import BadSignatureError
from jcs import canonicalize
def b64url_decode(s: str) -> bytes:
padding = "=" * ((4 - len(s) % 4) % 4)
return base64.urlsafe_b64decode(s + padding)
def verify_receipt(receipt: dict) -> bool:
# Подпись — это структурированный объект: {"alg", "sig", "public_key"}.
sig_obj = receipt.get("signature")
if not sig_obj or sig_obj.get("alg") != "EdDSA":
return False
# Восстановите полезную нагрузку, которая была фактически подписана (всё, кроме подписи).
payload = {k: v for k, v in receipt.items() if k != "signature"}
canonical_bytes = canonicalize(payload)
try:
verify_key = signing.VerifyKey(b64url_decode(sig_obj["public_key"]))
verify_key.verify(canonical_bytes, b64url_decode(sig_obj["sig"]))
return True
except BadSignatureError:
return False
Эта функция принимает квитанцию и возвращает True, если подпись действительна, и False в противном случае. Ни сетевых запросов, ни внешних сервисов, ни доверия третьим лицам не требуется.
Для демонстрации обнаружения подлога блокнот показывает:
tool_args_hash.Это практическая демонстрация того, что квитанции защищены от подлога: любое изменение, даже минимальное, ломает подпись.
Одна подписанная квитанция защищает одно действие. Цепочка квитанций защищает последовательность действий.
flowchart LR
R0[Квитанция 0<br/>генезис] --> R1[Квитанция 1]
R1 --> R2[Квитанция 2]
R2 --> R3[Квитанция 3]
R1 -. previous_receipt_hash .-> R0
R2 -. previous_receipt_hash .-> R1
R3 -. previous_receipt_hash .-> R2
Каждая квитанция содержит хеш предыдущей. Чтобы без следа удалить квитанцию 2, злоумышленник должен либо:
previous_receipt_hash квитанции 3 (что ломает подпись квитанции 3), ИЛИЕсли приватный ключ хранится в аппаратном ключевом хранилище, а вы публикуете публичный ключ вместе с каждой квитанцией, ни одна атака невозможна без обнаружения.
В блокноте показано:
previous_receipt_hash каждой квитанции совпадает с хешем предыдущей.Так создается журнал аудита, который внешний аудитор может проверить без доверия к вам.
Это самый важный раздел урока. Квитанции мощны, но их сила ограничена.
Квитанции доказывают три вещи:
Квитанции НЕ доказывают:
policy_id была действительно выполнена или что эта политика разрешила бы действие при проверке. Квитанция фиксирует заявленное, а не исполненное.Эта граница важна по двум причинам:
Частая ошибка — думать, что «у нас есть квитанции» значит «нас контролируют». Нет. Квитанции — это основа. Управление — это система, построенная на ней.
Пункт 3 выше заслуживает отдельного раздела: квитанция действия говорит «этот ключ подписал этот контент», но никогда не говорит «человек одобрил это». Для действий с высокими рисками (возвраты, удаления, переводы денег) системы управления все чаще требуют именно этого отсутствующего утверждения, и оно создаётся теми же примитивами, что вы уже реализовали в этом уроке.
Последующий блокнот code_samples/human-authorization-receipts.ipynb добавляет второй тип квитанции — human.approval.v1 — в том же формате конверта, что и квитанции урока (типизированная полезная нагрузка, подписанная Ed25519 по каноническому JCS, с объектом signature вне подписанных байт). Названный утверждающий подписывает полное каноническое действие и его дайджест до выполнения; квитанция действия агента содержит тот же дайджест действия и parent_approval_ref — receipt_hash одобрения, по той же конвенции что previous_receipt_hash в цепочке выше. Одна функция verify_chain проверяет оба объекта под отдельными закрепленными реестрами ключей (ключи утверждающих и ключи агентов), чтобы код был общий, но власти разделены.
Цена этой конструкции, сформулированная аккуратно: человек одобрил именно это действие, и агент выполнил именно это одобренное действие. Отказы в блокноте — это то, что делает свойство реальным, а не просто заявленным:
Каждый отказ имеет отдельное объяснение, чтобы аудитор мог понять, устарела ли власть или изменилось ли выполненное действие. Правило, которое учит блокнот: сама по себе подписанная квитанция не дает власти. Власть существует только если обе квитанции по-прежнему закреплены за одним и тем же каноническим действием во время исполнения. Квитанция с одобрением человеком — это обучающая композиция, определённая этим уроком, а не тип квитанции из draft-farley-acta-signed-receipts.
Python-код в этом уроке намеренно минимален, чтобы вы могли прочитать каждую строку и точно понять, что происходит. В продакшене у вас есть два варианта:
Построить напрямую на криптографических примитивах. Показанные выше 50 строк достаточно для многих случаев. PyNaCl (Ed25519) и пакет jcs (канонический JSON) — хорошо поддерживаемые и проверенные библиотеки.
Использовать продакшен-библиотеку квитанций. Несколько проектов с открытым кодом реализуют тот же шаблон с дополнительными функциями (ротация ключей, пакетная проверка, распространение JWK-набора, интеграция с политиками):
draft-farley-acta-signed-receipts, ревизия 02). Образовательная плоская квитанция этого урока отличается от конверта {payload, signature} в драфте и не считается соответствующей реализацией. Драфт публикует общий набор тестов на соответствие (agent-governance-testvectors) для реализаций, ориентированных на формат передачи.protect-mcp (npm) и @veritasacta/verify (npm) реализуют Node-реализацию подписания и офлайн-проверки квитанций, предназначенную для создания нечитаемых журналов MCP-серверов, включая сценарий с ожиданием со-согласия, где приостановленное действие генерирует квитанцию одобрения, связанную с дайджестом действия (WebAuthn в десктопном сценарии), та же схема одобрения, что и в блокноте авторизации человека выше.pip install nobulex) предоставляет тот же шаблон подписи Ed25519 + JCS на Python с интеграциями LangChain и CrewAI, включая опубликованные тестовые векторы и карту соответствия, созданную через OWASP PR #2210.Выбор между самостоятельной реализацией и использованием библиотеки аналогичен выбору между написанием собственной JWT-библиотеки и использованием проверенной: оба варианта разумны; библиотека экономит время и снижает площадь аудита; самостоятельный путь заставляет понять каждую основу. Этот урок обучает самостоятельному пути, чтобы дать фундамент для любого выбора.
Проверьте свои знания перед выполнением практического упражнения.
1. Квитанция подписана приватным ключом Ed25519 агента. У аудитора есть только публичный ключ. Может ли аудитор проверить квитанцию офлайн?
2. Злоумышленник изменяет поле policy_id квитанции, чтобы заявить, что действие подчинялось более мягкой политике. Подпись была сделана по исходной полезной нагрузке. Что произойдет при проверке?
3. Почему квитанция включает tool_args_hash и result_hash, а не исходные аргументы и результат?
4. Поле previous_receipt_hash связывает каждую квитанцию с ее предшественником. Если злоумышленник тихо удалит одну квитанцию из середины цепочки, что станет недействительным?
5. Квитанция успешно проходит проверку. Доказывает ли это, что действие агента было правильным, корректным или соответствовало политике?
Откройте файл code_samples/18-signed-receipts.ipynb и выполните все четыре раздела:
Сложное задание 1: расширьте схему квитанции дополнительным полем по вашему выбору (например, ID запроса для трассировки), обновите логику канонической подписи, чтобы включить его, и подтвердите, что квитанция успешно проходит проверку. Затем измените поле после подписи и подтвердите, что проверка не проходит. Это заставит вас понять, как каждый байт канонического кодирования влияет на подпись.
Сложное задание 2: вычислите SHA-256 для двух ваших квитанций вместе (конкатенируя их канонические байты в детерминированном порядке) и встройте полученный дайджест как новое поле в третью квитанцию перед подписью. Проверьте, что все три квитанции проходят проверку. Вы только что создали доказательство включения в один шаг: любой, у кого есть третья квитанция, может доказать существование первых двух на момент подписания, не раскрывая их содержимого. Это паттерн, который используют квитанции с выборочным раскрытием в масштабах (Меркле-коммиты, RFC 6962).
Криптографические квитанции дают агентам ИИ аудит, который:
Они не заменяют проверку входных данных, применение политики или инфраструктуру идентификации. Они служат основой для этих уровней. При развертывании агентов в регулируемых нагрузках, рабочих процессах с несколькими организациями или любой ситуации, где будущий аудитор не может считать вас надёжным, квитанции делают аудиторскую цепочку честной.
Самое важное: квитанции доказывают, кто что сказал и когда. Они не доказывают, что сказанное было истинным или правильным. Держите это различие строго. Это разница между честной системой происхождения и вводящей в заблуждение.
Когда вы будете готовы перейти от этого урока к развертыванию агентов с подписанными квитанциями в реальной среде:
https://your-org.example.com/.well-known/agent-keys.json.Присоединяйтесь к Microsoft Foundry Discord для общения с другими учащимися, участия в часах консультаций и получения ответов на вопросы по агентам ИИ.
В этом уроке рассмотрена одиночная подпись квитанции и цепочка с хешами. Те же примитивы составляют несколько более продвинутых паттернов, которые вы можете встретить по мере развития вашего управления:
authorization_*) и после-исполнение (result_*) с независимыми подписями, полезно когда решение об авторизации и наблюдаемый результат производятся разными участниками или в разное время. Это дополнительно накладывается на формат квитанций, описанный в уроке.result_hash. Реальные полезные нагрузки зачастую богаче, чем один результат вызова инструмента: предрешающие рассуждения (предсказание модели, рассмотренные варианты, доказательства и полнота, риск, цепочка подотчетности, результат фильтра) могут жить внутри полезной нагрузки, запечатлённой одной квитанцией. Это сохраняет формат квитанции минимальным и одновременно позволяет схеме полезной нагрузки эволюционировать по доменам.signature.alg может содержать ML-DSA-65 (стандарт постквантовой подписи NIST) по мере необходимости миграции. Планируйте переходный период с двойной подписью квитанций.Отказ от ответственности: Этот документ был переведен с использованием сервиса машинного перевода Co-op Translator. Несмотря на наши усилия по обеспечению точности, имейте в виду, что автоматический перевод может содержать ошибки или неточности. Оригинальный документ на его исходном языке следует считать авторитетным источником. Для получения критически важной информации рекомендуется обратиться к профессиональному человеческому переводу. Мы не несем ответственности за любые недоразумения или неправильные толкования, возникшие в результате использования этого перевода.