צפו בסרטון השיעור: אבטחת סוכני AI עם קבלות קריפטוגרפיות
(סרטון השיעור והתמונה המקדימה יתווספו על ידי צוות התוכן של מיקרוסופט לאחר המיזוג, בהתאמה לתבנית של שיעור 14 / 15.)
שיעור זה יכסה:
בסיום שיעור זה, תדעו כיצד:
דמיינו שפרסתם סוכן AI עבור Contoso Travel. הסוכן קורא בקשות מלקוחות, מפעיל API לטיסות כדי לאתר אפשרויות, ומזמין מושבים עבור הלקוח. ברבעון הקודם, הסוכן עיבד 50,000 הזמנות.
היום מגיע בוחן. הוא שואל שאלה פשוטה: “הראה לי מה הסוכן שלך עשה.”
אתם מוסרים את קבצי הלוג שלכם. הבוחן מסתכל עליהם ושואל שאלה קשה יותר: “איך אני יודע שהלוגים הללו לא נערכו?”
זו הבעיה של שביל ביקורת. רוב הפריסות של סוכנים כיום מסתמכות על:
אף אחד מאלה לא יכול לענות על שאלת הבוחן מבלי לדרוש מהבוחן לסמוך על מישהו (אתם, ספק הענן שלכם, ספק מסד הנתונים שלכם). לשימוש פנימי, האמון הזה לרוב מקובל. לעומסי עבודה מוסדרים (כספים, בריאות, כל דבר ברגולציה תחת חוק ה-AI של האיחוד האירופי), זה לא מקובל.
קבלות קריפטוגרפיות פותרות זאת בכך שכל פעולה של סוכן ניתנת לאימות באופן עצמאי. הבוחן אינו צריך לסמוך עליכם. הוא צריך רק את המפתח הציבורי שלכם ואת הקבלה עצמה.
קבלה היא אובייקט JSON שמתעד מה סוכן עשה, חתום בחתימה דיגיטלית.
flowchart LR
A[הסוכן מפעיל כלי] --> B[בניית מטען החשבונית]
B --> C[קנוניזציה של JSON לפי RFC 8785]
C --> D[חשיש SHA-256]
D --> E[חתימה Ed25519]
E --> F[חשבונית עם חתימה]
F --> G[הבודק מאמת במצב לא מקוון]
G --> H{האם החתימה תקפה?}
H -- yes --> I[הוכחה נגד הטעיה]
H -- no --> J[החשבונית נדחתה]
קבלה מינימלית נראית כך:
{
"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..."
}
}
שלוש תכונות עושות את העבודה:
החתימה. הקבלה חתומה על ידי שער הסוכן באמצעות מפתח פרטי Ed25519. כל מי שיש לו את המפתח הציבורי המתאים יכול לאמת את החתימה באופן לא מקוון. זיוף בכל שדה מבטל את החתימה.
קידוד קנוני. לפני החתימה, הקבלה מסודרת לפי סכמת קנוניזציה של JSON (JCS, RFC 8785). זה מבטיח ששני מימושים המייצרים את אותה הקבלה הלוגית מייצרים פלט זהה בדיוק בבייטים. ללא קנוניזציה, סדרות JSON שונות היו מייצרות חתימות שונות על אותו תוכן.
שרשור חשיש. השדה previous_receipt_hash מקשר כל קבלה לזו שקדמה לה. הסרת או שינוי סדר של קבלה שוברים כל קבלה שבאה אחריה. זיוף הופך לגלוי ברמת השרשרת אפילו אם חתימות בודדות עוקפות.
יחד התכונות הללו מספקות שלוש הבטחות:
אינכם צריכים ספרייה מיוחדת כדי ליצור קבלה. הפונקציות הקריפטוגרפיות זמינות נרחבות והלוגיקה היא כמה עשרות שורות פייתון.
התרגילים המעשיים ב־code_samples/18-signed-receipts.ipynb מראים את כל התהליך. הגרסה המסכמת:
import json
import hashlib
import base64
from nacl import signing
from jcs import canonicalize # JSON קנוני עפ"י 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()}"
# צור או טען מפתח חתימה (בסביבת ייצור, אחסן במאגר מפתחות)
signing_key = signing.SigningKey.generate()
verify_key = signing_key.verify_key
# בנה את המטען של הקבלה (עדיין ללא חתימה)
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,
}
# בצע קנוניזציה, חשיש, חתום.
canonical_bytes = canonicalize(payload)
message_hash = hashlib.sha256(canonical_bytes).digest()
signature_bytes = signing_key.sign(message_hash).signature
# צרף אובייקט חתימה מובנה.
receipt = {
**payload,
"signature": {
"alg": "EdDSA",
"sig": b64url_nopad(signature_bytes),
"public_key": b64url_nopad(bytes(verify_key)),
},
}
זו כל תשתית החתימה. התרגילים במחברת מדגימים כל שלב.
האימות הוא הפעולה ההפוכה:
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:
# החתימה היא אובייקט מבנה: {"alg", "sig", "public_key"}.
sig_obj = receipt.get("signature")
if not sig_obj or sig_obj.get("alg") != "EdDSA":
return False
# להרכיב מחדש את המטען שנחתם בפועל (הכל חוץ מהחתימה).
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
פונקציה זו מקבלת קבלה ומחזירה True אם החתימה תקפה, ו־False אחרת. אין קריאת רשת, אין תלות בשירות, אין צורך באמון באף צד שלישי.
כדי לראות את זיהוי הזיוף בפעולה, המחברת מדגימה:
tool_args_hash.זו הדגמה מעשית שקבלות הן מוליכות לזיוף: כל שינוי, אפילו קטן, שוברים את החתימה.
קבלה חתומה אחת מגנה על פעולה אחת. שרשרת של קבלות מגנה על רצף פעולות.
flowchart LR
R0[קבלה 0<br/>בראשית] --> R1[קבלה 1]
R1 --> R2[קבלה 2]
R2 --> R3[קבלה 3]
R1 -. previous_receipt_hash .-> R0
R2 -. previous_receipt_hash .-> R1
R3 -. previous_receipt_hash .-> R2
כל קבלה מתעדת את החשיש של הקבלה שלפניה. להסיר בשקט את הקבלה השנייה, התוקף יצטרך או:
previous_receipt_hash של הקבלה השלישית (שוברת את חתימת הקבלה השלישית), אואם המפתח הפרטי מאוחסן בארון מפתחות חומרתי ואתם מפרסמים את המפתח הציבורי עם כל קבלה, אף אחד מהתקפות הללו אינו אפשרי מבלי להתגלות.
המחברת מראה:
previous_receipt_hash של כל קבלה מתאים לחשיש האמיתי של הקבלה הקודמת.זוהי הדרך שבה יוצרים שביל ביקורת שבוחן חיצוני יכול לאמת מבלי לסמוך עליכם.
זה החלק החשוב ביותר בשיעור זה. הקבלות עוצמתיות אך העוצמה מוגבלת.
הקבלות מוכיחות שלושה דברים:
הקבלות אינן מוכיחות:
policy_id אכן הוערכה, או שהיא הייתה מאפשרת פעולה זו אם הייתה נבדקת. הקבלה מתעדת את מה שטען, לא מה שאכפו.הגבול הזה חשוב משתי סיבות:
טעות נפוצה היא להניח ש”יש לנו קבלות” משמעותו “אנו נשלטים.” זה לא נכון. הקבלות הן יסוד. השליטה היא המערכת שבונים מעליהן.
סעיף 3 לעיל שווה לסעיף נפרד: קבלה לפעולה אומרת “מפתח זה חתם על תוכן זה,” לא “אדם אישר זאת.” עבור פעולות בסיכון גבוה (החזרים, מחיקות, העברות כספים), מסגרות שליטה דורשות יותר ויותר את ההצהרה החסרה הזו, והיא ניתנת ליצירה באמצעות אותם פרימיטיבים שבניתם כבר בשיעור זה.
המחברת המשך code_samples/human-authorization-receipts.ipynb מוסיפה סוג שני של קבלה, human.approval.v1, באותו פורמט כמעטפות של הקבלות בשיעור (מטען מקודד בחתימת Ed25519 על SHA-256 הקנוני שלו, עם אובייקט signature מחוץ לבייטים החתומים). מאשר בשם חותם על הפעולה הקנונית המלאה והמעבד שלה לפני ביצוע; קבלת הפעולה של הסוכן נושאת את אותו המעבד של הפעולה ו־parent_approval_ref, החשיש של האישור, בהתאם לאותו קונבנציה של previous_receipt_hash בשרשרת שבניתם למעלה. אחת verify_chain מבצעת אימות על שני המסמכים תחת רשימות מפתחות מנעלות נפרדות (מפתחות המאשרים מול מפתחות הסוכן), כך שנתיב הקוד משותף אבל הרשויות לעולם לא.
התכונה שנוספת בזה, מנוסחת בקפידה: האדם אישר את הפעולה המדויקת הזו, והסוכן ביצע בדיוק את אותה פעולה שאושרה. התסריטים של מחברת הבחירה הן מה שעושים את התכונה ריאלית ולא רק מודגשת:
כל כשל מסרב מסיבה שונה, כך שבוחן שקורא סירוב יכול לדעת אם הרשות התיישנה או שהפעולה שבוצעה השתנתה. הכלל שהמחברת מלמדת: אישור חתום אינו רשות בפני עצמו. הרשות קיימת רק אם שתי הקבלות עדיין מתקשרות לאותה פעולה קנונית בזמן הביצוע. נתיב החתימה המשותפת במחברת Internet-Draft זו שעוקבת אחרי השיעור (draft-farley-acta-signed-receipts) הוא התבנית בסטנדרטים לכך.
קוד הפייתון בשיעור זה מכוון להיות מינימלי כדי שתוכלו לקרוא כל שורה ולהבין בדיוק מה קורה. בייצור יש לכם שתי אפשרויות:
לבנות ישירות על הפרימיטיבים הקריפטוגרפיים. 50 השורות שראיתם למעלה מספיקות לרבים מהמקרים. PyNaCl (Ed25519) ו־jcs (JSON קנוני) הן ספריות מתוחזקות ומבוקרות היטב.
להשתמש בספריית קבלות ייצורית. מספר פרויקטים פתוחים מיישמים את אותה התבנית עם תכונות נוספות (סיבוב מפתחות, אימות אצווה, הפצת סט JWK, אינטגרציה עם מנועי מדיניות):
draft-farley-acta-signed-receipts, סקירה 02) שנמצא בתהליך תקני, עם סט התאמה משותף (agent-governance-testvectors) שמיישמים עצמאיים מאמתים כדי לקבל פלט קנוני זהה בבייטים.protect-mcp (npm) ו־@veritasacta/verify (npm) מספקות מימוש Node לחתימת קבלות ואימות לא מקוון, מיועדות לעטוף כל שרת MCP עם שביל ביקורת מוליך לזיוף, כולל זרימת החזקה־לחתימה שבה פעולה מושהה פולגת קבלת אישור המקושרת למעבד הפעולה (WebAuthn בתהליך שולחני), באותו תבנית קבלת אישור-אנוש שמוזכר למעלה.pip install nobulex) מספקת את אותו תבנית חתימת Ed25519 + JCS בפייתון עם אינטגרציות ל-LangChain ו-CrewAI, כולל וקטורי בדיקה לחציית אימות ותרשים תאימות שנתרם דרך OWASP PR #2210.ההחלטה בין לכתוב לבד לבין להשתמש בספרייה היא כמו ההחלטה בין לכתוב ספריית JWT לבד לבין להשתמש בקיימת ונבדקת: שתיהן סבירות; הספרייה חוסכת זמן ומפחיתה את אזור הביקורת; הגישה מהבסיס מחייבת להבין כל פרימיטיב. שיעור זה מלמד את הנתיב מהבסיס כדי שתהיה לכם יסוד לשתי האפשרויות.
בדקו את הבנתכם לפני המעבר לתרגיל המעשי.
1. קבלה חתומה במפתח הפרטי Ed25519 של הסוכן. לבוחן יש רק את המפתח הציבורי. האם הבוחן יכול לאמת את הקבלה באופן לא מקוון?
2. תוקף משנה את שדה policy_id של קבלה כדי לטעון שנוהלה על ידי מדיניות מרחיבה יותר. החתימה הייתה על המטען המקורי. מה קורה במהלך האימות?
3. מדוע הקבלה כוללת tool_args_hash ו-result_hash במקום את הפרמטרים והתוצאה הגולמיים?
4. שדה previous_receipt_hash מקשר כל קבלה לקודמתה. אם תוקף מוחק בשקט קבלה אחת מאמצע שרשרת, מה הופך ללא חוקי?
5. קבלה מאומתת באופן נקי. האם זה מוכיח שהפעולה של הסוכן הייתה נכונה, תקפה או תואמת למדיניות?
פתח את code_samples/18-signed-receipts.ipynb והשלים את כל ארבעת החלקים:
אתגר מתיחה 1: הרחב את סכמת הקבלה בשדה נוסף לבחירתך (למשל, מזהה בקשה למעקב), עדכן את הלוגיקה של החתימה הקנונית לכלול אותו, ואשר שהקבלה עדיין עברה סבוב אימות. לאחר מכן, שנה את השדה אחרי החתימה ואשר כי האימות נכשל. זה מאלץ אותך להבין כיצד כל בית בקידוד הקנוני תורם לחתימה.
אתגר מתיחה 2: חשב יחד גיבוב SHA-256 לשתי הקבלות שלך (שרשר את הבתים הקנוניים שלהן בסדר קבוע) והטמע את הרב-גיבוב כתוסף שדה בקבלה שלישית לפני חתימתה. אמת שכל שלוש הקבלות עוברות סבוב. זה יצר הוכחת הכללה חד-שלבית: כל מי שמחזיק בקבלה השלישית יכול להוכיח ששתיהן הראשונות התקיימו בזמן חתימתה, בלי צורך לגלות את תוכנם. זהו הדפוס שקבלות גילוי סלקטיבי משתמשות בו בקנה מידה (התחייבויות מרקל, RFC 6962).
קבלות קריפטוגרפיות נותנות לסוכני AI מסלול ביקורת שהוא:
הם אינם תחליף לאימות קלט, לאכיפת מדיניות או לתשתית זהויות. הם בסיס לשכבות אלה. כאשר מפעילים סוכנים בסביבות מבוקרות, תהליכי עבודה בין-ארגוניים או כל סביבת בה לא ניתן להניח שמבקר עתידי ייתן אמון, הקבלות הן האופן בו עושים את מסלול הבדיקה הוגן.
ההבחנה החשובה ביותר: הקבלות מוכיחות מי אמר מה, ומתי. הן לא מוכיחות שמה שנאמר היה נכון או מדויק. שמור על הבחנה זו בקפידה. זה ההבדל בין מערכת שייכות כנה ומערכת מטעה.
כשאתה מוכן לעבור מהשיעור לפריסה של סוכנים שחתומים על קבלות בסביבה אמיתית:
https://your-org.example.com/.well-known/agent-keys.json.הצטרף ל-Microsoft Foundry Discord כדי לפגוש לומדים אחרים, להשתתף במשרדי שעות, ולקבל תשובות לשאלותיך על סוכני AI.
שיעור זה מכסה חתימת קבלה יחידה ורצפים בשרשרת גיבוב. אותם פרימיטיבים מרכיבים כמה דפוסים מתקדמים יותר שתיתקל בהם ככל שמדיניות הממשל שלך מתפתחת:
authorization_*) ואחרי ביצוע (result_*) עם חתימות עצמאיות, שימושי כאשר החלטת האישור והתוצאה הנצפית מיוצרים על ידי שחקנים שונים או בזמנים שונים. זה נבנה מעל לפורמט הקבלה שנלמד בשיעור זה.result_hash. מטענים אמיתיים לעיתים עשירים יותר מתוצאה של קריאת כלי יחידה: נימוקים לפני החלטה (תחזיות מודל, אפשרויות שנשקלו, ראיות ושלמותן, סיכון, שרשרת אחריות, תוצאה של שער) יכולים להתקיים כולם בתוך המטען, שמוחתם בקבלה אחת. זה משאיר את פורמט הקבלה מינימלי תוך מתן גמישות להתפתחות סכמות מטען לפי תחום.signature.alg יכול לשאת ML-DSA-65 (תקן החתימה הפוסט-קוואנטום של NIST) כשצריך לעבור. תכנן תקופת מעבר שבה הקבלות חתומות כפול.כתב ויתור: מסמך זה תורגם באמצעות שירות תרגום אוטומטי Co-op Translator. למרות שאנו שואפים לדיוק, יש לקחת בחשבון שתרגומים אוטומטיים עלולים להכיל שגיאות או אי-דיוקים. יש להחשיב את המסמך המקורי בשפתו הטבעית כמקור הסמכות. למידע קריטי מומלץ להשתמש בתרגום מקצועי על ידי מתרגם אדם. אנו לא אחראים לכל אי-הבנה או פירוש שגוי הנובע מהשימוש בתרגום זה.