Funkcja PLCreateSelfSignedCertificate w PDFlibPas buduje samopodpisany certyfikat RSA/SHA-256 i eksportuje go, wraz z kluczem prywatnym, wprost do pliku PFX chronionego hasłem, używając wyłącznie Win32 CryptoAPI już zainstalowanego na każdej maszynie z Windows. Żadnego zewnętrznego narzędzia, żadnego urzędu certyfikacji, żadnego ręcznego kroku makecert czy OpenSSL: jedno wywołanie funkcji, jeden certyfikat wystarczająco dobry, aby napędzić test podpisywania
Scenariusz, który czyni tę funkcję wartą posiadania, to niemal zawsze potok CI. Test dymny podpisywania potrzebuje prawdziwego PFX z prawdziwym kluczem prywatnym za nim, a zacommitowanie takiego do repozytorium to własny problem bezpieczeństwa, ponieważ zacommitowany klucz prywatny to wyciekły klucz prywatny od chwili, gdy ten commit ląduje. Wywołanie makecert.exe lub OpenSSL ze skryptu buildowego też działa, ale wtedy potok zależy od narzędzia, które musi być zainstalowane, znalezione na PATH i utrzymywane w spójnej wersji na każdym agencie builda. Generowanie certyfikatu wewnątrz tego samego procesu, który uruchamia test, tymi samymi wywołaniami Win32 CryptoAPI, które Windows już dostarcza, całkowicie usuwa tę zależność
Co faktycznie produkuje PLCreateSelfSignedCertificate?
PLCreateSelfSignedCertificate produkuje plik PFX chroniony hasłem, niosący samopodpisany certyfikat RSA i jego klucz prywatny, podpisany sha256RSA, sterowany pięcioma parametrami: SubjectName, PFXFileName, PFXPassword, ValidDays i KeyBits, i zwraca zwykłą logiczną flagę sukcesu. SubjectName akceptuje pełny łańcuch X.500, taki jak 'CN=Alice, O=Example', a gołej nazwie bez znaku = automatycznie dodaje się prefiks CN=. ValidDays poniżej 1 wraca do wartości domyślnej 365, a KeyBits spoza zakresu 1024 do 16384 wraca do domyślnego 2048. PDFlibPas dostarcza tę funkcję od v3.224.0, dostępną nie tylko z jednostki Delphi, ale też przez powierzchnie DLL i ActiveX, a jej własny komentarz dokumentacyjny jest bezpośredni co do tego, gdzie przestaje być użyteczna: każda popularna przeglądarka oznacza samopodpisany certyfikat jako niezaufany, chyba że ktoś jawnie go zainstaluje, więc traktuj to, co produkuje, jako certyfikat do ćwiczenia ścieżki kodu, nie jako podpis, o którego zaufanie powinien być proszony ktokolwiek poza twoim zespołem
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;
Dlaczego CryptGenKey koduje długość klucza w parametrze flag?
CryptGenKey pakuje dwa niepowiązane ustawienia w jeden parametr dwFlags. Dolne słowo niesie flagi zachowania, w tym CRYPT_EXPORTABLE, podczas gdy górne słowo, dla klucza wymiany kluczy RSA, niesie żądaną długość klucza w bitach. Przekazanie 2048 tak, jakby to była tylko kolejna flaga, ląduje w dolnym słowie zamiast, gdzie nie pasuje do żadnej flagi zachowania, jaką definiuje CryptoAPI, więc wywołanie generuje klucz o dowolnej domyślnej długości, do której dostawca wraca, a nie o długości, o jaką wywołujący myślał, że prosi. Uzyskanie faktycznego 2048-bitowego klucza RSA oznacza wcześniejsze przesunięcie liczby do górnego słowa
// 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;
Co się dzieje, jeśli zapomnisz o CRYPT_EXPORTABLE?
Usuń CRYPT_EXPORTABLE z tej samej wartości flag, a CryptGenKey wciąż się powiedzie, ale oznaczy wygenerowany klucz prywatny jako nieeksportowalny na poziomie CSP. Wszystko niżej w potoku też nadal zgłasza sukces: CertCreateSelfSignCertificate zwraca ważny kontekst certyfikatu, a PFXExportCertStoreEx, nawet wywołane z EXPORT_PRIVATE_KEYS, i tak się powiedzie i zapisze plik PFX, który się otwiera, parsuje i wygląda całkowicie zwyczajnie. To, czego nie zawiera, to klucz prywatny, ponieważ CSP odmówił wypuszczenia go z kontenera kluczy, a PFXExportCertStoreEx nigdy nie traktuje tej odmowy jako powodu, by cały eksport zawiódł
Awaria ujawnia się dopiero później, i zupełnie gdzie indziej: wywołanie podpisujące otwiera ten PFX, znajduje certyfikat bez dołączonego klucza prywatnego, i zgłasza dokładnie ten błąd, jaki dostałbyś z uszkodzonego lub złego PFX, nie z brakującej flagi trzy warstwy wcześniej. Każdy debugujący wyłącznie od strony podpisywania może spalić popołudnie na niewłaściwym pliku, zanim zda sobie sprawę, że prawdziwy błąd to jeden brakujący bit w czasie generowania klucza, w zupełnie innym wywołaniu funkcji, być może w zupełnie innym skrypcie buildowym
Dlaczego ProvType musi się zgadzać między CryptAcquireContextW a certyfikatem?
ProvType musi się zgadzać, ponieważ CertCreateSelfSignCertificate rozwiązuje klucz prywatny nowego certyfikatu przez rekord CRYPT_KEY_PROV_INFO, a jedno pole w tym rekordzie, ProvType, musi nazywać dokładnie tę samą wartość typu CSP, jaka została przekazana do CryptAcquireContextW, gdy otwierano kontener kluczy, PROV_RSA_AES, liczbowo 24, w implementacji PDFlibPas. Ustaw ProvType na zero, lub na dowolną stałą dostawcy inną niż ta, do której faktycznie należy kontener, a certyfikat wciąż może zostać utworzony, ale jego zapisane powiązanie z powrotem do klucza prywatnego przestaje rozwiązywać się do kontenera, który go trzyma, co ujawnia się później jako awaria podpisywania lub eksportu, która nie ma nic wspólnego z faktyczną zawartością kryptograficzną certyfikatu
// 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
Składanie tego razem: od kontenera GUID do PFX chronionego hasłem
Łańcuch wywołań wewnątrz PLCreateSelfSignedCertificate podąża jedną prostą linią, otwierając świeży kontener kluczy nazwany od nowo wygenerowanego GUID, tak aby równoczesne uruchomienia CI nigdy nie kolidowały o nazwy kontenerów, generując parę kluczy RSA wewnątrz niego z dwiema flagami omówionymi powyżej, kodując SubjectName do blob nazwy X.500 przez CertStrToNameW, i wywołując CertCreateSelfSignCertificate z oknem ważności obliczonym z ValidDays i przekazanym jako zwykła struktura w kształcie SYSTEMTIME. Wynikowy kontekst certyfikatu trafia do magazynu certyfikatów w pamięci, otwartego przez CertOpenStore i CERT_STORE_PROV_MEMORY, wyłącznie po to, żeby PFXExportCertStoreEx miał magazyn, z którego eksportować, ponieważ to API działa na uchwycie magazynu, a nie na gołym kontekście certyfikatu
// 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 sama podąża za zwykłą dwuprzebiegową konwencją Win32: wywołaj ją raz z buforem o zerowej długości, aby poznać, ile bajtów potrzebuje PFX, zaalokuj tyle, a następnie wywołaj ją ponownie, aby wypełnić bufor. Gdy tylko bajty są na dysku, PDFlibPas usuwa jednorazowy kontener kluczy przez CRYPT_DELETEKEYSET, zamiast go zostawiać, ponieważ PFX już niesie własną kopię każdego bajtu materiału kluczowego, który trzymał kontener. Pomiń to sprzątanie, a każde wywołanie PLCreateSelfSignedCertificate zostawia osierocony, nazwany GUID-em kontener kluczy siedzący w profilu wywołującego użytkownika, co jest dokładnie tym rodzajem wycieku, który agent CI uruchamiający tę funkcję przy każdym buildzie będzie gromadził przez miesiące, zanim ktokolwiek to zauważy
Czy samopodpisany certyfikat jest bezpieczny do podpisywania produkcyjnego?
Nie: samopodpisany certyfikat jest bezpieczny do ćwiczenia ścieżki kodu podpisywania i niebezpieczny dla podpisu, któremu ktokolwiek poza zespołem miałby zaufać, ponieważ nic nie łączy go z powrotem z korzeniem, któremu oprogramowanie strony ufającej już ufa. Naturalnym kolejnym krokiem dla takiego PFX jest faktyczne wywołanie podpisujące, omówione w budowie warsztatu zgodności i podpisywania w Delphi za pomocą PDFlibPas, gdzie PFX zbudowany w ten sposób napędza połowę podpisującą potoku, który uruchamia też preflight PDF/A i audyty ByteRange. Podpisywanie to jednak tylko połowa tego, co otacza certyfikat, a druga połowa to dokładnie to, gdzie samopodpisany liść ma zawieść: podpisywanie i walidacja PAdES w Delphi za pomocą PDFlibPas omawia sprawdzenia łańcucha zaufania, jakie uruchamia walidator zgodności, a walidator, który przechodzi łańcuch z powrotem do zaufanego korzenia, nie ma powodu ufać certyfikatowi, który ta funkcja wymyśliła pięć minut temu z niczego
PLCreateSelfSignedCertificate to jedna funkcja spośród API certyfikatów i podpisywania w bibliotece PDF PDFlibPas dla Delphi i C++Buildera, i istnieje dokładnie dla luki opisanej tutaj: testu podpisywania, który potrzebuje za sobą prawdziwej pary kluczy i niczego zewnętrznego, aby ją wygenerować