מאמר טכני

בדיקת חתימות PDF ורמות PAdES בדלפי

קיבלת PDF חתום ואתה צריך להציג בצופה שלך מי חתם עליו, מתי הוא נחתם, האם החתימה מכסה את כל הקובץ ועד כמה היא מתקדמת לקראת תאימות ארוכת טווח. רכיב ה-PDFium עבור דלפי ולזרוס עונה על כל ארבע השאלות באמצעות קריאות לקריאה בלבד: FPDF_GetSignatureCount ומשפחת FPDFSignatureObj_* חושפים את מילון החתימה, ו-TPdf.ValidatePades מסווג את רמת הבסיס של PAdES. זהו הראשון מבין שלושה מאמרים על חתימות PDF באמצעות PDFium; השניים שיבואו אחריו מכסים יצירת חתימת B-B והוספת חותמות זמן לטווח ארוך. עם זאת, גבול אחד שייך להתחלה: כל מה שכאן הוא בדיקה, וקריאת מה שחתימה טוענת היא משימה שונה מאימות הקריפטוגרפיה שלה או החלטה האם אתה סומך על החותם

מדוע מילון חתימת PDF אינו סתם גוש בתים

חתימת PDF היא מילון, ולא קובץ מצורף אטום, ושני הערכים החשובים ביותר שלו מספרים לך כמה מהקובץ מוגן בפועל. ISO 32000-1 §12.8 מגדיר את מילון החתימה עם ערכי /ByteRange ו-/Contents. ה-/Contents מחזיק מבנה CMS SignedData בקידוד הקסדצימלי (RFC 5652), המעטפת הקריפטוגרפית הנושאת את תעודת החותם, המאפיינים החתומים וערך החתימה עצמו. /ByteRange הוא החלק שמפתחים מעריכים בחסר: זהו מערך של שני טווחי היסט-אורך המכסים יחד את כל הקובץ למעט מחרוזת ההקס של /Contents. הפער הזה הוא בדיוק המקום שבו יושבים בתיי החתימה, ושני הטווחים משני הצדדים הם בדיוק מה שהחתימה מתחייבת אליו

תכנון ה-ByteRange הוא מה שמאפשר לבצע ביקורת על שמירה אינקרמנטלית. מכיוון שחותם אינו יכול לבצע hash לבתיי חתימה שאינם קיימים עדיין, הקובץ מפוצל סביב ממלא המקום של /Contents וכל השאר עובר hash לתוך החתימה. חתימה ששלה ה-ByteRange אינו מגיע לקצה הקובץ היא סימן אזהרה: תוכן שיתווסף לאחר הטווח המכוסה, באמצעות עדכון אינקרמנטלי מאוחר יותר, לא ישבור את החתימה למרות שהוא משנה את מה שהקורא רואה. לכן הדבר הראשון שבודק רציני בודק אינו מי חתם, אלא האם החתימה מכסה את הבתים שהיא מתיימרת לאשר

קריאת מילון החתימה באמצעות ממשק ה-API לקריאה בלבד של PDFium

רכיב ה-PDFium מציף את מילון החתימה דרך שני איברים לקריאה בלבד: SignatureCount ורשומת Signature[Index]. מתחת למכסה המנוע הם קוראים ל-FPDF_GetSignatureCount,‏ FPDF_GetSignatureObject, ולגישות (accessors) של FPDFSignatureObj_* עבור /SubFilter,‏ /ByteRange,‏ /Contents,‏ /Reason וזמן החתימה. קריאה בלבד היא ביטוי המפתח כאן: PDFium יכולה למנות ולקרוא חתימות אך אין לה API ליצירה או כתיבה של אחת, וזו הסיבה שצד החתימה של סדרה זו מיושם על ידי הספרייה עצמה ולא על ידי PDFium

var
  Pdf: TPdf;
  i: Integer;
  Sig: TPdfSignature;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    for i := 0 to Pdf.SignatureCount - 1 do
    begin
      Sig := Pdf.Signature[i];
      Writeln('SubFilter : ', Sig.Encoding);        // ETSI.CAdES.detached, adbe.pkcs7.detached, ...
      Writeln('Signed at : ', Sig.Time);            // signer date string, e.g. D:20260708120000+02'00'
      Writeln('Reason    : ', Sig.Reason);
      Writeln('CMS length: ', Length(Sig.Content)); // raw DER SignedData taken from /Contents
      Writeln('DocMDP    : ', Sig.Permission);      // 0 = no certification, 1..3 = MDP level
    end;
  finally
    Pdf.Free;
  end;
end;

כל רשומת TPdfSignature ממופה ישירות למילון. Encoding הוא ה-/SubFilter, השדה האבחוני ביותר, מכיוון שהוא נוקב בשם מטפל החתימה ומפריד מיד חתימת ETSI.CAdES.detached מודרנית מחתימה מיושנת או אסורה. Time הוא זמן החתימה המוצהר על ידי המחבר כמחרוזת תאריך PDF, שהיא טענה של החותם ולא זמן מהימן. Content הוא ה-CMS SignedData הגולמי, ו-Permission חושף את רמת אישור ה-DocMDP (0 עבור חתימת אישור רגילה, 1 עד 3 עבור חתימת אישור הנועלת שינויים מאוחרים יותר). השדה היחיד שהרשומה אינה מציפה הוא ה-ByteRange המנותח, והשמטה זו היא מכוונת, מכיוון ש-ValidatePades מבצע את חישוב כיסוי ה-ByteRange עבורך במקום שתעשה זאת שוב ידנית

מה ההבדל בין PAdES B-B,‏ B-T,‏ B-LT ו-B-LTA?

ארבע רמות הבסיס של PAdES יוצרות סולם מחתימה תקפה מינימלית לחתימה שנבנתה לשרוד עשורים של ארכוב, וכל רמה מכילה בקפידה את זו שמתחתיה. ETSI EN 319 142-1 מגדיר אותן כ-B-B,‏ B-T,‏ B-LT ו-B-LTA. ‏B-B (בסיסית - Basic) היא החתימה בתוספת המאפיינים החתומים המנדטוריים ותו לא. B-T (חותמת זמן - Timestamp) מוסיפה חותמת זמן אמינה לפי RFC 3161 מעל החתימה, כך שרגע החתימה מאושר על ידי רשות חותמות זמן במקום להיות מוצהר על ידי שעון החותם. B-LT (טווח ארוך - Long-Term) מטמיעה את חומרי האימות — שרשרת התעודות, ובאופן אופציונלי תגובות OCSP או CRL — בתוך הקובץ, כך שעדיין ניתן לאמת את החתימה שנים מאוחר יותר כאשר תשתית ההנפקה איננה. B-LTA (טווח ארוך עם חותמת זמן לארכיון) עוטפת חותמת זמן של מסמך סביב חומרים אלו, ובכך מגנה על הנתונים ארוכי הטווח עצמם ומעניקה לך נקודה לביצוע חותמת זמן מחדש לפני שהקריפטוגרפיה הבסיסית מתיישנת

הקריאה המעשית היא לגבי אופק הזיכרון. חתימת B-B עונה על \"האם מישהו חתם על זה\". B-T עונה על \"ומתי, בצורה מוכחת\". B-LT עונה על \"והאם אני עדיין יכול לבדוק זאת לאחר פקיעת תוקף התעודות\". B-LTA עונה על \"והאם הבדיקה הזו עדיין תחזיק מעמד בעוד עשרים שנה\". פרופילי רגולציה בוחרים שלב: הקשרים רבים של חשבוניות אלקטרוניות ו-eIDAS דורשים לפחות B-T, ומנדטים של ארכוב שואפים ל-B-LT או B-LTA. לדעת לאיזה שלב מסמך מגיע בפועל, לפני שאתה מקבל או דוחה אותו, זה כל הנושא של שלב הבדיקה

זיהוי רמת הבסיס באמצעות TPdf.ValidatePades

רכיב ה-PDFium מצמצם את כל שאלת הרמה לקריאה אחת. TPdf.ValidatePades מחזיר רשומת TPadesValidationResult ששדה ה-Level שלה הוא TPadesLevel — ‏plNone,‏ plUnknown,‏ plB_B,‏ plB_T,‏ plB_LT או plB_LTA — לצד קבוצת בעיות, ספירת חתימות וספירת חותמות זמן של מסמך. הרמה מוסקת באופן מונוטוני: המאמת קובע B-B תחילה, ואז מקדם ל-B-T אם קיימת חותמת זמן של חתימה או חותמת זמן של מסמך, ל-B-LT אם הקטלוג נושא /DSS עם תעודות וסימן /Extensions /ESIC רמה 1, ול-B-LTA אם חותמת זמן של מסמך וסימן ESIC רמה 2 קיימים שניהם. שני עוזרים הופכים את התוצאה למעשית: IsCompliant הוא True רק כאשר הרמה מגיעה לפחות ל-B-B וקבוצת הבעיות ריקה, ועוזר ה-IsCompliantAt מאפשר לקבוע רצפת מדיניות כגון plB_T

var
  Pdf: TPdf;
  R: TPadesValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePades;
    case R.Level of
      plNone:    Writeln('No PAdES signature present');
      plUnknown: Writeln('Signature present but level undeterminable');
      plB_B:     Writeln('PAdES B-B   (basic)');
      plB_T:     Writeln('PAdES B-T   (trusted timestamp)');
      plB_LT:    Writeln('PAdES B-LT  (long-term material embedded)');
      plB_LTA:   Writeln('PAdES B-LTA (archive timestamp)');
    end;
    Writeln('Signatures   : ', R.SignatureCount);
    Writeln('DocTimeStamps: ', R.DocTimeStampCount);
    if R.IsCompliantAt(plB_T) then
      Writeln('Meets the B-T policy floor')
    else
      Writeln('Below the required B-T level');
  finally
    Pdf.Free;
  end;
end;

מדוע adbe.pkcs7.sha1 הוא SubFilter אסור?

מכיוון ש-SHA-1 שבור ומטפל ה-adbe.pkcs7.sha1 כולל אותו פנימית. תת-מסנן (SubFilter) זה מבצע hash מוקדם למסמך עם SHA-1 לפני עטיפתו ב-PKCS#7, ו-SHA-1 פגיע להתנגשויות מזה שנים, ולכן סעיף 6.3 של EN 319 142-1 אוסר עליו לחלוטין עבור חתימת בסיס. ValidatePades מעלה את ppeiForbiddenSubFilter כשהוא מזהה adbe.pkcs7.sha1 או adbe.x509.rsa_sha1, ומעלה את ppeiBadDigestAlgorithm כאשר ה-CMS עצמו משתמש ב-MD5 או ב-SHA-1 כתמצית ההודעה (סעיף 6.2.1). אלו שתי בדיקות נפרדות התופסות את אותו סוג של חולשה בשתי שכבות שונות

לקבוצת הבעיות יש 26 איברים בסך הכל, ואלו שתפגוש לעיתים הקרובות ביותר מתרכזים סביב מבנה וכיסוי. ppeiByteRangeNotCoveringFile היא בדיקת הכיסוי שתוארה קודם לכן. ppeiForbiddenCertKey מופעל כאשר מילון החתימה נושא ערך /Cert, ש-PAdES אוסר עליו מכיוון שהשרשרת חייבת לשכון בתוך ה-CMS‏ SignedData.certificates במקום זאת. ppeiMissingSigningCertificate,‏ ppeiMissingContentType ו-ppeiMissingMessageDigest מסמנים מאפיינים חתומים מנדטוריים שחסרים, ו-ppeiDetachedContentViolation תופס חתימה שמטמיעה בטעות את התוכן החתום במקום לנתק אותו. מניית הקבוצה הופכת דחייה פשוטה לאבחון שתוכל לתעד בלוג

var
  R: TPadesValidationResult;
  Issue: TPadesValidationIssue;
begin
  R := Pdf.ValidatePades;
  if R.Issues <> [] then
    for Issue := Low(TPadesValidationIssue) to High(TPadesValidationIssue) do
      if Issue in R.Issues then
        Writeln('Issue: ',
          GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;

מה ש-ValidatePades אינה בודקת

ValidatePades מאמתת מבנה, לא אמון, ובלבול בין השניים הוא הטעות המסוכנת. תוצאה של plB_LTA פירושה שהמסמך מכיל חתימת B-LTA במבנה תקין עם כל המאפיינים, החומרים וחותמות הזמן המנדטוריים במקומות הנכונים — היא אינה פירושה שהחתימה תקפה מבחינה קריפטוגרפית, ששרשרת התעודות מגיעה לשורש שאתה סומך עליו, או שאף תעודה בשרשרת לא בוטלה. המאמת אינו מבצע בכוונה שום אימות קריפטוגרפי: הוא אינו מחשב מחדש את החתימה מעל ה-ByteRange, אינו בונה או מעריך את שרשרת האמון, ואינו בודק את מצב ביטול ה-OCSP או ה-CRL. הפיצול הזה הוא מכוון ומועיל, מכיוון שבדיקה מבנית היא מהירה, דטרמיניסטית לחלוטין, ואינה דורשת מפתחות, רשת או קריפטו של הפלטפורמה, ולכן ValidatePades רצה באופן זהה ב-Windows,‏ Linux ו-macOS כ-Pascal טהור מעל בתיי הקובץ. אימות שרשרת אמון, לעומת זאת, אינו נפרד ממדיניות — באילו שורשים אתה סומך, כיצד אתה מאחזר ביטולים, עד כמה סובלנות חותמת הזמן שלך קשיחה — והוא שייך לשלב מאוחר יותר התלוי במאגר התעודות של הפלטפורמה, לכן התייחס ל-ValidatePades עוברת כשער ההכרחי לכך שחתימה מעוצבת נכון, ואז מסור חתימה תקינה מבנית לאימות קריפטוגרפי אמיתי לפני שתסתמך עליה

מעבר מבני זה הוא המקום הנכון להתחיל בו, והוא משתלב באופן טבעי עם הבדיקות הרחבות יותר של קריאה בלבד בביקורת סיכוני אבטחה של PDF באמצעות PDFium Component ועם עבודת תאימות פורמטים כגון אימות מסמכי PDF/X מוכנים להדפסה. ברגע שתוכל לקרוא ולסווג חתימה, השלב הבא הוא הפקת אחת: המאמר השני בסדרה זו מכסה חתימה על קובצי PDF עם PAdES B-B, והשלישי מרחיב זאת לחותמות זמן אמינות וחתימות ארוכות טווח של B-LT ו-B-LTA. בדיקת החתימה לקריאה בלבד ומאמת ה-ValidatePades המוצגים כאן הם חלק מ-PDFium Component עבור דלפי, C++Builder ולזרוס