レッスンビデオを見る: 暗号化レシートを使ったAIエージェントのセキュリティ
(レッスン14 / 15のパターンに合わせて、Microsoftコンテンツチームによってマージ後にレッスンビデオとサムネイルが追加されます。)
このレッスンでは以下を扱います:
このレッスンを修了すると、以下ができるようになります:
Contoso Travel用にAIエージェントを展開したと想像してください。エージェントは顧客のリクエストを読み取り、フライトAPIを呼び出してオプションを調べ、顧客に代わって座席を予約します。前四半期には、エージェントは5万件の予約を処理しました。
今日、監査人がやってきました。単純な質問をします。「あなたのエージェントが何をしたか見せてください。」
あなたはログファイルを渡します。監査人はそれを見て、より難しい質問をします。「これらのログが編集されていないとどうやってわかるのですか?」
これが監査証跡問題です。現在ほとんどのエージェント展開は以下に依存しています:
いずれも、監査人が誰か(あなた、クラウドプロバイダー、データベースベンダー)を信頼しなければ、監査人の質問に答えることができません。内部利用ではその信頼は通常受け入れられますが、規制対象の作業(金融、医療、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正規化スキーム(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つの行動を保護します。レシートの連鎖は一連の行動を守ります。
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を改変すれば署名が破綻する、または秘密鍵がハードウェアキーボルトにあり、公開鍵を各レシートと共に公開していれば、検出なしにどちらも不可能です。
ノートブックでは、
previous_receipt_hashが直前レシートの実際のハッシュと一致することを検証する。これが外部監査人があなたを信頼せずに検証できる監査証跡の生成方法です。
このセクションはこのレッスンで最も重要です。レシートは強力ですが、その力には限界があります。
レシートが証明する3つのこと:
レシートが証明しないこと:
policy_idで参照されたポリシーが実際に評価されたか、評価されていれば行動が許されるか。レシートは主張を記録するが実施結果を示さない。この境界は2つの理由で重要です:
よくある誤解は、「レシートがあれば統治されている」と思うことですが実際は違います。レシートは基盤であり、統治はその上に構築されるシステムです。
このレッスンのPythonコードは、全ての行を読んで何が起きているか正確に理解できるよう意図的に最小限にしています。本番運用では2つの選択肢があります:
暗号プリミティブの上に直接構築する。 上記の50行ほどで多くのユースケースは十分。PyNaCl(Ed25519)とjcsパッケージ(正規化JSON)は維持管理され監査済み。
本番用レシートライブラリを使う。 複数のオープンソースで同様のパターンを追加機能(鍵ローテーション、バッチ検証、JWKセット配布、ポリシーエンジン連携)つきで実装:
draft-farley-acta-signed-receipts)に準拠。protect-mcp(npm)や@veritasacta/verify(npm)は、Tamper-evidentな監査証跡でMCPサーバをラップするためのNode実装。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を開き、4つのセクションをすべて完了してください:
Stretch challenge 2: 2つのレシートをSHA-256でハッシュします(正準バイト列を決定的な順序で連結)。生成したダイジェストを3番目のレシートの新しいフィールドとして埋め込み、署名前に追加します。3つのレシート全てが正常にラウンドトリップすることを検証してください。これにより、ワンステップの包含証明を構築しました。3番目のレシートを持つ誰もが、1番目と2番目のレシートが署名時に存在したことを、その中身を明らかにすることなく証明できます。これは選択的開示レシートがスケールで使用するパターンです(Merkleコミットメント、RFC 6962)。
暗号署名レシートはAIエージェントに以下のような監査証跡を提供します:
これらは入力検証、ポリシー施行、またはアイデンティティ基盤の代わりではありません。これらの層の基盤となるものです。規制されたワークロードや複数組織間のワークフロー、将来の監査人が信用できない環境にエージェントを展開する際、レシートは監査証跡を正直なものにします。
最も重要なポイントは:レシートは「誰が、何を、いつ言ったか」を証明します。「言われたことが真実か正しいか」は証明しません。この区別をしっかり持つこと。これは正直な由来システムと誤解を招くものとの違いです。
このレッスンから本番環境でレシート署名エージェントを展開する段階に進むとき:
https://your-org.example.com/.well-known/agent-keys.json。Microsoft Foundry Discordに参加して、他の学習者と交流し、オフィスアワーに出席して、AIエージェントの質問を解決しましょう。
このレッスンは単一レシート署名とハッシュチェインのシーケンスを扱います。ガバナンス姿勢が成熟するにつれて遭遇するいくつかの高度なパターンは同じプリミティブで構成されています:
authorization_*)と実行後(result_*)に分け、独立署名する実装もある。認可決定と観測結果が別の主体または別時点で生成される場合に有用。今回のレッスンで扱うレシート形式の上に加算的に構成可能。result_hashに入れるバイト列を封印します。現実のペイロードは単一のツール呼び出し結果より豊かで、事前の意志決定推論(モデル予測、検討された選択肢、証拠とその完全性、リスク姿勢、説明責任の連鎖、ゲートの結果など)がすべてペイロードに入り、単一レシートで封印されます。これによりレシート形式は最小限でありながら、ドメインごとにペイロードスキーマを進化させられます。signature.algフィールドに必要に応じてNISTのポスト量子署名標準ML-DSA-65を設定可能。レシートが二重署名される移行期間を計画してください。(カリキュラム管理者によって決定予定)
免責事項: 本書類は AI 翻訳サービス Co-op Translator を使用して翻訳されています。正確性を期していますが、自動翻訳には誤りや不正確な部分が含まれる可能性があることをご承知おきください。原文の原語版が正式な情報源とみなされるべきです。重要な情報については、専門の人間による翻訳を推奨します。本翻訳の利用により生じたいかなる誤解や解釈違いについても、当方は責任を負いかねます。