Watch the lesson video: Securing AI Agents with Cryptographic Receipts
(Lektionsvideo och miniatyrbild kommer att läggas till av Microsofts innehållsteam efter sammanslagning, i enlighet med mönstret för lektion 14 / 15.)
Denna lektion kommer att täcka:
Efter att ha genomgått denna lektion kommer du att kunna:
Föreställ dig att du har distribuerat en AI-agent för Contoso Travel. Agenten läser kundförfrågningar, anropar en flyg-API för att söka efter alternativ och bokar platser åt kunden. Förra kvartalet bearbetade agenten 50 000 bokningar.
Idag dyker en revisor upp. De ställer en enkel fråga: “Visa mig vad din agent gjorde.”
Du lämnar över loggfilerna. Revisorn tittar på dem och ställer den svårare frågan: “Hur vet jag att dessa loggar inte har redigerats?”
Detta är revisionsspårsproblemet. De flesta agentdistributioner idag förlitar sig på:
Inget av dessa kan svara revisorns fråga utan att revisorn måste lita på någon (dig, din molnleverantör, din databastillverkare). För intern användning är det ofta acceptabelt. För reglerade arbetsbelastningar (finans, sjukvård, allt under EU AI-förordningen) är det inte det.
Kryptografiska kvitton löser detta genom att göra varje agentåtgärd oberoende verifierbar. Revisorn behöver inte lita på dig. De behöver bara din offentliga nyckel och själva kvittot.
Ett kvitto är ett JSON-objekt som registrerar vad en agent gjorde, signerat med en digital signatur.
flowchart LR
A[Agent anropar ett verktyg] --> B[Bygg kvittopayload]
B --> C[Kanonisera JSON RFC 8785]
C --> D[SHA-256 hash]
D --> E[Ed25519 signera]
E --> F[Kvitto med signatur]
F --> G[Revisor verifierar offline]
G --> H{Signatur giltig?}
H -- ja --> I[Manipulationssäkert bevis]
H -- nej --> J[Kvitto avvisat]
Ett minimalt kvitto ser ut så här:
{
"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..."
}
}
Tre egenskaper gör jobbet:
Signaturen. Kvittot är signerat av agentens gateway med en Ed25519-privat nyckel. Vem som helst med motsvarande offentliga nyckel kan verifiera signaturen offline. Manipulation av något fält ogiltigförklarar signaturen.
Kanonisk kodning. Innan signering serialiseras kvittot med JSON Canonicalization Scheme (JCS, RFC 8785). Detta säkerställer att två implementationer som producerar samma logiska kvitto producerar bytes-identisk output. Utan kanonisering skulle olika JSON-serializers producera olika signaturer för samma innehåll.
Hash-kedjning. Fältet previous_receipt_hash länkar varje kvitto till föregående. Att ta bort eller omordna ett kvitto bryter varje kvitto som kom efter. Manipulation blir synlig på kedjenivå även om individuella signaturer kringgås.
Tillsammans ger dessa egenskaper tre garantier:
Du behöver inget speciellt bibliotek för att producera ett kvitto. De kryptografiska primitiva funktionerna finns allmänt tillgängliga och logiken är några tiotals rader Python.
De praktiska övningarna i code_samples/18-signed-receipts.ipynb går igenom hela flödet. Sammanfattad version:
import json
import hashlib
import base64
from nacl import signing
from jcs import canonicalize # RFC 8785 kanonisk 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()}"
# Generera eller ladda en signeringsnyckel (i produktion, lagra i ett nyckelvalv)
signing_key = signing.SigningKey.generate()
verify_key = signing_key.verify_key
# Bygg kvittots nyttolast (ingen signatur än)
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,
}
# Kanonisera, hasha, signera.
canonical_bytes = canonicalize(payload)
message_hash = hashlib.sha256(canonical_bytes).digest()
signature_bytes = signing_key.sign(message_hash).signature
# Fäst ett strukturerat signaturobjekt.
receipt = {
**payload,
"signature": {
"alg": "EdDSA",
"sig": b64url_nopad(signature_bytes),
"public_key": b64url_nopad(bytes(verify_key)),
},
}
Det är hela signeringspipeline. Övningarna i notebooken går igenom varje steg.
Verifiering är den omvända operationen:
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:
# Signaturen är ett strukturerat objekt: {"alg", "sig", "public_key"}.
sig_obj = receipt.get("signature")
if not sig_obj or sig_obj.get("alg") != "EdDSA":
return False
# Återskapa det nyttolast som faktiskt signerades (allt utom signaturen).
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
Denna funktion tar ett kvitto och returnerar True om signaturen är giltig, False annars. Ingen nätverksanrop, inget tjänsteberoende, inget krav på tillit till tredje part.
För att se manipulation upptäckt i praktiken går notebooken igenom:
tool_args_hash.Detta är den praktiska demonstrationen att kvitton är manipulationssynliga: varje ändring, hur liten den än är, bryter signaturen.
Ett enda signerat kvitto skyddar en åtgärd. En kedja av kvitton skyddar en sekvens.
flowchart LR
R0[Kvitto 0<br/>genesis] --> R1[Kvitto 1]
R1 --> R2[Kvitto 2]
R2 --> R3[Kvitto 3]
R1 -. previous_receipt_hash .-> R0
R2 -. previous_receipt_hash .-> R1
R3 -. previous_receipt_hash .-> R2
Varje kvitto registrerar hashvärdet av föregående kvitto. För att tyst ta bort kvitto 2 skulle en angripare behöva antingen:
previous_receipt_hash (bryter kvitto 3:s signatur), ELLEROm den privata nyckeln finns i en hårdvarunyckellåda och du publicerar den offentliga nyckeln med varje kvitto är ingen av dessa attacker praktiskt genomförbara utan upptäckt.
Notebooken går igenom:
previous_receipt_hash matchar den faktiska hashen för föregående kvitto.Så här producerar du ett revisionsspår som en extern revisor kan verifiera utan att behöva lita på dig.
Detta är den viktigaste delen av lektionen. Kvitton är kraftfulla men deras kraft är begränsad.
Kvitton bevisar tre saker:
Kvitton bevisar INTE:
policy_id faktiskt utvärderades eller att den skulle ha tillåtit denna åtgärd om den kontrollerats. Kvittot registrerar vad som påstås, inte vad som genomfördes.Denna gräns är viktig av två skäl:
Ett vanligt misstag är att anta att “vi har kvitton” betyder “vi är styrda.” Det gör det inte. Kvitton är en grund. Styrning är systemet du bygger ovanpå.
Pythons koden i denna lektion är avsiktligt minimal så att du kan läsa varje rad och exakt förstå vad som händer. I produktion har du två alternativ:
Bygg direkt på de kryptografiska primitiva funktionerna. De 50 raderna du såg ovan räcker för många användningsområden. PyNaCl (Ed25519) och paketet jcs (kanonisk JSON) är väl underhållna och granskade bibliotek.
Använd ett produktionsbibliotek för kvitton. Flera open source-projekt implementerar samma mönster med ytterligare funktioner (nyckelrotation, batch-verifiering, distribution av JWK-set, integration med policy-motorer):
draft-farley-acta-signed-receipts) som för närvarande är i standardiseringsprocess.protect-mcp (npm) och @veritasacta/verify (npm) tillhandahåller en Node-baserad implementation av kvittosignering och offline-verifiering, avsett för att omsluta vilken MCP-server som helst med ett manipulationssäkert revisionsspår.pip install nobulex) ger samma Ed25519 + JCS signeringsmönster i Python med integrationer för LangChain och CrewAI, inklusive publicerade tvärvaliderings testvektorer samt en efterlevnadskarta bidragen via OWASP PR #2210.Beslutet mellan att rulla egen lösning och använda ett bibliotek liknar valet mellan att skriva eget JWT-bibliotek och använda ett testat: båda är rimliga; biblioteket spar tid och minskar granskningsyta; från-grunden-metoden tvingar dig att förstå varje primitiv. Denna lektion lär från-grunden-vägen så att du har grunden för båda valen.
Testa din förståelse innan du går vidare till övningen.
1. Ett kvitto signeras med agentens privata Ed25519-nyckel. Revisorn har bara den offentliga nyckeln. Kan revisorn verifiera kvittot offline?
2. En angripare ändrar fältet policy_id i ett kvitto för att påstå att det styrdes av en mer tillåtande policy. Signaturen var över den ursprungliga payloaden. Vad händer vid verifiering?
3. Varför inkluderar kvittot tool_args_hash och result_hash i stället för råa argument och resultat?
4. Fältet previous_receipt_hash länkar varje kvitto till sin föregångare. Om en angripare tyst tar bort ett kvitto från mitten av en kedja, vad blir ogiltigt?
5. Ett kvitto verifieras korrekt. Bevisar det att agentens handling var korrekt, hållbar eller följer policy?
Öppna code_samples/18-signed-receipts.ipynb och slutför alla fyra sektionerna:
Stretchutmaning 2: hasha två av dina kvitton tillsammans med SHA-256 (sammanfoga deras kanoniska bytes i en deterministisk ordning) och bädda in den resulterande digesten som ett nytt fält på ett tredje kvitto innan du signerar det. Verifiera att alla tre kvitton fortfarande kan verifieras fram och tillbaka. Du har just byggt ett inklusionsbevis i ett steg: vem som helst som har det tredje kvittot kan bevisa att de två första fanns vid tidpunkten då det signerades, utan att behöva avslöja innehållet. Detta är mönstret som selektivt avslöjande kvitton använder i skala (merkleåtaganden, RFC 6962).
Kryptografiska kvitton ger AI-agenter en revisionskedja som är:
De är inte en ersättning för indata-validering, policygenomförande eller identitetsinfrastruktur. De är en grund för dessa lager. När du distribuerar agenter i reglerade arbetsflöden, flervårdsorganisationsarbetsflöden eller någon miljö där en framtida revisor inte kan antas lita på dig, är kvitton hur du gör revisionskedjan ärlig.
Det viktigaste att ta med sig: kvitton bevisar vem som sade vad, när. De bevisar inte att det som sades var sant eller rätt. Håll den distinktionen noga. Det är skillnaden mellan ett ärligt ursprungssystem och ett vilseledande.
När du är redo att gå vidare från denna lektion till att distribuera kvitto-signade agenter i en riktig miljö:
https://your-org.example.com/.well-known/agent-keys.json.Gå med i Microsoft Foundry Discord för att träffa andra lärande, delta i kontorstider och få svar på dina AI Agent-frågor.
Denna lektion täcker enkel kvittosignering och hash-kedjade sekvenser. Samma primitiva bygger in i flera mer avancerade mönster som du kan möta när din styrningsposition mognar:
authorization_*) och efter-exekverings- (result_*) halvor med oberoende signaturer, användbara när auktorisationsbeslutet och det observerade resultatet produceras av olika aktörer eller vid olika tidpunkter. Detta bygger additivt på kvittoformatet som lärs ut i denna lektion.result_hash. Riktiga nyttolaster är ofta rikare än ett enda verktygsanropsresultat: förbeslutsresonemang (modellförutsägelse, övervägda alternativ, bevis och dess fullständighet, riskposition, ansvarskedja, grindresultat) kan alla finnas i nyttolasten förseglade av ett enda kvitto. Detta håller kvittoformatet minimalt samtidigt som nyttolastscheman kan utvecklas domän för domän.signature.alg kan bära ML-DSA-65 (NIST:s post-kvantsignaturstandard) när du behöver migrera. Planera för en övergångsperiod där kvitton är dubbelsignade.Bygga datoranvändaragenter (CUA)
(Bestäms av kursansvariga)
Ansvarsfriskrivning: Detta dokument har översatts med hjälp av AI-översättningstjänsten Co-op Translator. Även om vi strävar efter noggrannhet, var vänlig notera att automatiska översättningar kan innehålla fel eller brister. Det ursprungliga dokumentet på dess modersmål bör betraktas som den auktoritativa källan. För kritisk information rekommenderas professionell mänsklig översättning. Vi ansvarar inte för några missförstånd eller feltolkningar som uppstår till följd av användningen av denna översättning.