ב-builds של PDFium Component לפני v3.126.2, ה-TPdf.AnalyzeSignatureRevisions יכל לדרג עריכת תוכן עמוד אמיתית בתור שינוי הערה מותר תחת DocMDP P=3, כי גרף התפקידים של המהדורות שלו התייחס אל הפניית ה-/P של ה-widget של החתימה בחזרה אל העמוד שלה בתור בעלות. מאז v3.126.2, PDFium Component מפריד קשתות ניווט מקשתות של payload בבעלות, ולכן תוכן עמוד נשאר תוכן עמוד. דיווח הבאג שמאחורי התיקון הזה נראה הרמטי על הנייר. חוזה מוסמך מתיר הערות, צד נגדי מוסיף שמירה מצטברת אחת, והמנתח אומר שכל שינוי מאוחר מותר. ואז מישהו עושה diff לעמודים המרונדרים וסכום התשלום בעמוד 2 שונה
המאמר הזה הוא ההמשך מנקודת מבט התוקף של סקירת ניתוח שינויי מהדורות אחרי חתימה, ולכן הוא מדלג על יסודות בניית המהדורות מחדש ודירוג ה-DocMDP ונכנס ישר אל גרף האובייקטים: איך בעלות מודלה, מדוע כיוון של קשת מכריע פסק אבטחה, מה השתנה ב-v3.126.2, ואיך מבקרים את לוגיקת הקבלה שלכם
מדוע עריכת עמוד עברה בתור שינוי הערה תחת DocMDP P=3?
עריכת העמוד עברה כי גרף התפקידים הישן הלך אחרי כל הפניה עקיפה במילון כאילו האובייקט שאליו מפנים שייך למפנה, וה-widget של החתימה מצביע בחזרה אל העמוד שלו. מילון הערה נושא /P, הפניה עקיפה לאובייקט העמוד שעליו הוא יושב (ISO 32000-1 §12.5.2). הרשומה הזאת היא רמז ניווט. ה-widget אינו הבעלים של העמוד; העמוד הוא הבעלים של ה-widget דרך מערך ה-/Annots שלו
המנתח משייך לכל אובייקט קבוצת ביטי תפקיד לפני שהוא מדרג שינויים מאוחרים: עמוד, הערה, טופס וחומר אימות. אובייקטי שורש מקבלים את התפקיד מהמילון של עצמם, והתפקיד אז מתפשט אל כל מה שהם מפנים אליו. בהפצה הישנה השרשרת הלכה כך:
- ה-widget של החתימה הוא
/Subtype /Widgetעם/FT /Sig, ולכן הוא מקבל את תפקיד ההערה - ה-
/Pשל ה-widget דוחף את תפקיד ההערה אל מילון העמוד, שכבר מחזיק את תפקיד העמוד - העמוד דוחף את שני התפקידים אל
/Contents,/Resources, ודרך/Parentמעלה אל עץ ה-Pages והלאה אל כל עמוד אח - מילון stream תוכן כמו
<< /Length 812 >>אינו מחזיק/Type, ולכן המסווג נפל בחזרה אל ביטי התפקיד ובדק את תפקיד ההערה לפני תפקיד העמוד
ה-content stream המשונה לכן יצא בתור prckAnnotation. תחת ISO 32000-1 §12.8.2.2, DocMDP P=3 מתיר שינויי הערות, ולכן ההחלטה הייתה prdAllowed והדוח התגלגל אל prasAllowed. אותו קובץ תחת P=2 נדחה, אבל רק במקרה: P=2 אוסר שינויי הערות, ולכן ה-stream שתויג שגוי נדחה מהסיבה הלא נכונה. לולאת הפצה קבועה של ארבעה מעברים הוסיפה חולשה שנייה. Payload שהגיע דרך מערך עקיף, או דרך שרשרת ארוכה שמספרי האובייקטים שלה רצים אחורה, עשוי שלא לקבל אף תפקיד בכלל
מדוע מאמת חתימות חייב לשאול מי הבעלים של אובייקט?
מאמת חתימות חייב לשאול מי הבעלים של אובייקט כי עדכונים מצטברים של PDF (ISO 32000-1 §7.5.6) מאפשרים לכל אחד לצרף מהדורה שמגדירה מחדש מספר אובייקט קיים, והגוף המוגדר מחדש לא מצהיר מה הוא. החתימה עדיין מאומתת, כי היא מכסה רק את בייטי המהדורה שלה עצמה. כל הגנה מפני מניפולציה אחרי חתימה נשענת לכן על מיפוי כל אובייקט שהשתנה אל המבנה שמשתמש בו, ואז על שאילתה האם החותם התיר למבנה הזה להשתנות
כמה מחלקות התקפה מפורסמות עובדות בדיוק בפער הזה. התקפות incremental saving מצרפות מהדורה שמחליפה תוכן עמוד ונשענות על מאמת שבודק רק את הטווח החתום. התקפות shadow שותלות תוכן נסתר לפני החתימה ומפעילות אותו אחר כך בשינוי קטן שנראה תמים. התקפות על מסמכים מוסמכים מנצלות את העובדה ש-P=2 ו-P=3 מתירים במפורש כמה עריכות מאוחרות, ואז מלבישות עריכה אסורה בתור מותרת. מאמת שמסווג אובייקטים לפי תוויות כמו /Type /Annot, או לפי כל נתיב הפניה שבמקרה מגיע אליהם, חשוף למחלקה השלישית: לתוקף מספיק מבנה אחד מותר שמגיע אל המבנה האסור
לכן השאלה אינה אילו אובייקטים השתנו אלא מי הבעלים שלהם. Content stream שמגיע מעמוד דרך /Contents הוא תוכן עמוד לא משנה מה עוד מצביע עליו. הערה שמצביעה בחזרה אל העמוד דרך /P אומרת היכן ההערה חיה, לא מה היא בבעלותה
איך PDFium Component v3.126.2 ממדל בעלות?
PDFium Component v3.126.2 מתייחס להפניות חוזרות בתור ניווט, משאיר אותן מחוץ להפצת תפקידים, ומכריע אילו מפתחות נחשבים ניווט מהתפקיד המבני של המילון שמחזיק אותם, ולא משם המפתח לבדו. הטבלה מסכמת את מפתחות הניווט שכבר לא נושאים בעלות
| מילון הבעלים | מפתחות שמטופלים בתור ניווט | הפניה למפרט |
|---|---|---|
| עמוד או צומת Pages | /Parent, /Kids, /Annots | ISO 32000-1 §7.7.3 |
| הערה או widget | /P | ISO 32000-1 §12.5.2 |
| מילון widget או שדה | /Parent | ISO 32000-1 §12.7.3 |
סינון לפי שם מפתח באופן גלובלי היה יוצר חור חדש. משאב פונט או XObject יכול באופן לגיטימי להיקרא /P, /Parent או /Annots, ומילון /Resources שמשמיט את רשומת ה-/P שלו מההפצה היה מאפשר לתוקף להסתיר XObject בבעלות עמוד מאחורי שם משאב תמים. ב-v3.126.2 מסנן הניווט חל רק כשמילון הבעלים באמת הוא עמוד, צומת Pages, הערה, widget או שדה. אם אחד מהמילונים האלה נושא מפתח ניווט משוכפל, כמו שתי רשומות /P ב-widget, המנתח לא מנחש איזו עותקת צופה תשתמש; בניית התפקידים נכשלת והחתימה נהיית Indeterminate
כמה כללים נוספים סוגרים את נתיבי שינוי התוויות הנותרים:
- צמתי Pages הם שורשי תפקיד-עמוד בזכות עצמם, ולכן משאבים שמורשים מעץ ה-Pages (ISO 32000-1 §7.7.3.4) נכנסים להקשר העמוד דרך בעלות אמיתית, ולא דרך הליכת
/Parentמעמוד בן - תפקיד הערה או טופס שמגיע אל קטלוג, צומת Pages, עמוד, הערה או מילון שדה נעצר שם, כי אובייקטים מבניים אלה מבססים את התפקידים של עצמם ותפקיד payload נכנס אינו אמור לדרוס אותם
- תפקיד העמוד הוא הסמכות בזמן סיווג: אובייקט בבעלות עמוד הוא
prckPageContentגם אם מהדורה מאוחרת משכתבת אותו עם/FTמזויף, תווית/Type /Annot, או משתפת אותו עם appearance stream - Form XObject שמשמש רק בתור מראה של שדה או הערה שומר על קטגוריית הטופס או ההערה שלו, ולכן התחדשות מראה רגילה אחרי מילוי טופס עדיין מדורגת תחת כללי ההרשאה הרגילים
- Widget בלי
/FTמשלו פותר את טיפוס השדה היורש דרך שרשרת ה-/Parent, ושרשרת שאינה נפתרת מכשילה את בניית התפקידים במקום לברור ברירת מחדל של הערה - ביטי תפקיד מכל מהדורה מאוחרת ממוזגים אל תפקידי המהדורה המכוסה, ולכן עדכון מאוחר לא יכול למחוק קשר בעלות-עמוד קודם על ידי ניתוק stream קודם ועריכתו אחר כך
נקודת שבע במקום מספר מעברים קבוע
נגישות התפקידים ב-v3.126.2 רצה בתור תור עבודה שחוזר עד שאף אובייקט לא מקבל ביט תפקיד חדש, מה שהוא נקודת שבע אמיתית ללא תלות בעומק שרשרת או במספור אובייקטים. מערכים עקיפים כמו מערך /Contents השמור בתור אובייקט עצמאי נסרקים גם הם. לכל אובייקט יכולים להצטרף לכל היותר ארבעה ביטי תפקיד נבדלים, ולכן התור חסום בארבע כניסות לכל מספר אובייקט; חריגה מהתקציב הזה מעלה prrResourceLimitExceeded. הפניה אל אובייקט חופשי, אי-התאמת generation או כותרת אובייקט שבורה מעלים prrMalformedRevisionChain, ו-payload בתוך object stream דחוס מעלה prrCompressedObjectUnresolved. כל אחד מהכשלים האלה מסתיים ב-prasIndeterminate, לעולם לא בפסק מותר, וכשהכשל קורה בזמן בניית תפקידי המהדורה המכוסה החתימה לא מדווחת Changes בכלל
הרוטינה הבאה מפרטת את עריכות תוכן העמודים ששורדות את הניתוח הזה. שינוי prckPageContent לעולם לא מדורג prdAllowed: DocMDP P=1, 2 או 3 הופך אותו ל-prdDisallowed, וחתימה בלי DocMDP מדרגת אותו prdSuspicious
uses
SysUtils, TypInfo, PDFium, FPdfPades;
procedure ListPageContentEdits(const FileName: string);
const
ShadowTag: array[Boolean] of string = (' (unreferenced shadow)', '');
var
Pdf: TPdf;
Report: TPadesRevisionAnalysisReport;
Sig: TPadesSignatureRevisionAnalysis;
Change: TPadesRevisionObjectChange;
i, j: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
Report := Pdf.AnalyzeSignatureRevisions; // record, אין מה לשחרר
for i := 0 to High(Report.Signatures) do
begin
Sig := Report.Signatures[i];
if not (prrPageContentChanged in Sig.Risks) then
Continue;
Writeln(Format('Signature %d (DocMDP P=%d): page content changed',
[Sig.SignatureIndex, Sig.DocMdpPermission]));
for j := 0 to High(Sig.Changes) do
begin
Change := Sig.Changes[j];
if Change.Kind <> prckPageContent then
Continue;
Writeln(Format(' revision %d object %d %d R %s%s',
[Change.RevisionIndex, Change.ObjectNumber, Change.Generation,
GetEnumName(TypeInfo(TPadesRevisionDecision), Ord(Change.Decision)),
ShadowTag[Change.IsAuthoritative]]));
end;
end;
finally
Pdf.Free;
end;
end;
מה קורה כש-FieldMDP והערה חולקות אובייקט אחד?
כשה-/V של שדה וה-/Contents של הערה מצביעים על אותו אובייקט עקיף, v3.126.2 שומר על נעילת ה-FieldMDP בתוקף גם כשהשינוי מסווג בתור עריכת הערה. את התרחיש קל לבנות ביד: חותם נועל את השדה Total עם FieldMDP (ISO 32000-1 §12.8.2.4), והתוקף גורם ל-/Contents של הערת טקסט להפנות את אותו אובייקט מחרוזת שמחזיק את ערך השדה. תחת P=3 עריכת ההערה מותרת, ולכן לפני התיקון שכתוב אותה מחרוזת משותפת שינה ערך שדה נעול עם פסק מותר
האובייקט נושא עכשיו גם את תפקיד ההערה וגם את תפקיד הטופס, והחלטת ההערה בודקת מחדש את הצד של הטופס בכל פעם שלחתימה יש טרנספורם FieldMDP:
- תחת P=2 שינוי ההערה נפסל מלכתחילה, בדיוק כפי שהיה
- עם FieldMDP
All, כל שדה נעול, ולכן השינוי המשותף הואprdDisallowed - עם FieldMDP
IncludeאוExclude, המנתח לא יכול לעקוב סקלר משותף בחזרה אל שם שדה אחד, ולכן ההחלטה היאprdIndeterminateולא ניחוש - בלי FieldMDP, כלל ההערה של P=3 חל והשינוי נשאר מותר
פרט דיווח אחד חשוב לקוד שער. המקרה המשותף מדווח בתור Kind = prckAnnotation עם Decision = prdIndeterminate, ו-prrFieldMdpUnresolved מתווסף לקבוצת הסיכונים רק עבור שינויים שסווגו בתור שדות טופס. שער שמחפש prrFieldMdpUnresolved ומתעלם מ-Status מפספס את המקרה הזה לגמרי
איך קוד Delphi צריך להיכשל בביטחון על ניתוח מהדורות?
קוד Delphi צריך לקבל מסמך חתום רק כשה-Status של הניתוח הוא prasNoLaterChanges או prasAllowed ואין סיכון מבני, והוא צריך להתייחס ל-prasIndeterminate ול-prasSuspicious בתור לא אמינים, ולא בתור אזהרות לרשום ולהעביר. Indeterminate פירושו שהמנתח לא יכל להוכיח שהמהדורות המאוחרות הותרו; עבור תוקף, קלט שמייצר Indeterminate בצורה אמינה שווה ערך לקלט שמייצר Allowed אם הקוד שלכם מעביר אותו. ה-AnalyzePadesSignatureRevisions הגלובלי מקבל כל TStream וקורא אותו ממיקום 0, מה שמתאים ל-handlers של העלאות שלא צריכים לרנדר את המסמך
uses
Classes, SysUtils, TypInfo, FPdfPades;
const
// הגדרות כפולות נרשמות בלי להוריד את ה-Status
BlockingRisks: TPadesRevisionRisks = [prrMalformedRevisionChain,
prrDuplicateObjectDefinition, prrUnreferencedObjectDefinition,
prrSignatureObjectRedefined, prrCompressedObjectUnresolved,
prrResourceLimitExceeded];
function SignedRevisionsAcceptable(const FileName: string;
out Reason: string): Boolean;
var
Source: TFileStream;
Report: TPadesRevisionAnalysisReport;
begin
Result := False;
Reason := '';
Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Report := AnalyzePadesSignatureRevisions(Source);
finally
Source.Free;
end;
if Report.SignatureCount = 0 then
begin
Reason := 'no signature anchors the analysis';
Exit;
end;
if Report.Risks * BlockingRisks <> [] then
begin
Reason := 'structural risk in the revision chain';
Exit;
end;
case Report.Status of
prasNoLaterChanges, prasAllowed:
Result := True;
else
// prasIndeterminate ו-prasSuspicious הן דחיות, לא אזהרות
Reason := 'revision status ' +
GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus), Ord(Report.Status));
end;
end;
שני גבולות ראויים לניסוח חד. ה-TPadesRevisionAnalysisReport לא אומר דבר על שלמות CMS או על אמון בתעודות, ולכן השער הזה יושב לצד אימות קריפטוגרפי ואימות אמון, לא במקומם. וגרף בעלות נכון לא הופך את P=3 לבטוח עבור כל זרימת עבודה. P=3 באמת מתיר הערות, והערה עם appearance אטום יכולה לכסות טקסט חתום בלי לגעת באף content stream. אם המסמכים המוסמכים שלכם הם חוזים ולא עותקי סקירה, או מסמכים עם P=2 או מנתבים שינויי הערה מותרים אל בן אדם, כמו בעוזר הזה:
function AllowedAnnotationEditsUnderP3(
const Report: TPadesRevisionAnalysisReport): Integer;
var
i, j: Integer;
begin
Result := 0;
for i := 0 to High(Report.Signatures) do
if Report.Signatures[i].DocMdpPermission = 3 then
for j := 0 to High(Report.Signatures[i].Changes) do
if (Report.Signatures[i].Changes[j].Kind = prckAnnotation) and
(Report.Signatures[i].Changes[j].Decision = prdAllowed) then
Inc(Result);
end;
צ'קליסט ביקורת מהדורות חתימות
השתמשו ברשימה הזאת כדי לבדוק אם צינור האימות שלכם היה חשוף ואם הוא עכשיו נכשל בביטחון:
- Builds של PDFium Component לפני v3.126.2 יכלו לדווח
prasAllowedעבור עריכות תוכן עמודים במסמכי DocMDP P=3; הריצו מחדש אתTPdf.AnalyzeSignatureRevisionsעל קבצי P=3 מוסמכים שקיבלו builds ישנים - בדקו מחדש מסמכי P=3 עם נעילות FieldMDP שבהם ערך שדה והערה עשויים לחלוק אובייקט עקיף
- קבלו רק
prasNoLaterChangesו-prasAllowed; התייחסו ל-prasIndeterminateול-prasSuspiciousבתור לא אמינים - בדקו גם את
Report.Risksוגם אתReport.Status, כיprrDuplicateObjectDefinitionלא משנה את ה-Status לבדו - אל תקראו מערך
Changesריק בתור תוצאה נקייה כשה-Status של החתימה הוא Indeterminate; בניית תפקידים שנכשלה לא מדווחת שינויים - אל תסתמכו על
prrFieldMdpUnresolvedלבדו כדי לתפוס בעיות FieldMDP, כי המקרה של ההערה המשותפת עולה רק דרך ההחלטה וה-Status - הכריעו אם שינויי הערה מותרים תחת P=3 דורשים סקירה אנושית בזרימת העבודה שלכם
- נתחו את בייטי הקובץ המקוריים; מסמך ששוכתב על ידי
SaveAsכבר לא מחזיק את שרשרת המהדורות
ניתוח מהדורות הוא שכבה אחת של בדיקת חתימה. צוותו אותו עם בדיקת חתימות דיגיטליות של PDF ורמות PAdES עבור המילון ורמת ה-baseline, ועם ביקורת סיכוני אבטחה רחבה של PDF עבור JavaScript, פעולות הפעלה וקבצים משולבים. TPdf.AnalyzeSignatureRevisions, AnalyzePadesSignatureRevisions וגרף התפקידים מודע-הבעלות שתואר כאן נשלחים ב-PDFium Component עבור Delphi, C++Builder ו-Lazarus