מאמר טכני

חתימת PAdES מרוחקת ב-PDFium VCL: HSM ומפתחות ענן

PDFiumPas מפצל חתימת PAdES לשתי קריאות כך שהמפתח הפרטי לעולם לא צריך להיות בתהליך שלכם. PreparePadesRemoteSignature כותב עדכון מצטבר עם מציין מקום ריק וברוחב קבוע ב-/Contents ומחזיר רשומת בקשה שנושאת את תקציר המסמך SHA-256, את ה-ByteRange המדויק וטביעת אצבע של הקובץ המוכן. CompletePadesRemoteSignature לוקח את ה-CMS המנותק ששירות החתימה שלכם מחזיר ומטמיע אותו לתוך אותו חריץ שמור

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

מדוע מפתח מרוחק לא יכול להשתמש בקריאת החתימה הרגילה?

מפני ש-SignPadesBytes מניחה שפעולת החתימה מתרחשת בתוך הקריאה. היא בונה את העדכון המצטבר, מחשבת את התקציר מעל ה-ByteRange, חותמת עליו, וכותבת את התוצאה, הכל לפני שהיא חוזרת. זה בדיוק נכון כשהמפתח יושב בחנות התעודות של Windows או בקובץ PKCS#12 שטענתם

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

הפרוטוקול הדו-שלבי

שלב אחד מכין את המסמך. PDFiumPas מוסיף את שדה החתימה ומילון הערך, שומר ContentsSize בייטים של מרחב מקודד-הקס ב-/Contents, מחשב את ה-ByteRange סביב אותו שמור, ומפיק TPadesRemoteSigningRequest המכיל את FormatVersion, PreparedFingerprint, DocumentDigest, את ByteRange בעל ארבעת האיברים, ContentsHexOffset ו-ContentsSize

הערך היחיד ששירות החתימה שלכם צריך הוא DocumentDigest: ה-SHA-256 שה-SignedData מסוג CAdES המוחזר חייב לשאת כתקציר ההודעה שלו. כל השאר ברשומה קיים כדי ששלב שני יוכל להוכיח שהקובץ שהוא משלים הוא הקובץ שממנו חושב אותו תקציר

uses
  FPdfPades;

var
  Options: TPadesRemoteSignOptions;
  Request: TPadesRemoteSigningRequest;
  Source, Prepared, Session: TFileStream;
begin
  Options := TPadesRemoteSignOptions.Default;
  Options.Reason := 'Approved by finance';
  Options.Location := 'Lisbon';
  Options.Name := 'A. Moreira';
  Options.SigningTimeUtc := NowUtc;
  Options.ContentsSize := 16384;   // בייטי הקס שמורים ל-CMS

  Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
  Prepared := TFileStream.Create('contract.prepared.pdf', fmCreate);
  try
    PreparePadesRemoteSignature(Source, Prepared, Options, Request);
  finally
    Prepared.Free;
    Source.Free;
  end;

  // שמרו את ההפעלה כך שריצה מאוחרת יותר - או מכונה אחרת - תוכל לסיים אותה
  Session := TFileStream.Create('contract.signreq', fmCreate);
  try
    SavePadesRemoteSigningRequest(Session, Request);
  finally
    Session.Free;
  end;

  SendDigestToSigningService(Request.DocumentDigest);
end;

מה Complete דוחה, ומדוע כל בדיקה קיימת?

ההשלמה היא המקום שבו עיצוב חתימה מרוחקת בדרך כלל משתבש, כך שהאימות בלתי מתפשר בכוונה. CompletePadesRemoteSignature דוחה PDF מוכן שטביעת האצבע שלו כבר לא תואמת את הבקשה, ByteRange שלא תואם לקואורדינטות מציין המקום שנרשמו, תוחמי /Contents ששונו, מציין מקום שכבר אינו ריק, CMS גדול מהשמור, CMS שאינו בדיוק ערך DER אחד, צורת SignedData לא נתמכת, תכונת signing-certificate-v2 חסרה, ו-CMS שתקציר ההודעה שלו לא שווה לתקציר המסמך המוכן

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

דרישת signing-certificate-v2 היא עניין תאימות PAdES ולא עניין שלמות. ETSI EN 319 142 דורש שתעודת החתימה תהיה קשורה בתוך התכונות החתומות, ו-CMS שחסר את התכונה הזו אינו חתימת PAdES גם אם הוא מאומת קריפטוגרפית. דחייתו בהשלמה פירושה שאתם מגלים את זה כאן, לא בדוח אימות מלקוח, נושא שנחקר בהרחבה במדוע מאמתים דוחים חתימות PAdES

var
  Request: TPadesRemoteSigningRequest;
  Session, Prepared, Dest: TFileStream;
  CmsDer: TBytes;
begin
  Session := TFileStream.Create('contract.signreq', fmOpenRead);
  try
    Request := LoadPadesRemoteSigningRequest(Session);
  finally
    Session.Free;
  end;

  CmsDer := FetchDetachedCmsFromService;   // מוחזר על ידי ה-HSM או ה-TSP

  Prepared := TFileStream.Create('contract.prepared.pdf', fmOpenRead);
  Dest := TFileStream.Create('contract.signed.pdf', fmCreate);
  try
    try
      CompletePadesRemoteSignature(Prepared, Dest, Request, CmsDer);
    except
      on E: EPadesCrypto do
        // כל דחייה נושאת סיבה ספציפית; רשמו אותה מילה במילה
        FailSession(E.Message);
    end;
  finally
    Dest.Free;
    Prepared.Free;
  end;
end;

חציית גבולות תהליך ומכונה

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

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

קביעת גודל מציין המקום

ContentsSize הוא הפרמטר היחיד שחייבים לחשוב עליו, מפני שהוא נקבע לפני שה-CMS קיים בכלל. הוא סופר את השמור המקודד-הקס, כך ש-CMS מסוג DER בגודל 6 KB זקוק לפחות ל-12 KB של מקום, והמימוש מגביל את השמור ל-64 MiB

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

מה בא אחרי החתימה

חתימה מרוחקת שהושלמה היא PAdES B-B. אימות ארוך-טווח זקוק לחותמת זמן ולחומר האימות, שהם עדכון מצטבר נפרד שמוסיף DSS ומילוני VRI פר-חתימה שלו, המתוארים בחתימות ארוכות-טווח עם חותמות זמן RFC 3161 ו-DSS. זה שלב מקומי: הוא מוסיף תעודות, תגובות OCSP ו-CRL-ים, אף אחד מהם לא זקוק למפתח הפרטי

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

PDFiumPas הוא רכיב ל-Delphi ול-Lazarus סביב מנוע PDFium עם מחסנית PAdES טבעית ב-Pascal, כך שחתימה, חותמות זמן ואימות עובדים ללא כלי שורת פקודה חיצוניים. תיעוד API מלא וגרסת ניסיון נמצאים בדף רכיב PDFium ל-Delphi