HotPDF מאמת חתימות דיגיטליות במסמכי PDF טעונים באמצעות שלוש מתודות של THotPDF: הפונקציות GetLoadedSignatureInfo, VerifyLoadedSignature ו-VerifyLoadedSignatureEx, שהוצגו בגרסה v2.259.0. הרכיב מחשב מחדש את ערכי הגיבוב (hash) של מקטעי ה-/ByteRange של הקובץ המקורי, בודק את מאפיין ה-messageDigest של ה-CMS, ומריץ אימות RSA PKCS#1 v1.5 מול תעודת החותם המוטמעת, כשהוא מחזיר svValid כאשר בתי המסמך שלמים
התרחיש הוא שגרתי אך הסיכונים אינם כאלה. צד שני מחזיר חוזה חתום, זרימת העבודה שלכם צריכה לתעד אותו, ומישהו שואל את השאלה היחידה שקובעת: האם זהו המסמך ששלחנו, בית אחר בית, חתום על ידי התעודה שהוא מצהיר עליה? מענה על כך בקוד הוא צד האימות של סיפור החתימה; צד החתימה, בנייה והטמעה של חתימות PAdES מלכתחילה, מכוסה במאמר המלווה על יצירת חתימות דיגיטליות PAdES עם HotPDF. מאמר זה עוסק בכיוון השני: קובץ PDF מגיע כבר חתום, ואתם מעוניינים בפסק דין תכנותי במקום בצילום מסך של סימון ה-V הירוק של Acrobat
כיצד PDF חתום מוכיח שלא בוצעו בו שינויים?
חתימת PDF מגינה על טווחי בתים סמוכים ספציפיים של הקובץ, ולא על מושג מופשט של "המסמך". תקן ISO 32000-1 §12.8 מגדיר את המנגנון: שדה טופס החתימה נושא מילון שבו ערך ה-/Contents מחזיק מכולת CMS SignedData (תקן RFC 5652), ומערך ה-/ByteRange שלו מציין את אזורי הקובץ המדויקים שהחתימה מכסה, לפי סעיף §12.8.1. המערך הוא רשימה של זוגות היסט ואורך, ובפועל שני מקטעים: כל מה שלפני מחרוזת ה-hex של ה-/Contents, וכל מה שאחריה. ערך החתימה אינו יכול לכסות את עצמו, ולכן הקובץ מגוהב סביב החור הזה
לתכנון זה יש תוצאה המעצבת את כל ה-API: אימות חייב לגהב (hash) את הבתים סדרתיים המקוריים, בדיוק כפי שהם יושבים בדיסק. מודל אובייקטים מנותח אינו יעיל לכך, מכיוון שסדרה מחדש של מסמך שלא שונה מייצרת בתים שונים. לפיכך, HotPDF מאמת מול קובץ המקור שממנו נטען המסמך, או מול TStream של בתים גולמיים שאתם מספקים, ולעולם אינה מול הייצוג שלו בזיכרון
קריאת מטא-נתונים של חתימה לפני ביצוע אימות כלשהו
GetLoadedSignatureInfo מפענח את מילון החתימה ואת מכולת ה-CMS שלו מבלי לגעת בבית בודד של המסמך, מה שהופך אותו לקריאה הראשונה הנכונה כאשר אתם רק צריכים להציג מי חתם ומתי. שדות החתימה ממוספרים מ-0 לפי סדר שדות הטופס, והפונקציה GetLoadedSignatureFieldCount מציינת כמה שדות קיימים. רשומת ה-THPDFSignatureInfo המוחזרת נושאת את שם השדה, /SubFilter, השם הנפוץ (Common Name) של תעודת החותם, שמות ה-distinguished של הנושא והמנפיק, מספר סידורי, תאריכי תוקף, זמן החתימה (מתוך המאפיין החתום כאשר הוא קיים, ואם לא מתוך ערך ה-/M של המילון), שם אלגוריתם הגיבוב, והמחרוזות /Reason, /Location ו-/ContactInfo. חבר ה-Status שלה נותר svNotVerified, תווית כנה עבור "פוענח, אך לא אומת"
var
Pdf: THotPDF;
Info: THPDFSignatureInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('signed-contract.pdf');
for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
begin
Info := Pdf.GetLoadedSignatureInfo(I);
Writeln('Field: ', Info.FieldName);
Writeln('Signer: ', Info.SignerName);
Writeln('Issuer: ', Info.IssuerDN);
Writeln('Algorithm: ', Info.HashAlgorithm);
Writeln('SubFilter: ', Info.SubFilter);
end;
finally
Pdf.Free;
end;
end;
הרצת הבדיקה הקריפטוגרפית
VerifyLoadedSignatureEx מבצע את האימות המלא עבור מסמך שנטען מקובץ ומחזיר את רשומת המידע המאוכלסת בקריאה אחת: הוא פותח מחדש את קובץ המקור, מגבהב את מקטעי ה-/ByteRange באמצעות אלגוריתם הגיבוב של SignerInfo, משווה את התוצאה מול המאפיין החתום messageDigest (לפי RFC 5652 §5.4), ואז מאמת באמצעות RSA את החתימה על פני קידוד ה-DER SET מחדש של המאפיינים החתומים. כאשר חתימה אינה נושאת מאפיינים חתומים, בדיקת ה-RSA רצה ישירות על גבי גיבוב המסמך במקום זאת. החתימות הנתמכות הן RSA PKCS#1 v1.5 עם גיבובי SHA-1, SHA-256, SHA-384 או SHA-512, המכסים את מסנני המשנה adbe.pkcs7.detached ו-ETSI.CAdES.detached המיוצרים על ידי כלי חתימה מובילים
var
Status: THPDFSignatureVerifyStatus;
Info: THPDFSignatureInfo;
begin
Status := Pdf.VerifyLoadedSignatureEx(0, Info);
case Status of
svValid:
if Info.CoversWholeDocument then
Writeln('Valid; signature covers the whole file')
else
Writeln('Valid; file was extended after signing');
svDigestMismatch:
Writeln('Document bytes changed after signing');
svSignatureInvalid:
Writeln('RSA check failed over signed attributes');
svUnsupportedAlgorithm:
Writeln('Non-RSA key or unknown digest algorithm');
svMalformed:
Writeln('CMS container could not be parsed');
svSourceUnavailable:
Writeln('No source bytes; use the TStream overload');
end;
end;
שני פרטי מימוש ראויים להכרה מכיוון שהם מסבירים כשלים שנראים מסתוריים מבחוץ. ראשית, בדיקת המאפיינים החתומים רגישה במיוחד לגבי קידוד: בתוך הקובץ המאפיינים מתויגים כ-[0] IMPLICIT, אך החתימה חושבה על פני צורת ה-DER SET OF שלהם, כך שהמאמת מתייג מחדש לפני הגיבוב, בדיוק כפי שהם דורש תקן RFC 5652 §5.4. מאמת שנבנה ידנית ומגבהב את הבתים כפי שהם מופיעים בקובץ יפסול כל מסמך שחתום כראוי. שנית, ה-/Contents מרופד באופן מסורתי באפסים לטובת תקציב בתים שמור מראש, ולכן המאמת קוטם את ה-blob של ה-DER לאורך האמיתי של ה-SEQUENCE החיצוני שלו לפני הפענוח; אפסים תועים בסוף הנראים כמו זבל הם נורמליים, ולא שחיתות נתונים. אותה משפחה של סכנות פענוח ASN.1, בצד ייבוא התעודות, היא הנושא של המאמר על הקשחת אבטחת PKCS#12 ו-ASN.1 ב-HotPDF
על מה חתימה תקפה בעצם מגינה?
svValid פירושו בדיוק זה: הבתים המצוינים על ידי /ByteRange מגוהבים לערך שעליו חתם החותם, והחתימה מאומתת תחת המפתח הציבורי של התעודה המוטמעת במכולת ה-CMS. מדובר בשלמות בתים בתוספת קישור למפתח, ותו לא. שרשרת תעודות ואימות אמון נמצאים במפורש מחוץ לטווח של המאמת של HotPDF: הוא אינו עובר על השרשרת עד לשורש (root), אינו בודק ביטול תעודות ואינו מתייעץ עם מאגר אמון כלשהו. תעודה בחתימה עצמית של תוקף שחתם מחדש על מסמך שונה תאומת כ-svValid, מכיוון שהמתמטיקה עקבית פנימית. השאלה האם החותם הוא מי שהוא טוען להיות, והאם כדאי לבטוח בו, היא החלטת מדיניות השייכת לשכבה נפרדת, בין אם זו רשימת התעודות המאושרות של הארגון שלכם, מאגר התעודות של Windows או רשות אימות
הדגל CoversWholeDocument שומר מפני פער מעודן יותר. חתימה מכסה תמיד רק את ה-/ByteRange שלה, ומנגנון העדכון התוספתי של PDF מאפשר להוסיף תוכן לאחר חתימה מבלי לבטל את תוקפה, וזה חלק מהעיצוב וכך פועלות זרימות עבודה מרובות חתימות. הדגל מחושב במהלך האימות והוא חיובי רק כאשר שני המקומות בתוספת מרווח ה-/Contents משתרעים על פני כל הקובץ. כאשר מתקבל svValid עם CoversWholeDocument שלילי, הגרסה החתומה שלמה אך הקובץ מכיל תתוספות מאוחרות יותר, ומה שתוספות אלו שינו היא החלטה שזרימת העבודה שלכם צריכה לקבוע האם לקבל
מסמכים שנטענו מזרם ומסמכים מוצפנים זקוקים לבתי המקור שלהם
הגרסאות ללא פרמטרים של VerifyLoadedSignature ו-VerifyLoadedSignatureEx תלויות בכך שהרכיב יזכור מאיזה קובץ הגיע המסמך. טעינת מסמך מתוך זרם פירושה שאין שם קובץ לפתוח מחדש; הדבר נכון גם לאחר נתיב הטעינה מחדש של סיסמה המשמש למסמכים מוצפנים, זרימת העבודה המתוארת במאמר על הצפנת PDF ב-AES-256 עם HotPDF. בשני המקרים, הגרסאות המבוססות על קבצים מחזירות svSourceUnavailable במקום לנחש. הפתרון הוא הגרסה המקבלת TStream, המאפשרת לכם להעביר את הבתים הגולמיים המקוריים מכל מקום שבו שמרתם אותם, קובץ שעדיין ברשותכם, באפר זיכרון או blob של בסיס נתונים
var
Src: TFileStream;
Status: THPDFSignatureVerifyStatus;
Info: THPDFSignatureInfo;
begin
// Stream-loaded document: the component holds no source
// file name, so supply the original bytes yourself.
Src := TFileStream.Create('signed-contract.pdf',
fmOpenRead or fmShareDenyWrite);
try
Status := Pdf.VerifyLoadedSignature(0, Src, Info);
if Status <> svValid then
Writeln('Verification failed: ', Ord(Status));
finally
Src.Free;
end;
end;
דיווח על מה שלא ניתן לאמת
מאמת היודע רק "תקף" ו"לא תקף" ידווח באופן שגוי על מסמכים שהוא פשוט לא מבין, ולכן פירוט הסטטוס מפריד בין המקרים שממשק המשתמש שלכם צריך להבחין ביניהם. svDigestMismatch פירושו שבתי המסמך השתנו לאחר החתימה, סימן השינוי הקלאסי. svSignatureInvalid פירושו שהבתים מגוהבים כראוי אך בדיקת ה-RSA נכשלה, מה שמצביע על ערך חתימה פגום או מזויף. svUnsupportedAlgorithm היא התשובה הכנה עבור מפתחות ECDSA וגיבובים לא מוכרים: החתימה עשויה להיות תקינה לחלוטין, HotPDF פשוט אינו יכול לבדוק אותה, ודיווח עליה כ"לא תקפה" יפגע במסמך תקין. svMalformed מסמן מכולת CMS שלא ניתן היה לפענח כלל. עבור בדיקות סף, VerifyAllLoadedSignatures מחזיר אמת רק כאשר קיים לפחות שדה חתימה אחד וכל אחד מהם מאומת כ-svValid, ערך בוליאני יחיד ונוח עבור צינור קליטת ארכיון הדוחה כל דבר פחות מכך
אימות חתימות, חתימת PAdES, הצפנת AES-256 וממשק ה-API לעריכת מסמכים טעונים מסופקים כולם באותה ספריית VCL טבעית עבור דלפי ו-C++Builder, ללא תלות בקובצי DLL חיצוניים; רשימת התכונות המלאה וגרסאות ה-IDE הנתמכות נמצאים בדף המוצר רכיב HotPDF