Функцията PLCreateSelfSignedCertificate на PDFlibPas изгражда самоподписан RSA/SHA-256 сертификат и го експортира заедно с частния ключ директно в защитен с парола PFX файл, използвайки Win32 CryptoAPI, който вече е наличен във всеки Windows. Не са нужни външен инструмент, сертифициращ орган или ръчна стъпка с makecert.exe или OpenSSL: едно извикване създава сертификат, достатъчен за тест на подписването
Сценарият, в който тази функция е най-полезна, почти винаги е CI pipeline. Smoke тестът за подписване се нуждае от реален PFX с реален частен ключ, а записването му в repository създава собствен проблем със сигурността, защото записаният private key е изтекъл от момента, в който commit-ът попадне в хранилището. Извикването на makecert.exe или OpenSSL от build script също работи, но тогава pipeline-ът зависи от инструмент, който трябва да е инсталиран, да се намира в PATH и да има съвместима версия на всеки build agent. Генерирането на сертификата в същия процес, който изпълнява теста, със същите 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 unit, но и през DLL и ActiveX повърхностите, а нейният doc comment ясно показва границата на употреба: всеки широко използван viewer маркира самоподписания сертификат като недоверен, докато някой не го инсталира изрично, затова използвайте резултата за упражняване на code path, а не за подпис, на който някой извън екипа трябва да се довери
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-ът се връща, вместо с дължината, поискана от извикващия код. За действителен 2048-битов RSA ключ числото трябва първо да се измести в горната дума
// 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 връща валиден certificate context, а PFXExportCertStoreEx, дори извикано с EXPORT_PRIVATE_KEYS, също завършва успешно и записва PFX файл, който се отваря, анализира и изглежда напълно нормален. Това, което липсва, е частният ключ, защото CSP не е позволил той да напусне key container-а, а PFXExportCertStoreEx не приема този отказ като причина да провали целия export
Грешката се проявява по-късно и на съвсем друго място: извикване за подписване отваря този PFX, намира сертификат без прикрепен частен ключ и съобщава точно грешката, която бихте получили от повреден или неправилен PFX, а не от липсващ флаг няколко слоя по-рано. Всеки, който отстранява проблема само от страната на подписването, може да изгуби часове в проверка на грешния файл, преди да разбере, че действителната причина е един липсващ bit при генерирането на ключа, в друго извикване на функция, а понякога и в друг build script
Защо ProvType трябва да съвпада между CryptAcquireContextW и сертификата?
ProvType трябва да съвпада, защото CertCreateSelfSignCertificate намира частния ключ на новия сертификат чрез запис CRYPT_KEY_PROV_INFO, а едно поле в този запис, ProvType, трябва да посочва точно същата стойност за CSP тип, подадена на CryptAcquireContextW, когато е отворен key container-ът — PROV_RSA_AES, числово 24 в реализацията на PDFlibPas. Ако зададете ProvType на нула или на друга константа за provider, сертификатът все пак може да бъде създаден, но записаната връзка към частния ключ вече не сочи към container-а, който го съдържа, и това по-късно се проявява като грешка при подписване или export, която няма общо с действителното криптографско съдържание на сертификата
// 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 следва права линия: отваря нов key container с име от новогенериран GUID, така че конкурентните CI изпълнения да не се сблъскват заради имената на контейнерите, генерира RSA key pair в него с двата разгледани по-горе флага, кодира SubjectName в X.500 name blob чрез CertStrToNameW и извиква CertCreateSelfSignCertificate с период на валидност, изчислен от ValidDays и предаден като обикновена структура във форма на SYSTEMTIME. Полученият certificate context се поставя във вътрешен certificate store, отворен с CertOpenStore и CERT_STORE_PROV_MEMORY, единствено за да има store, от който PFXExportCertStoreEx да експортира, защото този API работи с handle към store, а не с отделен certificate context
// 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 изтрива временния key container с CRYPT_DELETEKEYSET, вместо да го оставя, защото PFX вече съдържа собствено копие на всеки байт от ключовия материал, който контейнерът е държал. Ако пропуснете това почистване, всяко извикване на PLCreateSelfSignedCertificate оставя осиротял key container с име от GUID в профила на извикващия потребител, което е точно видът теч, който CI агент, изпълняващ тази функция при всеки build, може да натрупва месеци наред, преди някой да го забележи
Безопасен ли е самоподписаният сертификат за подписване в продукция?
Не: самоподписаният сертификат е подходящ за упражняване на code path за подписване, но не е подходящ за подпис, на който се очаква да се довери някой извън екипа, защото не води до root, на който софтуерът на relying party вече се доверява. Естествената следваща стъпка за подобен PFX е реално извикване за подписване, разгледано в изграждането на работна среда за съответствие и подписване в Delphi с PDFlibPas, където създаденият по този начин PFX захранва частта за подписване на pipeline, който изпълнява и PDF/A preflight, и одити на ByteRange. Подписването е само едната страна на сертификата, а другата е точно мястото, където самоподписаният leaf трябва да се провали: PAdES подписването и валидирането в Delphi с PDFlibPas обхваща проверките на trust chain, които изпълнява validator за съответствие, а validator, който проследява веригата до доверен root, няма причина да се довери на сертификат, измислен от тази функция преди пет минути от нищото
PLCreateSelfSignedCertificate е една от API функциите за сертификати и подписване в PDF библиотеката PDFlibPas за Delphi и C++Builder и съществува точно за описаната тук празнина: тест за подписване, който се нуждае от реална key pair и от нищо външно за нейното генериране