חתימת PDF היא בעיקר חשבונאות בייטים, וחשבונאות בייטים היא המקום שבו זה הולך לא כשורה. הקריפטוגרפיה רצה על קוד שנבדק במשך שני עשורים, וחלק זה כמעט ולא נכשל. מה שנכשל בייצור הוא צנוע יותר: מקום שמור שנקבע קטן מדי לחתימה האמיתית, hash שנלקח על פני קטע לא נכון של הקובץ, או "שמירה" לאחר חתימה שבשקט כתבה מחדש בייטים שהחתימה כבר הקפיאה. הנח את הבייטים בצורה נכונה וסימן הביקורת הירוק ידאג לעצמו
HotPDF מכסה חתימה עבור Delphi ו-C++Builder בשלושה רמות, ואתה בוחר ביניהן על ידי מענה על שאלה אחת: היכן חי המפתח הפרטי? קובץ PFX על הדיסק זקוק לקריאת פונקציה בודדת. מפתח נעול ב-HSM או שירות חתימה מרוחק זקוק לרצף reserve-hash-insert, מפני שאין ספרייה שיכולה להגיע לתוך טוקן ולשלוף את המפתח. חתימה שצריכה לעמוד בתקנות אירופאיות זקוקה למבני PAdES baseline מעל לכך. הסעיפים להלן עוקבים אחרי ההתקדמות הזו
כיצד /ByteRange מצמד את הבייטים החתומים
חתימה חייבת לחיות בתוך הקובץ שהיא חותמת, ואינה יכולה לחתום על עצמה. PDF מתמודד עם הפרדוקס על ידי השארת חור. לפני החתימה, הכותב שומר ערך /Contents בגודל קבוע מלא אפסים ומתעד מערך /ByteRange עבור שתי הטווחים משני צידיו: כל מה שלפני החור, כל מה שאחריו. החותם hash-ת שני הטווחים הללו וכותב את BLOB CMS שנוצר לתוך החור כהקסדצימלי. המלכודת נמצאת במילה קבוע. אתה מתחייב לגודל החור הזה לפני שאתה יודע כמה גדולה תהיה החתימה הגמורה, אז ההשמרה חייבת להיות הערכת-יתר בטוחה. שמונה קילובייטים מחזיקים בנוחות חתימת CMS מנותקת עם שרשרת אישורים קצרה
HotPDF מפצל את שני המקרים לשתי קריאות, ובלבול ביניהן הוא טעות מוקדמת נפוצה. AddSignatureField מפיל שדה ריק וגלוי שאדם יחתום עליו מאוחר יותר בצופן. AddSignedSignatureField יוצר את השדה ושומר את חור ה-/Contents, שהוא זה שאתה רוצה כאשר קוד, ולא אדם, ישלים את החתימה. תן לחותם חיצוני שדה ריק ואין לו מה למלא
נתיב הקריאה הבודדת: חתימה מ-PFX
כאשר האישור והמפתח הפרטי שלו יושבים בקובץ PFX/PKCS#12 שהתהליך שלך יכול לקרוא, הצינור כולו מצטמצם לפונקציית מחלקה:
if THotPDF.SignPDFWithPFX('invoice-unsigned.pdf', 'invoice-signed.pdf',
'company-cert.pfx', 'pfx-password') then
Writeln('Signed: invoice-signed.pdf')
else
raise Exception.Create('PFX signing failed');
כאשר זה נכשל, ה-PDF הוא לעתים רחוקות הבעיה. ה-PFX הוא. HotPDF קורא מיכלים המוגנים עם PBES2, כלומר נגזרת מפתח PBKDF2 על AES-256-CBC. PFX שיוצא אל ידי אשף אישורים ישן של Windows, או OpenSSL לפני גרסה 3.0, עטוף בדרך כלל ב-RC2 ישן או 3DES במקום, ופשוט לא ינתח. התיקון הוא לייצא מחדש את המיכל פעם אחת עם הגנה מודרנית; OpenSSL של היום עושה זאת כברירת מחדל, ואינו שינוי קוד. לכן כאשר חתימה מתה מיידית על אישור שעובד בכל מקום אחר, הסתכל כיצד ה-PFX נוצר לפני שאתה חושד בקוד שלך
נתיב reserve-hash-insert עבור HSMs וטוקנים
נתיב הקריאה הבודדת מניח שהתהליך שלך יכול לקרוא את המפתח כקובץ. יותר ויותר הוא לא יכול. המפתח יושב ב-HSM, על טוקן USB, או מאחורי ה-API של שירות חתימה, ואין דרך לספרייה להגיע אליו ישירות. HotPDF מטפל בכך על ידי פירוק החתימה לצעדים ברמת בייטים: כתוב מסמך placeholder, בקש מהספרייה את טווחי ה-hash, העבר את קלט ה-hash למה שמחזיק את המפתח, ואז שטח את ה-CMS שהוחזר לתוך החור
var
Doc: THotPDF;
Fs: TFileStream;
PdfBytes, HashInput, SigHex: AnsiString;
R1Start, R1Len, R2Start, R2Len, CStart, CLen: Integer;
begin
// 1. Write the document with a reserved /Contents hole
Doc := THotPDF.Create(nil);
try
Doc.FileName := 'placeholder.pdf';
Doc.BeginDoc;
Doc.CurrentPage.AddSignedSignatureField('Sig1',
Rect(50, 100, 350, 150), 8192, 'adbe.pkcs7.detached',
'Contract approval', 'Boston, MA', 'legal@example.com');
Doc.EndDoc;
finally
Doc.Free;
end;
// 2. Load the saved bytes; the returned offsets are 0-based
Fs := TFileStream.Create('placeholder.pdf', fmOpenRead);
try
SetLength(PdfBytes, Fs.Size);
Fs.ReadBuffer(PdfBytes[1], Fs.Size);
finally
Fs.Free;
end;
THotPDF.PreparePDFForSigning(PdfBytes, R1Start, R1Len, R2Start, R2Len,
CStart, CLen);
// 3. Hash both spans and sign externally (HSM, token, service)
HashInput := Copy(PdfBytes, R1Start + 1, R1Len) +
Copy(PdfBytes, R2Start + 1, R2Len);
SigHex := SignWithHsm(HashInput); // your integration: returns CMS as hex
// 4. Splice the signature into the reserved hole
THotPDF.InsertSignatureHex(PdfBytes, SigHex);
Fs := TFileStream.Create('signed.pdf', fmCreate);
try
Fs.WriteBuffer(PdfBytes[1], Length(PdfBytes));
finally
Fs.Free;
end;
end;
שני פרטים ברצף זה גורמים לרוב הכשלים המתקופים. הראשון הוא ש-PreparePDFForSigning עובד על הבייטים של קובץ גמור. ה-placeholder חייב להיכתב ולהישמר במלואו לפני שה-offsets משמעותיים; חשב אותם מול זרם שעדיין מורכב ולא יסתדרו עם הבייטים שתשרוף לבסוף. השני הוא גודל ההשמרה, שוב. 8192 הבייטים שביקשת חייבים להחזיק את ה-CMS הסופי, וחתימה הנושאת אישורים ביניים, או כזו שהשירות מעטר עם תכונות חתומות, יכולה לחרוג ממנה. InsertSignatureHex לא יגדיל את החור כדי לפנות מקום. הרמז הוא צינור שחותם בסדר עם אישור אחד ונכשל עם הבא; התיקון הוא לשחזר את ה-placeholder עם השמרה שנמדדה מחתימה אמיתית שנוצרה על ידי החותם בפועל, לא מנחשת
בסיסי PAdES, וחותמות הזמן שמחזיקות חתימה בחיים
אם אתה חותם לפי כללים אירופאיים, התקן הפועל הוא ETSI EN 319 142-1, שמערים ארבעה רמות PAdES baseline. B-B היא החתימה הפשוטה. B-T מוסיפה חותמת זמן מהימנה שמוכיחה מתי נעשתה. B-LT מטמיעה את חומר האימות, האישורים ונתוני ביטול, בתוך המסמך כדי שניתן יהיה לבדוק אותו שנים לאחר מכן. B-LTA מרבד חותמות זמן מסמך תקופתיות מעל, כך שהראיות שורדות את האלגוריתמים שעליהם נבנתה. HotPDF פולט את מבני צד-המסמך לכל רמה:
// PAdES baseline signature field (ETSI EN 319 142-1)
Pdf.CurrentPage.AddPAdESSignatureField(
'ApprovalSig', Rect(50, 100, 350, 150), 'B-B',
'Contract approval', 'Boston, MA', 'legal@example.com');
// Document timestamp: larger reservation for the TSA token and chain
Pdf.CurrentPage.AddDocumentTimestampSignature('ArchiveTS', 16384);
ההשמרה של 16384 בייטים על חותמת הזמן מכוונת. רשות חותמות זמן מחזירה טוקן שגורר את שרשרת האישורים שלה, כך שהיא בדרך כלל זקוקה ליותר מקום מ-8 KB שחתימה פשוטה מסתפקת בו. חותמות הזמן של המסמך הן גם המנגנון שמאחורי B-LTA: חתימה מחדש של ארכיון חתום כל כמה שנים, עם אלגוריתמים שעדיין נוכחיים, היא מה שמשאיר מסמך שחתמת ב-2026 ניתן לאימות ב-2040
מילה על מחרוזות הסיבה, המיקום ואנשי הקשר שקריאות השדה מקבלות: הן מטא-נתונים נוחים ותו לא. HotPDF מאחסן אותן כרשומות מילון פשוטות ומציג אותן במראה החתימה הגלוי, אך אף validator לא בודק אותן מול שום דבר. מלא אותן בעקביות מנתוני זרימת העבודה שלך, מפני שמבקרים אכן קוראים אותן, ואז אל תעולם תטעה אותן לראיות. הטענה הקריפטוגרפית האמיתית חיה כולה ב-CMS ושרשרת האישורים שלו, ו-validator מתעלם לחלוטין מהטקסט הגלוי
לאחר החתימה, הקובץ יכול רק לגדול
ברגע שחתימה קיימת, הבייטים בתוך טווחיה קפואים. הדרך הלגיטימית היחידה לשנות את הקובץ לאחר מכן היא עדכון מצטבר לפי ISO 32000-1 §7.5.6, שמצרף אובייקטים חדשים ומשתנים אחרי הבייטים המקוריים ומשרשר חלק הפניה חוצה-הפניה חדש בחזרה אליהם. בדרך זו, החתימה נשארת תקפה לתיקון שלה וצופן מדווח על המצב הכנה: התיקון החתום שלם, המסמך הורחב לאחר מכן. סריאליזציה מחדש של הקובץ כולו במקום זה כותבת מחדש את הטווחים החתומים, מה שהורס את החתימה גם כאשר שום דבר גלוי לא השתנה. אותו מנגנון תיקון הוא גם כיצד מסמך אחד נושא מספר חתימות: כל חתימה חדשה נוחתת בעדכון מצטבר משלה, וטווחיה מכסים את כל מה שלפניה, כולל החתימות הקודמות. המכניקה append-only, ומתי בטוח לדחוס אותן, מכוסים במאמר על זרמי אובייקטים ועדכונים מצטברים
שתי גבולות ראויות להחזיק בראש בזמן שאתה מעצב. מצב פלט PDF/A של HotPDF דוחה שדות חתימה לחלוטין, כך שתאימות ארכיון וחתימה מוטמעת חייבות לשלוח כקבצים נפרדים. וחתימה לא אומרת דבר על סודיות: היא מוכיחה מי ייצר מסמך ושלא השתנה מאז, אך כל אחד עדיין יכול לקרוא אותו. הסתרת התוכן היא עבודה נפרדת, מטופלת על ידי הצפנת AES-256 ומדיניות הרשאות
מה שתבנה, בדוק אותו עם משהו שאינו הקוד שכתב את הקובץ. פתח את הפלט בלוח החתימות של Acrobat ואשר שלושה דברים: החתימה תקפה, הזהות מתחברת לשורש שציפית לו, והלוח מדווח על אין שינויים מאז החתימה. ואז הפוך בייט בודד בתוך הטווח החתום של עותק throwaway ואשר שהלוח מכנה כעת את המסמך שונה. צינור חתימה שמעולם לא ראית לדחות קובץ שנפגע הוא כזה שהאימות שלו לא נבדק באמת
כל שלושת שכבות החתימה מגיעות עם HotPDF Component עבור Delphi ו-C++Builder; דף המוצר מקשר את הפניית API החתימה המלאה