Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
381 changes: 381 additions & 0 deletions 18-securing-ai-agents/README.md

Large diffs are not rendered by default.

673 changes: 673 additions & 0 deletions 18-securing-ai-agents/code_samples/18-signed-receipts.ipynb

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions 18-securing-ai-agents/code_samples/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Lesson 18 — Securing AI Agents with Cryptographic Receipts
#
# These three packages are sufficient for the entire lesson.
# All have permissive licenses (Apache-2.0 / MIT / BSD).

pynacl>=1.5.0 # Ed25519 signing and verification (libsodium binding)
jcs>=0.2.1 # RFC 8785 JSON Canonicalization Scheme

# The "ipykernel" package is required to run the Jupyter notebook
# in environments that do not have it installed by default.
ipykernel>=6.0.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"type": "agent.tool_call.v1",
"agent_id": "contoso-travel-bot",
"tool_name": "lookup_flights",
"tool_args_hash": "sha256:47578acca4df262c8f172b91493b818a26d042e7beb8e7b121e2bc3776152746",
"result_hash": "sha256:ffd8f2415a40774c57ee0fb4aa78df7bed9c02dbdee37b707cac6ff8712ba7ad",
"policy_id": "contoso-travel-policy-v3",
"timestamp": "2026-04-25T14:30:00Z",
"sequence": 0,
"previous_receipt_hash": null,
"signature": {
"alg": "EdDSA",
"sig": "ZrU64gQd-rs_jgYaHFl2PbuhY6PY8ZsapXBf7GZSxym0Egbvn9dJmFOuO_fj7T4AcV80lzxylaXpznlKUmJSDw",
"public_key": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"type": "agent.tool_call.v1",
"agent_id": "contoso-travel-bot",
"tool_name": "lookup_flights",
"tool_args_hash": "sha256:47578acca4df262c8f172b91493b818a26d042e7beb8e7b121e2bc3776152746",
"result_hash": "sha256:ffd8f2415a40774c57ee0fb4aa78df7bed9c02dbdee37b707cac6ff8712ba7ad",
"policy_id": "contoso-travel-policy-PERMISSIVE",
"timestamp": "2026-04-25T14:30:00Z",
"sequence": 0,
"previous_receipt_hash": null,
"signature": {
"alg": "EdDSA",
"sig": "ZrU64gQd-rs_jgYaHFl2PbuhY6PY8ZsapXBf7GZSxym0Egbvn9dJmFOuO_fj7T4AcV80lzxylaXpznlKUmJSDw",
"public_key": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[
{
"type": "agent.tool_call.v1",
"agent_id": "contoso-travel-bot",
"tool_name": "lookup_flights",
"tool_args_hash": "sha256:6b23306e65d36b71033768db5ddc02917ace3d93cc3b79039e2bafd11321dbc2",
"result_hash": "sha256:9c096fac92e1df080d08a5dfb75c301c289721e0d1aea15dd73ada98f2f5ae7d",
"policy_id": "contoso-travel-policy-v3",
"timestamp": "2026-04-25T14:30:00Z",
"sequence": 0,
"previous_receipt_hash": null,
"signature": {
"alg": "EdDSA",
"sig": "5rQAm4RMiAlfXmqufD9HJWQnFxRBbkpKkQmktgUGQUzZcEqsVofLsXmr7RkizTqYOCSaWjCGd-_Yy-7jloo1Cg",
"public_key": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
}
},
{
"type": "agent.tool_call.v1",
"agent_id": "contoso-travel-bot",
"tool_name": "hold_seat",
"tool_args_hash": "sha256:88661084df13196bd517c3b27567d0a4119cacc5e277cb9425003419ce217047",
"result_hash": "sha256:3e21b88f2e9056fcaab1cfd967debf51a5f059d9f06d012ab6c17c999cbc994f",
"policy_id": "contoso-travel-policy-v3",
"timestamp": "2026-04-25T14:30:00Z",
"sequence": 1,
"previous_receipt_hash": "sha256:99afc07cbdc4341c52d958dfeebea77c86cbb901c620c3ca762db0a9fc2086b5",
"signature": {
"alg": "EdDSA",
"sig": "cLcIEcm9aLdV4FelzCMNfBv-CAhqXsSOFoJHXyeWyzMT-V5L0zj8vPm28cf-zjEzNvtoMbOOeGES35wduMT-Bw",
"public_key": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
}
},
{
"type": "agent.tool_call.v1",
"agent_id": "contoso-travel-bot",
"tool_name": "confirm_booking",
"tool_args_hash": "sha256:808580d45edcb9738741896a55fa35c58f264951e38e526d7cba672fac53ae7e",
"result_hash": "sha256:4f5c18243e26b616c3f93cb000f887a3ad6f0994abaada8ad23f7e0a6cdd7328",
"policy_id": "contoso-travel-policy-v3",
"timestamp": "2026-04-25T14:30:00Z",
"sequence": 2,
"previous_receipt_hash": "sha256:79cbd0a4a623b2d9a7d7a35aafae725a35449a9e07fb4fdf6f6beb5abc8cca83",
"signature": {
"alg": "EdDSA",
"sig": "aXwWF0mOAGGUfLM_hY6Ut46Z3PWZ0o6w-yzqYToSI262YMnIX979SzeNKz48YeuPxVFKFbuB0m9A4GiS0aQzDQ",
"public_key": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
}
}
]
56 changes: 56 additions & 0 deletions 18-securing-ai-agents/code_samples/sample_receipts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Sample Receipt Fixtures

Three pre-generated receipt files for inspection without running the notebook.

| File | What it is |
|---|---|
| `01_valid_receipt.json` | A valid signed receipt for a `lookup_flights` tool call. Verification returns True. |
| `02_tampered_receipt.json` | The same receipt with one field modified after signing. Verification returns False. |
| `03_chain_three_receipts.json` | A chain of three valid receipts (search, hold, book) with `previous_receipt_hash` linking each to the prior one. |

## Verifying the samples

The notebook walks through verification in four sections. To verify these fixtures
directly without running through the notebook narrative:

```python
import json
from pathlib import Path

# Assumes you have completed the imports and helper functions
# from sections 1 and 2 of 18-signed-receipts.ipynb.

valid = json.loads(Path("01_valid_receipt.json").read_text())
print(f"Valid receipt: {verify_receipt(valid)}") # True

tampered = json.loads(Path("02_tampered_receipt.json").read_text())
print(f"Tampered receipt: {verify_receipt(tampered)}") # False

chain = json.loads(Path("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'}")
```

## How these were generated

The fixtures use the same code path as the notebook, with one fixed signing key
and fixed timestamps for byte-reproducibility. To regenerate:

```bash
python3 generate_fixtures.py
```

(Script is at `generate_fixtures.py` in this directory.)

## What students learn from inspecting raw JSON

Reading the raw receipt format builds intuition that the cells in the notebook
do not always provide. Students who skim the JSON often notice:

1. The signature is an opaque base64url string, but every other field is plain
readable JSON. The signature does not encrypt the content; it attests to it.
2. The `public_key` is embedded in the receipt. An auditor needs nothing else
to verify (subject to trusting that the key actually belongs to the claimed
issuer; see the lesson README on identity infrastructure).
3. Modifying a single character of any field, then re-comparing this file with
`02_tampered_receipt.json`, makes the byte-level mechanism concrete.
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""
Generate the three sample receipt fixtures in this directory.

Uses a fixed Ed25519 signing key and a fixed timestamp so the output is
byte-reproducible. Run this from the repo root or from within this folder.

Output:
01_valid_receipt.json
02_tampered_receipt.json
03_chain_three_receipts.json
"""
import copy
import hashlib
import json
import base64
from pathlib import Path

from nacl import signing
from jcs import canonicalize


# Fixed key for byte-reproducible fixtures. NEVER reuse in production.
FIXED_SK_HEX = "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"
FIXED_TS = "2026-04-25T14:30:00Z"


def b64url_nopad(data: bytes) -> str:
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")


def sha256_canonical(obj) -> str:
canonical = canonicalize(obj)
return f"sha256:{hashlib.sha256(canonical).hexdigest()}"


def sign_receipt(payload: dict, signing_key, verify_key) -> dict:
canonical = canonicalize(payload)
message_hash = hashlib.sha256(canonical).digest()
sig_bytes = signing_key.sign(message_hash).signature
return {
**payload,
"signature": {
"alg": "EdDSA",
"sig": b64url_nopad(sig_bytes),
"public_key": b64url_nopad(bytes(verify_key)),
},
}


def receipt_hash(receipt: dict) -> str:
canonical = canonicalize(receipt)
return f"sha256:{hashlib.sha256(canonical).hexdigest()}"


def make_receipt(tool_name, tool_args, tool_result, sequence,
previous_receipt_hash, signing_key, verify_key,
timestamp=FIXED_TS):
payload = {
"type": "agent.tool_call.v1",
"agent_id": "contoso-travel-bot",
"tool_name": tool_name,
"tool_args_hash": sha256_canonical(tool_args),
"result_hash": sha256_canonical(tool_result),
"policy_id": "contoso-travel-policy-v3",
"timestamp": timestamp,
"sequence": sequence,
"previous_receipt_hash": previous_receipt_hash,
}
return sign_receipt(payload, signing_key, verify_key)


def main():
here = Path(__file__).parent
sk = signing.SigningKey(bytes.fromhex(FIXED_SK_HEX))
vk = sk.verify_key

# Fixture 1: a valid receipt
valid = make_receipt(
tool_name="lookup_flights",
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},
],
sequence=0,
previous_receipt_hash=None,
signing_key=sk,
verify_key=vk,
)
(here / "01_valid_receipt.json").write_text(
json.dumps(valid, indent=2) + "\n"
)

# Fixture 2: tamper with one field after signing
tampered = copy.deepcopy(valid)
tampered["policy_id"] = "contoso-travel-policy-PERMISSIVE"
(here / "02_tampered_receipt.json").write_text(
json.dumps(tampered, indent=2) + "\n"
)

# Fixture 3: chain of three receipts
r0 = make_receipt(
tool_name="lookup_flights",
tool_args={"origin": "SYD", "destination": "LAX"},
tool_result=[{"flight": "QF11", "price": 1850}],
sequence=0,
previous_receipt_hash=None,
signing_key=sk,
verify_key=vk,
)
r1 = make_receipt(
tool_name="hold_seat",
tool_args={"flight": "QF11", "seat": "14A", "hold_minutes": 30},
tool_result={"hold_id": "H8472", "expires_at": "2026-06-15T15:00:00Z"},
sequence=1,
previous_receipt_hash=receipt_hash(r0),
signing_key=sk,
verify_key=vk,
)
r2 = make_receipt(
tool_name="confirm_booking",
tool_args={"hold_id": "H8472", "payment_token": "tok_redacted"},
tool_result={"booking_ref": "CT-09182", "status": "confirmed"},
sequence=2,
previous_receipt_hash=receipt_hash(r1),
signing_key=sk,
verify_key=vk,
)
(here / "03_chain_three_receipts.json").write_text(
json.dumps([r0, r1, r2], indent=2) + "\n"
)

print(f"Wrote three fixtures to {here}")


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Do you have suggestions or found spelling or code errors? [Raise an issue](https
| Building Computer Use Agents (CUA) | [Link](./15-browser-use/README.md) | | [Link](https://docs.browser-use.com/examples/templates/playwright-integration) |
| Deploying Scalable Agents | Coming Soon | | |
| Creating Local AI Agents | Coming Soon | | |
| Securing AI Agents | Coming Soon | | |
| Securing AI Agents | [Link](./18-securing-ai-agents/README.md) | | [Link](https://aka.ms/ai-agents-beginners/collection?WT.mc_id=academic-105485-koreyst) |

## 🎒 Other Courses

Expand Down