ai-agents-for-beginners 如何用 Ed25519 签名生成 Agent 工具调用收据并离线检测篡改

ai-agents-for-beginners 如何用 Ed25519 签名生成 Agent 工具调用收据并离线检测篡改 ai-agents-for-beginners 如何用 Ed25519 签名生成 Agent 工具调用收据并离线检测篡改【免费下载链接】ai-agents-for-beginners18 Lessons to Get Started Building AI Agents项目地址: https://gitcode.com/GitHub_Trending/ai/ai-agents-for-beginnersAgent 审计时最难回答的问题不是agent 做了什么而是怎么证明这些日志没被改过。ai-agents-for-beginners 项目的第 18 课 18-securing-ai-agents/README.md 给出的方案是为每次 agent 工具调用生成一份 JSON 收据用 Ed25519 私钥对其 JCS 规范字节签名验证方只凭收据里自带的公钥就能离线验签——任何字段被改过验证都会失败。本文按 18-securing-ai-agents/code_samples/18-signed-receipts.ipynb 的顺序走通完整流程生成密钥、构造 payload、签名、离线验证、篡改检测再到把多张收据串成一条哈希链。只需要 Python 环境和两个库不需要 Azure 账号或任何模型服务。收据结构与三个关键机制一张最小收据长这样来自课程 README其中 hash 值在文档中就是缩写形式{ 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 签名收据由 agent 网关用私钥签名持有公钥的人可以离线验证篡改任何字段都会使签名失效。JCS 规范编码RFC 8785签名前先把 payload 序列化成规范 JSON保证两个实现产生同一逻辑收据时得到逐字节相同的输出。不做规范化的话不同 JSON 序列化器会对同一内容产生不同签名。哈希链previous_receipt_hash把每张收据指向前一张。删除或重排任何一张其后所有收据的链校验都会断掉。三者共同提供三项保证attribution这把钥匙签了这份内容、integrity内容自签名后未变、ordering这张收据在那张之后。注意tool_args_hash和result_hash存的是摘要而不是原始内容一方面收据可能在不能泄露 PII 或业务数据的环境里归档传输另一方面收据体积有界。准备环境安装依赖依赖只有两个加密库加一个标准库版本要求见 code_samples/requirements.txtpip install pynacl1.5.0 jcs0.2.1pynaclEd25519 签名与验证libsodium 绑定jcsRFC 8785 JSON Canonicalization SchemeSHA-256 来自 Python 标准库hashlib如果直接在 Jupyter 里跑 notebook还需要ipykernel6.0.0notebook 的 Setup 单元格使用的安装命令是%pip install -q pynacl jcs。下面的代码与 notebook 中 Setup、Helper Utilities 到 Section 3 的单元格一致可以按顺序粘进一个脚本或 notebook 执行。生成 Ed25519 签名密钥import json import hashlib import base64 import copy from datetime import datetime, timezone from nacl import signing from nacl.exceptions import BadSignatureError from jcs import canonicalize def b64url_nopad(data: bytes) - str: Base64url-encode bytes without padding (RFC 4648 Section 5). return base64.urlsafe_b64encode(data).decode(ascii).rstrip() def b64url_decode(s: str) - bytes: Decode a base64url string that may be missing padding. padding * ((4 - len(s) % 4) % 4) return base64.urlsafe_b64decode(s padding) def sha256_canonical(obj) - str: SHA-256 of a Python objects JCS-canonical JSON form. canonical canonicalize(obj) return fsha256:{hashlib.sha256(canonical).hexdigest()} signing_key signing.SigningKey.generate() verify_key signing_key.verify_key public_key_b64 b64url_nopad(bytes(verify_key)) print(fPublic key (Ed25519, 32 bytes): {public_key_b64})notebook 里打印出的公钥如g3SyD_ecOKa1L8RQ79-pDy9em81H-O_jzp9VG4a3EP0是某次运行的示例值每次重新生成密钥都会不同。课程明确说明生产环境的签名密钥应放在 HSM、Azure Key Vault 一类的受保护存储中这里在内存里生成只是教学演示。构造收据 payload以 Contoso Travel 的 agent 查询 SYD→LAX 航班为例notebook Section 1 的示例数据tool_args { origin: SYD, destination: LAX, departure_date: 2026-06-15, passengers: 2, } tool_result [ {flight: QF11, price: 1850, stops: 0}, {flight: UA864, price: 1620, stops: 1}, {flight: DL11, price: 1740, 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: datetime.now(timezone.utc).strftime(%Y-%m-%dT%H:%M:%SZ), sequence: 0, previous_receipt_hash: None, }字段含义agent_id记录谁执行tool_name/tool_args_hash/result_hash记录调用了什么工具、参数和结果的摘要policy_id记录声称适用的策略sequence和previous_receipt_hash用于链式组织。这里agent_id、policy_id、航班数据都是课程示例值换成你自己 agent 的标识即可timestamp由当前 UTC 时间生成所以每次运行不同。对 JCS 规范字节直接签名def sign_receipt(payload: dict, signing_key: signing.SigningKey, verify_key) - dict: Sign a receipt payload. Returns the receipt with attached signature and public key. The signature and public_key fields are NOT part of the canonical signed bytes. canonical canonicalize(payload) signature_bytes signing_key.sign(canonical).signature return { **payload, signature: { alg: EdDSA, sig: b64url_nopad(signature_bytes), public_key: b64url_nopad(bytes(verify_key)), }, } receipt sign_receipt(payload, signing_key, verify_key) print(json.dumps(receipt, indent2))两个容易写错的点课程文档都明确给了规则直接对 JCS 规范字节签名。PureEdDSA 内部会对消息做哈希不要再加一层SHA-256(JCS(payload))预哈希——那是对不同字节签名验签必然失败。SHA-256 在本课里只用于内容摘要tool_args_hash、result_hash和收据链链接。signature对象本身不在被签名范围内验证时需要先把它剥离出来。离线验证只需公钥验证是签名的逆操作剥离 signature 字段对剩余 payload 重新做 JCS 规范化用收据里内嵌的公钥校验def verify_receipt(receipt: dict) - bool: Verify a receipts Ed25519 signature. Returns True if valid, False otherwise. sig_obj receipt.get(signature) if not sig_obj or sig_obj.get(alg) ! EdDSA: return False # Reconstruct the payload that was actually signed (everything except signature). payload {k: v for k, v in receipt.items() if k ! signature} canonical canonicalize(payload) try: verify_key signing.VerifyKey(b64url_decode(sig_obj[public_key])) verify_key.verify(canonical, b64url_decode(sig_obj[sig])) return True except BadSignatureError: return False is_valid verify_receipt(receipt) print(fReceipt is valid: {is_valid})这就是离线的含义没有网络调用、不依赖任何服务、不需要查询任何密钥目录——验证方只需要收据本身公钥就嵌在收据里。刚生成的收据预期输出Receipt is valid: True。notebook 还带一个负向对照用来把签名范围这条规则变成可执行的检查把签名换成对SHA-256(JCS(payload))摘要的签名构造一张预哈希收据# Regression control for the signature scope in draft revision 02. A receipt # signed over SHA-256(JCS(payload)) is a signature over different bytes and # must not verify as a direct-JCS Ed25519 receipt. prehashed_signature signing_key.sign(hashlib.sha256(canonicalize(payload)).digest()).signature prehashed_receipt { **receipt, signature: {**receipt[signature], sig: b64url_nopad(prehashed_signature)}, } print(fPre-hashed receipt valid: {verify_receipt(prehashed_receipt)})预期输出Pre-hashed receipt valid: False。正向用例证明直接签 JCS 字节的路径可用负向对照则证明签名范围规则被真正执行了。篡改检测改一个字段验证即失败tampered copy.deepcopy(receipt) # Modify the policy_id field (this is what an attacker might do to claim # the action was governed by a more permissive policy than was actually used). original_policy tampered[policy_id] tampered[policy_id] contoso-travel-policy-PERMISSIVE print(fOriginal policy_id: {original_policy}) print(fTampered policy_id: {tampered[policy_id]}) print() print(fTampered receipt valid? {verify_receipt(tampered)})预期输出Tampered receipt valid? False。原理改了policy_id之后 JCS 规范字节变了而签名是对原始字节计算的二者不再匹配。notebook 提示还可以改tool_name、agent_id或timestamp重跑任何改动都会得到无效收据——攻击者没有私钥就无法重签。收据链让删除和重排也能被检测单张收据保护单次动作多步 agent 需要链。每张收据把前一张的哈希含签名的完整收据做 JCS 后取 SHA-256写进自己的previous_receipt_hashdef receipt_hash(receipt: dict) - str: Compute the chain hash of a complete receipt (including signature). This becomes the previous_receipt_hash of the next receipt in the chain. canonical canonicalize(receipt) digest hashlib.sha256(canonical).hexdigest() return fsha256:{digest} def make_receipt( tool_name: str, tool_args: dict, tool_result, sequence: int, previous_receipt_hash, signing_key, verify_key, agent_id: str contoso-travel-bot, policy_id: str contoso-travel-policy-v3, ) - dict: Convenience: build, sign, and return a receipt for one tool call. payload { type: agent.tool_call.v1, agent_id: agent_id, tool_name: tool_name, tool_args_hash: sha256_canonical(tool_args), result_hash: sha256_canonical(tool_result), policy_id: policy_id, timestamp: datetime.now(timezone.utc).strftime(%Y-%m-%dT%H:%M:%SZ), sequence: sequence, previous_receipt_hash: previous_receipt_hash, } return sign_receipt(payload, signing_key, verify_key)按搜索 → 占座 → 确认预订构造三张收据数据取自 notebook Section 3r0 make_receipt( tool_namelookup_flights, tool_args{origin: SYD, destination: LAX, date: 2026-06-15}, tool_result[{flight: QF11, price: 1850}], sequence0, previous_receipt_hashNone, signing_keysigning_key, verify_keyverify_key, ) r1 make_receipt( tool_namehold_seat, tool_args{flight: QF11, seat: 14A, hold_minutes: 30}, tool_result{hold_id: H8472, expires_at: 2026-06-15T15:00:00Z}, sequence1, previous_receipt_hashreceipt_hash(r0), signing_keysigning_key, verify_keyverify_key, ) r2 make_receipt( tool_nameconfirm_booking, tool_args{hold_id: H8472, payment_token: tok_redacted}, tool_result{booking_ref: CT-09182, status: confirmed}, sequence2, previous_receipt_hashreceipt_hash(r1), signing_keysigning_key, verify_keyverify_key, ) chain [r0, r1, r2]链验证一次检查三件事每张收据的签名有效、除创世收据外previous_receipt_hash等于前一张实际哈希、sequence等于其零基下标def verify_chain(chain: list) - list[dict]: Verify a sequence of receipts: 1. Each receipts signature must verify. 2. Each receipt (except the genesis) must reference the previous receipts hash. 3. Sequence numbers must match each receipts zero-based position in the chain. Returns a list of per-receipt result dicts. results [] for i, receipt in enumerate(chain): sig_ok verify_receipt(receipt) if i 0: chain_ok receipt[previous_receipt_hash] is None else: expected receipt_hash(chain[i - 1]) chain_ok receipt[previous_receipt_hash] expected seq_ok receipt[sequence] i results.append({ index: i, tool: receipt[tool_name], signature_valid: sig_ok, chain_link_valid: chain_ok, sequence_valid: seq_ok, overall_valid: sig_ok and chain_ok and seq_ok, }) return results for r in verify_chain(chain): status VALID if r[overall_valid] else INVALID print(fReceipt {r[index]} ({r[tool]:18}): {status})未篡改的链预期输出三行VALIDlookup_flights / hold_seat / confirm_booking。再篡改链中间的收据——把占座时长从 30 分钟换成 9999 分钟并替换tool_args_hashtampered_chain [copy.deepcopy(r) for r in chain] tampered_chain[1][tool_args_hash] sha256_canonical( {flight: QF11, seat: 14A, hold_minutes: 9999} ) for r in verify_chain(tampered_chain): status VALID if r[overall_valid] else INVALID why if not r[overall_valid]: reasons [] if not r[signature_valid]: reasons.append(signature) if not r[chain_link_valid]: reasons.append(chain link) if not r[sequence_valid]: reasons.append(sequence) why (failed: , .join(reasons) ) print(fReceipt {r[index]} ({r[tool]:18}): {status}{why})notebook 记录的预期输出Receipt 0 ( lookup_flights): VALID Receipt 1 ( hold_seat): INVALID (failed: signature) Receipt 2 ( confirm_booking): INVALID (failed: chain link)Receipt 0 未动过且无前置依赖仍然有效Receipt 1 因tool_args_hash被改而签名失败Receipt 2 因previous_receipt_hash指向的是修改前的 Receipt 1 而链校验失败。也就是说篡改点之后的每一张收据都会暴露问题——即使攻击者能重签被改的 Receipt 1没有私钥做不到Receipt 2 的链链接失配仍会暴露篡改。可选不跑 notebook直接验证仓库里的示例收据仓库自带三份预生成的收据夹具sample_receipts 目录说明不用运行 notebook 就能核对验证逻辑文件内容文档给出的验证结果01_valid_receipt.json一次lookup_flights调用的有效签名收据验证返回True02_tampered_receipt.json同一收据签名后被改了一个字段验证返回False03_chain_three_receipts.json三张有效收据的链search, hold, book每张VALID前面步骤都跑完后用上面的verify_receipt/verify_chain直接读文件即可以下路径按仓库根目录写from pathlib import Path fixtures Path(18-securing-ai-agents/code_samples/sample_receipts) valid json.loads((fixtures / 01_valid_receipt.json).read_text()) print(fValid receipt: {verify_receipt(valid)}) # True tampered json.loads((fixtures / 02_tampered_receipt.json).read_text()) print(fTampered receipt: {verify_receipt(tampered)}) # False chain json.loads((fixtures / 03_chain_three_receipts.json).read_text()) for r in verify_chain(chain): print(f Receipt {r[index]} ({r[tool]}): {VALID if r[overall_valid] else INVALID})夹具用固定签名密钥和固定时间戳生成保证逐字节可复现generate_fixtures.py 可以重新生成它们脚本注释明确提示那个固定密钥绝不能在生产环境复用。直接读原始 JSON 也有教学价值signature是不透明的 base64url 串但其余字段都是可读 JSON——签名不加密内容只对内容作证public_key就嵌在收据里审计方不需要任何额外材料。可选用 ReceiptedTool 让工具调用自动产生收据实际部署中你不想让每个 agent 开发者都记得手动调make_receipt。notebook Section 4 给的是一个框架无关的包装器传入任意工具函数返回一个每次调用自动签名并追加收据的版本class ReceiptedTool: Wraps a tool function so every invocation produces a signed receipt. Receipts are appended to a chain held by this object. Accepts both positional and keyword arguments. The receipts tool_args field records args (as a list) and kwargs (as a dict) so the canonical hash binds to whichever the caller supplied. def __init__(self, name: str, fn, signing_key, verify_key, agent_id: str, policy_id: str): self.name name self.fn fn self.signing_key signing_key self.verify_key verify_key self.agent_id agent_id self.policy_id policy_id self.receipts: list [] def __call__(self, *args, **kwargs): result self.fn(*args, **kwargs) previous_hash receipt_hash(self.receipts[-1]) if self.receipts else None receipt make_receipt( tool_nameself.name, tool_args{args: list(args), kwargs: kwargs}, tool_resultresult, sequencelen(self.receipts), previous_receipt_hashprevious_hash, signing_keyself.signing_key, verify_keyself.verify_key, agent_idself.agent_id, policy_idself.policy_id, ) self.receipts.append(receipt) return result用 mock 工具跑三次调用def mock_lookup_flights(origin: str, destination: str, departure_date: str) - list: return [ {flight: QF11, price: 1850, stops: 0}, {flight: UA864, price: 1620, stops: 1}, ] receipted_lookup ReceiptedTool( namelookup_flights, fnmock_lookup_flights, signing_keysigning_key, verify_keyverify_key, agent_idcontoso-travel-bot, policy_idcontoso-travel-policy-v3, ) results_a receipted_lookup(originSYD, destinationLAX, departure_date2026-06-15) results_b receipted_lookup(originSYD, destinationNRT, departure_date2026-07-02) results_c receipted_lookup(originMEL, destinationSIN, departure_date2026-08-10) print(fTool was called {len(receipted_lookup.receipts)} times.) for r in verify_chain(receipted_lookup.receipts): status VALID if r[overall_valid] else INVALID print(fReceipt {r[index]} ({r[tool]}): {status})每次调用的结果自动进入一条链预期输出 3 张VALID收据。要接入 Microsoft Agent Framework 时把包装后的函数而不是原始函数注册为工具即可agent 框架本身不需要知道收据的存在。notebook 里给出了一段FoundryChatClientprovider.as_agent(tools[receipted_lookup])的集成代码它被明确标注为伪代码草图需要真实的 Foundry 项目凭据这里不展开本地 mock 版本足以演示自动收据的完整模式。收据证明什么不证明什么这是课程反复强调的边界验证收据时同样要记住收据证明的三件事Attribution特定私钥签了特定 payload。Integritypayload 自签名后未变。Ordering哈希链中这张收据在那张之后。收据不证明动作本身是否正确——错误答案和正确答案可以被同样干净地签上名。策略是否真的被执行——policy_id记录的是声称适用的策略不是强制结果。钥匙背后的人——收据只说这把钥匙签了这份内容把钥匙关联到具体的人或组织需要独立的身份基础设施。输入的真实性——如果 agent 被操纵的 prompt 驱动而行动收据仍会忠实地记录这个动作。收据在输入校验下游不能替代它。换句话说我们有收据不等于我们有治理收据让 agent 行为可审计、可防篡改正确性和策略执行是另外的层。另外按课程说明本课程的扁平教学收据格式与 IETF 草案draft-farley-acta-signed-receiptsrevision 02的{payload, signature}信封不同不是该草案的合规实现。需要人类批准了这个精确动作的能力时可以另看同目录的 human-authorization-receipts.ipynb它用同一套原语增加human.approval.v1收据类型属于本课之外的扩展场景。下一步走向生产时的检查项课程 README 的 Production Checklist 给出了从教学环境到生产的明确清单按文档原样列出把签名密钥移出开发者机器放进 Azure Key Vault、AWS KMS 或 HSM私钥不能留在源码控制或应用机器明文里。发布验证用公钥标准模式是在 well-known URL 上放 JWK SetRFC 7517例如https://your-org.example.com/.well-known/agent-keys.json域名换成你自己的组织域名。把链头外部锚定定期把最新链头哈希写入透明日志Sigstore Rekor、RFC 3161 时间戳服务或第二套内部系统。收据不可变存储追加型 blob 存储Azure Storage 不可变策略、AWS S3 Object Lock。规划保留期课程给的量级参考是每张收据约 500 字节每天 10K 次调用的 agent 一年约 1.8 GB。在运维手册里明确写清收据不覆盖的范围输入校验、策略执行、限流、身份基础设施等配套控制。验证层面的结论很简单verify_receipt返回True且verify_chain全部overall_valid时收据完整且顺序未被破坏任何False/INVALID都精确指出是签名、链链接还是序号出了问题。收据格式只依赖 Ed25519RFC 8032、JCSRFC 8785和 SHA-256验证端无需网络和服务依赖可以直接放进任何离线或跨组织的审计流程。【免费下载链接】ai-agents-for-beginners18 Lessons to Get Started Building AI Agents项目地址: https://gitcode.com/GitHub_Trending/ai/ai-agents-for-beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考