פונקציית ה-PLCreateSelfSignedCertificate של PDF Library for Delphi בונה אישור RSA/SHA-256 חתום-עצמית ומייצאת אותו, מפתח פרטי כלול, ישר לתוך קובץ PFX מוגן-סיסמה, באמצעות רק Win32 CryptoAPI שכבר מותקן בכל מחשב Windows. אין כלי חיצוני, אין רשות אישורים, אין שלב makecert או OpenSSL ידני: קריאת פונקציה אחת, אישור אחד טוב מספיק כדי להניע בדיקת חתימה
התרחיש שהופך את הפונקציה הזו לשווה-להחזיק כמעט תמיד צינור CI. בדיקת-עשן לחתימה זקוקה ל-PFX אמיתי עם מפתח פרטי אמיתי מאחוריו, והכנסת אחד למאגר היא בעיית אבטחה משלה, שכן מפתח פרטי שנעשה לו commit הוא מפתח פרטי דלוף מהרגע שה-commit ההוא נוחת. הרצת makecert.exe או קריאת OpenSSL משגרת בנייה עובדת גם היא, אבל אז הצינור תלוי בכלי שחייב להיות מותקן, נמצא ב-PATH, ונשמר עקבי-גרסה על פני כל סוכן-בנייה. יצירת האישור בתוך אותו תהליך שמריץ את הבדיקה, עם אותן קריאות Win32 CryptoAPI ש-Windows כבר שולח, מסירה את התלות ההיא לגמרי
מה PLCreateSelfSignedCertificate בפועל מייצרת?
PLCreateSelfSignedCertificate מייצרת קובץ PFX מוגן-סיסמה שמחזיק אישור RSA חתום-עצמית והמפתח הפרטי שלו, חתום עם sha256RSA, מונע על ידי חמישה פרמטרים: SubjectName, PFXFileName, PFXPassword, ValidDays, ו-KeyBits, והיא מחזירה דגל הצלחה Boolean פשוט. SubjectName מקבל מחרוזת X.500 מלאה כמו 'CN=Alice, O=Example', ושם ערום ללא סימן = בתוכו מקבל אוטומטית קידומת CN=. ValidDays מתחת ל-1 נופל בחזרה ל-365, ו-KeyBits מחוץ לטווח 1024 עד 16384 נופל בחזרה ל-2048. PDF Library for Delphi שולחת את הפונקציה הזו מאז v3.224.0, נגישה לא רק מיחידת Delphi אלא גם דרך משטחי ה-DLL וה-ActiveX, וההערת-תיעוד שלה עצמה ישירה לגבי איפה היא מפסיקה להיות שימושית: כל מציג מרכזי מסמן אישור חתום-עצמית כלא-מהימן אלא אם מישהו מתקין אותו במפורש, כך שהתייחס למה שהיא מייצרת כאישור להפעלת נתיב-קוד, לא חתימה שמישהו מחוץ לצוות שלך צריך להתבקש להישען עליה
var
Success: Boolean;
begin
Success := PLCreateSelfSignedCertificate(
'CN=PDF Library for Delphi CI Test, O=Example Corp',
'ci-test-signer.pfx',
'a-strong-throwaway-password',
365, // ValidDays
2048); // KeyBits
if not Success then
raise Exception.Create('Self-signed certificate generation failed');
end;
למה CryptGenKey מקודד את אורך המפתח בפרמטר הדגלים?
CryptGenKey אורזת שתי הגדרות בלתי-קשורות לתוך פרמטר dwFlags יחיד. המילה הנמוכה נושאת דגלי-התנהגות, CRYPT_EXPORTABLE ביניהם, בעוד המילה הגבוהה, עבור מפתח חילופי-מפתח RSA, נושאת את אורך המפתח המבוקש בסיביות. מסירת 2048 כאילו הוא סתם דגל נוסף מנחיתה אותו במילה הנמוכה במקום זאת, שם הוא לא תואם אף דגל-התנהגות ש-CryptoAPI מגדיר, כך שהקריאה מייצרת מפתח באורך ברירת-המחדל שהספק (provider) נופל בחזרה אליו במקום האורך שהקוד הקורא חשב שהוא ביקש. קבלת מפתח RSA בן 2048-סיביות בפועל אומרת הזזת המספר למילה הגבוהה קודם
// אורך המפתח יושב ב-16 הסיביות העליונות של דגלי CryptGenKey;
// המילה הנמוכה נושאת דגלי-התנהגות כמו CRYPT_EXPORTABLE.
if not CryptGenKey(hProv, AT_KEYEXCHANGE,
(Cardinal(KeyBits) shl 16) or CRYPT_EXPORTABLE, hKey) then
Exit;
מה קורה אם אתה שוכח CRYPT_EXPORTABLE?
השמט CRYPT_EXPORTABLE מאותו ערך דגלים ו-CryptGenKey עדיין מצליחה, אבל היא מסמנת את המפתח הפרטי שנוצר כבלתי-ניתן-לייצוא ברמת ה-CSP. כל מה שבמורד-הזרם ממשיך לדווח הצלחה גם: CertCreateSelfSignCertificate מחזירה הקשר-אישור תקף, ו-PFXExportCertStoreEx, אפילו נקראת עם EXPORT_PRIVATE_KEYS, מצליחה בכל זאת וכותבת קובץ PFX שנפתח, מפוענח, ונראה רגיל לגמרי. מה שהוא לא מכיל הוא המפתח הפרטי, משום ש-ה-CSP סירב לתת לו לצאת ממיכל-המפתח, ו-PFXExportCertStoreEx אף פעם לא מתייחסת לסירוב ההוא כסיבה להיכשל בכל הייצוא
הכשל מופיע רק מאוחר יותר, ובמקום אחר לגמרי: קריאת חתימה פותחת את ה-PFX ההוא, מוצאת אישור ללא מפתח פרטי מצורף, ומדווחת בדיוק את השגיאה שהיית מקבל מ-PFX פגום או שגוי, לא ממחוסר-דגל שלוש שכבות במעלה-הזרם. כל מי שמנפה שגיאות מצד-החתימה לבדו יכול לשרוף אחר-צהריים על הקובץ הלא-נכון לפני שהוא מבין שהבאג האמיתי הוא סיבית בודדת חסרה בזמן יצירת-מפתח, בקריאת פונקציה שונה לגמרי, אולי בסקריפט-בנייה שונה לגמרי
למה ProvType חייב להתאים בין CryptAcquireContextW לאישור?
ProvType חייב להתאים משום ש-CertCreateSelfSignCertificate פותרת את המפתח הפרטי של האישור החדש דרך רשומת CRYPT_KEY_PROV_INFO, ושדה אחד ברשומה ההיא, ProvType, חייב לנקוב באותו ערך-סוג-CSP בדיוק שנמסר ל-CryptAcquireContextW כאשר מיכל-המפתח נפתח, PROV_RSA_AES, מספרית 24, במימוש של PDF Library for Delphi. הגדר ProvType לאפס, או לכל קבוע-ספק אחר מלבד זה שהמיכל בפועל שייך אליו, והאישור עדיין יכול להיווצר, אבל הקישור הרשום שלו בחזרה למפתח הפרטי כבר לא נפתר למיכל שמחזיק אותו, מה שעולה על פני השטח מאוחר יותר ככשל-חתימה או ייצוא שאין לו שום קשר לתוכן הקריפטוגרפי בפועל של האישור
// סוג הספק ששימש לפתיחת מיכל המפתח חייב להתאים
// לסוג הספק הרשום במידע ספק-המפתח של האישור.
CryptAcquireContextW(hProv, PWideChar(Container), nil,
PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... צור את המפתח, בנה את ה-blob של שם הנושא, ואז:
KeyProvInfo.ProvType := PROV_RSA_AES; // אותו קבוע, בשני אתרי הקריאה
הרכבת הכל יחד: ממיכל GUID ל-PFX מוגן-סיסמה
שרשרת הקריאה בתוך PLCreateSelfSignedCertificate עוקבת אחר קו ישר אחד, פותחת מיכל-מפתח טרי בעל שם על שם GUID שזה עתה נוצר כך שריצות CI בו-זמניות אף פעם לא מתנגשות על שמות מיכל, מייצרת את זוג מפתחות ה-RSA בתוכו עם שני הדגלים המכוסים לעיל, מקדדת SubjectName לתוך blob שם X.500 דרך CertStrToNameW, וקוראת ל-CertCreateSelfSignCertificate עם חלון-תוקף שחושב מ-ValidDays ונמסר כמבנה בצורת SYSTEMTIME פשוט. הקשר-האישור שנוצר הולך לתוך אחסון-אישור בזיכרון שנפתח עם CertOpenStore ו-CERT_STORE_PROV_MEMORY, אך ורק כדי ש-PFXExportCertStoreEx יהיה לה אחסון לייצא ממנו, שכן ה-API ההוא עובד מול ידית-אחסון ולא הקשר-אישור ערום
// כל קריאה פותחת מיכל חד-פעמי בשם על שם GUID טרי:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... צור את המפתח, חתום עצמית על האישור, ייצא את ה-PFX ...
// ואז מחק את המיכל ברגע שה-PFX מחזיק עותק משלו של המפתח:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
PROV_RSA_AES, CRYPT_DELETEKEYSET);
PFXExportCertStoreEx עצמה עוקבת אחר מוסכמת שני-המעברים הרגילה של Win32: קרא לה פעם אחת עם מאגר באורך-אפס כדי לדעת כמה בייטים ה-PFX זקוק להם, הקצה בדיוק את זה, ואז קרא לה שוב כדי למלא את המאגר. ברגע שהבייטים על הדיסק, PDF Library for Delphi מוחקת את מיכל-המפתח החד-פעמי עם CRYPT_DELETEKEYSET במקום להשאיר אותו מאחור, משום שה-PFX כבר נושא עותק משלו של כל בייט של חומר-מפתח שהמיכל החזיק. דלג על הניקוי ההוא וכל קריאה ל-PLCreateSelfSignedCertificate משאירה מיכל-מפתח מיותם, בעל-שם-GUID, שיושב בפרופיל המשתמש הקורא, שזה בדיוק סוג הדליפה שסוכן CI שמריץ את הפונקציה הזו על כל בנייה יצבור לאורך חודשים לפני שמישהו שם לב
האם בטוח להשתמש באישור חתום-עצמית לחתימת ייצור?
לא: אישור חתום-עצמית בטוח להפעלת נתיב-קוד חתימה ולא-בטוח עבור חתימה שמישהו מחוץ לצוות צפוי לבטוח בה, משום ששום דבר לא משרשר אותו בחזרה לשורש שתוכנת הצד-הסומך כבר בוטחת בו. הצעד הטבעי הבא עבור PFX כזה הוא קריאת חתימה בפועל, מכוסה בבניית שולחן עבודה לתאימות וחתימה ב-Delphi עם PDF Library for Delphi, שם PFX שנבנה כך מניע את מחצית-החתימה של צינור שגם מריץ preflight של PDF/A וביקורות ByteRange. חתימה היא רק חצי ממה שיושב סביב אישור, עם זאת, והמחצית האחרת היא בדיוק המקום שאישור-עלה חתום-עצמית אמור להיכשל בו: חתימה ואימות PAdES ב-Delphi עם PDF Library for Delphi מכסה את בדיקות שרשרת-האמון שמאמת-תאימות מריץ, ומאמת שעובר את השרשרת בחזרה לשורש מהימן אין לו סיבה לבטוח באישור שהפונקציה הזו המציאה לפני חמש דקות מכלום
PLCreateSelfSignedCertificate היא פונקציה אחת מבין ה-API-ים של אישור וחתימה בספריית ה-PDF PDF Library for Delphi עבור Delphi ו-C++Builder, והיא קיימת בדיוק עבור הפער המתואר כאן: בדיקת חתימה שזקוקה לזוג מפתחות אמיתי מאחוריה ושום דבר חיצוני כדי לייצר אחד