רכיב ה-PDFium חותם על PDF עם חתימה דיגיטלית מסוג PAdES B-B באמצעות שיטת ה-SignPades שלו: הוא טוען את המסמך, מבצע hash לטווח הבתים החתום, בונה מבנה CAdES CMS ומצרף את החתימה כעדכון אינקרמנטלי. התשתית הקריפטוגרפית מיועדת ל-Windows בלבד, ולכן הגן על כל קריאה באמצעות PadesCryptoAvailable לפני שתחתום
התרחיש מוכר. חוזה PDF נוחת על שולחנך, המחלקה המשפטית רוצה שהוא ייחתם דיגיטלית לפני שהוא עובר הלאה, ואתה פונה לאותה בניית PDFium שבה אתה כבר משתמש כדי לרנדר ולבדוק מסמכים, רק כדי לגלות ש-PDFium אינה יכולה לכתוב חתימה כלל. ממשק ה-API לחתימות שלה הוא לקריאה בלבד. רכיב ה-PDFium סוגר פער זה על ידי בעלות על כל צינור החתימה ב-Pascal, מפונקציית ה-hash ועד להזרקה ברמת הבתים, ומאמר זה עובר על צינור זה מקצה לקצה
מדוע PDFium אינה יכולה לכתוב חתימה דיגיטלית?
PDFium חושפת חתימות כאובייקטים לקריאה בלבד ואינה מציעה דבר שיוצר אותן. משפחת FPDFSignatureObj_* מאפשרת לך למנות חתימה קיימת, לקרוא את ה-/Contents שלה ולבדוק את ה-/ByteRange שלה, אך אין מקבילה הבונה מילון חתימה, שומרת מקום ל-/Contents או כותבת טווח בתים; שמירה אינקרמנטלית קיימת (FPDF_SaveAsCopy עם FPDF_INCREMENTAL) אך אינה נושאת הוק (hook) של חתימה. כל רכיב החותם על PDF מעל PDFium חייב לפיכך לייצר כל בית של חתימה בעצמו, וזו הסיבה שרכיב ה-PDFium בונה את המנגנון משלוש יחידות Pascal טהורות. FPC 3.2.2 שולח את md5 ו-sha1 אך לא SHA-2 כלל, ומממשק ה-API של System.Hash SHA-256 בדלפי אינו תואם מקור עם FPC, ולכן FPdfSha256 הוא מימוש עצמאי של FIPS 180-4 השומר כל נתיב קוד CMS על סוג TSHA256Digest יחיד ללא הסתעפויות מהדר. FPdfAsn1 מספקת את מקודד וקורא ה-DER שמבני ה-CMS צריכים, ו-FPdfCms מרכיבה את ה-CAdES SignedData מעל שניהם
כיצד חותמים דיגיטלית על PDF בדלפי?
טען את המסמך, ולאחר מכן קרא ל-SignPades עם טביעת אצבע (thumbprint) של תעודה. רכיב ה-PDFium פותר את טביעת האצבע הזו מול מאגר התעודות \"MY\" של המשתמש הנוכחי, שולף את התעודה המתאימה ואת המפתח הפרטי שלה, וכותב עותק חתום לנתיב שאתה נוקב בשמו
uses
PDFium, FPdfCrypto;
procedure SignContract(const AThumbprint: string);
var
Pdf: TPdf;
begin
if not PadesCryptoAvailable then
raise Exception.Create('PAdES signing requires the Windows CNG backend');
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf'; // the document to sign
Pdf.Active := True;
// Second argument: SHA-1 thumbprint of a certificate in the Current
// User "MY" store. First argument: destination for the signed copy.
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
raise Exception.Create('Signing failed');
finally
Pdf.Free;
end;
end;
PadesCryptoAvailable הוא הבדיקה שאתה מבצע תחילה, תמיד. ב-Windows הוא מחזיר True ותשתית ה-crypt32/ncrypt פעילה; בכל פלטפורמה אחרת הוא מחזיר False וקריאה לחתימה תעלה חריגת EPadesCrypto. התייחסות להגנה זו כחובה מונעת מבניית Linux או macOS להיכשל בזמן ריצה בנתיב שאינו יכול לעבוד שם. טביעת האצבע עצמה היא ה-hash מסוג SHA-1 של התעודה, אותו ערך שמנהל התעודות של Windows מציג בלשונית ה-Details שלו, והיא נוקבת בשם חותם ספציפי מבלי להציב חומרי מפתח בקוד המקור שלך
מה נכנס ל-CMS: מאפיינים חתומים ו-RFC 5652
חתימת בסיס של PAdES אינה חתימת RSA גולמית על הקובץ; היא מבנה CAdES CMS SignedData הנושא קבוצה מנדטורית של מאפיינים חתומים, ו-FPdfCms.BuildSignedData פולט בדיוק את הקבוצה הזו: content-type, message-digest, ו-signing-certificate-v2, מאפיין ה-ESS הקושר את החתימה לתעודת החותם לפי hash. פרט אחד כאן מכשיל כמעט כל מימוש CMS שנוצר ידנית. תקן RFC 5652 §5.4 דורש שתמצית המאפיינים החתומים תחושב מעל קידוד DER SET OF, תג 0x31, בעוד שאותם מאפיינים נוסעים בתוך SignerInfo תחת תג IMPLICIT [0], 0xA0. רכיב ה-PDFium מקודד את קבוצת המאפיינים פעם אחת, מחשב תמצית לצורת ה-0x31, ואז משכתב רק את בית התג המוביל ל-0xA0 לצורך פליטה, כך שחוצץ אחד משרת את שני התפקידים ללא מעבר שני על העץ
var
Pdf: TPdf;
Opts: TPadesSignOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Opts := TPadesSignOptions.Default;
Opts.CertificateThumbprint := 'a1b2c3d4e5f6...'; // signer in the MY store
Opts.Reason := 'I approve this agreement';
Opts.Location := 'Berlin, DE';
Opts.ContentsSize := 16384; // hex width of /Contents
if not Pdf.SignPades('contract-signed.pdf', Opts) then
raise Exception.Create('Signing failed');
finally
Pdf.Free;
end;
end;
העמסת האפשרויות מוסיפה את מטא-נתוני מילון החתימה שמתקן ISO 32000-1 §12.8.1 מגדיר: Reason, Location, ContactInfo ו-Name, כולם אופציונליים וכולם נכתבים לתוך מילון ערך החתימה. מגבלה אחת שקל להיתקל בה: אם הגדרת את CommitmentTypeOid כדי להוסיף מאפיין חתום של CAdES המציין את סוג המחויבות (commitment-type-indication), אל תגדיר גם את Reason; תקן ETSI EN 319 142-1 §6.3 אוסר על נשיאת שניהם, מכיוון ששניהם מביעים את אותה כוונה באמצעים שונים
כיצד ה-ByteRange וחריץ (slot) ה-/Contents מתאימים יחד?
חתימה חייבת לכסות את כל הקובץ למעט הבתים המחזיקים את החתימה עצמה, ו-PAdES פותר את המעגליות הזו עם ממלא מקום ברוחב קבוע ש-SignPadesBytes מנהל בדיוק. הוא שומר מחרוזת הקסדצימלית של /Contents בגודל ContentsSize בתים (16384 כברירת מחדל, גדול בנוחות מ-CMS SignedData טיפוסי), מבצע סריאליזציה לעדכון האינקרמנטלי כדי לאתר את ההיסט המדויק של החריץ, ואז מחשב את ה-/ByteRange כשני טווחים התוחמים את החריץ, כל מה שלפני התוחם הפותח של מחרוזת ההקס וכל מה שאחרי התוחם הסוגר שלה. ה-SHA-256 רץ מעל שני טווחים אלו בלבד. ה-CMS המוגמר מקודד להקס בתוך החריץ השמור, מרופד באפסים לרוחב הקבוע, ועדכון ההפניות הנגדיות מצרף. מכיוון שהרוחב קבוע מראש, מילוי החריץ אינו מסיט אף בית במורד הזרם, וזו הסיבה כולה שטווח הבתים נשאר תקף; בתיי המסמך המקוריים נשמרים ככתבם וכלשונם, כך שחתימה מוקדמת יותר על אותו קובץ שורדת ללא פגע בדיוק כפי שדורשת חתימה אינקרמנטלית של ISO 32000-1 §12.8.1
תשתית ה-Windows CNG ומגבלותיה
רכיב ה-PDFium חותם ב-Windows בלבד, וגבול זה הוא מכוון. FPdfCryptoWin מקשרת את crypt32.dll ו-ncrypt.dll באופן דינמי, ללא תלות ב-DLL בזמן הקומפילציה, ושרשרת החתימה היא CNG סטנדרטית: פתח את מאגר MY, מצא את תעודה לפי hash, השג את ה-handle של המפתח הפרטי שלה באמצעות CryptAcquireCertificatePrivateKey, וקרא ל-NCryptSignHash. מערכות RSA עם PKCS#1 v1.5, RSA-PSS ו-ECDSA נתמכות כולן. ECDSA זקוקה לתיקון אחד שהאחרות אינן זקוקות לו, מכיוון ש-NCryptSignHash מחזירה את צמד ה-r-and-s הגולמי של IEEE P1363 בעוד ש-CMS מצפה ל-SEQUENCE של ECDSA-Sig-Value בקידוד DER, ולכן התשתית מקודדת זאת מחדש לפי RFC 5480
var
Pdf: TPdf;
Opts: TPadesSignOptions;
Output: TFileStream;
begin
if not PadesCryptoAvailable then
Exit; // no signing backend on this platform
Opts := TPadesSignOptions.Default;
Opts.CertificateThumbprint := ReadThumbprintFromConfig;
Pdf := TPdf.Create(nil);
Output := TFileStream.Create('contract-signed.pdf', fmCreate);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Pdf.SignPadesToStream(Output, Opts);
finally
Output.Free;
Pdf.Free;
end;
end;
התוצאה המעשית היא שהמפתח הפרטי חייב לשכון במאגר התעודות של Windows. תעודה המוחזקת בקובץ PFX עובדת רק לאחר שאתה מייבא אותה למאגר של המשתמש הנוכחי, ובשלב זה טביעת האצבע שלה היא הערך שאתה מעביר ל-SignPades. למהדורה זו אין נתיב PKCS#11 או HSM ואין תשתית קבצי מפתחות תוכנה, כך שכאשר PadesCryptoAvailable מחזיר False פשוט אין אפשרות לחתימה במחשב ההוא
היכן ש-PAdES B-B נעצרת
PAdES B-B היא הבסיס, הרצפה של ארבע רמות ה-PAdES: היא מוכיחה מי חתם ושהבתים לא שונו מאז, ותו לא. חתימת B-B אינה נושאת חותמת זמן אמינה, ולכן אינה יכולה להוכיח מתי החתימה התרחשה, ואינה מטמיעה נתוני ביטול, ולכן מאמת שנים מאוחר יותר חייב לאחזר את שרשרת התעודות ומצבה בעצמו. פערים אלו הם בדיוק מה שהרמות הגבוהות יותר סוגרות. כאשר אתה זקוק לזמן חתימה שמבקר יקבל, הוספת חותמת זמן לפי RFC 3161 ו-DSS לאימות לטווח ארוך מעבירה את החתימה ל-B-T ומעלה; כאשר אתה רוצה לקרוא חתימה מוגמרת בחזרה ולאשר לאיזו רמה היא הגיעה, בדיקת חתימת PDF ורמת ה-PAdES שלה היא הכלי הנלווה; ולפני שחותמים על משהו בכלל, ביקורת סיכוני אבטחה של PDF מספרת לך על מה אתה עומד לחתום
שיטות ה-SignPades המוצגות כאן מגיעות עם PDFium Component עבור דלפי ו-C++Builder, לצד בדיקת החתימה לקריאה בלבד ש-PDFium מספקת מהקופסה