Titta på lektionsvideon: Säkerställa AI-agenter med kryptografiska kvitton
(Lektionsvideo och miniatyr ska läggas till av Microsofts innehållsteam efter sammanslagning, med mönster enligt lektion 14 / 15.)
Denna lektion kommer att täcka:
Efter att ha avslutat 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 leta upp alternativ och bokar platser för kundens räkning. Förra kvartalet behandlade agenten 50 000 bokningar.
Idag anländer en revisor. Hen ställer en enkel fråga: “Visa mig vad din agent gjorde.”
Du överlämnar dina loggfiler. Revisorn tittar på dem och ställer den svårare frågan: “Hur vet jag att dessa loggar inte har redigerats?”
Det här är revisionsspårsproblemet. De flesta agentdistribueringar idag förlitar sig på:
Ingen av dessa kan svara på revisorns fråga utan att kräva att revisorn litar på någon (dig, din molnleverantör, din databastillverkare). För intern användning är det förtroendet ofta acceptabelt. För reglerade arbetsbelastningar (finans, vård, allt som omfattas av EU:s AI-lag) ä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 kvittot självt.
Ett kvitto är ett JSON-objekt som registrerar vad en agent gjorde, signerat med en digital signatur.
flowchart LR
A[Agenten anropar ett verktyg] --> B[Bygg kvittensdata]
B --> C[Standardisera JSON RFC 8785]
C --> E[Ed25519 signerar standardiserade bytes]
E --> F[Kvittenser med signatur]
F --> G[Revisor verifierar offline]
G --> H{Signatur giltig?}
H -- yes --> I[Manipulationssäker bevisning]
H -- no --> J[Kvittens avvisad]
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 utför arbetet:
Signaturen. Kvittot signeras 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 ger byte-identisk output. Utan kanonisering skulle olika JSON-serialiserare ge olika signaturer för samma innehåll.
Hash-kedjning. Fältet previous_receipt_hash länkar varje kvitto till det föregående. Att ta bort eller omordna ett kvitto bryter varje efterföljande kvitto. Manipulation blir synlig på kedjenivå även om enskilda signaturer kringgås.
Tillsammans ger dessa egenskaper tre garantier:
Du behöver inte något speciellt bibliotek för att producera ett kvitto. De kryptografiska primitiva finns allmänt tillgängliga och logiken är ett par dussin rader Python.
Övningarna i code_samples/18-signed-receipts.ipynb går igenom hela flödet. Sammanfattningsversionen:
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 mottagningsdata (ingen signatur ännu)
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 och signera JCS byte direkt. PureEdDSA hashar internt.
canonical_bytes = canonicalize(payload)
signature_bytes = signing_key.sign(canonical_bytes).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 signeringskedjan. Övningarna i notebooken går igenom varje steg.
Verifiering är den inversa 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 nyttolasten som faktiskt signerades (allt utom signaturen).
payload = {k: v for k, v in receipt.items() if k != "signature"}
canonical_bytes = canonicalize(payload)
try:
verify_key = signing.VerifyKey(b64url_decode(sig_obj["public_key"]))
verify_key.verify(canonical_bytes, 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 förtroende krävs för någon tredje part.
För att se detektering av manipulation i praktiken går notebooken igenom:
tool_args_hash.Detta är den praktiska demonstrationen att kvitton är manipulationssäkra: varje ändring, hur liten den än är, bryter signaturen.
Ett enskilt signerat kvitto skyddar en åtgärd. En kedja av kvitton skyddar en sekvens.
flowchart LR
R0[Kvitto 0<br/>ursprung] --> 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 hashen av föregående kvitto. För att tyst ta bort kvitto 2 måste en angripare antingen:
previous_receipt_hash (bryter kvitto 3:s signatur), ELLEROm den privata nyckeln är i ett hårdvarunyttjanderum och du publicerar den publika nyckeln med varje kvitto, är varken den ena eller den andra attacken möjlig utan upptäckt.
Notebooken går igenom:
previous_receipt_hash matchar den faktiska hashen av 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 denna lektion. 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 kontrollerades. Kvittot registrerar vad som påstods, inte vad som verkställdes.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å.
Punkt 3 ovan förtjänar en egen sektion: ett åtgärdskvitto säger “denna nyckel signade detta innehåll,” aldrig “en människa auktoriserade detta.” För högriskåtgärder (återbetalningar, raderingar, banköverföringar) kräver styrningsramverk i ökande grad just det saknade uttalandet, och det är producerbart med samma primitiva som du redan byggde in i denna lektion.
Uppföljningsnotebooken code_samples/human-authorization-receipts.ipynb lägger till en andra kvittotyp, human.approval.v1, i samma kuvertform som lektionens kvitton (en typad payload signerad med Ed25519 över dess kanoniska JCS-bytes, med signature-objektet utanför de signerade byten). En namngiven godkännare signerar hela den kanoniska åtgärden och dess digest före exekvering; agentens åtgärdskvitto bär samma åtgärdsdigest och en parent_approval_ref, receipt_hash för godkännandet, samma konvention som previous_receipt_hash i kedjan du byggde ovan. En verify_chain går över båda artefakterna under separata fastställda nyckelregister (godkännarnycklar vs agentnycklar), så kodvägen är delad men myndigheterna aldrig.
Egenskapen detta ger, uttryckt noggrant: människan godkände denna exakta åtgärd, och agenten utförde exakt den godkända åtgärden. Notebookens avvisningsscenarier är vad som gör egenskapen verklig snarare än påstådd:
Varje fel leder till avvisande med en distinkt anledning, så en revisor som läser ett avslag kan se om auktoriteten gick ut eller om den utförda åtgärden ändrades. Reglen som notebooken lär ut: ett signerat godkännande är inte auktoritet i sig. Auktoritet finns bara om båda kvittona fortfarande binder till samma kanoniska åtgärd vid exekvering. Mänskliga-godkännande-kvittot är en utbildande sammansättning definierad av denna lektion, inte en kvittotyp definierad i draft-farley-acta-signed-receipts.
Python-koden i denna lektion är avsiktligt minimal så att du kan läsa varje rad och förstå exakt vad som händer. I produktion har du två alternativ:
Bygg direkt på de kryptografiska primitiva. De 50 rader du såg ovan räcker för många användningsfall. PyNaCl (Ed25519) och paketet jcs (kanoniskt JSON) är väl underhållna och granskade bibliotek.
Använd ett produktionskvittobibliotek. Flera öppen källkod-projekt implementerar samma mönster med extra funktioner (nyckelrotation, batchverifiering, JWK Set-distribution, integration med policy-motorer):
draft-farley-acta-signed-receipts, version 02). Denna lektions platta utbildande kvitto skiljer sig från draftens {payload, signature}-kuvert och presenteras inte som en konform implementation. Draften publicerar en delad konformitetssvit (agent-governance-testvectors) för implementationer som riktar in sig på dess tråformat.protect-mcp (npm) och @veritasacta/verify (npm) tillhandahåller en Node-baserad implementation för kvittosignering och offline-verifiering, avsedd för att omsluta vilken MCP-server som helst med ett manipulationssäkert revisionsspår, inklusive ett håll-för-medundertecknande-flöde där en pausad åtgärd emitterar ett godkännandekvitto bundet till åtgärdsdigesten (WebAuthn-stött i desktop-flödet), samma godkännandekvittomönster som den mänskliga-auktorisationsnotebooken ovan.pip install nobulex) tillhandahåller samma Ed25519 + JCS-signeringsmönster i Python med LangChain- och CrewAI-integrationer, inklusive publicerade kryssvalideringstestvektorer och en efterlevnadskartläggning bidragen via OWASP PR #2210.Beslutet mellan att rulla egen och använda ett bibliotek speglar beslutet mellan att skriva sitt eget JWT-bibliotek och använda ett testat: båda är rimliga; biblioteket sparar tid och minskar granskningsyta; från-scratch-ansatsen tvingar dig att förstå varje primitiv. Denna lektion lär ut från-scratch-vägen så att du har grunden för båda valen.
Testa din förståelse innan du går vidare till praktikövningen.
1. Ett kvitto är signerat med agentens privata Ed25519-nyckel. Revisorn har endast den publika nyckeln. Kan revisorn verifiera kvittot offline?
2. En angripare modifierar 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 innehåller kvittot en tool_args_hash och result_hash istä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 raderar ett kvitto från mitten av en kedja, vad blir ogiltigt?
5. Ett kvitto verifieras utan fel. Bevisar det att agentens åtgärd var korrekt, rimlig eller policyföljande?
Öppna code_samples/18-signed-receipts.ipynb och slutför alla fyra sektioner:
Stretch-uppgift 1: utöka kvittoschemat med ett extra valfritt fält (t.ex. en förfrågnings-ID för spårning), uppdatera den kanoniska signeringslogiken att inkludera det och bekräfta att kvittot fortfarande klarar verifieringen. Ändra sedan fältet efter signering och bekräfta att verifieringen misslyckas. Detta tvingar dig att förstå hur varje byte i den kanoniska kodningen bidrar till signaturen.
Stretch-uppgift 2: Hasha två av dina kvitton med SHA-256 tillsammans (konkatenera deras kanoniska byte i en deterministisk ordning) och lägg in den resulterande digesten som ett nytt fält i ett tredje kvitto innan du signerar. Verifiera att alla tre kvitton fortfarande klarar verifieringen. Du har just byggt ett enkelstegs inkusionsbevis: vem som helst med det tredje kvittot kan bevisa att de två första existerade vid tidpunkten för signaturen, utan att behöva visa deras innehåll. Detta är mönstret som kvitton med selektiv avslöjning använder i stor skala (Merkle-åtaganden, RFC 6962).
Kryptografiska kvitton ger AI-agenter en revisionskedja som är:
De är inte en ersättning för inmatningsvalidering, policyuppföljning eller identitetsinfrastruktur. De är en grund för dessa lager. När du distribuerar agenter i reglerade arbetsflöden, flera organisationers processer eller i miljöer där en framtida revisor inte kan förutsättas lita på dig, är kvitton hur du gör revisionskedjan ärlig.
Den viktigaste lärdomen: kvitton bevisar vem som sa vad, när. De bevisar inte att det som sades var sant eller rätt. Håll den skillnaden tydligt. 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 kvittosignerade agenter i en verklig miljö:
https://your-org.example.com/.well-known/agent-keys.json.Gå med i Microsoft Foundry Discord för att träffa andra elever, delta i frågestunder och få svar på dina frågor om AI-agenter.
Denna lektion täcker enkel-kvittosignering och hash-kedjade sekvenser. Samma primitiva kan byggas ihop till flera mer avancerade mönster som du kan möta när din styrning mognar:
authorization_*) och efter-exekveringshalvor (result_*) med oberoende signaturer, användbart när behörighetsbeslut och observerat resultat produceras av olika aktörer eller vid olika tidpunkter. Detta läggs ovanpå kvittoschemat som lärs ut i denna lektion.result_hash. Riktiga nyttolaster är ofta rikare än ett enskilt verktygsanropsresultat: förbeslutsresonemang (modellers prognos, övervägda alternativ, bevis och dess fullständighet, riskbedömning, ansvarskedja, portresultat) kan allt leva i nyttolasten, förseglat av ett enda kvitto. Detta håller kvittoschemat minimalt medan nyttolast-scheman kan utvecklas per domän.signature.alg kan bära ML-DSA-65 (NIST:s post-kvant signaturstandard) när du behöver migrera. Planera för en övergångsperiod där kvitton dubbelsigneras.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.