פונקציית ה-PLCreateSelfSignedCertificate של PDFlibPas בונה אישור 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. PDFlibPas שולחת את הפונקציה הזו מאז v3.224.0, נגישה לא רק מיחידת Delphi אלא גם דרך משטחי ה-DLL וה-ActiveX, וההערת-תיעוד שלה עצמה ישירה לגבי איפה היא מפסיקה להיות שימושית: כל מציג מרכזי מסמן אישור חתום-עצמית כלא-מהימן אלא אם מישהו מתקין אותו במפורש, כך שהתייחס למה שהיא מייצרת כאישור להפעלת נתיב-קוד, לא חתימה שמישהו מחוץ לצוות שלך צריך להתבקש להישען עליה
var
Success: Boolean;
begin
Success := PLCreateSelfSignedCertificate(
'CN=PDFlibPas 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-סיביות בפועל אומרת הזזת המספר למילה הגבוהה קודם
// Key length lives in the upper 16 bits of the CryptGenKey flags;
// the low word carries behavior flags such as 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, במימוש של PDFlibPas. הגדר ProvType לאפס, או לכל קבוע-ספק אחר מלבד זה שהמיכל בפועל שייך אליו, והאישור עדיין יכול להיווצר, אבל הקישור הרשום שלו בחזרה למפתח הפרטי כבר לא נפתר למיכל שמחזיק אותו, מה שעולה על פני השטח מאוחר יותר ככשל-חתימה או ייצוא שאין לו שום קשר לתוכן הקריפטוגרפי בפועל של האישור
// The provider type used to open the key container must match the
// provider type recorded in the certificate's key-provider info.
CryptAcquireContextW(hProv, PWideChar(Container), nil,
PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... generate the key, build the subject name blob, then:
KeyProvInfo.ProvType := PROV_RSA_AES; // same constant, both call sites
הרכבת הכל יחד: ממיכל GUID ל-PFX מוגן-סיסמה
שרשרת הקריאה בתוך PLCreateSelfSignedCertificate עוקבת אחר קו ישר אחד, פותחת מיכל-מפתח טרי בעל שם על שם GUID שזה עתה נוצר כך שריצות CI בו-זמניות אף פעם לא מתנגשות על שמות מיכל, מייצרת את זוג מפתחות ה-RSA בתוכו עם שני הדגלים המכוסים לעיל, מקדדת SubjectName לתוך blob שם X.500 דרך CertStrToNameW, וקוראת ל-CertCreateSelfSignCertificate עם חלון-תוקף שחושב מ-ValidDays ונמסר כמבנה בצורת SYSTEMTIME פשוט. הקשר-האישור שנוצר הולך לתוך אחסון-אישור בזיכרון שנפתח עם CertOpenStore ו-CERT_STORE_PROV_MEMORY, אך ורק כדי ש-PFXExportCertStoreEx יהיה לה אחסון לייצא ממנו, שכן ה-API ההוא עובד מול ידית-אחסון ולא הקשר-אישור ערום
// Each call opens a throwaway container named after a fresh GUID:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... generate the key, self-sign the certificate, export the PFX ...
// then delete the container once the PFX holds its own copy of the key:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
PROV_RSA_AES, CRYPT_DELETEKEYSET);
PFXExportCertStoreEx עצמה עוקבת אחר מוסכמת שני-המעברים הרגילה של Win32: קרא לה פעם אחת עם מאגר באורך-אפס כדי לדעת כמה בייטים ה-PFX זקוק להם, הקצה בדיוק את זה, ואז קרא לה שוב כדי למלא את המאגר. ברגע שהבייטים על הדיסק, PDFlibPas מוחקת את מיכל-המפתח החד-פעמי עם CRYPT_DELETEKEYSET במקום להשאיר אותו מאחור, משום שה-PFX כבר נושא עותק משלו של כל בייט של חומר-מפתח שהמיכל החזיק. דלג על הניקוי ההוא וכל קריאה ל-PLCreateSelfSignedCertificate משאירה מיכל-מפתח מיותם, בעל-שם-GUID, שיושב בפרופיל המשתמש הקורא, שזה בדיוק סוג הדליפה שסוכן CI שמריץ את הפונקציה הזו על כל בנייה יצבור לאורך חודשים לפני שמישהו שם לב
האם בטוח להשתמש באישור חתום-עצמית לחתימת ייצור?
לא: אישור חתום-עצמית בטוח להפעלת נתיב-קוד חתימה ולא-בטוח עבור חתימה שמישהו מחוץ לצוות צפוי לבטוח בה, משום ששום דבר לא משרשר אותו בחזרה לשורש שתוכנת הצד-הסומך כבר בוטחת בו. הצעד הטבעי הבא עבור PFX כזה הוא קריאת חתימה בפועל, מכוסה בבניית שולחן עבודה לתאימות וחתימה ב-Delphi עם PDFlibPas, שם PFX שנבנה כך מניע את מחצית-החתימה של צינור שגם מריץ preflight של PDF/A וביקורות ByteRange. חתימה היא רק חצי ממה שיושב סביב אישור, עם זאת, והמחצית האחרת היא בדיוק המקום שאישור-עלה חתום-עצמית אמור להיכשל בו: חתימה ואימות PAdES ב-Delphi עם PDFlibPas מכסה את בדיקות שרשרת-האמון שמאמת-תאימות מריץ, ומאמת שעובר את השרשרת בחזרה לשורש מהימן אין לו סיבה לבטוח באישור שהפונקציה הזו המציאה לפני חמש דקות מכלום
PLCreateSelfSignedCertificate היא פונקציה אחת מבין ה-API-ים של אישור וחתימה בספריית ה-PDF PDFlibPas עבור Delphi ו-C++Builder, והיא קיימת בדיוק עבור הפער המתואר כאן: בדיקת חתימה שזקוקה לזוג מפתחות אמיתי מאחוריה ושום דבר חיצוני כדי לייצר אחד