Guarda il video della lezione: Proteggere gli agenti AI con Ricevute Criptografiche
(Video della lezione e anteprima verranno aggiunti dal team contenuti di Microsoft dopo la fusione, in linea con il modello delle lezioni 14 / 15.)
Questa lezione coprirà:
Dopo aver completato questa lezione, saprai come:
Immagina di aver distribuito un agente AI per Contoso Travel. L’agente legge le richieste dei clienti, chiama un’API di voli per cercare opzioni e prenota posti per conto del cliente. Lo scorso trimestre, l’agente ha elaborato 50.000 prenotazioni.
Oggi arriva un revisore. Fa una domanda semplice: “Mostrami cosa ha fatto il tuo agente.”
Gli consegni i file di log. Il revisore li guarda e fa una domanda più difficile: “Come faccio a sapere che questi log non sono stati modificati?”
Questo è il problema della traccia di controllo. La maggior parte delle distribuzioni di agenti oggi si basa su:
Nessuno di questi può rispondere alla domanda del revisore senza richiedere che egli si fidi di qualcuno (te, il tuo provider cloud, il tuo fornitore di database). Per uso interno, quella fiducia è spesso accettabile. Per carichi di lavoro regolamentati (finanza, sanità, qualsiasi cosa soggetta all’AI Act dell’UE), non lo è.
Le ricevute criptografiche risolvono questo rendendo ogni azione dell’agente indipendentemente verificabile. Il revisore non deve fidarsi di te. Serve solo la tua chiave pubblica e la ricevuta stessa.
Una ricevuta è un oggetto JSON che registra cosa ha fatto un agente, firmato con una firma digitale.
flowchart LR
A[L'agente invoca uno strumento] --> B[Costruisci il payload della ricevuta]
B --> C[Canonicalizza JSON RFC 8785]
C --> D[Hash SHA-256]
D --> E[Firma Ed25519]
E --> F[Ricevuta con firma]
F --> G[L'auditor verifica offline]
G --> H{Firma valida?}
H -- yes --> I[Prova a prova di manomissione]
H -- no --> J[Ricevuta respinta]
Una ricevuta minima appare così:
{
"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 proprietà sono responsabili:
La firma. La ricevuta è firmata dal gateway dell’agente usando una chiave privata Ed25519. Chiunque abbia la chiave pubblica corrispondente può verificare la firma offline. Manomettere qualsiasi campo invalida la firma.
Codifica canonica. Prima di firmare, la ricevuta viene serializzata usando lo JSON Canonicalization Scheme (JCS, RFC 8785). Ciò garantisce che due implementazioni che producono la stessa ricevuta logica producano un output byte-identico. Senza la canonicalizzazione, diversi serializzatori JSON produrrebbero firme diverse per lo stesso contenuto.
Concatenazione tramite hash. Il campo previous_receipt_hash collega ogni ricevuta a quella precedente. Rimuovere o riordinare una ricevuta rompe ogni ricevuta successiva. La manomissione diventa visibile a livello di catena anche se vengono superate le singole firme.
Insieme queste proprietà forniscono tre garanzie:
Non ti serve una libreria speciale per produrre una ricevuta. Le primitive crittografiche sono ampiamente disponibili e la logica è di poche decine di righe di Python.
Gli esercizi pratici in code_samples/18-signed-receipts.ipynb mostrano il flusso completo. La versione riassunta:
import json
import hashlib
import base64
from nacl import signing
from jcs import canonicalize # JSON canonico RFC 8785
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()}"
# Genera o carica una chiave per la firma (in produzione, memorizzala in un vault di chiavi)
signing_key = signing.SigningKey.generate()
verify_key = signing_key.verify_key
# Costruisci il payload della ricevuta (ancora senza firma)
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,
}
# Canonicalizza, hash, firma.
canonical_bytes = canonicalize(payload)
message_hash = hashlib.sha256(canonical_bytes).digest()
signature_bytes = signing_key.sign(message_hash).signature
# Allegare un oggetto firma strutturato.
receipt = {
**payload,
"signature": {
"alg": "EdDSA",
"sig": b64url_nopad(signature_bytes),
"public_key": b64url_nopad(bytes(verify_key)),
},
}
Questa è l’intera pipeline di firma. Gli esercizi nel notebook guidano ogni passaggio.
La verifica è l’operazione inversa:
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:
# La firma è un oggetto strutturato: {"alg", "sig", "public_key"}.
sig_obj = receipt.get("signature")
if not sig_obj or sig_obj.get("alg") != "EdDSA":
return False
# Ricostruisci il payload che è stato effettivamente firmato (tutto tranne la firma).
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
Questa funzione prende una ricevuta e restituisce True se la firma è valida, False altrimenti. Nessuna chiamata di rete, nessuna dipendenza da servizi, nessuna fiducia in terze parti richiesta.
Per vedere la rilevazione di manomissioni in azione, il notebook mostra:
tool_args_hash.Questa è la dimostrazione pratica che le ricevute sono tamper-evident: qualsiasi modifica, anche minima, rompe la firma.
Una singola ricevuta firmata protegge un’azione. Una catena di ricevute protegge una sequenza.
flowchart LR
R0[Ricevuta 0<br/>genesi] --> R1[Ricevuta 1]
R1 --> R2[Ricevuta 2]
R2 --> R3[Ricevuta 3]
R1 -. previous_receipt_hash .-> R0
R2 -. previous_receipt_hash .-> R1
R3 -. previous_receipt_hash .-> R2
Ogni ricevuta registra l’hash della ricevuta precedente. Per rimuovere silenziosamente la ricevuta 2, un attaccante dovrebbe:
previous_receipt_hash della ricevuta 3 (rompe la firma della ricevuta 3), OSe la chiave privata si trova in un hardware key vault e pubblichi la chiave pubblica con ogni ricevuta, nessuno dei due attacchi è fattibile senza essere rilevato.
Il notebook mostra:
previous_receipt_hash di ogni ricevuta corrisponda all’hash reale della ricevuta precedente.Così produci una traccia di controllo che un revisore esterno può verificare senza dover fidarsi di te.
Questa è la sezione più importante di questa lezione. Le ricevute sono potenti ma il loro potere è limitato.
Le ricevute dimostrano tre cose:
Le ricevute NON dimostrano:
policy_id sia stata effettivamente valutata, o che avrebbe permesso questa azione se verificata. La ricevuta registra ciò che è stato dichiarato, non ciò che è stato applicato.Questo confine è importante per due motivi:
Un errore comune è assumere che “abbiamo ricevute” significhi “siamo governati.” Non è così. Le ricevute sono una fondazione. La governance è il sistema che costruisci sopra.
Il punto 3 sopra merita una sezione propria: una ricevuta di azione dice “questa chiave ha firmato questo contenuto”, mai “un umano ha autorizzato questo.” Per azioni ad alto rischio (rimborsi, cancellazioni, trasferimenti bancari), i framework di governance richiedono sempre più esattamente questa dichiarazione mancante, e si può produrre con le stesse primitive che hai già costruito in questa lezione.
Il notebook successivo code_samples/human-authorization-receipts.ipynb aggiunge un secondo tipo di ricevuta, human.approval.v1, nella stessa forma di busta delle ricevute della lezione (un payload tipizzato firmato da Ed25519 sulla sua SHA-256 canonica, con l’oggetto signature fuori dai byte firmati). Un approvatore nominato firma l’azione canonica completa e il suo digest prima dell’esecuzione; la ricevuta dell’azione dell’agente porta lo stesso digest dell’azione e un parent_approval_ref, l’receipt_hash dell’approvazione, la stessa convenzione del previous_receipt_hash nella catena che hai costruito sopra. Una singola verify_chain passa entrambi gli artefatti sotto registri di chiavi fissati separati (chiavi approvatore vs chiavi agente), così il percorso del codice è condiviso ma le autorità mai.
La proprietà acquistata, spiegata con attenzione: l’umano ha approvato questa esatta azione, e l’agente ha eseguito esattamente quell’azione approvata. Gli strumenti di rifiuto nel notebook sono ciò che rende reale la proprietà invece che solo affermata:
Ogni fallimento rifiuta per un motivo distinto, così un revisore che legge un rifiuto può capire se l’autorità è scaduta o se l’azione eseguita è cambiata. La regola che insegna il notebook: un’approvazione firmata non è autorità da sola. L’autorità esiste solo se entrambe le ricevute legano ancora la stessa azione canonica al momento dell’esecuzione. Il percorso di co-firma nell’Internet-Draft seguito da questa lezione (draft-farley-acta-signed-receipts) è la forma standardizzata di questo modello.
Il codice Python in questa lezione è intenzionalmente minimo così puoi leggere ogni riga e capire esattamente cosa succede. In produzione, hai due opzioni:
Costruire direttamente sulle primitive crittografiche. Le 50 righe viste sopra sono sufficienti per molti casi d’uso. PyNaCl (Ed25519) e il pacchetto jcs (JSON canonico) sono librerie ben mantenute e controllate.
Usare una libreria di ricevute di produzione. Diversi progetti open-source implementano lo stesso modello con funzionalità aggiuntive (rotazione chiavi, verifica batch, distribuzione Set JWK, integrazione con motori di policy):
draft-farley-acta-signed-receipts, revisione 02) attualmente nel processo di standardizzazione, con una suite di conformità condivisa (agent-governance-testvectors) che implementazioni indipendenti incrociano per verificare un output canonico byte-identico.protect-mcp (npm) e @veritasacta/verify (npm) forniscono un’implementazione Node di firma di ricevute e verifica offline, pensata per incapsulare qualsiasi server MCP con traccia di controllo tamper-evident, incluso un flusso di co-firma tenuto in sospeso in cui un’azione in pausa emette una ricevuta di approvazione legata al digest dell’azione (WebAuthn supportato nel flusso desktop), lo stesso modello di ricevuta di approvazione umano sopra.pip install nobulex) fornisce lo stesso modello di firma Ed25519 + JCS in Python con integrazioni LangChain e CrewAI, inclusi vettori di test pubblicati per la convalida incrociata e una mappatura di conformità contribuita tramite OWASP PR #2210.La decisione tra costruire la tua soluzione o usare una libreria rispecchia la scelta tra scrivere la tua libreria JWT e usarne una testata: entrambe sono ragionevoli; la libreria fa risparmiare tempo e riduce la superficie di controllo; il metodo da zero ti obbliga a capire ogni primitiva. Questa lezione insegna il percorso da zero così hai la base per entrambe le scelte.
Metti alla prova la tua comprensione prima di passare all’esercizio pratico.
1. Una ricevuta è firmata con la chiave privata Ed25519 dell’agente. Il revisore ha solo la chiave pubblica. Può il revisore verificare la ricevuta offline?
2. Un attaccante modifica il campo policy_id di una ricevuta per affermare che fosse governata da una politica più permissiva. La firma era sul payload originale. Cosa succede durante la verifica?
3. Perché la ricevuta include un tool_args_hash e un result_hash invece degli argomenti grezzi e del risultato?
4. Il campo previous_receipt_hash collega ogni ricevuta al suo predecessore. Se un attaccante cancella silenziosamente una ricevuta da metà catena, cosa diventa invalido?
5. Una ricevuta verifica correttamente. Ciò dimostra che l’azione dell’agente è stata corretta, valida o conforme alla policy?
Apri code_samples/18-signed-receipts.ipynb e completa tutte e quattro le sezioni:
Sfida avanzata 1: estendi lo schema della ricevuta con un campo aggiuntivo a tua scelta (ad esempio, un ID di richiesta per il tracciamento), aggiorna la logica di firma canonica per includerlo e conferma che la ricevuta pass inalterata attraverso la verifica. Poi modifica quel campo dopo la firma e conferma che la verifica fallisce. Questo ti costringe a capire come ogni byte della codifica canonica contribuisce alla firma.
Sfida avanzata 2: Applica SHA-256 su due delle tue ricevute insieme (concatenando i loro byte canonici in un ordine deterministico) e incorpora il digest risultante come nuovo campo su una terza ricevuta prima di firmarla. Verifica che tutte e tre le ricevute passino ancora la verifica. Hai appena costruito una prova di inclusione a un passo: chiunque abbia la terza ricevuta può dimostrare che le prime due esistevano al momento della firma, senza doverne rivelare il contenuto. Questo è il modello usato dalle ricevute a rivelazione selettiva su larga scala (impegni Merkle, RFC 6962).
Le ricevute crittografiche danno agli agenti AI una traccia di audit che è:
Non sono un sostituto per la validazione degli input, l’applicazione delle policy o l’infrastruttura di identità. Sono una base per questi livelli. Quando distribuisci agenti in ambienti regolamentati, flussi di lavoro multi-organizzazione o in qualsiasi contesto dove un futuro auditor non può essere dato per assunto fidarsi di te, le ricevute sono il modo per rendere la traccia di audit onesta.
Il messaggio più importante: le ricevute dimostrano chi ha detto cosa e quando. Non dimostrano che ciò che è stato detto era vero o corretto. Tieni salda questa distinzione. È la differenza tra un sistema di provenienza onesto e uno fuorviante.
Quando sei pronto a passare da questa lezione al deploy di agenti con firma di ricevute in un ambiente reale:
https://your-org.example.com/.well-known/agent-keys.json.Unisciti al Microsoft Foundry Discord per incontrare altri apprendenti, partecipare alle ore di ufficio e risolvere i tuoi dubbi sugli AI Agents.
Questa lezione tratta la firma di una singola ricevuta e sequenze concatenate tramite hash. Le stesse primitive compongono diversi schemi più avanzati che potresti incontrare man mano che la tua postura di governance matura:
authorization_*) e metà post-esecuzione (result_*) con firme indipendenti, utile quando la decisione di autorizzazione e il risultato osservato sono prodotti da attori diversi o in tempi diversi. Questo si combina in modo additivo al formato di ricevuta insegnato in questa lezione.result_hash. I payload reali sono spesso più ricchi di un semplice risultato di una chiamata a uno strumento: il ragionamento pre-decisione (predizione modello, opzioni considerate, prove e loro completezza, postura di rischio, catena di responsabilità, esito del gate) può vivere tutto dentro il payload, sigillato da una sola ricevuta. Questo mantiene il formato della ricevuta minimale consentendo agli schemi del payload di evolversi dominio per dominio.signature.alg può portare ML-DSA-65 (lo standard NIST per firme post-quantistiche) quando serve migrare. Pianifica un periodo di transizione in cui le ricevute sono firmate in doppio.Disclaimer: Questo documento è stato tradotto utilizzando il servizio di traduzione AI Co-op Translator. Sebbene ci impegniamo per garantire la precisione, si prega di notare che le traduzioni automatizzate possono contenere errori o imprecisioni. Il documento originale nella sua lingua nativa deve essere considerato la fonte autorevole. Per informazioni critiche, si raccomanda una traduzione professionale effettuata da un essere umano. Non siamo responsabili per eventuali malintesi o interpretazioni errate derivanti dall’uso di questa traduzione.