レッスン動画を視聴する:暗号証明書でAIエージェントを保護する
(レッスン動画とサムネイルは統合後にMicrosoftコンテンツチームが追加します。レッスン14/15のパターンに合わせます。)
このレッスンでは以下を扱います:
このレッスンを終えると、以下が理解できます:
Contoso TravelのAIエージェントを配備したと想像してください。このエージェントは顧客の要求を読み、フライトAPIを呼び出しながら座席を予約します。前四半期には50,000件の予約を処理しました。
今日、監査人が来て簡単な質問をします:「あなたのエージェントが何をしたか見せてください。」
あなたはログファイルを渡します。監査人はそれを見てより難しい問いをします:「どうやってこれらのログが編集されていないとわかるのですか?」
これが監査トレイル問題です。今日の多くのエージェント配備は以下に依存しています:
これらはいずれも監査人の質問に答えるのに、監査人が誰か(あなた、クラウドプロバイダー、DBベンダー)を信用しなければならない。内部利用なら許容されがちですが、規制対象のワークロード(金融、ヘルスケア、EU AI法の対象など)では許されません。
暗号証明書は、各エージェント行動を独立して検証可能にすることでこの問題を解決します。監査人はあなたを信用する必要がありません。公開鍵と証明書のみがあればよいのです。
証明書は、エージェントがしたことを記録する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 # 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,
}
# 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 フィールドの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で参照されたポリシーが実際に評価されたか、チェックしたらこの行動を許可していたとは限らない。証明書は主張を記録するものであり、実行を記録するものではない。この境界は二つの理由で重要です:
よくある誤解は「証明書がある=ガバナンスがある」と考えることですがそうではありません。証明書は基盤です。ガバナンスはその上に作るシステムです。
上記3番目は独立した節に値します:行動証明書は「この鍵がこの内容に署名した」と言い、「人間が承認した」とは決して言いません。リスクの高い行動(返金、削除、送金)には、ガバナンスフレームワークがますます正に欠けているその声明を求めています。今回のレッスンで既に構築したプリミティブで生成可能です。
続くノートブック code_samples/human-authorization-receipts.ipynb は第二の証明書種別 human.approval.v1 を追加します。今回のレッスンの証明書と同じエンベロープ形状(型付きペイロードをEd25519でJCS正規化バイト上に署名、signatureオブジェクトは署名対象バイトの外)です。実行前に名前付き承認者が全正規化行動とダイジェストに署名し、エージェントの行動証明書は同じ行動ダイジェストと承認のreceipt_hashを指すparent_approval_refを持ちます。以前のチェーンの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を通じたコンプライアンスマッピングが含まれます。自作とライブラリ利用の選択は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エージェントに、以下のような監査履歴を提供します:
入力検証、ポリシー施行、IDインフラの代わりになるものではありません。これらのレイヤーの土台となるものです。規制された業務や複数組織のワークフロー、将来の監査者に信頼されない可能性がある状況にエージェントを展開するとき、レシートは監査履歴を正直なものにします。
最も重要なポイント:レシートは誰がいつ何を言ったかを証明します。言われたことが真実または正しいことを証明するわけではありません。その区別をしっかり持ってください。それこそが正直な由来システムと誤解を招くものの違いです。
このレッスンを終えてレシート署名エージェントを実環境に展開する準備ができたら:
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 を使用して翻訳されています。正確性を期していますが、自動翻訳には誤りや不正確な部分が含まれる可能性があることをご承知おきください。原文の原語版が正式な情報源とみなされるべきです。重要な情報については、専門の人間による翻訳を推奨します。本翻訳の利用により生じたいかなる誤解や解釈違いについても、当方は責任を負いかねます。