レッスン動画を見る: 暗号化レシートによる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..."
}
}
以下の三つのプロパティが機能しています:
署名。エージェントのゲートウェイが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 # 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を変更する(レシート3の署名は無効になる)か、秘密鍵がハードウェアキーボルトに格納されており、各レシートに公開鍵が添付されている場合、どちらの攻撃も検知なしには実行不可能です。
ノートブックでは以下を扱います:
previous_receipt_hashが実際の前のレシートのハッシュと一致することを検証するこれにより外部監査人があなたを信頼せずとも検証可能な監査証跡が実現します。
このレッスンで最も重要なセクションです。レシートは強力ですが、その力には限界があります。
レシートは三つのことを証明します:
レシートは証明しません:
policy_idに記されたポリシーが本当に評価された、あるいはチェックされたなら許可するポリシーだったこと。レシートは主張を記録するだけで、実行を保証しません。この境界は重要で、二つの理由があります:
「レシートがある=ガバナンスされている」と考えるのは誤りです。レシートは基礎であり、ガバナンスはそこに作るシステムです。
このレッスンのPythonコードは故意に最小限にしてあり、すべてのコード行を理解可能にしています。実運用では二つの選択肢があります:
暗号プリミティブに直接組み上げる。 上記の約50行で多くのユースケースに十分。PyNaCl(Ed25519)とjcsパッケージ(正準JSON)はよくメンテされ監査もされているライブラリです。
本番用レシートライブラリを使う。 複数のオープンソースプロジェクトが、同じパターンに追加機能(鍵ローテーション、一括検証、JWKセット配布、ポリシーエンジン連携)を実装しています:
draft-farley-acta-signed-receipts)で、現在標準化プロセス中です。protect-mcp(npm)と@veritasacta/verify(npm)はNodeベースでレシート署名とオフライン検証を実装し、改ざん検知可能な監査証跡付きのMCPサーバラッピングに使われます。独自実装とライブラリ利用の選択は、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つが署名時に存在していたことを、それらの内容を明かすことなく証明できます。これは選択的開示レシートが大規模に使用するパターンです(マークルコミットメント、RFC 6962)。
暗号学的レシートは、AIエージェントに以下のような監査証跡を提供します:
これらは入力検証、ポリシー施行、またはIDインフラの代替ではありません。そうした層の基礎となるものです。規制対象のワークロード、多組織のワークフロー、あるいは将来の監査者があなたを信頼できない環境でエージェントを展開する際には、監査証跡の誠実性を確保するためにレシートが必要です。
最も重要なポイントは、レシートは「誰が何をいつ言ったか」を証明するものであり、「言われたことが正しいかどうか」を証明するものではないということです。この区別を厳密に持ち続けてください。これは誠実な出所システムと誤解を与えるシステムの違いです。
このレッスンを終えて、実際の環境でレシート署名済みエージェントを展開する準備ができたら:
https://your-org.example.com/.well-known/agent-keys.jsonに置くことです。Microsoft Foundry Discord に参加して、ほかの学習者と交流し、オフィスアワーに参加し、AIエージェントに関する質問を解決しましょう。
本レッスンでは単一レシート署名とハッシュチェーン化された連鎖を扱いました。ガバナンス体制が成熟するにつれて出会う可能性のあるより高度なパターンは以下の通りです:
authorization_*)と事後実行(result_*)の半分に分割し、それぞれ独立署名する実装もあります。これは承認決定と観察結果が異なるアクターや時点で生成される場合に有用です。本レッスンのレシート形式に加算的に組み合わせ可能です。result_hashに格納したバイト列を封印します。実際のペイロードは単一ツールの結果よりも豊富なことが多く、意思決定前の推論(モデル予測、考慮した選択肢、証拠やその完全性、リスク体制、説明責任チェーン、ゲート結果)も単一レシートで封印できます。レシート形式を小さくしつつ、ペイロードスキーマをドメインごとに進化させることができます。signature.alg フィールドにML-DSA-65(NISTのポスト量子署名基準)を指定して移行が可能です。移行期間中はレシートを二重署名にする計画を立てましょう。(カリキュラム管理者によって決定予定)
免責事項: 本書類は AI 翻訳サービス Co-op Translator を使用して翻訳されています。正確性を期していますが、自動翻訳には誤りや不正確な部分が含まれる可能性があることをご承知おきください。原文の原語版が正式な情報源とみなされるべきです。重要な情報については、専門の人間による翻訳を推奨します。本翻訳の利用により生じたいかなる誤解や解釈違いについても、当方は責任を負いかねます。