PDFium VCL שולח בקשות חותמת זמן של RFC 3161 דרך libcurl ביעדים שאינם Windows, כשהוא קשור בצורה דינמית לשמונה סימבולים, בשיקוף הצורה של ה-backend ל-Windows שקושר אל WinHTTP. שתי הגדרות אפשרויות מכריעות אם ההעברה אמינה תחת עומס, ואת היחידה כולה אימתו על מכונה שלא יכלה לקמפל אותה עבור פלטפורמת היעד שלה
חותמות זמן הן מה שהופך חתימה למשהו ששורד פקיעת תוקף של תעודה, וזו פעולת רשת שיושבת בתוך פעולת חתימה. השילוב הזה נותן לבחירת ההעברה משקל שהיא בדרך כלל לא מקבלת: היא רצה על worker thread, היא מדברת עם שרת שלא בשליטתך, ותקיעה שם משתקת pipeline של חתימה ולא טעינת עמוד
למה libcurl במקום לקוח ה-HTTP של FPC?
כי החלופה גוררת stack של TLS אל תוך הריפוזיטורי ואז מכריחה אותך לתחזק את זיהוי הגרסאות שלו. הדרך המובנת מאליה ב-Free Pascal היא fphttpclient עם שכבת ה-sockets של OpenSSL, והיא נכשלת בפרטים: ה-bindings של OpenSSL ב-FPC 3.2.2 מזהים OpenSSL 3.x באופן לא אמין ברוב ההפצות הנוכחיות, ו-macOS מוסיף מעל זה את ההבדלים של LibreSSL. מה שמתחיל כקריאת HTTP קטנה נהיה תחזוקה שוטפת של ABI של TLS של מישהו אחר
libcurl מסתדר לבד עם ה-backend של ה-TLS ומאמת שרשראות מול ה-trust store של הפלטפורמה, כך שהצד של הפסקל לא צריך אף אחת מאלה. שכבת הקישור היא שמונה סימבולים. המספר הזה הוא הטיעון: שטח קטן יותר בין הקוד שלך לבין תלות שזזה אומר פחות מקומות שבהם שדרוג של הפצה ישבור אותך, והוא מתלכד עם ה-backend הקיים ל-Windows, שקושר קומץ נקודות כניסה של WinHTTP באותה צורה
uses
FPdfTsaFpc;
var
ReqDer, RespDer: TBytes;
begin
if not TsaHttpAvailable then
raise Exception.Create('no HTTP transport for timestamping');
Writeln('TSA transport: ', TsaHttpBackendName);
ReqDer := BuildTimeStampQuery(DocumentDigest);
if PostTimeStampQuery('https://tsa.example.org/tsr', ReqDer, RespDer) then
AttachTimeStampToken(RespDer)
else
raise Exception.Create('timestamp request failed');
end;
הכרזה על פונקציית C variadic בפסקל
curl_easy_setopt ו-curl_easy_getinfo הן variadic בצד ה-C, וב-Object Pascal אין דרך להביע את זה. הגישה שעובדת היא להכריז כמה prototypes קבועים, אחד לכל מחלקת ארגומנטים, כולם מצביעים על אותו סימבול מיוצא: variant שלוקח long, variant שלוקח מצביע, וכך הלאה, כשהבחירה באתר הקריאה לפי מה שאתה בעצם מעביר
זה בטוח מסיבה ספציפית ששווה להבין ולא להעתיק. כל אחד מסוגי הארגומנטים האלה עובר ברגיסטר שלם תחת conventions הקריאה שפעילים בפלטפורמה, וזה בדיוק המקום שממנו המימוש ב-C קורא עם va_arg. התרגיל לכן מחזיק מעמד עבור שלמים, מצביעים ו-handles, והוא לא מחזיק מעמד עבור ארגומנטים של נקודה צפה, שנוסעים ברגיסטרים אחרים. אל תוסיף variant שלוקח double מתוך הנחה שהתבנית ממשיכה להחזיק גם שם
// סימבול מיוצא אחד, כמה prototypes קבועים. כל variant מעביר את הארגומנט
// שלו ברגיסטר שלם, וזה המקום שממנו הצד של ה-C קורא אותו. variant של
// נקודה צפה לא יעבוד ואסור להוסיף אותו
type
TCurlSetOptLong = function(Handle: Pointer; Option: Integer;
Value: NativeInt): Integer; cdecl;
TCurlSetOptPtr = function(Handle: Pointer; Option: Integer;
Value: Pointer): Integer; cdecl;
var
curl_easy_setopt_long: TCurlSetOptLong;
curl_easy_setopt_ptr: TCurlSetOptPtr;
שתי הגדרות שמכריעות אם הבקשה מסתיימת
הראשונה היא כותרת Expect: ריקה במפורש. libcurl מפעיל את ה-handshake של HTTP 100-continue עבור גופי בקשה מעל בערך קילובייט אחד, ושאילתת חותמת זמן עם בקשת תעודה בדרך כלל עוברת את הסף הזה. חלק משרתי TSA לעולם לא עונים על ה-continuation, ולכן הלקוח ממתין timeout מלא לפני שהוא שולח גוף שהשרת היה מקבל מיד. שליחת כותרת Expect: ריקה משביתה את ה-handshake, והבקשה עוברת ב-round trip אחד
השנייה היא CURLOPT_NOSIGNAL, שחייבת להיות מוגדרת. בלעדיה libcurl מממש את ה-timeout של פתרון השמות עם SIGALRM, והמנגנון הזה אינו thread-safe. החתימה רצה על worker thread, כך שהתנהגות ברירת המחדל היא קריסה נסתרת שמופיעה תחת concurrency ולעולם לא בבדיקה חד-חוטית. הגדרת הדגל משביתה את הנתיב מבוסס ה-signals ומשלמת על זה רק בגסות של ה-timeout של ה-resolver
שני הפגמים חולקים פרופיל שהופך אותם ליקרים לאיתור מאוחר יותר. אף אחד מהם לא מופיע בבדיקה פונקציונלית מול שרת מתנהג-היטב על חוט יחיד. שניהם מופיעים בפרודקשן, מול TSA מסוים אחד, תחת עומס. כשאתה קושר ספריית רשת, קרא מה ברירות המחדל שלה מניחות על ה-process שלך לפני שאתה מניח שהן תואמות
איך מאמתים קוד שהקומפיילר שלך לעולם לא יראה?
בכך שעדיין גורמים לקומפיילר לראות אותו, דרך עותק נשלט. למכונת הפיתוח כאן אין cross-compiler של Linux או של macOS, ולכן הענפים שאינם Windows של יחידת החותמות לעולם לא מגיעים למחולל הקוד בבנייה רגילה. קוד שלעולם לא מקומפל הוא קוד שנרקב בשקט: שינוי שם בטיפוס משותף, רשימת פרמטרים שהשתנתה, תלות יחידה שנוספה, ואף אחד לא שם לב במשך חודשים
הטכניקה מכנית. מעתיקים את היחידה לתיקייה זמנית, משנים לה את השם, ומחליפים כל תנאי Windows, גם את הצורה {$IFDEF MSWINDOWS} וגם את הצורה {$IF DEFINED(MSWINDOWS), בסימבול שלעולם לא מוגדר. ואז מקמפלים את העותק. כשכל 3,828 השורות מקומפלות, הוכחת שהנתיב שאינו Windows משתמש ביחידות שקיימות, קורא לפונקציות backend עם חתימות תואמות, ומפנה לטיפוסים שנמצאים ב-scope. זו לא הוכחה שההעברה עובדת, ושום דבר פרט לפלטפורמת היעד לא ייתן לך את זה. זו הוכחה שהענף לא שבור כבר עכשיו, וזהו מצב הכישלון שבאמת מצטבר
ההרגל הנלווה הוא להשאיר את יחידת ה-libcurl עצמה נקייה מ-guards של פלטפורמות, כך שהיא משתתפת בבניית ה-Windows הרגילה גם כששום דבר שם לא מפנה אליה. ה-build היומי אז ממשיך להגן על התחביר ועל הטיפוסים שלה בחינם. יחידה שמקומפלת רק על פלטפורמה שאין לך היא יחידה בלי שום קומפיילר שבודק אותה, ואותה חשיבה חלה על כל עבודת ה-cross-compiler המתוארת במלכודות cross-compiler של Delphi ו-FPC
הגבלת מה שחוזר
תגובת חותמת זמן היא מבנה DER קטן, ושום דבר בהעברה לא אוכף את זה. שרת שנפרץ, שהוגדר בצורה שגויה, או שפשוט מכוון ל-URL הלא נכון יכול להחזיר stream שרירותי, ולקוח שקורא עד שהחיבור נסגר יצבור אותו בשמחה. שתי ההעברות לכן מגבילות את התגובה, וזה המקום הנכון למגבלה: סירוב ברמת ההעברה מונע מלכתחילה הקצאה של גוף ענק, ואילו בדיקה ברמת ה-parser נורית רק אחרי שהזיכרון כבר הוקצה
אותה חשיבה חלה גם על ה-URL. ה-backend מקבל רק schemes שהוא יכול לדבר בהם באופן מועיל, כך שטעות קונפיגורציה נכשלת מיד עם הודעה ברורה במקום להימסר ל-libcurl כדי שיפרש אותה בכל דרך שתמיכת הפרוטוקולים שלו מרשה
איפה ההעברה יושבת בסיפור החתימה
חותמות זמן הן הצעד הראשון של סיפור האימות ארוך-הטווח ולא כל הסיפור. את ה-token צריך לצרף לחתימה, את חומר האימות צריך לרשום ב-document security store, וחותמות ארכיון צריך לחדש לפני שהנוכחית נחלשת. את הקשת כולה מכסה המאמר בחתימות PDF ארוכות טווח עם חותמות זמן של RFC 3161 וה-DSS
ההעברה היא גם חתיכה אחת מעמדת ניידות רחבה יותר: ה-loader של הספרייה הנטיבית שמתואר בטעינת הספרייה הנטיבית בכל יעד מטפל באותה משפחת בעיות עבור הבינארי של PDFium עצמו. בשני המקרים התבנית זהה, לקשור מספר קטן של סימבולים בצורה דינמית, לדווח בדיוק מה נכשל בקישור, ולעולם לא לתת לתלות חסרה להפוך לכישלון של link-time שמונע מהאפליקציה להתחיל
ה-backend לחותמות זמן ל-Windows וה-backend לשאר היעדים מגיעים שניהם עם רכיב ה-PDFium ל-Delphi, כשהבחירה לפי יעד ולא לפי קונפיגורציה, כך שאפליקציית Lazarus על Linux ואפליקציית Delphi על Windows מייצרות את אותה חתימה עם חותמת זמן דרך צנרת שונה