מאמר טכני

רשימות מהימנות של eIDAS וחתימות PDF מוסמכות

להכריע שחתימת PDF מוסמכת תחת eIDAS פירושו לענות על שאלה שאין לה שום קשר לקריפטוגרפיה: האם התעודה הונפקה על ידי שירות אמון שמדינה חברה רשמה כמוסם, ברגע שהחתימה נעשתה. התשובה חיה ברשימה מהימנה, מסמך XML שמתפרסם לכל טריטוריה, וכל ערכו של אותו מסמך תלוי באותנטיות שלו. לכן רכיב PDFium מסרב להביט בפנים של כזו עד שמישהו ערב לה. TPdfEuropeanTrustedList.ParseAuthenticated מוסיר את כל הבייטים הגולמיים אל IPdfTrustedListAuthenticator שהקורא מספק לפני שהוא מנתח שירות יחיד, והוא יוצר snapshot רק אם המאמת הזה עובר במפורש

זרימת אימות-לפני-ניתוח עבור רשימה מהימנה אירופית ב-Delphi: XML גולמי ממשיכה טרייה או snapshot במטמון עובר דרך IPdfTrustedListAuthenticator לפני ש-TPdfEuropeanTrustedList מנתח משהו
רשימות טריות ובמטמון פוגשות את אותו מאמת, ו-snapshot קיים רק אחרי שהוא עובר

הסדר הזה הוא העיצוב. הכול אחר בתכונה הזאת נובע ממנו, כולל החלקים שנראים לא נוחים

מנותח אינו מהימן

רשימה מהימנה שמנותחת נקי אומרת לכם שה-XML תקין מבחינה תחבירית. היא לא אומרת לכם דבר על מי כתב אותה. מאחר שהרשימה היא מה שההחלטה כולה שלכם על סטטוס מוסמך נשענת עליה, לקבל אחת רק כי היא מנותחת היה הופך את ההחלטה לחסרת משמעות: תוקף שיכול להחליף את הרשימה יכול להכריז על רשות תעודה משלו כמוסמכת

אותו היגיון חל על מטמון, וזו המלכודת ששווה לקרוא בשמה. מטמון ה-snapshot מאחסן את ה-XML המקורי יחד עם digest של SHA-256, וקל היה להתייחס ל-digest תואם בעת טעינה כהוכחה שהרשימה אמיתית. הוא איננו. digest שחושב על ידי אותו תהליך שאחסן את הקובץ, בלי שום מפתח מעורב, מאמת רק שהבייטים לא השתנו מאז שכתבתם אותם; אם הרשימה הייתה מזויפת כשנשמרה במטמון, ה-digest מאשר שזו אותה רשימה מזויפת. לכן טעינת snapshot ממטמון עוברת דרך אותו מאמת כמו ניתוח של טרייה. שלמות ואותנטיות הן תכונות שונות ורק אחת מהן צריכה מפתח

uses
  FPdfTrustedList;

type
  TListAuthenticator = class(TInterfacedObject, IPdfTrustedListAuthenticator)
  public
    function Authenticate(const XmlData: TBytes;
      out AuthenticationDetails: string): Boolean;
  end;

function TListAuthenticator.Authenticate(const XmlData: TBytes;
  out AuthenticationDetails: string): Boolean;
begin
  // המדיניות שלכם חיה כאן: אמתו את חתימת ה-XMLDSIG העטופה
  // מול תעודת חתימת הרשימה שנעצה מחוץ לערוץ,
  // ותארו מה בדקתם עבור שובל הביקורת
  Result := VerifyEnvelopedXmlSignature(XmlData, FPinnedListSigner);
  if Result then
    AuthenticationDetails := 'XMLDSIG verified against pinned LOTL signer';
end;

var
  List: TPdfEuropeanTrustedList;
  Cache: TFileStream;
begin
  List := TPdfEuropeanTrustedList.ParseAuthenticated(RawXml,
    TListAuthenticator.Create, TPdfTrustedListOptions.Default);
  // ה-snapshot קיים רק כי המאמת אמר כן
  Cache := TFileStream.Create('tl-de.snapshot', fmCreate);
  try
    List.SaveCache(Cache);
  finally
    Cache.Free;
  end;
end;

המאמת אינו קובע את מדיניות הרשת

למאמת PAdES אין עסק להכריע כיצד להגיע אל רשימת רשימות האמון, אם דרך proxy, באיזו תדירות לנסות שוב, או מה לעשות כשטריטוריה בלתי נגישה. אלה החלטות אפליקציה ופריסה, ובסביבות מוסדרות הן מבוקרות לעיתים קרובות. לכן עדכונים מגיעים דרך IPdfTrustedListSource, שמקבל URI ותקרת בייטים ומחזיר בייטים

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

אינווריאנטי עדכון שאוכף רכיב הרשימה המהימנה של PDFium: טריטוריה ללא שינוי, מספר רצף שגדל בקפדנות ושעת הנפקה שלעולם לא נעה אחורה, שיחד חוסמים התקפות הורדת גרסה
שלוש בדיקות מונוטוניות מפרידות עדכון אמיתי מרשימה מנוגנת שוב או מוחלפת

מגבלות מנתח, ובלי DTD כלל

TPdfTrustedListOptions מגביל את גודל ה-XML, מניין האסימונים, עומק הקינון, מספר השירותים, מספר התעודות וגודל תעודה בודדת, עם פונקציית מחלקה Default שמספקת ערכים שמישים. רשימות מהימנות הן מסמכים מפורסמים בגודל צפוי, ולכן הגבולות זולים לקביעה ואין רשימה לגיטימית שצריכה לחרוג מהם

בנפרד ובלא תנאי, המנתח דוחה הצהרות DTD וישויות. זה סוגם הן את מניעת השירות של הרחבת ישויות והן את נתיב חשיפת ישויות חיצוניות בסירוב אחד, וזה לא עולה דבר כי רשימות מהימנות לא משתמשות בישויות. כל מנתח XML שנגיש מקלט לא מהימן צריך להיות מוגדר כך; ההבדל כאן הוא שהסירוב אינו ניתן לתצורה, ולכן אי אפשר לכבות אותו בשינוי אפשרות מתוך רצון טוב

סטטוס מוסמך נרשם לצד אמון השרשרת, לא מומזג אליו

צד ההערכה נפרד בכוונה. TPadesTrustValidationOptions.QualifiedTrustEvaluator מקבל IPdfQualifiedTrustEvaluator, שה-snapshot של הרשימה המהימנה מממש. במהלך האימות המעריך מקבל את תעודת העלה, את השרשרת ושעת אימות, מתאים תעודות שירות בהשוואת DER מדויקת מול החותם והשרשרת, משלב את סטטוס השירות, מזהה סוג השירות וה-URI של המאשרים באותה עת, ומחזיר רשומת הערכה

התוצאה נוחתת בשני מקומות על כל חתימה: QualifiedTrustStatus כסטטוס גס, ו-QualifiedTrust כהערכה המלאה עם טריטוריה, שם ספק, שם שירות, מזהה סוג, סטטוס ושעת התחלה של הסטטוס. מה שהוא לא עושה הוא לשנות את CertificateTrustStatus. אמון שרשרת מערכת וסטטוס מוסמך עונים על שאלות שונות, ודוח שמצמיד אותם אינו יכול להבחין בין "מהימן אבל לא מוסמך" לבין "מוסמך אבל השרשרת לא מאומתת", שניהם אמיתיים ושניהם דורשים טיפול שונה

הערכת אמון מוסמך של PAdES ב-Delphi: ה-IPdfQualifiedTrustEvaluator מתאים תעודות שירות בהשוואת DER מדויקת וממלא את QualifiedTrustStatus ו-QualifiedTrust בזמן ש-CertificateTrustStatus נשאר ללא מגע
אמון שרשרת וסטטוס מוסמך של eIDAS נרשמים זה לצד זה כך ששני הממצאים נשארים נראים
var
  Options: TPadesTrustValidationOptions;
  Report: TPadesValidationResult;
  I: Integer;
begin
  Options := TPadesTrustValidationOptions.Default;
  Options.CheckRevocation := True;
  Options.QualifiedTrustEvaluator := List;      // ה-snapshot המאומת
  Options.QualifiedValidationTime := SigningTime; // לא Now
  Report := Pdf.ValidatePadesTrust(Options);

  for I := 0 to High(Report.Signatures) do
    if Report.Signatures[I].QualifiedTrustStatus = pcsValid then
      Writeln(Format('signature %d qualified by %s / %s (%s)',
        [I, Report.Signatures[I].QualifiedTrust.Territory,
         Report.Signatures[I].QualifiedTrust.ProviderName,
         Report.Signatures[I].QualifiedTrust.ServiceName]))
    else if Report.Signatures[I].QualifiedTrustStatus = pcsIndeterminate then
      // אין שירות תואם, או שה-snapshot אינו יכול לענות לזמן הזה
      Writeln(Format('signature %d: qualified status undetermined', [I]));
end;

למה שעת האימות אינה עכשיו

כי מוסמכות היא תכונה של רגע. שירות אמון יכול לקבל סטטוס מוסמך, לאחר מכן לאבד אותו בהשמטה, ומאוחר עוד להיות מוחזר, וכל אחד מהמעברים האלה נושא שעת התחלה ברשימה. חתימה שנעשתה בזמן שהשירות היה מוסמך נשארת מוסמכת לאחר מכן; חתימה שנעשתה לפני המענק אינה הופכת מוסמכת רטרואקטיבית. הערכה מול הזמן הנוכחי לכן נותנת תשובה שגויה בשני הכיוונים

הרשימה נושאת את מה שנחוץ לכך: לכל רשומת שירות יש שעת התחלה של סטטוס ודגל שמבחין רשומות היסטוריות מעדכניות, והמעריך משלב אותן מול הזמן שאתם מספקים. בפועל הזמן הזה בא מחותמת זמן מהימנה על החתימה ולא משעת החתימה הנטענת ב-CMS, וזו הסיבה שחומר אימות לטווח ארוך חשוב גם עבור שאלה שנראית כחיפוש מדיניות; צד חותמת הזמן וה-DSS מכוסה במאמר החתימות לטווח ארוך

מה עדיין עליכם לבנות

שלושה דברים, ואף אחד מהם לא שייך לספריית PDF. המאמת, כלומר אימות XMLDSIG בפועל מול תעודת חתימת רשימה שהשגתם דרך ערוץ שאתם סומכים עליו. מדיניות המשיכה, כלומר כיצד ובאיזו תדירות אתם מרעננים, ומה האפליקציה שלכם עושה כשרענון נכשל. והיקף הטריטוריות, כלומר אילו רשימות אתם נושאים מלכתחילה, שהיא החלטה עסקית לגבי באילו מדינות חברות הצדדים שלכם חותמים

מה שאתם מקבלים מהרכיב הוא החלק שקל לטעות בו בעדינות: סדר אימות-לפני-ניתוח, ניתוח XML מסוגר וחסר ישויות, אינווריאנטי עדכון מונוטוניים, התאמת שירות לפי DER מדויק, הערכת סטטוס היסטורי, ותוצאה שנשארת נפרדת מאמון שרשרת רגיל. אם הבעיה המיידית שלכם בסיסית יותר, שמאמת דוחה חתימה שאתם סבורים שהיא תקינה, הגורמים הרגילים מקוטלגים במדוע מאמתים דוחים חתימות PAdES, ומשטח בחינת החתימות מתואר בבחינת חתימות ורמות PAdES. יכולות הרכיב מופיעות בדף המוצר של PDFium Delphi component