レッスン動画を視聴する:暗号証明書でAIエージェントを保護する
(レッスン動画とサムネイルは統合後にMicrosoftコンテンツチームが追加します。レッスン14/15のパターンに合わせます。)
このレッスンでは以下を扱います:
このレッスンを終えると、以下が理解できます:
Contoso TravelのAIエージェントを配備したと想像してください。このエージェントは顧客の要求を読み、フライトAPIを呼び出しながら座席を予約します。前四半期には50,000件の予約を処理しました。
今日、監査人が来て簡単な質問をします:「あなたのエージェントが何をしたか見せてください。」
あなたはログファイルを渡します。監査人はそれを見てより難しい問いをします:「どうやってこれらのログが編集されていないとわかるのですか?」
これが監査トレイル問題です。今日の多くのエージェント配備は以下に依存しています:
これらはいずれも監査人の質問に答えるのに、監査人が誰か(あなた、クラウドプロバイダー、DBベンダー)を信用しなければならない。内部利用なら許容されがちですが、規制対象のワークロード(金融、ヘルスケア、EU AI法の対象など)では許されません。
暗号学的レシートは、署名された声明を独立して 検証可能にすることでこの問題を絞り込みます。監査人は依然として署名鍵と 声明を作成したコンポーネントを信頼するための帯外の理由が必要です。組み込み鍵は自己整合性を証明し、 発行者の身元や適用箇所を証明するものではありません。
証明書は、エージェントがしたことを記録するJSONオブジェクトで、デジタル署名されています。
flowchart LR
A[Agent invokes a tool] --> B[Build receipt payload]
B --> C[Canonicalize JSON RFC 8785]
C --> E[Ed25519 sign canonical bytes]
E --> F[Receipt with signature]
F --> G[Auditor verifies offline]
G --> H{Signature valid?}
H -- yes --> I[Signed statement is intact]
H -- no --> J[Receipt rejected]
最小限の証明書はこうなります:
{
"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 のハンズオン演習は全体のフローを通して説明します。ソース管理された code_samples/18-signed-receipts.execution-receipt.json は、ノートブックの正確なハッシュ、Python 環境、セル数、および最新のクリーン実行からのチェックを、レッスンの保存された出力を変更することなく記録します。要約版:
import json
import hashlib
import base64
from nacl import signing
from jcs import canonicalize # RFC 8785 canonical 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()}"
# Generate or load a signing key (in production, store in a key vault)
signing_key = signing.SigningKey.generate()
verify_key = signing_key.verify_key
TRUSTED_PUBLIC_KEYS = {b64url_nopad(bytes(verify_key))} # pinned out of band by the verifier
# Build the receipt payload (no signature yet)
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,
}
# Canonicalize and sign the JCS bytes directly. PureEdDSA hashes internally.
canonical_bytes = canonicalize(payload)
signature_bytes = signing_key.sign(canonical_bytes).signature
# Attach a structured signature object.
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 CryptoError
from jcs import canonicalize
def b64url_decode(s: str) -> bytes:
padding = "=" * ((4 - len(s) % 4) % 4)
return base64.b64decode(s + padding, altchars=b"-_", validate=True)
def verify_receipt(receipt: dict, trusted_public_keys: set[str]) -> bool:
# The signature is a structured object: {"alg", "sig", "public_key"}.
if not isinstance(receipt, dict):
return False
sig_obj = receipt.get("signature")
if not isinstance(sig_obj, dict) or sig_obj.get("alg") != "EdDSA":
return False
try:
# An inline key proves only self-consistency. Trust is configured out of band.
public_key = sig_obj.get("public_key")
signature = sig_obj.get("sig")
if not isinstance(public_key, str) or not isinstance(signature, str):
return False
if public_key not in trusted_public_keys:
return False
payload = {k: v for k, v in receipt.items() if k != "signature"}
canonical_bytes = canonicalize(payload)
verify_key = signing.VerifyKey(b64url_decode(public_key))
verify_key.verify(canonical_bytes, b64url_decode(signature))
return True
except (CryptoError, TypeError, ValueError, KeyError, OverflowError):
return False
この関数はレシートと依頼先のピン留めされた鍵セットを受け取り、返します
True は、署名が受け入れられた鍵で有効な場合にのみ返されます。
検証にはネットワーク呼び出しは不要ですが、鍵の信頼は依然として外部の設定による決定事項です。
改竄検出のデモのため、ノートブックは以下を解説します:
tool_args_hash フィールドの1バイトを変更。これが証明書が改竄検知可能である実質的な証明です:どんな小さな変更でも署名が破綻します。
単一署名証明書は一つの行動を保護します。証明書のチェーンは複数行動の連続を保護します。
flowchart LR
R0[Receipt 0<br/>genesis] --> R1[Receipt 1]
R1 --> R2[Receipt 2]
R2 --> R3[Receipt 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で参照されたポリシーが実際に評価されたか、チェックしたらこの行動を許可していたとは限らない。証明書は主張を記録するものであり、実行を記録するものではない。この境界は二つの理由で重要です:
よくある誤解は「証明書がある=ガバナンスがある」と考えることですがそうではありません。証明書は基盤です。ガバナンスはその上に作るシステムです。
上記の項目3は独立したセクションに値します: アクションの受領書は “この鍵がこのコンテンツに署名した,” と記載し、決して “人間がこれを承認した.” とは言いません。高リスクのアクション(返金、削除、電信送金)については、別の承認者の署名で正確な行為を拘束することができます。鍵を人間に結びつけることと、人間の存在を確認することは別個の責任として残ります。
続きのノートブック code_samples/human-authorization-receipts.ipynb は、レッスンのレシートと同じエンベロープ形状で、2番目のレシートの種類, human.approval.v1, を追加します (型付きペイロードがその正規化された JCS バイトに対して Ed25519 で署名され、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) のJCSおよび署名スコープ規約を利用します。本レッスンの教育的平坦証明書はドラフトの{payload, signature}エンベロープと異なり、準拠実装として提示していません。ドラフトは対応する共通テストスイート (agent-governance-testvectors) を公開し、対応実装ではワイヤフォーマットを標的とします。protect-mcp(npm) と @veritasacta/verify(npm) パッケージはNodeベースの署名とオフライン検証を提供し、不正改ざん監査可能なMCPサーバのラッピングが可能です。停止フローでアクション承認証明を発行し、デスクトップフローでWebAuthn支援を含み、上述の人間承認ノートブックのパターンと同様です。pip install nobulex) はPythonで同じEd25519 + JCS署名パターンをLangChainとCrewAIとの統合つきで提供し、公開された相互検証テストベクターとOWASP PR #2210を通じたコンプライアンスマッピングが含まれます。
- EMILIA プロトコル は補完的な厳密実行強制レイヤーを提供します: オフラインで検証可能な承認レシート、固定された WebAuthn または定足数の権限、実行境界での一度きりの消費、および明示的な不確定な結果の経路。フレームワークアダプタは、レシートを外部効果が発生したことの証明ではなく、必要な証拠として扱います。自作とライブラリ利用の選択は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エージェントに、以下のような監査履歴を提供します:
それらは入力検証、ポリシーの施行、実行者の承認、 結果の証拠、完全性管理、またはアイデンティティ基盤の代わりにはなりません。これらは基盤 それらの層のための基盤です。規制された、または複数組織にまたがるワークフローでは、レシートは特定の 発行者のステートメントを監査人自身の信頼設定の下で独立して検証可能にします。
最も重要な結論: レシートは特定の鍵が特定の コンテンツです。添付されたタイムスタンプは、別途時間が証明されない限り、署名者の主張です。 レシートは、その内容が真実であること、ある行為が実行されたこと、または 記録セットが完全であることを証明しません。これらの区別を厳密に保ってください。
このレッスンを終えてレシート署名エージェントを実環境に展開する準備ができたら:
Microsoft Foundry Discord に参加して、他の学習者と交流し、オフィスアワーに参加してAIエージェントの質問を解決しましょう。
このレッスンでは単一レシート署名とハッシュチェーンを扱いました。ガバナンス姿勢が成熟するにつれて遭遇するかもしれない、いくつかのより高度なパターンは以下のようにこれらのプリミティブの組み合わせによって構成されます:
authorization_*)と実行後(result_*)の二つに分割し、それぞれ独立して署名します。承認判断と結果観察が別の主体や別の時点で行われる場合に有用です。このパターンは本レッスンで教えたレシート形式に加法的に合成されます。result_hashに入れたバイト列を封印します。実際のペイロードは単一のツール呼び出し結果よりも豊かであることが多いです:事前判断プロセス(モデル予測、検討中の選択肢、証拠とその完全性、リスク姿勢、説明責任の連鎖、ゲート判定結果)をペイロード内に含めて一つのレシートで封印できます。これによりレシート形式は最小限に保ちつつ、ドメインごとにペイロードスキーマを進化させられます。alg ラベルだけでは移行とは言えません: プロファイルは ML-DSA 鍵エンコーディング、正規化された署名バイト列、検証、リライイングパーティのポリシー、ダウングレード耐性、および移行のセマンティクスを定義する必要があります。互換性を主張する前に、共有テストベクターに対して二重署名による移行を計画し、テストしてください。免責事項: 本書類は AI 翻訳サービス Co-op Translator を使用して翻訳されています。正確性を期していますが、自動翻訳には誤りや不正確な部分が含まれる可能性があることをご承知おきください。原文の原語版が正式な情報源とみなされるべきです。重要な情報については、専門の人間による翻訳を推奨します。本翻訳の利用により生じたいかなる誤解や解釈違いについても、当方は責任を負いかねます。