צפו בסרטון השיעור: אבטחת סוכני בינה מלאכותית עם קבלות קריפטוגרפיות
(סרטון השיעור והתמונה המקדימה יתווספו על ידי צוות התוכן של Microsoft לאחר המיזוג, בהתאמה לדפוס של שיעור 14 / 15.)
בשיעור זה תלמדו על:
לאחר השלמת השיעור תדעו כיצד:
דמיינו שפיתחתם סוכן בינה מלאכותית עבור Contoso Travel. הסוכן קורא בקשות לקוחות, קורא ל-API של טיסות לבדיקת אפשרויות, ומזמין מקומות לטובת הלקוח. ברבעון האחרון, הסוכן ביצע 50,000 הזמנות.
היום מגיע מבקר. הוא שואל שאלה פשוטה: “הראה לי מה הסוכן שלך עשה.”
אתם מוסרים לו את קבצי הלוג שלכם. המבקר מסתכל עליהם ושואל שאלה קשה יותר: “איך אני יודע שקבצי הלוג האלה לא עברו עריכה?”
זו בעיית מסלול הביקורת. רוב פריסות הסוכנים היום מסתמכות על:
אף אחד מהם אינו יכול לענות על שאלת המבקר מבלי שהמבקר יצטרך לבטוח במישהו (בכם, בספק הענן שלכם, בספק מסד הנתונים שלכם). לשימוש פנימי, אמון זה לעיתים קרובות מקובל. לעומסי עבודה עם רגולציה (פיננסים, בריאות, כל דבר תחת חוק ה-AI של האיחוד האירופי) זה לא מתקבל על הדעת.
קבלות קריפטוגרפיות פותרות זאת בכך שכל פעולה של הסוכן ניתנת לאימות באופן עצמאי. המבקר אינו צריך לבטוח בכם, הוא זקוק רק למפתח הציבורי שלכם ולקבלה עצמה.
קבלה היא אובייקט JSON המתעד מה הסוכן עשה, החתום עם חתימה דיגיטלית.
flowchart LR
A[הסוכן מפעיל כלי] --> B[בנה מטען של קבלה]
B --> C[התקן JSON RFC 8785]
C --> 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 Canonicalization Scheme (JCS, RFC 8785). זה מבטיח ששתי מימושים שמפיקים את אותה הקבלה הלוגית, מפיקים פלט זהה ביתר. ללא קידוד קנוני, סיריאליזרים שונים היו מייצרים חתימות שונות עבור אותו תוכן.
שרשרת גיבוב. שדה 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,
}
# המרת JCS לקנוני וחתימתו ישירות. PureEdDSA מבצע חשיש פנימית.
canonical_bytes = canonicalize(payload)
signature_bytes = signing_key.sign(canonical_bytes).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)
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
פונקציה זו מקבלת קבלה ומחזירה 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
כל קבלה מתעדת את גיבוב הקבלה שלפניה. להסרת קבלה 2 בשקט, התוקף יצטרך או:
previous_receipt_hash של קבלה 3 (שובר את חתימת קבלה 3), אואם המפתח הפרטי מאוחסן בארון מפתחות חומרתי ואתה מפרסם את המפתח הציבורי עם כל קבלה, אף אחד מהתקיפות האלו אינו אפשרי ללא גילוי.
המחברת מראה:
previous_receipt_hash של קבלה תואם לגיבוב האמיתי של הקבלה הקודמת.כך אתה מייצר מסלול ביקורת שהמבקר החיצוני יכול לאמת ללא צורך באמון בך.
זהו הסעיף החשוב ביותר בשיעור זה. הקבלות חזקות אך הכוח שלהן מוגבל.
קבלות מוכיחות שלושה דברים:
קבלות אינן מוכיחות:
policy_id הוערכה, או שהיא הייתה מתירה פעולה זו אם נבדקה. הקבלה מתעדת מה נטען, לא מה נאכף.הגבול הזה חשוב משתי סיבות:
טעות נפוצה היא להניח ש”יש לנו קבלות” זה “אנחנו נשלטים.” זה לא נכון. קבלות הן יסוד. שלטון הוא המערכת שבונים מעליו.
סעיף 3 לעיל ראוי לסעיף משלו: קבלת פעולה אומרת “מפתח זה חתם על תוכן זה,” לעולם לא “אדם אישר את זה.” לפעולות עם סיכון גבוה (החזרים, מחיקות, העברות כספים), מסגרות ממשל דורשות יותר ויותר את ההצהרה החסרה הזו, והיא ניתנת ליצירה עם אותן פרימיטיבים שכבר בניתם בשיעור זה.
המחברת הנלווית code_samples/human-authorization-receipts.ipynb מוסיפה סוג קבלה שני, human.approval.v1, באותו מיכל כמו קבלות השיעור (מטען ממוין חתום עם Ed25519 על בתים קנוניים JCS, עם האובייקט signature מחוץ לבתים החתומים). מאשר ממוסמך חותם על הפעולה הקנונית המלאה ועיכול שלה לפני ביצוע; קבלת הפעולה של הסוכן נושאת את אותו עיכול פעולה ו-parent_approval_ref, גיבוב הקבלה של האישור, באותו נורמה כמו previous_receipt_hash בשרשרת שבניתם למעלה. verify_chain אחד בודק את שני המסמכים תחת רשומות מפתחות נעוצות נפרדות (מפתחות מאשרים מול מפתחות סוכנים), כך שדרך הקוד משותפת אך הרשויות לעולם אינן משותפות.
התכונה שנרכשת, מנוסחת בקפידה: האדם אישר את הפעולה המדויקת הזו, והסוכן ביצע בדיוק את הפעולה המאושרת. הביטולים במחברת הם מה שהופך את התכונה מאמירה ריקה למציאות:
כל כישלון נדחה מסיבה שונה, כך שמבקר שקורא לדחייה יכול לדעת אם הסמכות פגה או שהפעולה שבוצעה השתנתה. הכלל שהמחברת מלמדת: אישור חתום אינו סמכות בפני עצמו. סמכות קיימת רק אם שתי הקבלות עדיין מקשרות לאותה פעולה קנונית בזמן ביצוע. קבלת האישור האנושי היא קומפוזיציה חינוכית שהוגדרה על ידי שיעור זה, לא סוג קבלה מוגדר ב-draft-farley-acta-signed-receipts.
קוד הפייתון בשיעור זה הוא במכוון מינימלי כדי שתוכלו לקרוא כל שורה ולהבין בדיוק מה קורה. בייצור יש לכם שתי אפשרויות:
לבנות ישירות על הפרימיטיבים הקריפטוגרפיים. 50 השורות שראיתם למעלה מספקות לרבות שימושים. PyNaCl (Ed25519) ו-package jcs (JSON קנוני) הן ספריות מטופחות ומבוקרות.
להשתמש בספריית קבלות לייצור. מספר פרויקטים בקוד פתוח מיישמים את אותו דפוס עם תכונות נוספות (סיבוב מפתחות, אימות באצווה, הפצת JWK Set, אינטגרציה עם מנועי מדיניות):
draft-farley-acta-signed-receipts, גרסה 02). הקבלה החינוכית והשטוחה בשיעור שונה מהעטיפה {payload, signature} של הטיוטה ואינה מוצגת כמימוש תואם. הטיוטה מפרסמת סט תואם משותף (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. למרות שאנו שואפים לדיוק, יש לקחת בחשבון שתרגומים אוטומטיים עלולים להכיל שגיאות או אי-דיוקים. יש להחשיב את המסמך המקורי בשפתו הטבעית כמקור הסמכות. למידע קריטי מומלץ להשתמש בתרגום מקצועי על ידי מתרגם אדם. אנו לא אחראים לכל אי-הבנה או פירוש שגוי הנובע מהשימוש בתרגום זה.