レッスンビデオを見る:暗号化されたレシートでAIエージェントを保護する
(マイクロソフトのコンテンツチームがマージ後にレッスン14/15のパターンに合わせてレッスン動画とサムネイルを追加予定です)
このレッスンで扱う内容:
このレッスンを終えると、以下を理解できるようになります:
Contoso TravelのAIエージェントを展開したと想像してください。エージェントは顧客のリクエストを読み、フライトAPIを呼び出してオプションを検索し、顧客に代わって座席を予約します。前四半期にエージェントは50,000件の予約を処理しました。
今日、監査人が来ました。彼らは単純な質問をします:「あなたのエージェントが何をしたのか見せてください。」
あなたはログファイルを渡します。監査人はそれを見て、より難しい質問をします:「これらのログが改ざんされていないことをどうやって知れますか?」
これが監査証跡問題です。今日のほとんどのエージェント展開では以下に依存しています:
これらのどれも、監査人が誰かを信頼することなしに監査人の質問に答えることはできません(あなた、クラウドプロバイダー、データベースベンダー)。内部利用ではその信頼は時に許容されますが、規制されたワークロード(金融、医療、EU AI法の対象など)では許されません。
暗号化レシートは、各エージェントの行動を独立して検証可能にすることでこれを解決します。監査人はあなたを信頼する必要はありません。あなたの公開鍵とレシート自体だけがあれば十分です。
レシートは、エージェントが行ったことを記録し、デジタル署名されたJSONオブジェクトです。
flowchart LR
A[エージェントがツールを呼び出す] --> B[レシートペイロードを構築]
B --> C[JSONを正規化 RFC 8785]
C --> D[SHA-256 ハッシュ]
D --> 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..."
}
}
3つの性質が機能しています:
署名。レシートはエージェントのゲートウェイによってEd25519の秘密鍵で署名されます。対応する公開鍵を持つ者なら誰でも署名をオフラインで検証可能です。どのフィールドも改ざんされると署名は無効になります。
正準エンコード。署名前にレシートはJSON Canonicalization Scheme (JCS, RFC 8785) を使ってシリアル化されます。これにより、同一の意味内容を持つ2つの実装がバイト単位で同じ出力を生成します。正準化がなければ、異なるJSONシリアライザが同じ内容に異なる署名を生じさせます。
ハッシュ連鎖。previous_receipt_hash フィールドが各レシートを前のレシートにリンクします。レシートを削除または並び替えると、その後に続くすべてのレシートが壊れます。個々の署名が回避されてもチェーンレベルで改ざんが可視化されます。
これらの性質は3つの保証を提供します:
レシート作成には特別なライブラリは不要です。暗号原始関数は広く利用可能で、ロジックは数十行のPythonコードです。
code_samples/18-signed-receipts.ipynb のハンズオン演習でフルフローを詳しく解説しています。要約版:
import json
import hashlib
import base64
from nacl import signing
from jcs import canonicalize # RFC 8785 標準の JSON
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,
}
# 標準化し、ハッシュ化し、署名する。
canonical_bytes = canonicalize(payload)
message_hash = hashlib.sha256(canonical_bytes).digest()
signature_bytes = signing_key.sign(message_hash).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)
message_hash = hashlib.sha256(canonical_bytes).digest()
try:
verify_key = signing.VerifyKey(b64url_decode(sig_obj["public_key"]))
verify_key.verify(message_hash, b64url_decode(sig_obj["sig"]))
return True
except BadSignatureError:
return False
この関数はレシートを受け取り、署名が有効なら True、そうでなければ False を返します。ネットワーク呼び出し不要、サービス依存なし、第三者の信頼も不要です。
改ざん検知の実例についてはノートブックで:
tool_args_hash フィールドの1バイトを変更。これがレシートが改ざん検知可能である実用的な証明です。どんなに小さな変更でも署名を壊します。
1つの署名済みレシートは1つのアクションを保護します。レシートのチェーンがシーケンスを保護します。
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の署名破損)、または秘密鍵がハードウェアキーボルト内にあり、公開鍵を各レシートに公開していれば、どちらの攻撃も検出なしには不可能です。
ノートブックでは以下を説明します:
previous_receipt_hash が前のレシートの実際のハッシュと合致することを検証。こうして外部監査人があなたを信頼しなくても検証可能な監査証跡を作ります。
このセクションはレッスンの中で最も重要です。レシートは強力ですが、その力には限界があります。
レシートが証明する3つのこと:
レシートが証明しないこと:
policy_id のポリシーが実際に評価されたか、その行動を許可したか。レシートは主張を記録し、強制は記録しません。この境界が重要な理由は2つあります:
よくある誤解は、「レシートがあれば統治されている」ということですが、そうではありません。レシートは基盤です。統治はその上に構築するシステムです。
上記項目3は独立したセクションに値します:行動レシートは「この鍵がこの内容に署名した」と言い、「人間が承認した」とは言いません。高リスク行動(返金、削除、銀行送金など)には、統治フレームワークは欠けているその宣言をますます要求しています。これはこのレッスンで既に構築したプリミティブで生成可能です。
後続のノートブック code_samples/human-authorization-receipts.ipynb は、レッスンのレシートと同じエンベロープ形状(Ed25519でその正準SHA-256に署名された型付けペイロード、署名オブジェクトは署名バイトの外)で第二のレシート種類 human.approval.v1 を追加します。命名された承認者が実行前に完全な正準行動とそのダイジェストに署名し、エージェントの行動レシートは同じ行動ダイジェストと parent_approval_ref(承認の receipt_hash)を持ちます。これは上で作った previous_receipt_hash と同じ慣習です。一つの verify_chain は異なる固定鍵レジストリ(承認者鍵とエージェント鍵)で両アーティファクトを検証し、コードパスは共通ですが権限は別です。
ここで得られる性質は慎重に述べると:人間はこの正確な行動を承認し、エージェントはまさにその承認された行動を実行したということです。ノートブックの拒否フィクスチャはこの性質を主張ではなく実態にしています:
各失敗は明確な理由で拒否されるため、監査人は権限が陳腐化したのか実行行動が変わったのか判断できます。ノートブックが教えるルール:「署名された承認は単独で権限ではない。両方のレシートが実行時に同じ正準行動に結びつく場合にのみ権限は存在する」。このレッスンが従う同じInternet-Draft (draft-farley-acta-signed-receipts) にある共同署名パスがこのパターンの標準トラック形状です。
このレッスンのPythonコードはあえて最小限にしているので全行読んで正確に何が起きているか理解できます。本番環境では2つの選択肢があります:
暗号プリミティブの直接利用。上記の50行は多くのユースケースに十分です。PyNaCl (Ed25519) と jcs パッケージ(正準JSON)は信頼性が高く監査済みのライブラリです。
本番用レシートライブラリの利用。いくつかのオープンソースプロジェクトが同じパターンを追加機能付きで実装しています(鍵ローテーション、一括検証、JWK Set配布、ポリシーエンジンと連携など):
draft-farley-acta-signed-receipts、リビジョン02)に従っており、共同適合性スイート(agent-governance-testvectors)を使って独立実装がバイト単位同一の正準出力を検証しています。protect-mcp (npm) と @veritasacta/verify (npm) パッケージはNodeベースのレシート署名・オフライン検証を提供し、署名保留フロー含む改ざん検出監査証跡をあらゆるMCPサーバにラップすることを想定(デスクトップフローのWebAuthn対応承認レシートパターンと同じヒューマン承認パターン)。pip install nobulex) は同じEd25519 + JCS署名パターンを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 を開き、以下の4つのセクションを完了してください:
ストレッチチャレンジ1: 任意のフィールド(例:トレース用のリクエストID)を追加してレシートスキーマを拡張し、正規署名ロジックを更新し、レシートが検証に問題なく通ることを確認してください。その後、署名後にフィールドを変更して、検証が失敗することを確認してください。これにより、正規のエンコードの各バイトが署名にどのように寄与するか理解が深まります。
ストレッチチャレンジ2: 2つのレシートの正規化バイトを決定論的な順序で連結してSHA-256ハッシュを取り、そのダイジェストを3つ目のレシートの新しいフィールドとして埋め込み、署名してください。3つのレシートすべてが検証に問題なく通ることを確認します。これにより、一歩入り込んだ包含証明が構築されます:3つ目のレシートを持つ者は、最初の2つがその署名時点で存在していたことを内容を開示せずに証明できます。これはセレクティブ・ディスクロージャレシートが大規模に使用するパターンです(Merkleコミットメント、RFC 6962)。
暗号化レシートはAIエージェントに以下の監査証跡を提供します:
それらは入力検証やポリシー適用、身元基盤の代替ではありません。これらの層の基盤となるものです。規制されたワークロード、多機関のワークフロー、将来の監査人があなたを信用しない可能性がある環境にエージェントを展開する場合、監査証跡を誠実に保つ手段としてレシートが必要です。
最も重要な要点:レシートは「誰が何を言ったか」を証明しますが、「言われたことが真実または正しいこと」を証明しません。この区別を強く意識してください。これは誠実な出自システムと誤解を招くものの違いです。
このレッスンから卒業し、レシート署名済みエージェントを実運用に展開する準備ができたら:
https://your-org.example.com/.well-known/agent-keys.json。Microsoft Foundry Discord に参加して、他の学習者と会い、オフィスアワーに参加し、AIエージェントに関する質問に答えを得てください。
このレッスンは単一レシート署名とハッシュチェーンのシーケンスをカバーしています。同じプリミティブを組み合わせて、ガバナンス体制が成熟すると遭遇するかもしれないいくつかの高度なパターンが作られます:
authorization_* と実行後の result_* の2つに分けて独立した署名とし、認証決定と実際の結果が別の実行者によってまたは別の時間に生成される場合に有用です。このレッスンのレシート形式の上に加算可能に合成されます。result_hash に入れたバイトを封印します。実際のペイロードは単一のツール呼び出し結果より豊富なことが多く、事前の意思決定根拠(予測、検討した選択肢、証拠およびその完全性、リスク姿勢、アカウンタビリティチェーン、ゲート結果)を含めて単一のレシートで封印可能です。レシート形式は最小限に保ちつつペイロードスキーマをドメインごとに進化させることができます。signature.alg フィールドに移行時に必要な場合は NISTのポスト量子署名標準 ML-DSA-65 を指定できます。移行期間はレシートを二重署名する計画が必要です。免責事項: 本書類は AI 翻訳サービス Co-op Translator を使用して翻訳されています。正確性を期していますが、自動翻訳には誤りや不正確な部分が含まれる可能性があることをご承知おきください。原文の原語版が正式な情報源とみなされるべきです。重要な情報については、専門の人間による翻訳を推奨します。本翻訳の利用により生じたいかなる誤解や解釈違いについても、当方は責任を負いかねます。