צפו בסרטון השיעור: אבטחת סוכני בינה מלאכותית עם קבלות קריפטוגרפיות
(סרטון השיעור והתמונה הממוזערת יתווספו על ידי צוות התוכן של מיקרוסופט לאחר המיזוג, בהתאמה לתבנית השיעור 14 / 15.)
בשיעור זה נתמקד ב:
בסיום שיעור זה, תדעו כיצד:
דמיין שהפעלת סוכן בינה מלאכותית עבור Contoso Travel. הסוכן קורא בקשות לקוחות, קורא ל-API של טיסות לחיפוש אפשרויות, ומזמין מקומות בשם הלקוח. ברבעון האחרון עיבד הסוכן 50,000 הזמנות.
היום מגיע מבקר. הוא שואל שאלה פשוטה: “הראה לי מה הסוכן שלך עשה.”
אתה מוסר את קבצי הלוג שלך. המבקר מסתכל בהם ושואל את השאלה המאתגרת יותר: “איך אני יודע שלוגים אלה לא נערכו?”
זו הבעיה של רישום הביקורת. רוב פריסות הסוכנים כיום מסתמכות על:
אף אחד מהם אינו יכול לענות לשאלת המבקר מבלי לדרוש ממנו לסמוך על מישהו (עליך, ספק הענן שלך, או ספק בסיס הנתונים שלך). לשימוש פנימי האמון הזה מקובל לרוב. עבור עומסים מפוקחים (פיננסים, בריאות, כל דבר כפוף ל-EU AI Act), זה לא מקובל.
קבלות קריפטוגרפיות פותרות את הבעיה בכך שהן מאפשרות כל פעולה של סוכן להיות ניתנת לאימות עצמאי. המבקר אינו צריך לסמוך עליך. הוא זקוק רק למפתח הציבורי שלך ולקבלה עצמה.
קבלה היא אובייקט 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 Canonicalization Scheme (JCS, RFC 8785). זה מבטיח ששתי ישומות שמפיקות את אותו תוכן לוגי יספיקו פלט זהה בבייטים. ללא קנוניזציה, מסדרי JSON שונים היו מייצרים חתימות שונות על אותו תוכן.
שרשור hash. השדה 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
כל קבלה מתעדת את ה-hash של הקבלה שקודם לה. להסיר בשקט את קבלה מספר 2, התוקף היה צריך:
previous_receipt_hash של קבלה 3 (שובש את חתימת קבלה 3), אואם המפתח הפרטי מאוחסן בארון מפתחות חומרתי ואתה מפרסם את המפתח הציבורי עם כל קבלה, אף אחת מהתקיפות איננה ברת ביצוע ללא גילוי.
המחברת עוברת על:
previous_receipt_hash תואם ל-hash האמיתי של הקבלה הקודמת.כך מייצרים רישום ביקורת שמבקר חיצוני יכול לאמת מבלי לסמוך עליך.
זו החלק החשוב ביותר בשיעור זה. הקבלות חזקות אך הכוח שלהן מוגבל.
הקבלות מוכיחות שלושה דברים:
הקבלות אינן מוכיחות:
policy_id הוערכה בפועל, או שהייתה מתירה את הפעולה אם נבדקה. הקבלה מתעדת את הטענה, לא את האכיפה.גבול זה חשוב משתי סיבות:
טעות נפוצה היא להניח ש”הקבלות קיימות” = “יש ממשל.” זה לא נכון. הקבלות הן יסוד. הממשל הוא המערכת שבונים מעליו.
קוד הפייתון בשיעור זה הוא מכוון מינימלי כדי שתוכל לקרוא כל שורה ולהבין בדיוק מה קורה. בייצור יש לך שתי אפשרויות:
לבנות ישירות על הפרימיטיבים הקריפטוגרפיים. 50 השורות שראית למעלה מספיקות לרבות מהמקרים. ספריית PyNaCl (Ed25519) והחבילה jcs (JSON קנוני) הן ספריות מתוחזקות ומבוקרות היטב.
להשתמש בספריית קבלות לייצור. מספר פרויקטים קוד פתוח מיישמים את אותו תבנית עם תכונות נוספות (סיבוב מפתחות, אימות באצווה, הפצת JWK Set, אינטגרציה עם מנועי מדיניות):
draft-farley-acta-signed-receipts) שעומדת בתהליך תקינה.protect-mcp (npm) ו-@veritasacta/verify (npm) מספקות מימוש Node של חתימת קבלות ואימות offline, מיועדות לעטוף כל שרת MCP ברשומת ביקורת המגנה מפני זיופים.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 והשלם את כל ארבעת הסעיפים:
אתגר מתיחה 2: חשב SHA-256 לשתי הקבלות שלך יחד (שרשר את הבתים הקנוניים שלהן בסדר דטרמיניסטי) ושתל את התמצית המתקבלת כשדה חדש בקבלה שלישית לפני החתימה שלה. אשר שכל שלוש הקבלות עדיין עוברות אימות באופן תקין. כך בנית הוכחת הכללה בשלב אחד: כל מי שיש לו את הקבלה השלישית יכול להוכיח שהשניים הראשונים התקיימו בזמן שנחתם, מבלי שיצטרך לחשוף את תוכנן. זה התבנית שחשבונות גלויות סלקטיביות משתמשות בה בקנה מידה (מחוייבויות מרקל, RFC 6962).
חשבונות קריפטוגרפיים נותנים לסוכני AI דרך ביקורת שהיא:
הם אינם תחליף לאימות קלט, לאכיפת מדיניות, או לתשתית זהות. הם הבסיס לשכבות אלו. כאשר אתה משלב סוכנים בעומסים מפוקחים, בזרימות עבודה רב-ארגוניות, או בכל סביבה שבה לא ניתן להניח שמבקר עתידי יבין אותך, החשבונות הם איך שאתה מוודא שדרך הביקורת כנה.
הלקח החשוב ביותר: חשבונות מוכיחים מי אמר מה ומתי. הם אינם מוכיחים שהדברים שנאמרו היו נכונים או אמיתיים. החזק הבחנה זו בהדגשה. זו ההבדל בין מערכת מקורית הגונה לבין מערכת מטעה.
כאשר אתה מוכן לעבור מהשיעור הזה לפריסה של סוכנים עם חתימות קבלה בסביבה אמיתית:
https://your-org.example.com/.well-known/agent-keys.json.הצטרף ל-Microsoft Foundry Discord לפגוש לומדים אחרים, להשתתף בשעות מיון ולקבל תשובות על שאלותיך לגבי סוכני AI.
השיעור הזה מכסה חתימה על קבלה יחידה ושרשרות מבוססות hash. אותם כלים מרכיבים מספר דפוסים מתקדמים יותר שתפגוש כשמעמד הממשל שלך יתבסס:
authorization_*) ולחלק לאחר ביצוע (result_*) עם חתימות עצמאיות, שימושי כאשר החלטת ההרשאה והתוצאה שצפו הן של שחקנים שונים או בזמנים שונים. זה מצטבר על פורמט הקבלה שנלמד בשיעור זה.result_hash. מטענים אמיתיים לעיתים קרובות מורכבים יותר מתוצאה של קריאה אחת: נימוקים מקדמיים להחלטה (חיזוי מודל, אפשרויות שנשקלו, ראיות ושלמותן, מצב סיכונים, שרשרת אחריות, תוצאת שער) יכולים להיכלל במטען, מאומתים ע”י קבלה יחידה. זה שומר על פורמט הקבלה מינימלי ומשאיר מקום לאבני דרך להתפתח תחום-תחום.signature.alg יכול לשאת ML-DSA-65 (תקן החתימה לאחר-כמתי של NIST) בעת צורך במעבר. תכנן תקופת מעבר שבה הקבלות חתומות פעמיים.(יקבע על ידי מנהלי התכנית הלימודית)
כתב ויתור: מסמך זה תורגם באמצעות שירות תרגום אוטומטי Co-op Translator. למרות שאנו שואפים לדיוק, יש לקחת בחשבון שתרגומים אוטומטיים עלולים להכיל שגיאות או אי-דיוקים. יש להחשיב את המסמך המקורי בשפתו הטבעית כמקור הסמכות. למידע קריטי מומלץ להשתמש בתרגום מקצועי על ידי מתרגם אדם. אנו לא אחראים לכל אי-הבנה או פירוש שגוי הנובע מהשימוש בתרגום זה.