ai-agents-for-beginners

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.)

Securing AI Agents with Cryptographic Receipts

Introduction

This lesson will cover:

Learning Goals

After completing this lesson, you will know how to:

The Problem: Your Agent’s Audit Trail

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.

What is a Cryptographic Receipt?

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:

  1. 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.

  2. 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.

  3. 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:

Producing a Receipt in Python

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.

Verifying a Receipt and Detecting Tampering

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:

  1. Producing a valid receipt and confirming it verifies.
  2. Modifying one byte of the tool_args_hash field.
  3. Re-running verification and seeing it fail.

This is the practical demonstration that receipts are tamper-evident: any modification, however small, breaks the signature.

Chaining Receipts for Multi-Step Agents

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:

If 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:

  1. Building a chain of three receipts.
  2. Verifying that each receipt’s previous_receipt_hash matches the actual hash of the prior receipt.
  3. Tampering with one receipt in the middle and seeing the chain break at exactly that point.

This is how you produce an audit trail an external auditor can verify without trusting you.

What Receipts Prove (and What They Do Not)

This is the most important section of this lesson. Receipts are powerful but their power is bounded.

Receipts prove three things:

  1. Attribution: a specific key signed a specific payload.
  2. Integrity: the payload has not changed since signing.
  3. Ordering: this receipt came after that receipt in the hash chain.

Receipts do NOT prove:

  1. Correctness: that the agent’s action was the right action. A receipt can be signed for a wrong answer just as cleanly as for a right answer.
  2. Policy compliance: that the policy referenced in 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.
  3. Identity beyond the key: the receipt says “this key signed this content.” It does not say “this human authorized this.” Connecting a key to a person or organization requires separate identity infrastructure (a directory, a public key registry, etc.).
  4. Truthfulness of inputs: if the agent receives a manipulated prompt and acts on it, the receipt records the action faithfully. Receipts are downstream of input validation, not a substitute for it.

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.

Production References

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:

  1. 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.

  2. 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):

    • The receipt format used in this lesson follows an IETF Internet-Draft (draft-farley-acta-signed-receipts) currently in the standards process.
    • The Microsoft Agent Governance Toolkit composes receipts with Cedar-based policy decisions; see Tutorial 33 in that repository for an end-to-end example.
    • The 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.

Knowledge Check

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?

Answer Yes. Ed25519 verification requires only the public key and the signed bytes. No network call, no service dependency. This is the property that makes receipts useful in air-gapped, multi-organization, or low-trust audit settings.

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?

Answer Verification fails. The signature was computed over the canonical bytes of the original payload; modifying any field changes the canonical bytes, which changes the SHA-256 hash, which makes the signature invalid. The attacker would need the private key to produce a fresh valid signature, which they do not have.

3. Why does the receipt include a tool_args_hash and result_hash rather than the raw arguments and result?

Answer Two reasons. First, the receipt may need to be archived or transmitted in environments where leaking the raw content (PII, business data) is a problem. Hashing keeps the receipt small and the content private; the auditor verifies that the hash matches a separately-stored copy of the actual content. Second, hashes have a fixed size; a receipt with hashes is bounded in size regardless of how large the inputs and outputs were.

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?

Answer Every receipt that came after the deleted one. Their `previous_receipt_hash` fields no longer match the actual chain (because the receipt they referenced no longer exists, or the chain now points to a different predecessor). To hide the deletion, the attacker would have to re-sign every later receipt, which requires the private key.

5. A receipt verifies cleanly. Does that prove the agent’s action was correct, sound, or compliant with policy?

Answer No. A valid receipt proves three things: attribution (this key signed this content), integrity (the content has not changed), and ordering (this receipt came after that receipt). It does NOT prove that the action was correct, that the policy named in `policy_id` was actually evaluated, or that the agent followed every rule. Receipts make agent behavior auditable, not necessarily correct. This is the most important boundary in the lesson.

Practice Exercise

Open code_samples/18-signed-receipts.ipynb and complete all four sections:

  1. Section 1: Sign your first receipt and verify it.
  2. Section 2: Tamper with the receipt and observe verification fail.
  3. Section 3: Build a three-receipt chain and verify the chain integrity.
  4. Section 4: Apply the pattern to an agent built with the Microsoft Agent Framework: wrap a tool call in receipt-signing, then verify the receipt independently.

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).

Conclusion

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.

Production Checklist

When you are ready to graduate from this lesson to deploying receipt-signed agents in a real environment:

Got More Questions about Securing AI Agents?

Join the Microsoft Foundry Discord to meet with other learners, attend office hours, and get your AI Agents questions answered.

Beyond This Lesson

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:

Additional Resources

Previous Lesson

Building Computer Use Agents (CUA)

Next Lesson

(To be determined by curriculum maintainers)