Watch the lesson video: Securing AI Agents with Cryptographic Receipts
(Lesson video and thumbnail to be added by the Microsoft content team post-merge, matching the lesson 14 / 15 pattern.)
This lesson will cover:
After completing this lesson, you will know how to:
Imagine you have deployed an AI agent for Contoso Travel. The agent reads customer requests, calls a flights API to look up options, and books seats on the customer’s behalf. Last quarter, the agent processed 50,000 bookings.
Today an auditor arrives. They ask a simple question: “Show me what your agent did.”
You hand over your log files. The auditor looks at them and asks the harder question: “How do I know these logs were not edited?”
This is the audit-trail problem. Most agent deployments today rely on:
None of these can answer the auditor’s question without requiring the auditor to trust someone (you, your cloud provider, your database vendor). For internal use, that trust is often acceptable. For regulated workloads (finance, healthcare, anything subject to the EU AI Act), it is not.
Cryptographic receipts solve this by making each agent action independently verifiable. The auditor does not need to trust you. They need only your public key and the receipt itself.
A receipt is a JSON object that records what an agent did, signed with a digital signature.
flowchart LR
A[Agent invokes a tool] --> B[Build receipt payload]
B --> C[Canonicalize JSON RFC 8785]
C --> D[SHA-256 hash]
D --> E[Ed25519 sign]
E --> F[Receipt with signature]
F --> G[Auditor verifies offline]
G --> H{Signature valid?}
H -- yes --> I[Tamper-evident proof]
H -- no --> J[Receipt rejected]
A minimal receipt looks like this:
{
"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..."
}
}
Three properties are doing the work:
The signature. The receipt is signed by the agent’s gateway using an Ed25519 private key. Anyone with the corresponding public key can verify the signature offline. Tampering with any field invalidates the signature.
Canonical encoding. Before signing, the receipt is serialized using JSON Canonicalization Scheme (JCS, RFC 8785). This ensures that two implementations producing the same logical receipt produce byte-identical output. Without canonicalization, different JSON serializers would produce different signatures for the same content.
Hash chaining. The previous_receipt_hash field links each receipt to the one before it. Removing or reordering a receipt breaks every receipt that came after it. Tampering becomes visible at the chain level even if individual signatures are bypassed.
Together these properties provide three guarantees:
You do not need a special library to produce a receipt. The cryptographic primitives are widely available and the logic is a few dozen lines of Python.
The hands-on exercises in code_samples/18-signed-receipts.ipynb walk through the full flow. The summary version:
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
# 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, hash, sign.
canonical_bytes = canonicalize(payload)
message_hash = hashlib.sha256(canonical_bytes).digest()
signature_bytes = signing_key.sign(message_hash).signature
# Attach a structured signature object.
receipt = {
**payload,
"signature": {
"alg": "EdDSA",
"sig": b64url_nopad(signature_bytes),
"public_key": b64url_nopad(bytes(verify_key)),
},
}
That is the entire signing pipeline. The exercises in the notebook walk through each step.
Verification is the inverse operation:
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:
# The signature is a structured object: {"alg", "sig", "public_key"}.
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_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
This function takes a receipt and returns True if the signature is valid, False otherwise. No network call, no service dependency, no trust required in any third party.
To see tampering detection in action, the notebook walks through:
tool_args_hash field.This is the practical demonstration that receipts are tamper-evident: any modification, however small, breaks the signature.
A single signed receipt protects one action. A chain of receipts protects a sequence.
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
Each receipt records the hash of the receipt before it. To remove receipt 2 silently, an attacker would need to either:
previous_receipt_hash field (breaks receipt 3’s signature), ORIf the private key is in a hardware key vault and you publish the public key with each receipt, neither attack is feasible without detection.
The notebook walks through:
previous_receipt_hash matches the actual hash of the prior receipt.This is how you produce an audit trail an external auditor can verify without trusting you.
This is the most important section of this lesson. Receipts are powerful but their power is bounded.
Receipts prove three things:
Receipts do NOT prove:
policy_id was actually evaluated, or that it would have permitted this action if checked. The receipt records what was claimed, not what was enforced.This boundary matters for two reasons:
A common mistake is to assume that “we have receipts” means “we are governed.” It does not. Receipts are a foundation. Governance is the system you build on top.
The Python code in this lesson is intentionally minimal so you can read every line and understand exactly what is happening. In production, you have two options:
Build directly on the cryptographic primitives. The 50 lines you saw above are sufficient for many use cases. PyNaCl (Ed25519) and the jcs package (canonical JSON) are well-maintained and audited libraries.
Use a production receipt library. Several open-source projects implement the same pattern with additional features (key rotation, batch verification, JWK Set distribution, integration with policy engines):
draft-farley-acta-signed-receipts) currently in the standards process.protect-mcp (npm) and @veritasacta/verify (npm) packages provide a Node-based implementation of receipt signing and offline verification, intended for wrapping any MCP server with a tamper-evident audit trail.The decision between rolling your own and using a library mirrors the decision between writing your own JWT library and using a tested one: both are reasonable; the library saves time and reduces audit surface; the from-scratch approach forces you to understand every primitive. This lesson teaches the from-scratch path so you have the foundation for either choice.
Test your understanding before moving to the practice exercise.
1. A receipt is signed with the agent’s private Ed25519 key. The auditor has only the public key. Can the auditor verify the receipt offline?
2. An attacker modifies the policy_id field of a receipt to claim it was governed by a more permissive policy. The signature was over the original payload. What happens during verification?
3. Why does the receipt include a tool_args_hash and result_hash rather than the raw arguments and result?
4. The previous_receipt_hash field links each receipt to its predecessor. If an attacker silently deletes one receipt from the middle of a chain, what becomes invalid?
5. A receipt verifies cleanly. Does that prove the agent’s action was correct, sound, or compliant with policy?
Open code_samples/18-signed-receipts.ipynb and complete all four sections:
Stretch challenge 1: extend the receipt schema with an additional field of your own choosing (for example, a request ID for tracing), update the canonical signing logic to include it, and confirm that the receipt still round-trips through verification. Then modify the field after signing and confirm verification fails. This forces you to understand how every byte of the canonical encoding contributes to the signature.
Stretch challenge 2: SHA-256-hash two of your receipts together (concatenate their canonical bytes in a deterministic order) and embed the resulting digest as a new field on a third receipt before signing it. Verify that all three receipts still round-trip. You have just built a one-step inclusion proof: anyone holding the third receipt can prove the first two existed at the time it was signed, without needing to reveal their contents. This is the pattern that selective-disclosure receipts use at scale (Merkle commitments, RFC 6962).
Cryptographic receipts give AI agents an audit trail that is:
They are not a substitute for input validation, policy enforcement, or identity infrastructure. They are a foundation for those layers. When you are deploying agents into regulated workloads, multi-organization workflows, or any setting where a future auditor cannot be assumed to trust you, receipts are how you make the audit trail honest.
The most important takeaway: receipts prove who said what, when. They do not prove that what was said was true or right. Hold that distinction tightly. It is the difference between an honest provenance system and a misleading one.
When you are ready to graduate from this lesson to deploying receipt-signed agents in a real environment:
https://your-org.example.com/.well-known/agent-keys.json.Join the Microsoft Foundry Discord to meet with other learners, attend office hours, and get your AI Agents questions answered.
This lesson covers single-receipt signing and hash-chained sequences. The same primitives compose into several more advanced patterns you may encounter as your governance posture matures:
authorization_*) and post-execution (result_*) halves with independent signatures, useful when the authorization decision and the observed result are produced by different actors or at different times. This composes additively on top of the receipt format taught in this lesson.result_hash. Real-world payloads are often richer than a single tool call result: pre-decision reasoning (model prediction, options considered, evidence and its completeness, risk posture, accountability chain, gate outcome) can all live inside the payload, sealed by a single receipt. This keeps the receipt format minimal while letting payload schemas evolve domain-by-domain.signature.alg field can carry ML-DSA-65 (the NIST post-quantum signature standard) when you need to migrate. Plan for a transition period where receipts are dual-signed.Building Computer Use Agents (CUA)
(To be determined by curriculum maintainers)