Funkcija PLCreateSelfSignedCertificate u PDFlibPas-u gradi samopotpisani RSA/SHA-256 sertifikat i izvozi ga, zajedno sa privatnim ključem, direktno u PFX datoteku zaštićenu lozinkom, koristeći samo Win32 CryptoAPI koji je već instaliran na svakom Windows računaru. Nisu potrebni spoljni alat, sertifikaciono telo ni ručni korak sa makecert ili OpenSSL: jedan poziv funkcije i jedan sertifikat dovoljan za test potpisivanja
Situacija u kojoj je ova funkcija najkorisnija gotovo je uvek CI cevovod. Probni test potpisivanja zahteva pravi PFX sa pravim privatnim ključem, a čuvanje takve datoteke u repozitorijumu samo po sebi predstavlja bezbednosni problem, jer privatni ključ postaje procureo čim se nađe u komitu. Pozivanje makecert.exe ili OpenSSL-a iz skripte za izgradnju takođe radi, ali cevovod tada zavisi od alata koji mora biti instaliran, pronađen na PATH-u i usklađen po verziji na svakom agentu izgradnje. Generisanje sertifikata u istom procesu koji izvršava test, istim Win32 CryptoAPI pozivima koje Windows već isporučuje, potpuno uklanja tu zavisnost
Šta PLCreateSelfSignedCertificate zaista proizvodi
PLCreateSelfSignedCertificate pravi PFX datoteku zaštićenu lozinkom koja sadrži samopotpisani RSA sertifikat i privatni ključ, potpisan algoritmom sha256RSA, na osnovu pet parametara: SubjectName, PFXFileName, PFXPassword, ValidDays i KeyBits, a vraća običnu Boolean zastavicu uspeha. SubjectName prihvata punu X.500 vrednost kao što je 'CN=Alice, O=Example', a golo ime bez znaka = automatski dobija prefiks CN=. Vrednost ValidDays manja od 1 vraća se na 365, a KeyBits izvan opsega od 1024 do 16384 vraća se na 2048. PDFlibPas isporučuje ovu funkciju od v3.224.0, dostupnu ne samo iz Delphi jedinice već i kroz DLL i ActiveX površine, a njen komentar u dokumentaciji jasno navodi granicu korisnosti: svi glavni čitači samopotpisani sertifikat označavaju kao nepouzdan dok ga neko izričito ne instalira, pa ga koristite za izvršavanje putanje koda, a ne za potpis kome bi iko izvan tima trebalo da veruje
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;
Zašto CryptGenKey kodira dužinu ključa u parametru zastavica
CryptGenKey pakuje dve nepovezane postavke u jedan parametar dwFlags. Donja reč sadrži zastavice ponašanja, među njima CRYPT_EXPORTABLE, dok gornja reč za RSA ključ razmene nosi traženu dužinu ključa u bitovima. Ako 2048 prosledite kao da je samo još jedna zastavica, završava u donjoj reči, gde ne odgovara nijednoj zastavici ponašanja koju CryptoAPI definiše, pa poziv pravi ključ podrazumevane dužine na koju se dobavljač vraća, a ne dužine koju je pozivalac mislio da je zatražio. Za stvarni RSA ključ od 2048 bitova broj prvo mora da se pomeri u gornju reč
// 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;
Šta se dešava ako zaboravite CRYPT_EXPORTABLE
Ako iz iste vrednosti zastavica izostavite CRYPT_EXPORTABLE, CryptGenKey i dalje uspeva, ali privatni ključ označava kao neizvoziv na nivou CSP-a. Sve nizvodno takođe nastavlja da prijavljuje uspeh: CertCreateSelfSignCertificate vraća važeći kontekst sertifikata, a PFXExportCertStoreEx, čak i kada se pozove sa EXPORT_PRIVATE_KEYS, uspeva i upisuje PFX datoteku koja se otvara, parsira i izgleda sasvim obično. Ono što joj nedostaje jeste privatni ključ, jer CSP nije dozvolio da napusti kontejner ključa, a PFXExportCertStoreEx to odbijanje nikada ne tretira kao razlog da obori ceo izvoz
Greška se pojavljuje tek kasnije i na sasvim drugom mestu: poziv za potpisivanje otvori taj PFX, pronađe sertifikat bez pridruženog privatnog ključa i prijavi istu grešku koju biste dobili od oštećenog ili pogrešnog PFX-a, a ne od zastavice koja nedostaje tri sloja iznad. Ko otklanja grešku samo sa strane potpisivanja može provesti popodne tražeći problem u pogrešnoj datoteci pre nego što shvati da je stvarna greška jedan nedostajući bit u trenutku generisanja ključa, u potpuno drugom pozivu funkcije, možda i u potpuno drugoj skripti za izgradnju
Zašto ProvType mora da se podudara između CryptAcquireContextW i sertifikata
ProvType mora da se podudara zato što CertCreateSelfSignCertificate privatni ključ novog sertifikata razrešava kroz zapis CRYPT_KEY_PROV_INFO, a jedno polje tog zapisa, ProvType, mora da navede potpuno istu vrednost tipa CSP-a koja je prosleđena funkciji CryptAcquireContextW pri otvaranju kontejnera ključa, u implementaciji PDFlibPas-a PROV_RSA_AES, brojčano 24. Ako ProvType postavite na nulu ili na bilo koju konstantu dobavljača koja se razlikuje od one kojoj kontejner zaista pripada, sertifikat i dalje može biti napravljen, ali zapisana veza nazad do privatnog ključa više ne vodi do kontejnera koji ga sadrži, pa se kasnije javlja greška pri potpisivanju ili izvozu koja nema veze sa stvarnim kriptografskim sadržajem sertifikata
// 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
Sastavljanje celine: od GUID kontejnera do PFX-a zaštićenog lozinkom
Lanac poziva unutar funkcije PLCreateSelfSignedCertificate prati jednu ravnu putanju: otvara svež kontejner ključa nazvan po novom GUID-u, tako da se paralelna CI izvršavanja nikada ne sudare oko imena kontejnera, generiše RSA par ključeva u njemu pomoću dve prethodno opisane zastavice, kodira SubjectName u X.500 blok imena kroz CertStrToNameW i poziva CertCreateSelfSignCertificate sa periodom važenja izračunatim iz ValidDays i prosleđenim kao obična struktura oblika SYSTEMTIME. Dobijeni kontekst sertifikata smešta se u memorijsku prodavnicu sertifikata otvorenu pomoću CertOpenStore i CERT_STORE_PROV_MEMORY, samo da bi PFXExportCertStoreEx imao prodavnicu iz koje izvozi, jer taj API radi sa rukohvatom prodavnice, a ne sa samostalnim kontekstom sertifikata
// 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 prati uobičajenu Win32 konvenciju sa dva prolaza: prvi put se poziva sa baferom dužine nula da bi se saznalo koliko bajtova PFX zahteva, zatim se alocira toliko prostora i funkcija poziva drugi put da popuni bafer. Kada su bajtovi na disku, PDFlibPas briše privremeni kontejner ključa pomoću CRYPT_DELETEKEYSET umesto da ga ostavi, jer PFX već nosi sopstvenu kopiju svakog bajta materijala ključa koji je kontejner sadržao. Ako se čišćenje preskoči, svaki poziv funkcije PLCreateSelfSignedCertificate ostavlja napušten kontejner ključa sa imenom GUID-a u profilu korisnika koji poziva funkciju, upravo onu vrstu curenja koju CI agent koji ovo izvršava pri svakoj izgradnji može mesecima da gomila pre nego što je neko primeti
Da li je samopotpisani sertifikat bezbedan za potpisivanje u produkciji
Ne: samopotpisani sertifikat je bezbedan za proveru putanje koda za potpisivanje, ali nije bezbedan za potpis kome bi neko izvan tima trebalo da veruje, jer ga ništa ne povezuje sa korenom kojem softver strane koja se oslanja na sertifikat već veruje. Prirodan sledeći korak za ovakav PFX jeste stvarni poziv za potpisivanje, obrađen u tekstu izgradnje radnog okruženja za usaglašenost i potpisivanje u Delphi-ju sa PDFlibPas-om, gde ovako napravljen PFX pokreće polovinu cevovoda za potpisivanje koja takođe izvršava PDF/A preflight i ByteRange provere. Potpisivanje je ipak samo polovina priče oko sertifikata, a druga polovina je upravo mesto na kojem samopotpisani krajnji sertifikat treba da padne: PAdES potpisivanje i validacija u Delphi-ju sa PDFlibPas-om pokrivaju provere lanca poverenja koje izvršava validator usaglašenosti, a validator koji lanac prati do pouzdanog korena nema razlog da veruje sertifikatu koji je ova funkcija pre pet minuta izmislila ni iz čega
PLCreateSelfSignedCertificate je jedna od funkcija među API-jima za sertifikate i potpisivanje u PDF biblioteci PDFlibPas za Delphi i C++Builder, i postoji upravo zbog ovde opisane praznine: testa potpisivanja kome je potreban pravi par ključeva, bez spoljnog generatora