(課程影片和縮圖將由微軟內容團隊於合併後新增,符合第 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 Canonicalization Scheme (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 欄位的其中一個位元組。這是收據能夠篡改證明的實際示範:任何微小修改都會破壞簽名。
單一簽署收據保護一次行動。收據鏈保護一連串行動。
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,完成全部四節:
挑戰加碼 1:新增自己選擇的欄位(例如,用於追蹤的請求 ID)到收據結構,更新標準簽署邏輯將其納入,並確認收據能完整通過簽署與驗證流程。簽署完成後再修改此欄位,並驗證會失敗。此挑戰促使你理解標準編碼中每一字節如何影響簽名。 挑戰任務 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 翻譯而成。雖然我們致力於確保準確性,但請注意,機器自動翻譯可能包含錯誤或不準確之處。原始文件的母語版本應被視為權威來源。對於重要資訊,建議進行專業人工翻譯。我們不對因使用本翻譯而產生的任何誤解或誤釋承擔責任。