בדיקת שירות שלם¶
כתבנו שירות, נתנו לו חשבון מצומצם, הגדרנו DACL לpipe והוספנו בדיקת בעלות על עבודות. עכשיו אפשר להריץ לקוח אחד, לקבל תשובה תקינה ולהכריז שהמערכת מוכנה. אבל הבדיקה הזאת כמעט לא נגעה בגבולות שבנינו. היא בדקה רק מסלול שבו כל התנאים נוחים.
נבחר מוצר קטן כדי לבדוק את התכנון שלנו. השירות מקבל עבודות משני משתמשים, שומר תוצאות בתיקייה שלו ומאפשר לכל משתמש לראות ולבטל את העבודות שלו. מנהל המכונה רשאי להתקין, לשדרג ולעצור את השירות. לפי המדיניות שבחרנו, עצם היכולת לנהל את השירות אינה נותנת פקודה עסקית לצפייה בעבודות של משתמש אחר.
ההבחנה האחרונה חשובה. מי שמנהל מחשב יכול להיות בעל יכולות רחבות מחוץ לפרוטוקול שלנו, ולכן לא נבטיח הגנה מלאה מפני מנהל המכונה. עם זאת, עדיין נרצה שהפרוטוקול יאכוף את המדיניות שהגדרנו ולא ירחיב הרשאות בטעות. אנחנו מפרידים בין מודל האיום של המחשב לבין החוזה של פעולה מסוימת.
נכתוב מה אנחנו מאמינים¶
ההנחה הראשונה היא שהזהות מגיעה מחיבור שנבדק על ידי המערכת. ההנחה השנייה היא שרשומת הבעלות על עבודה נשמרת במאגר שהלקוח לא יכול לשנות ישירות. ההנחה השלישית היא שקובץ ההרצה וקובצי התצורה המחייבים אינם ניתנים להחלפה בידי אותו לקוח. אם אחת מהן לא נכונה, בדיקת SID יפה בתוך handler לא תציל את המערכת.
נשרטט את גבולות התהליך ואת המשאבים שביניהם.
User A client --+
+--> named pipe --> service --> private journal
User B client --+ |
+--> result storage
Installer/admin --> service configuration + executable directory
השרטוט אינו הוכחה, אבל הוא מכריח אותנו לשאול מי יכול לכתוב בכל מקום. נבדוק גם את תיקיית האב של קובץ ההרצה: הגנה על הקובץ עצמו אינה כל הסיפור אם אפשר להחליף אותו דרך הרשאות על התיקייה. נבדוק נתיב מצוטט, תלויות DLL ותצורה שהשירות טוען בזמן ההפעלה.
גם לאובייקט השירות עצמו יש זכויות נפרדות, למשל שאילתת מצב, התחלה, עצירה ושינוי תצורה. לא נסיק מהרשאת קריאה לexe שמותר לשנות את רשומת השירות. המיפוי מופיע בתיעוד זכויות השירות ומנהל השירותים.
נבודד את מדיניות ההחלטה¶
לפני בדיקות מערכת, אפשר לבדוק מודל קטן של הפעולה העסקית. בקוד הבא מחרוזות הזהות הן נתוני בדיקה פנימיים בלבד. במוצר הן יוחלפו בזהות שנגזרה מהtoken, ולא בשדה שהלקוח שלח. שמרו בpolicy_model.cpp.
#include <cassert>
#include <mutex>
#include <string>
#include <utility>
struct identity {
bool authenticated;
std::string user;
};
enum class decision { denied, cancelled, already_finished };
class job {
const std::string owner_;
std::mutex lock_;
bool finished_ = false;
unsigned cancellations_ = 0;
public:
explicit job(std::string owner) : owner_(std::move(owner)) {}
decision cancel(const identity& caller) {
std::lock_guard guard(lock_);
if (!caller.authenticated || caller.user != owner_)
return decision::denied;
if (finished_) return decision::already_finished;
finished_ = true;
++cancellations_;
return decision::cancelled;
}
unsigned cancellation_count() {
std::lock_guard guard(lock_);
return cancellations_;
}
};
int main() {
identity alice{true, "alice"};
identity bob{true, "bob"};
identity unverified{false, "alice"};
job a("alice");
auto denied_bob = a.cancel(bob);
auto denied_fake = a.cancel(unverified);
assert(denied_bob == decision::denied);
assert(denied_fake == decision::denied);
assert(a.cancellation_count() == 0);
auto accepted = a.cancel(alice);
auto repeated = a.cancel(alice);
assert(accepted == decision::cancelled);
assert(repeated == decision::already_finished);
assert(a.cancellation_count() == 1);
}
בנו בלי NDEBUG כדי שבדיקות הassert ירוצו.
הבדיקה לא מסתפקת בתשובת denied. היא גם מוודאת שמונה השינויים נשאר אפס. אחרת יכולנו לכתוב מימוש ששינה את העבודה ורק אחר כך בדק את הזהות. זאת דרך טובה להפוך דרישת אבטחה לתכונה נצפית של המערכת: בקשה שנדחתה אינה גורמת לאפקט האסור.
המודל לא בודק את ווינדוס, את הDACL או את הצלחת ההתחזות. הוא בודק רק את מדיניות הפעולה. עכשיו נוסיף את הבדיקות שחסרות במקום להציג אותו כהוכחה לכל המוצר.
מטריצת זהויות¶
ניצור במכונת המעבדה שני משתמשים רגילים וחשבון שירות מוגדר. נבדוק לקוח מאותה זהות בשני sessions, לקוח מזהות אחרת, וסוכן שמתנתק ומתחבר מחדש. אם המוצר מבודד לפי משתמש, אותו משתמש בשני sessions עשוי לראות את אותן עבודות. אם המוצר מבודד לפי logon session, נצטרך מדיניות אחרת; SID של משתמש בלבד לא מספיק להוכיח אותה.
נבדוק גם מסלול שבו הלקוח אינו מאפשר רמת התחזות שהשרת דורש. התוצאה צריכה להיות דחייה או שימוש במנגנון אימות אחר שהוגדר מראש. היא לא יכולה להיות "נמשיך עם token השירות". אחרי כל מסלול כשל נבדוק שהthread לא נשאר עם token לקוח.
נוסיף בדיקות גבול פרוטוקול: אורך הודעה מעל המותר, גרסה לא נתמכת, פקודה לא מוכרת, מספר עבודה שאינו קיים וחיבור שנסגר באמצע frame. אין צורך לייצר קלט אקראי לפני שיש לנו ציפייה ברורה לכל אחד מהמקרים האלה.
מחזור התקנה ושדרוג¶
נריץ את ההתקנה ממכונה נקייה ולא רק מתוך תיקיית הbuild. נבדוק שהחשבון של השירות יכול לקרוא את התוכנה ולכתוב רק במיקומים שנועדו לכך. נוודא שהלקוח לא יכול להחליף DLL שהשירות טוען, ושכשל בפתיחת תצורה אינו גורם למעבר אוטומטי לתצורה פתוחה יותר.
בשדרוג נפריד בין עצירת קבלה, ניקוז, החלפת בינארי והחלפת schema. נזריק כשל בכל גבול, ונבדוק איזו גרסה מותר להפעיל אחרי הכשל. אם הקוד החדש כתב נתונים שהקוד הישן אינו יודע לקרוא, העתקת exe ישן אינה rollback של המערכת.
נבדוק גם client ישן מול service חדש ולהפך, לפי גרסאות שאנחנו מבטיחים לתמוך בהן. החיבור יכול להצליח ברמת הpipe בזמן שהפרוטוקול כבר אינו תואם. נרצה דחיית גרסה ברורה לפני שהשרת מפרש body לפי מבנה לא נכון.
תרחיש חקירה¶
נניח שמשתמש ב קיבל denied כשניסה לבטל עבודה של משתמש א, אבל העבודה אכן בוטלה. יש לנו שתי השערות: שינוי לפני בדיקת הרשאה, או אירוע ביטול עצמאי ממשתמש א בזמן הבדיקה. נשתמש במזהי בקשה ואירועי מעבר מצב כדי להבחין ביניהן. רק העובדה שהבדיקה הסתיימה באותו זמן אינה מוכיחה איזה request גרם לשינוי.
נוסיף לבדיקת regression מזהה בקשה ייחודי ואפס פעולות מתחרות של הבעלים. נוודא שהמצב לא השתנה ושאין אירוע ביצוע עם המזהה שנדחה. לאחר מכן נריץ שוב עם תחרות מכוונת ונבדוק שהקשר הסיבתי נשמר. כך אנחנו עוברים מהשערה כללית לבדיקה שמסוגלת להפריך אותה.
מה מגישים בביקורת¶
נגיש תכנון קצר של גבולות האמון, טבלת בדיקות עם זהות ותוצאה צפויה, קוד שמאפשר לשחזר כשל אחד ותיקון עם בדיקה חוזרת. ליד כל טענה נציין אם היא נשענת על חוזה API, על תצפית במכונת המעבדה או על החלטת מוצר שלנו.
לא נכתוב "מאובטח" בתא אחד ונמשיך הלאה. נכתוב למשל: משתמש ב אינו יכול לבטל עבודה של משתמש א דרך גרסת הפרוטוקול הזאת, תחת הDACL והחשבונות שתועדו, ונראה את הבדיקה. ניסוח כזה נותן למי שממשיך את הפרויקט דרך להבין מה הוכח ומה עדיין צריך לחקור.