PDF Library for Delphi (PDFlibPas) מחלצת את התעודות בתוך חתימת PDF בהליכת DER טהורה על ה-SignedData של ה-CMS המאוחסן ב-/Contents, בלי CryptoAPI בתמונה. מאז v3.539.10 כל קריאה מקוננת מוגבלת על ידי רכיב האב שלה, הריפוד האפסי אחרי ה-CMS נחתך באורך שה-CMS מצהיר, ומזהי אובייקטים מקודדים את ה-subidentifier הראשון המשולב שלהם ב-base-128. כלל הגבולות ותיקון ה-OID החליפו שניהם קוד שהפיק תשובות שגויות בלי להעלות חריגה, וכלל הריפוד מונע מהקורא המחמיר לדחות חתימות אמיתיות
צד הקריאה חשוב יותר ממה שנדמה. כלי long-term validation צריכים לחלץ את תעודת החותם ואת המנפיקים שלה מתוך חתימה קיימת לפני שהם יכולים למשוך נתוני ביטול, דוח ביקורת צריך לומר מי חתם, ולבניין Lazarus על לינוקס אין פונקציות הודעה של Windows להישען עליהן. פרסר בעמדה כזאת נדיר שמתרסק על קלט רע. מצב הכישלון שכואב הוא מניין תעודות שכולל בייטים ששייכים לשכן, התאמת חותם מול השדה הלא נכון, או OID ששוקת שוקט הופך ל-OID אחר. pipeline של חתימות שנבנה מעל זה מדווח שטויות משוכנעות
חילוץ תעודות החותם מתוך PDF חתום
חמש מתודות TPDFlib מכסות את צד הקריאה, וכולן מקבלות InputFile, Password, FieldName: כל קריאה פותחת את הקובץ לקריאה בלבד, עונה וסוגרת אותו שוב. GetSignatureEmbeddedCertificateCount ו-GetSignatureEmbeddedCertificateDER מפרטות את התעודות שב-certificates set לפי סדר הקידוד, GetSignatureSignerCertificateDER מחזירה את התעודה שהניבה SignerInfo נתון, ו-GetSignatureCertificateChainLength / GetSignatureCertificateChainDER הולכות מהחותם הזה אל המנפיק הרחוק ביותר שהחתימה עצמה נושאת. האינדקסים מתחילים מאפס. שמרו את התוצאות ב-AnsiString, וזו הסיבה שהספרייה מחזירה אותן כך: blob של DER שעובר דרך string או TStrings עובר המרת קידוד וחוזר מקולקל
uses
SysUtils, Classes, PDFlibrary;
procedure SaveDer(const FileName: string; const Der: AnsiString);
var
Fs: TFileStream;
begin
Fs := TFileStream.Create(FileName, fmCreate);
try
if Der <> '' then
Fs.WriteBuffer(Der[1], Length(Der));
finally
Fs.Free;
end;
end;
const
Src = 'contract-signed.pdf';
Field = 'Signature1';
var
Pdf: TPDFlib;
ChainLen, I: Integer;
SignerDer, LastDer: AnsiString;
begin
Pdf := TPDFlib.Create;
try
WriteLn('Certificates in the CMS: ',
Pdf.GetSignatureEmbeddedCertificateCount(Src, '', Field));
SignerDer := Pdf.GetSignatureSignerCertificateDER(Src, '', Field, 0);
if SignerDer = '' then
raise Exception.Create('signer certificate missing or not matched');
SaveDer('signer.cer', SignerDer);
ChainLen := Pdf.GetSignatureCertificateChainLength(Src, '', Field, 0);
for I := 0 to ChainLen - 1 do
SaveDer(Format('chain-%d.cer', [I]),
Pdf.GetSignatureCertificateChainDER(Src, '', Field, 0, I));
if ChainLen > 0 then
begin
LastDer := Pdf.GetSignatureCertificateChainDER(Src, '', Field, 0, ChainLen - 1);
WriteLn('Next issuer, if any: ', Pdf.GetCertificateIssuerURLs(LastDer));
end;
finally
Pdf.Free;
end;
end;
שני דברים בפלט הזה דורשים תשומת לב. מניין 0 אינו אבחנה: שדה חסר, סיסמה שגויה, blob שאינו DER, ו-SignedData שפשוט משמיט את ה-certificates set האופציונלי — כולם חוזרים כ-0 או כמחרוזת ריקה, אז רשמו ל-log את שם השדה לצד המספר. וגם שרשרת שנגמרת לפני תעודה בהנפקה עצמית אינה שגיאה. בונה השרשרת משתמש רק בתעודות המוטמעות בחתימה, ולכן את המנפיקים שנותרו צריך למשוך דרך הכתובות ש-GetCertificateIssuerURLs מדווח
כמה מ-/Contents הוא באמת ה-CMS?
רק הקידומת שה-SEQUENCE החיצוני מצהיר עליה שייכת ל-CMS, ו-PLTrimCMSPadding חותך את כל מה שאחריה. חותם שומר את מחרוזת ה-hex של /Contents עוד לפני שה-CMS קיים, כי את ה-/ByteRange המתואר ב-ISO 32000-1 §12.8.1 צריך לקבע קודם, ולכן ה-slot ממודד בנדיבות והזנב שאינו בשימוש הוא אפסים. PLTrimCMSPadding קורא את ה-TLV הראשון, דורש tag $30, ומחזיר את הבייטים עד סוף אותו אלמנט; כל דבר שאינו מתחיל ב-SEQUENCE תקין חוזר ריק. הרמה העליונה היא המקום היחיד שבו בייטים עוקבים הם חוקיים, וההבחנה הזאת חשובה לסעיף הבא: כלל מחמיר של "האלמנט חייב לצרוך את כל הבאפר" היה דוחה כל חתימה אמיתית, בזמן שכלל סלחני שמיושם בכל עומק מאפשר לשדות מקוננים לקרוא בייטים שלא שייכים להם
למה קורא DER צריך את היסט הסיום של ההורה?
אלמנט מקונן תקין רק אם הוא נגמר בתוך ההורה שלו, ובדיקה מול סוף הבאפר לא מוכיחה את זה. ה-DERReadTLV הנמוך ב-PDFlibASN1 מגביל כל אלמנט מול המחרוזת כולה, שזו הבדיקה הנכונה לאובייקט החיצוני ביותר והלא נכונה לכל מה שמתחתיו. דמיינו SignerInfo שה-issuerAndSerialNumber שלו מצהיר על 40 בייטים בזמן שה-Name של המנפיק בתוכו טוען 60. כל בייט עדיין בבאפר, ולכן קורא המוגבל לבאפר מקבל את ה-Name, קורא את המספר הסידורי מתוך אלגוריתם ה-digest שאחריו, ואז משווה את הזוג הזה מול התעודות המוטמעות. לפני v3.539.10 ה-walker של ה-CMS קרא בדיוק כך. התיקון הוא wrapper קטן שנושא את מיקום הסיום של ההורה אל כל קריאה
function ReadTLVWithin(const Data: AnsiString; ParentEnd: Integer;
var Offset: Integer; out Tag: Byte; out Start, Len: Integer): Boolean;
begin
Result := False;
// אין מה לקרוא בתוך ההורה: מסרבים לפתוח קריאה
if (Offset < 1) or (Offset >= ParentEnd) then
Exit;
if not DERReadTLV(Data, Offset, Tag, Start, Len) then
Exit;
// ה-Offset עומד עכשיו אחד אחרי האלמנט; אסור שיחצה את ההורה
Result := Offset <= ParentEnd;
end;
// כל רמה מקליטה את סופה שלה ומעבירה אותו הלאה:
// OuterEnd := end of ContentInfo (RFC 5652 section 3)
// ExplicitEnd := end of content [0] EXPLICIT
// ContentEnd := end of SignedData (RFC 5652 section 5.1)
// SignerEnd / InnerEnd for SignerInfo and issuerAndSerialNumber
היחידה, PDFlibCMSRead, מקרימה עכשיו את הסופים האלה דרך ה-ContentInfo, ה-wrapper של ה-[0] EXPLICIT, שדות ה-SignedData עד ה-signerInfos, ה-SignerIdentifier בשתי הצורות שלו — issuerAndSerialNumber ו-[0] subjectKeyIdentifier (RFC 5652 §5.3) — ושדות ה-tbsCertificate שנקראים מכל תעודה מוטמעת בעת ההתאמה לחותם. בתוך ה-certificates set וה-signerInfos set, אלמנט שרץ מעבר לסוף הקבוצה עוצר את הלולאה: PLExtractCMSCertificates מחזירה את התעודות שכבר קיבלה ולעולם לא מדביקה את בייטי ה-crls או ה-signerInfos שאחריהם אל האחרונה שבהן. התאמת ה-issuer-and-serial גם דורשת את שני החצאים, כי מספר סידורי הוא ייחודי רק בתוך מנפיק אחד
למה 2.999.3 יצא בתור 1.15.3?
שתי הקשתות הראשונות של OID מתמזגות ל-subidentifier אחד, לא לבייט אחד, וה-subidentifier הזה מקודד ב-base-128 כמו כל קשת אחרת. X.690 §8.19.4 מגדיר אותו כ-40 * arc1 + arc2; ה-DER_OID הקודם כתב את הערך הזה עם Byte(...), מה שנכון רק עד 127, הערך של 2.47. עבור 2.999 הסכום הוא 1079, ההמרה ל-byte שומרת 55, ו-55 מפוענח כ-1.15, כך שהמזהה קורא בשקט לענף אחר של העץ. ערכים מ-128 עד 255 נכשלים אחרת, ופולטים בייט אחד עם דגל continuation מוגדר שבולע את הקשת הבאה. רוב מזהי ה-PKI (1.2.840..., 2.5.29..., 0.4.0...) לעולם לא מגיעים לגבול, ולכן הבאג שרד; הקשתות של joint-iso-itu-t מ-2.48 ומעלה כן. DER_OID משרת גם את המקודד עבור signed attributes וגם את ה-matcher ב-DERFindExtensionByOID ובבדיקת ה-content-type של ה-SignedData, ולכן קידוד שגוי שבר גם כתיבה וגם חיפוש
uses
SysUtils, PDFlibASN1;
function Hex(const S: AnsiString): string;
var
I: Integer;
begin
Result := '';
for I := 1 to Length(S) do
Result := Result + IntToHex(Byte(S[I]), 2) + ' ';
Result := Trim(Result);
end;
begin
WriteLn(Hex(DER_OID('2.999.3'))); // 06 03 88 37 03
WriteLn(Hex(DER_OID('2.47.1'))); // 06 02 7F 01
WriteLn(Hex(DER_OID('2.48.1'))); // 06 03 81 00 01
WriteLn(Hex(DER_OID('2.5.29.14'))); // 06 03 55 1D 0E
WriteLn(Hex(DER_OID('1.2.840.113549.1.7.2'))); // 06 09 2A 86 48 86 F7 0D 01 07 02
end.
הערך המשולב מוחזק ב-UInt64 בכוונה. DER_OID מפרסר קשתות לתוך Int64, ולכן קשת שנייה חוקית יכולה להגיע עד Int64.MaxValue, והוספת 80 עבור arc1 = 2 גורמת גלישה במספר שלם מסומן בן 64 ביט. UInt64 נושא את Int64.MaxValue + 80 בלי להתגלגל, ובאפר העבודה בן עשרת הבייטים מחזיק את עשר קבוצות ה-7 ביט שערך בן 64 ביט צריך. וקטורי בדיקה ששווה לשמר הם אלה משני צידי הגבול: 2.47 חייב להישאר בייט אחד, ו-2.48 חייב להפוך לשניים
מה ה-walker של ה-CMS בצד הקריאה מבטיח?
PDFlibCMSRead מבטיח מבנה ושום דבר מעבר: הוא מחזיר בייטים שיושבים היכן ש-RFC 5652 אומר שהם אמורים לשבת ולא מאמת חתימה, digest או תקופת תוקף. ה-walker מקבל DER בלבד, ולכן DERReadTLV דוחה אורכים בלתי מוגדרים ומספרי tag מרובי בייטים, ו-CMS בקידוד BER מחותם שאינו תואם מדווח אפס תעודות במקום ניחוש חלקי. תעודות attribute ואלטרנטיבות ה-CertificateChoices האחרות מדולגות כי שום דבר במורד הזרם לא יכול להשתמש בהן. האימות הקריפטוגרפי נשאר אצל הקוד שמחזיק בו, שמתחיל בבדיקות כיסוי הבייטים המתוארות בחתימת PAdES ואימות ByteRange בדלפי וממשיך בסיווג מה השתנה אחרי ש-PDF נחתם
הלקח הרחב נכון לכל פורמט בינארי: "בתוך הבאפר" היא תכונה של בטיחות זיכרון, "בתוך ההורה" היא תכונה של נכונות, ופרסר צריך את שתיהן. אותה חשיבה על אורכים עוינים רצה גם בחיזוק פרסר PDF בפסקל נגד קבצים זדוניים. ממשקי חילוץ התעודות, בניית השרשרת והאימות לטווח ארוך שנדונו כאן מגיעים עם losLab PDF Library for Delphi, עבור Delphi, C++Builder ו-Lazarus