מאמר טכני

ראיות LTV של PAdES ו-seed values ב-HotPDF

PDF שחתמתם עליו זה עתה הוא חתימת B-B ולא יותר. הוא מוכיח מי חתם ושהבייטים לא זזו, אבל הוא אינו נושא שום הוכחה שתעודת החותם הייתה תקפה בעת החתימה, כך שמאמת שנים מאוחר יותר צריך לצאת לחפש נתוני ביטול שאולי כבר אינם קיימים. סגירת הפער פירושה כתיבת תגובות OCSP ו-CRL אל מאגר האבטחה ברמת המסמך, וב-HotPDF זו קריאה אחת: PopulatePAdESLTVEvidence עובר על כל חתימה טעונה, גוזר את בקשות הביטול מקבוצת התעודות, מבצע אותן דרך transport שאתם מספקים, וכותב את החומר שנמשך ועוד שרשרת ה-CMS אל ה-DSS. הוא מחזיר את מספר החתימות שהראיות שלהן נחתו, או מינוס אחת כשלמסמך אין שדה חתימה בכלל

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

למה הספרייה מסרבת לבצע HTTP משלה?

כי המקומות שדורשים חתימות B-LT הם המקומות שבהם לא ניתן לסמוך על ספרייה עם הרשת. שירותי חתימה רצים מאחורי proxies מאמתים עם שורשים ארגוניים. לשכבות חתימה מנותקות רשת אין נתיב אל responder והן חייבות לקבל ראיות מטמון. משטרי ביקורת דורשים שכל בקשה יוצאת תירשם ביומן על ידי האפליקציה, ולא תיקבר בתוך תלות. וסביבות בדיקה צריכות תגובות דטרמיניסטיות, מה שבלתי אפשרי אם הספרייה מחייגת בעצמה

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

זרימת PopulatePAdESLTVEvidence של HotPDF: ה-transport של FetchEvidence שהקורא מספק, שדות record הבקשה ותוצאות סטטוס לכל חתימה
כל בייט רשת עובר דרך ה-callback של FetchEvidence שלכם, ולכל חתימה יש סטטוס משלה כך ש-timeout אחד לעולם לא מבטל את הריצה
function FetchEvidence(const Request: THPDFSignatureEvidenceRequest;
  Attempt: Integer; CancellationToken: THPDFCancellationToken;
  out Response: TBytes; out RetryAfterMS: Cardinal;
  out ErrorMessage: UnicodeString): THPDFSignatureEvidenceTransportStatus;
begin
  RetryAfterMS := 0;
  try
    // Request.Kind אומר אם זו OCSP POST או CRL GET;
    // Request.ContentType ו-Request.Body כבר מוכנים,
    // ו-Request.MaxResponseBytes היא התקרה שעליכם לכבד
    Response := HttpExchange(Request.URI, Request.ContentType,
      Request.Body, Request.MaxResponseBytes);
    Result := setsSucceeded;
  except
    on E: Exception do
    begin
      ErrorMessage := E.Message;
      // setsRetry מאפשר למדיניות הניסיון החוזר להרפות; השתמשו
      // ב-setsPermanentFailure עבור 404 או URL שגוי
      Result := setsRetry;
    end;
  end;
end;

// שדרוג B-B אל B-LT בקריאה אחת לכל חתימה בקובץ הטעון
var
  Pdf: THotPDF;
  Upgraded: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginIncrementalUpdate('signed.pdf');
    Upgraded := Pdf.PopulatePAdESLTVEvidence(FetchEvidence,
      THPDFSignatureEvidenceRetryPolicy.Default);
    if Upgraded > 0 then
      // שמירת הוספה בלבד: הבייטים שהחתימות הקיימות מכסות
      // נשמרים מילה במילה
      Pdf.SaveIncrementalUpdate('signed-lt.pdf');
  finally
    Pdf.Free;
  end;
end;

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

השרשרת שה-CMS שכח לכלול

בדיקת ביטול צריכה את תעודת המנפיק, ומספר מפתיע של ערימות חתימה משמיט intermediates ממיכל ה-CMS. נתיב ההתאוששות הוא הרחבת Authority Information Access, שיטת הגישה 1.3.6.1.5.5.7.48.2, שמפרסמת URL שממנו אפשר להוריד את תעודת המנפיק. HPDFFetchAIAIntermediates עובר על אותם URL דרך אותו transport, מנתח את ה-DER מתוך כל תגובה, ומחזיר רק את התעודות שה-CMS כבר לא נשא, ממופתח לפי hash של DER כך שכפילויות ולולאות אינן יכולות להסתחרר

שני פרטים מכריעים אם זה עובד מול רשויות תעודה אמיתיות. הראשון הוא הקידוד: נקודות קצה של CA מגישות את התעודה כ-DER חשוף בערך באותה תדירות כמו PEM משוריין, ואין סוג תוכן אמין להבחין ביניהם. החישה העמידה היא טקסטואלית ואז מבנית. חפשו את הסמן -----BEGIN CERTIFICATE-----, הסירו את השריון ופענחו base64 אם הוא קיים, ובשני הנתיבים אמתו שהבייט הראשון של התוצאה הוא $30, תג ה-DER עבור SEQUENCE. השני הוא העומק: intermediate שנמשך יכול בעצמו לפרסם URL של AIA עבור המנפיק שלו, ולכן המהלך מוסיף מועמדים חדשים לתור ומשלים שרשראות קצרות בשני או שלושה קפיצות. יש להגביל זאת, ולזה מיועד הפרמטר MaxFetch

תרשים השלמת שרשרת AIA עבור HotPDF: משיכת URL של caIssuers, חישה של PEM מול DER, ניפוי כפילויות לפי hash של DER ותקרת העומק של MaxFetch
HPDFFetchAIAIntermediates עובר על URL של caIssuers דרך אותו transport, חש PEM משוריין ומגביל את התור עם MaxFetch

מהו seed value של חתימה, ומדוע הוא נכשל בשקט?

seed value הוא אילוץ שמחבר המסמך מצמיד לשדה חתימה כדי לומר לחותם איזה סוג חתימה מקובל: איזה SubFilter, איזה אלגוריתם digest, אילו סיבות, איזו גרסת PDF מינימלית, האם מידע ביטול חייב להיות מוטמע. הוא חי במילון /SV על השדה ומוגדר ב-ISO 32000-1 §12.7.5.5. HotPDF כותב אותו עם AttachPAdESSeedValue ובודק אותו עם CheckLoadedSignatureSeedValue, שמחזיר True כשהשדה בלי אילוצים או שכל אילוץ קיים עובר, וב-False קורא בשם האילוץ הנכשל הראשון דרך פרמטר פלט שאפשר להכניס ישירות להודעת שגיאה

המנגנון שהופך seed values לקלים לטעות בהם הוא ערך הדגלים /Ff המתואר ב-§12.7.5.5.3. ביט דלוק מסמן את האילוץ שלו כחובה: אי התאמה היא שגיאה והחותם חייב לסרב. ביט כבוי מסמן את אותו אילוץ כהעדפה: הערך מסנן מה ה-UI צריך להציע ולא יותר. שתי מלכודות נובעות מזה. ראשית, /Ff חי בתוך מילון ה-/SV, ולא על annotation ה-widget, ולכן קוד שקורא את ה-/Ff ברמת השדה מקבל תשובה ריקה לנצח ומסיק ששום דבר אינו כפוי. שנית, ההקצאות של הביטים אינן רצף פשוט של אחד, שניים, ארבע, שמונה; ב-HotPDF הכותב פולט 2 עבור SubFilter, 4 עבור MinVersion, 32 עבור AddRevInfo ו-64 עבור DigestMethod. קורא שמניח ביטים עוקבים מפענח כל אילוץ כאופציונלי ועובר כל בדיקה חוץ מהבדיקה החשובה

טבלת ביטי דגל של seed value לחתימת PAdES ב-HotPDF שמראה ביטי Ff 2, 4, 32 ו-64 וטיפול באילוץ כחובה מול כהעדפה
ערך /Ff חי בתוך /SV, וכל מיקום ביט מכריע אם אי התאמה היא סירוב מוחלט או העדפת UI
var
  Violation: AnsiString;
begin
  // שאלו את השדה אם הפרופיל שאנחנו עומדים לחתום איתו מותר
  if not Pdf.CheckLoadedSignatureSeedValue(0, 'ETSI.CAdES.detached',
       'SHA256', 'Approved for payment', 1, Violation) then
    raise Exception.Create('Signature field rejects this profile: ' +
      String(Violation));
  // האילוץ התקיים: ממשיכים בריצת החתימה
end;

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

איפה זה יושב על סולם ה-LTV

ארבע דרגות, וכל אחת צריכה את הדרגה שמתחתיה. B-B היא החתימה החשופה. B-T מוסיף חותמת זמן מהימנה, שמקבעת את שעת החתימה כך שמאמת יודע לאיזה רגע להעריך את הביטול. B-LT מוסיף את ראיות הביטול אל ה-DSS, וזה מה ש-PopulatePAdESLTVEvidence מבצע אוטומטית. B-LTA מוסיף חותמות זמן של מסמך שמתחדשות לפני שהקודמת מתחלשת, ומאריך תוקף ללא הגבלה; HotPDF חושף זאת כ-RenewPAdESLTATimestamp, שמוסיף חותמת זמן חדשה כגרסה מצטברת ומשמר כל חתימה, חותמת זמן ורשומת DSS קודמים ללא מגע

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

אזהרה מעשית אחת לגבי הסדר. אספו ראיות בהקדם האפשרי אחרי החתימה, רצוי באותה משימה. ה-responders שיכולים לענות עבור תעודה מחוברים כשהתעודה בתוקף ונעלמים שנים אחר כך, ולכן מסמך שעוזב את הצינורית שלכם כ-B-B עשוי שלא להיות ניתן לשדרוג שוב לעולם. HotPDF רץ כרכיב VCL מקורי עבור Delphi ו-C++Builder, וריצת הראיות כולה בתוך התהליך חוץ מה-transport שלכם; הפרופילים הנתמכים מופיעים בדף המוצר של HotPDF Delphi PDF component