Obejrzyj wideo z lekcji: Zabezpieczanie agentów AI za pomocą kryptograficznych pokwitowań
(Wideo z lekcji i miniaturka zostaną dodane przez zespół ds. zawartości Microsoft po scaleniu, zgodnie ze schematem lekcji 14 / 15.)
W tej lekcji omówimy:
Po ukończeniu tej lekcji będziesz potrafił:
Wyobraź sobie, że wdrożyłeś agenta AI dla Contoso Travel. Agent odczytuje prośby klientów, wywołuje interfejs API lotów, aby sprawdzić opcje, i rezerwuje miejsca w imieniu klienta. W ostatnim kwartale agent obsłużył 50 000 rezerwacji.
Dziś pojawia się audytor. Zadaje proste pytanie: “Pokaż mi, co zrobił Twój agent.”
Wręczasz mu pliki dziennika. Audytor je przegląda i zadaje trudniejsze pytanie: “Skąd mam wiedzieć, że te dzienniki nie zostały zmienione?”
To jest problem ścieżki audytu. Większość dzisiejszych wdrożeń agentów opiera się na:
Żadne z nich nie potrafi odpowiedzieć na pytanie audytora bez zmuszania go do zaufania komuś (tobie, twojemu dostawcy chmury, dostawcy bazy danych). W zastosowaniach wewnętrznych takie zaufanie jest często akceptowalne. W przypadku obciążeń regulowanych (finanse, opieka zdrowotna, wszystko objęte europejską ustawą o AI) nie jest.
Kryptograficzne pokwitowania rozwiązują ten problem, umożliwiając niezależną weryfikację każdego działania agenta. Audytor nie musi ufać tobie. Potrzebuje tylko twojego klucza publicznego i samego pokwitowania.
Pokwitowanie to obiekt JSON rejestrujący, co agent zrobił, podpisany podpisem cyfrowym.
flowchart LR
A[Agent wywołuje narzędzie] --> B[Zbuduj ładunek potwierdzenia]
B --> C[Kanonizacja JSON RFC 8785]
C --> D[Skrót SHA-256]
D --> E[Podpis Ed25519]
E --> F[Potwierdzenie z podpisem]
F --> G[Audytor weryfikuje offline]
G --> H{Podpis ważny?}
H -- yes --> I[Dowód wykazujący manipulację]
H -- no --> J[Potwierdzenie odrzucone]
Minimalne pokwitowanie wygląda tak:
{
"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..."
}
}
Trzy cechy sprawiają, że to działa:
Podpis. Pokwitowanie jest podpisane przez bramę agenta używając prywatnego klucza Ed25519. Każdy, kto ma odpowiedni klucz publiczny, może zweryfikować podpis offline. Każda modyfikacja jakiegokolwiek pola unieważnia podpis.
Kanoniczne kodowanie. Przed podpisaniem pokwitowanie jest serializowane za pomocą JSON Canonicalization Scheme (JCS, RFC 8785). Zapewnia to, że dwie implementacje generujące ten sam logiczny pokwitowanie dają identyczne bajtowo wyniki. Bez kanoniczności różni serializatorzy JSON wygenerowaliby różne podpisy dla tej samej zawartości.
Łączenie za pomocą hasha. Pole previous_receipt_hash łączy każde pokwitowanie z poprzednim. Usunięcie lub zmiana kolejności pokwitowania łamie każde pokwitowanie powstałe po nim. Manipulacja jest widoczna na poziomie łańcucha, nawet jeśli podpisy poszczególnych pokwitowań zostałyby obejściem.
Razem te cechy dają trzy gwarancje:
Nie potrzebujesz specjalnej biblioteki do tworzenia pokwitowania. Prymitywy kryptograficzne są powszechnie dostępne, a logika to kilka dziesiątek linii Pythona.
Ćwiczenia praktyczne w code_samples/18-signed-receipts.ipynb przeprowadzają przez cały proces. Podsumowanie:
import json
import hashlib
import base64
from nacl import signing
from jcs import canonicalize # Kanoniczny JSON zgodny z 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()}"
# Wygeneruj lub załaduj klucz do podpisu (w produkcji przechowuj w skarbcu kluczy)
signing_key = signing.SigningKey.generate()
verify_key = signing_key.verify_key
# Zbuduj ładunek potwierdzenia (jeszcze bez podpisu)
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,
}
# Kanonizuj, haszuj, podpisz.
canonical_bytes = canonicalize(payload)
message_hash = hashlib.sha256(canonical_bytes).digest()
signature_bytes = signing_key.sign(message_hash).signature
# Dołącz uporządkowany obiekt podpisu.
receipt = {
**payload,
"signature": {
"alg": "EdDSA",
"sig": b64url_nopad(signature_bytes),
"public_key": b64url_nopad(bytes(verify_key)),
},
}
To cały pipeline podpisywania. Ćwiczenia w notatniku przeprowadzają przez każdy krok.
Weryfikacja to operacja odwrotna:
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:
# Sygnatura jest obiektem strukturalnym: {"alg", "sig", "public_key"}.
sig_obj = receipt.get("signature")
if not sig_obj or sig_obj.get("alg") != "EdDSA":
return False
# Odtwórz ładunek, który został faktycznie podpisany (wszystko oprócz sygnatury).
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
Funkcja przyjmuje pokwitowanie i zwraca True, jeśli podpis jest ważny, False w przeciwnym razie. Bez wywołań sieciowych, bez zależności od usług, bez konieczności zaufania osobie trzeciej.
Aby zobaczyć działanie wykrywania manipulacji, notatnik pokazuje:
tool_args_hash.To praktyczny dowód na to, że pokwitowania są odporne na manipulacje: każda zmiana, choćby najmniejsza, łamie podpis.
Pojedyncze podpisane pokwitowanie chroni jedno działanie. Łańcuch pokwitowań chroni sekwencję.
flowchart LR
R0[Pokwitowanie 0<br/>geneza] --> R1[Pokwitowanie 1]
R1 --> R2[Pokwitowanie 2]
R2 --> R3[Pokwitowanie 3]
R1 -. previous_receipt_hash .-> R0
R2 -. previous_receipt_hash .-> R1
R3 -. previous_receipt_hash .-> R2
Każde pokwitowanie zapisuje hash pokwitowania poprzedniego. Aby potajemnie usunąć pokwitowanie 2, atakujący musiałby:
previous_receipt_hash pokwitowania 3 (to łamie podpis pokwitowania 3), LUBJeśli klucz prywatny jest w sprzętowym sejfie kluczy, a ty publikujesz klucz publiczny z każdym pokwitowaniem, żaden z tych ataków nie jest możliwy bez wykrycia.
Notatnik pokazuje:
previous_receipt_hash każdego pokwitowania odpowiada faktycznemu hashowi pokwitowania poprzedniego.W ten sposób tworzysz ścieżkę audytu, którą zewnętrzny audytor może zweryfikować bez konieczności zaufania tobie.
To najważniejsza część tej lekcji. Pokwitowania są potężne, ale ich moc jest ograniczona.
Pokwitowania dowodzą trzech rzeczy:
Pokwitowania NIE dowodzą:
policy_id została faktycznie oceniona lub że dopuściłaby to działanie, gdyby została sprawdzona. Pokwitowanie odnotowuje, co zostało zgłoszone, a nie co zostało wymuszone.Ta granica jest istotna z dwóch powodów:
Częstym błędem jest założenie, że “mam pokwitowania” oznacza “mam zarządzanie.” Tak nie jest. Pokwitowania są fundamentem. Zarządzanie to system, który z nich budujesz.
Punkt 3 powyżej zasługuje na osobną sekcję: pokwitowanie działania mówi “ten klucz podpisał tę zawartość”, nigdy “człowiek to zatwierdził”. Dla działań wysokiego ryzyka (zwroty, usunięcia, przelewy) ramy zarządzania coraz częściej wymagają dokładnie tego brakującego oświadczenia, a można je wygenerować za pomocą tych samych prymitywów, które zbudowałeś w tej lekcji.
Notatnik „code_samples/human-authorization-receipts.ipynb” dodaje drugi rodzaj pokwitowania, human.approval.v1, w tej samej formie koperty jak pokwitowania z lekcji (typowany ładunek podpisany Ed25519 po kanonicznym SHA-256, z obiektem signature na zewnątrz podpisanych bajtów). Nazwany zatwierdzający podpisuje pełne kanoniczne działanie i jego skrót przed wykonaniem; pokwitowanie działania agenta zawiera ten sam skrót działania oraz parent_approval_ref, czyli receipt_hash zatwierdzenia, w tej samej konwencji co previous_receipt_hash w łańcuchu powyżej. Jedna funkcja verify_chain obsługuje oba artefakty pod oddzielnymi przypiętymi rejestrami kluczy (klucze zatwierdzających vs klucze agentów), więc ścieżka kodu jest wspólna, ale autorytety nigdy.
Efekt, wyrażony precyzyjnie: człowiek zatwierdził dokładnie to działanie, a agent wykonał dokładnie to zatwierdzone działanie. W notatniku przypadki odrzuceń sprawiają, że właściwość jest rzeczywista, a nie tylko deklarowana:
Każda awaria odrzuca z odrębnym powodem, więc audytor czytający odrzucenie może rozpoznać, czy uprawnienia wygasły, czy działanie się zmieniło. Zasada nauczana przez notatnik: podpisane zatwierdzenie nie jest samo w sobie autorytetem. Autorytet istnieje tylko wtedy, gdy oba pokwitowania nadal wiążą się z tym samym kanonicznym działaniem w momencie wykonania. Ścieżka współpodpisu w tym samym projekcie Internet-Draft, który ta lekcja śledzi (draft-farley-acta-signed-receipts), jest oficjalnym wzorem tego schematu.
Kod Pythona z tej lekcji jest celowo zwięzły, abyś mógł przeczytać każdy wiersz i dokładnie zrozumieć, co się dzieje. W produkcji masz dwie opcje:
Buduj bezpośrednio na prymitywach kryptograficznych. 50 linii, które widziałeś powyżej, wystarczy dla wielu zastosowań. Biblioteki PyNaCl (Ed25519) i jcs (kanoniczny JSON) są dobrze utrzymane i audytowane.
Użyj biblioteki produkcyjnej do pokwitowań. Kilka projektów open-source implementuje ten sam wzór z dodatkowymi funkcjami (rotacja kluczy, weryfikacja masowa, dystrybucja zestawu JWK, integracja z silnikami polityk):
draft-farley-acta-signed-receipts, wersja 02) obecnie w procesie standaryzacji, z wspólnym zestawem testów zgodności (agent-governance-testvectors) pozwalającym niezależnym implementacjom na wzajemną weryfikację identyczności bajtowej wyjścia kanonicznego.protect-mcp (npm) i @veritasacta/verify (npm) dostarczają implementacji Node do podpisywania pokwitowań i weryfikacji offline, przeznaczone do opakowania dowolnego serwera MCP w odporną na manipulacje ścieżkę audytu, w tym przepływ hold-for-co-sign, gdzie wstrzymane działanie generuje pokwitowanie zatwierdzające związane ze skrótem działania (WebAuthn wspierany w trybie desktop), ten sam wzór pokwitowania zatwierdzenia jak w notatniku zatwierdzania przez człowieka powyżej.pip install nobulex) zapewnia ten sam wzór podpisu Ed25519 + JCS w Pythonie z integracją LangChain i CrewAI, w tym opublikowanymi wektorami testowymi do walidacji krzyżowej i mapowaniem zgodności dostarczonym poprzez OWASP PR #2210.Decyzja między tworzeniem własnego rozwiązania a użyciem biblioteki przypomina wybór między napisaniem własnej biblioteki JWT a użyciem przetestowanej: obie opcje są rozsądne; biblioteka oszczędza czas i zmniejsza powierzchnię audytu; własne rozwiązanie zmusza do zrozumienia każdego prymitywu. Ta lekcja uczy od podstaw, abyś miał fundament pod obie decyzje.
Sprawdź swoje rozumienie przed przejściem do ćwiczenia praktycznego.
1. Pokwitowanie jest podpisane prywatnym kluczem Ed25519 agenta. Audytor ma tylko klucz publiczny. Czy audytor może zweryfikować pokwitowanie offline?
2. Atakujący modyfikuje pole policy_id pokwitowania, twierdząc, że było ono regulowane bardziej liberalną polityką. Podpis dotyczył oryginalnego ładunku. Co się dzieje podczas weryfikacji?
3. Dlaczego paragon zawiera tool_args_hash i result_hash, zamiast surowych argumentów i wyniku?
4. Pole previous_receipt_hash łączy każdy paragon z jego poprzednikiem. Co stanie się nieważne, jeśli atakujący potajemnie usunie paragon ze środka łańcucha?
5. Paragon weryfikuje się poprawnie. Czy to dowód, że działanie agenta było poprawne, zgodne lub zgodne z polityką?
Otwórz code_samples/18-signed-receipts.ipynb i ukończ wszystkie cztery sekcje:
Wyzwanie dodatkowe 1: rozszerz schemat paragonu o dodatkowe pole dowolnego wyboru (np. identyfikator żądania do śledzenia), zaktualizuj logikę kanonicznego podpisu, aby je uwzględnić, i potwierdź, że paragon nadal przechodzi weryfikację. Następnie zmodyfikuj pole po podpisaniu i potwierdź, że weryfikacja się nie powiodła. To wymusza zrozumienie, jak każdy bajt kodowania kanonicznego wpływa na podpis.
Wyzwanie dodatkowe 2: skróć dwie swoje paragony SHA-256 razem (połącz ich kanoniczne bajty w deterministycznej kolejności) i wstaw powstały skrót jako nowe pole w trzecim paragonie przed podpisaniem. Zweryfikuj, że wszystkie trzy paragony nadal przechodzą weryfikację. Właśnie zbudowałeś dowód włączenia w jednym kroku: każdy posiadający trzeci paragon może udowodnić, że pierwsze dwa istniały w momencie jego podpisania, bez konieczności ujawniania ich zawartości. To wzór, którego używają paragony z selektywnym ujawnianiem na dużą skalę (zob. Merkle commitments, RFC 6962).
Kryptograficzne paragony dają agentom AI ślad audytu, który jest:
Nie zastępują walidacji wejścia, egzekwowania polityki ani infrastruktury tożsamości. Są podstawą dla tych warstw. Gdy wdrażasz agentów w regulowanych środowiskach, wieloorganizacyjnych przepływach pracy lub jakimkolwiek miejscu, gdzie przyszły audytor nie może zakładać Twojego zaufania, paragony uczciwie tworzą ślad audytowy.
Najważniejsza nauka: paragony dowodzą, kto co i kiedy powiedział. Nie dowodzą, że to, co powiedział, było prawdziwe lub słuszne. Trzymaj tę różnicę mocno. To odróżnia uczciwy system pochodzenia od wprowadzającego w błąd.
Gdy będziesz gotowy, by przejść z tej lekcji do wdrożenia agentów podpisujących paragony w realnym środowisku:
https://twoja-organizacja.example.com/.well-known/agent-keys.json.Dołącz do Microsoft Foundry Discord, aby spotkać się z innymi uczącymi się, uczestniczyć w godzinach konsultacji i uzyskać odpowiedzi na pytania dotyczące agentów AI.
Ta lekcja obejmuje podpisywanie pojedynczego paragonu i sekwencje połączone hasłami. Te same prymitywy tworzą kilka bardziej zaawansowanych wzorców, które możesz spotkać, gdy Twoja postawa zarządzania dojrzeje:
authorization_*) i po-wykonaniu (result_*) z niezależnymi podpisami, użyteczne, gdy decyzja autoryzacji i obserwowany wynik są tworzone przez różnych aktorów lub w różnym czasie. To dodaje się do formatu paragonu omawianego w tej lekcji.result_hash. Rzeczywiste zawartości często są bogatsze niż jeden wynik wywołania narzędzia: przemyślenia przed decyzją (predykcja modelu, rozważane opcje, dowody i ich kompletność, ocena ryzyka, łańcuch odpowiedzialności, wynik bramki) mogą być zawarte w payloadzie, zabezpieczone jednym paragonem. To utrzymuje format paragonu minimalnym, jednocześnie pozwalając schematom payloadu ewoluować w domenach.signature.alg może zawierać ML-DSA-65 (standard NIST dla podpisów post-kwantowych), gdy zajdzie potrzeba migracji. Zaplanuj okres przejściowy, gdy paragony są podpisane podwójnie.Tworzenie lokalnych agentów AI
Zastrzeżenie: Niniejszy dokument został przetłumaczony za pomocą usługi tłumaczenia AI Co-op Translator. Choć dążymy do dokładności, prosimy pamiętać, że automatyczne tłumaczenia mogą zawierać błędy lub niedokładności. Oryginalny dokument w jego języku źródłowym należy uznawać za autorytatywne źródło. W przypadku informacji krytycznych zalecane jest skorzystanie z profesjonalnego tłumaczenia wykonanego przez człowieka. Nie ponosimy odpowiedzialności za jakiekolwiek nieporozumienia lub błędne interpretacje wynikające z użycia tego tłumaczenia.