(課程視頻及縮圖將由 Microsoft 內容團隊在合併後添加,符合第 14 / 15 課的模式。)
本課程將涵蓋:
完成本課程後,您將會知道如何:
想像您為 Contoso Travel 部署了一個 AI 代理。該代理讀取客戶要求,呼叫航班 API 查詢選項,並代表客戶預訂座位。上個季度,該代理處理了 50,000 筆訂位。
今天來了一位稽核員。他們問一個簡單問題:「給我看您的代理做了什麼。」
您交出日誌檔案。稽核員閱讀後問更難的問題:「我怎麼知道這些日誌沒有被修改?」
這就是審計軌跡問題。當前多數代理部署依賴:
這些方法皆需稽核員信任某人(您、您的雲端供應商、您的資料庫廠商),對於內部使用通常可接受,但對監管工作負載(金融、醫療、受歐盟 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 字段的一個 byte。這是收執防篡改的實際演示:任何細微修改皆會破壞簽名。
單一簽名收執保護單一行動。收執鏈保護行動序列。
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 伺服器添加防篡改稽核軌跡。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 完成四個部分:
進階挑戰 2: 將你的兩張收據以 SHA-256 連結雜湊(以確定性順序串接它們的標準位元組)並將所得的摘要作為一個新欄位加到第三張收據上,再進行簽名。驗證這三張收據仍然能通過來回轉換。這即是你剛建造了一步包含證明:持有第三張收據的人可以證明前兩張存在於簽署當時,且無需揭露它們的內容。此模式即為可選披露收據在大規模中使用的模式(Merkle 承諾,RFC 6962)。
加密收據為 AI 代理提供一條審計軌跡,具備:
它們並非替代輸入驗證、政策強制或身份基礎設施,而是這些層的基礎。當你將代理部署於受監管的運行負載、多組織工作流,或任何未來審計者無法被假設信任你的環境中,收據即是你使審計軌跡誠實的手段。
最重要的帶走訊息:收據證明「誰在何時說了什麼」,但不證明所述內容為真實或正確。務必嚴守此區別。這是誠實來源系統和誤導性系統之間的差異。
當你準備從本課程升級到在真實環境部署簽署收據的代理時:
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 進行翻譯。雖然我們力求準確,但請注意,自動翻譯可能包含錯誤或不準確之處。原始文件的母語版本應被視為權威來源。對於重要資訊,建議尋求專業人工翻譯。我們不對因使用本翻譯而引起的任何誤解或曲解承擔責任。