PDFium VCL חותם מסמכי PAdES עם מפתח פרטי שיושב ב-Keychain של macOS דרך backend שפותר כל סימבול של Security ושל CoreFoundation בזמן ריצה עם dlopen ו-dlsym. שום דבר לא קשור בזמן קישור, כך ששם סימבול עם טעות הקלדה מתגלה כ-KeychainAvailable שמחזיר False וכ-KeychainMissingSymbols שמצביע על האשם, ולא כשגיאת linker או קריסה
את הבחירה הזאת כפתה אילוץ לא נעים, ואופן הטיפול בה מכליל. היחידה נכתבה על מכונה בלי SDK של macOS, כך שכל שם סימבול של framework וכל קבוע הגיעו מהתיעוד ואף אחד מהם לא יכול היה להיבדק מול header. התגובה הלא נכונה למצב כזה היא לכתוב את הקוד בזהירות ולקוות. הנכונה היא לסדר את הדברים כך שהטעויות הבלתי נמנעות יכריזו על עצמן בצורה הכי קלה לאיתור שאפשר
למה קישור דינמי הוא ההחלטה הנכונה גם על פלטפורמת היעד
כי היא ממירה משפחת כישלונות שעוצרת את התוכנית למשפחת כישלונות שמדווחת על עצמה. הפניה שגויה ל-framework שקושר בצורה סטטית נכשלת בזמן הקישור על היעד ולעולם לא נקשרת בשום מקום אחר. כזאת שקושרת בצורה דינמית ושגויה מניבה backend לא זמין ורשימת שמות שלא נפתרו, והריצה הראשונה על Mac הופכת את השאלה מלמה זה לא זמין לשורה אחת שמציינת טעות הקלדה
יש יתרון שני שמשתלם יום-יום ולא פעם אחת. כי היחידה לא קושרת שום framework, היא מקומפלת על כל פלטפורמה, כך שה-build הרגיל של Windows ממשיך לבדוק את התחביר שלה, את הטיפוסים שלה ואת סעיף ה-uses שלה. יחידה שמקומפלת רק על פלטפורמה שאף אחד בצוות אין לה היא יחידה בלי שום קומפיילר שמביט בה, והיא מתדרדרת בשקט עם כל refactor של טיפוס משותף
uses
FPdfCrypto, FPdfCryptoMac;
var
Options: TPadesSignerOptions;
begin
if not KeychainAvailable then
raise Exception.Create('Keychain backend unavailable, unresolved: ' +
KeychainMissingSymbols);
ConfigureKeychainSignerProvider; // מותקן כ-backend של החותמה של PAdES
ConfigureKeychainCmsVerifier; // וגם כ-backend לאימות
Writeln('signer backend : ', PadesCryptoBackendName);
Writeln('verify backend : ', PadesCmsVerificationBackendName);
Options := TPadesSignerOptions.Default;
Options.CertificateThumbprint := 'B1 3F 9C ...'; // SHA-1, בכל רישיות
Options.PaddingScheme := psRsaPss;
end;
שני סוגי סימבול מיוצא, שתי דרכים לקרוא אותם
זה הפרט המבלבל ביותר בכל הקישור, ולתפוס אותו הפוך זה להתקמפל נקי ולהיכשל בזמן ריצה. CoreFoundation ו-Security מייצאים שני דברים שונים זה מזה באופן קטגורי דרך אותה קריאת dlsym, והקוד חייב לדעת מה מאין
קבועים בעלי שם כמו מפתחות מחלקות הפריטים של ה-Keychain וה-singletons הבוליאניים של CoreFoundation הם משתנים מיוצאים שהתוכן שלהם הוא ה-CFStringRef או ה-CFBooleanRef שאתה רוצה. dlsym מחזיר את הכתובת של המשתנה הזה, ולכן חייבים לבצע dereference פעם אחת כדי להשיג את הערך. מבני callback-table כמו callbacks של מפתח וערך מילון הם מבנים מיוצאים, ו-dlsym מחזיר את הכתובת של המבנה, שהיא בדיוק המצביע שפונקציית יצירת המילון מצפה לו. לבצע dereference לזה אומר להעביר את מילת המכונה הראשונה במבנה כאילו הייתה מצביע
אף אחת מהטעויות לא מניבה שגיאת קומפילציה, ואף אחת לא מניבה שגיאת ריצה ברורה. מקבלים מצביע זבל שנכשל אי-שם במורד הזרם. הדרך להפוך את ההבחנה לבלתי אפשרית לטעות היא להפסיק לסמוך על הזיכרון: שתי פונקציות עזר, אחת שקושרת ומבצעת dereference ואחת שקושרת ולא, כך שאתר הקריאה מכריז איזה סוג סימבול הוא מבקש והפונקציה העוזרת אוכפת את השאר
// משתנה מיוצא: dlsym נותן את הכתובת של משתנה שמחזיק את
// ה-CFTypeRef, ולכן מבצעים dereference פעם אחת
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');
// מבנה מיוצא: dlsym נותן את הכתובת של המבנה עצמו, וזה בדיוק
// מה שה-API רוצה. לא לבצע dereference
FKeyCallbacks := BindStruct(CoreFoundationLib,
'kCFTypeDictionaryKeyCallBacks');
למה חתימת RSA-PSS צריכה שני fallbacks נפרדים?
כי האלגוריתם יכול להיעדר בשתי דרכים בלתי תלויות, ורק אחת מהן היא שאלת גרסה. הקבוע של אלגוריתם החתימה על digest של PSS הופיע ב-macOS 10.13, ולכן על מערכת ישנה יותר הסימבול פשוט לא נמצא שם והקישור מקבל nil. זו בדיקת הגרסה. בנפרד, על מערכת שבה הקבוע קיים, מפתח מסוים עדיין יכול לסרב לו, וה-framework עונה על השאלה הזאת דרך SecKeyIsAlgorithmSupported עבור אותו מפתח. מפתח מגובה חומרה או מפתח עם attributes מגבילים יכול לדחות PSS בזמן שמפתח תוכנה על אותה מכונה מקבל אותו
שתי הדרכים חייבות להגיע לאותו fallback: מעבר ל-PKCS#1 v1.5. והחלק הקריטי הוא שה-fallback צריך לשנות גם את מזהה האלגוריתם שנכתב לתוך מבנה ה-CMS, ולא רק את קריאת החתימה. להפיק מזהה אלגוריתם של PSS ובפועל לייצר חתימת v1.5 זה ליצור מסמך שכל verifier דוחה מיד, וזה גרוע במובהק מלדווח ש-PSS לא נתמך. downgrade זה מקובל, אי-התאמה בין מה שהכרזת לבין מה שעשית זה לא, וזה כלל כללי לקוד חתימות ולא גחמה של macOS. ההשלכות ברמת החתימה מפורטות בחתימת PDFs עם PAdES B-B
קידוד חתימות ECDSA, והיפוך ששווה לשים לב אליו
נתיב העקומות האליפטיות לא צריך שום המרה על macOS, וזה ההפך ממה שקישור PKCS#11 מחייב. אלגוריתם החתימה על digest של framework ה-Security עבור ECDSA מחזיר את החתימה כבר בצורת X9.62 DER, שזה בדיוק מה ש-CMS רוצה. token של PKCS#11 מחזיר במקום זאת את הזוג הגולמי ברוחב קבוע של P1363, שחייב לעבור קידוד מחדש לפני שהוא נכנס למבנה חתימה
כך שני backends שמממשים את אותו ממשק צריכים טיפול הפוך לאותו אלגוריתם, ואף אחד מהם לא טועה. זה בדיוק סוג ההבדל שהפשטה צריכה לבלוע ולא לחשוף: שכבת ה-PAdES מבקשת מ-provider לחתום, ו-conventions הקידוד נשארים בתוך ה-provider. אם הם דולפים מעלה, כל מי שקורא לממשק מסיים נושא conditional פר-backend. אותה תבנית מופיעה בסיפור החתימה המרוחקת המתואר בסשנים של חתימת PAdES מרוחקת מול HSM
// ממשק ה-provider זהה על כל פלטפורמה, ולכן הבחירה היא החלטה
// של האתחול ולא החלטה פר-קריאה
{$IFDEF DARWIN}
if KeychainAvailable then
ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
// provider ה-CNG של Windows מותקן על ידי יחידת הפלטפורמה
{$ENDIF}
if not PadesCryptoAvailable then
raise Exception.Create('no signing backend on this platform');
// מכאן והלאה קוד החתימה נייטרלי לפלטפורמה
Signer := ResolvePadesSigner(Options);
חוקי ספירת רפרנסים שיושבים שלוש שורות זה מזה
ניהול הזיכרון של Core Foundation הולך אחרי conventions של שמות, והמלכודת כאן היא שפונקציות עם conventions שונים מופיעות אחת לצד השנייה באותו בלוק קצר. פונקציה שמקבלה תעודה מאובייקט trust מחזירה הפניה שאולה שאסור לשחרר. פונקציות שמעתיקות תעודת חותם או מעתיקות את הנתונים שלה מחזירות הפניות בבעלות מלאה שחייבות להשתחרר. שלוש קריאות ברצף, שתי חוקיות בעלות, ושחרור של השאולה לא נכשל באותה שורה. הוא משחית retain count ומפיל משהו לא קשור אחר כך
ההתמודדות היא לקרוא את הפועל בשם של כל פונקציית framework לפני כתיבת הניקוי, בכל פעם, בלי יוצא מן הכלל. זו המקבילה של CoreFoundation לבדיקה אם API מחזיר עותק או view, והמחיר של טעות הוא קריסה אינטרמיטנטית ולא שגיאה
מה ה-backend הזה לא טוען
הוא מעולם לא רץ על macOS בזמן הכתיבה, ולומר את זה במפורש שימושי יותר מהבטחה מרומזת. מה שניתן להוכחה הוא צר יותר ועדיין שווה: היחידה מקומפלת על Windows כחלק מה-build היומי, כל סימבול framework נקשר לפי שם בזמן ריצה עם רישוי הכישלונות, ולוגיקת בחירת האלגוריתמים כולל שני ה-fallbacks של PSS היא פסקל רגיל שאפשר לסקור ולהסיק עליו. הריצה הראשונה על Mac תעבוד או שתניב רשימת שמות לתקן
המקבילה לאימות, שמשתמשת במפענח ה-CMS ברמה גבוהה יותר במקום להרכיב את מבנה ה-CMS ביד, מכוסה באימות חתימות PDF על macOS עם SecTrust, והיא חולקת את אותה תשתית קישור ואת אותה גישת אבחון
הרעיון הנייד כאן הוא על מיקום הסיכון ולא על macOS. כשאתה חייב לכתוב קוד מול ממשק שאי אפשר לאמת, בחר את הבנייה שבה טעויות הכי זולות לאיתור. קישור דינמי עם רשימה מפורשת של שמות שלא נפתרו הופך עשרים הנחות בלתי ניתנות לאימות לשורת אבחון אחת. שני ה-backends מגיעים כמקור עם רכיב ה-PDFium ל-Delphi, כך שאם שם סימבול כן צריך תיקון, זה שינוי בן שורה בעץ שלך ולא פנייה לתמיכה