Funkcija PLCreateSelfSignedCertificate u biblioteci PDFlibPas izrađuje samopotpisani RSA/SHA-256 certifikat i izvozi ga, zajedno s privatnim ključem, izravno u lozinkom zaštićenu PFX datoteku koristeći samo Win32 CryptoAPI koji je već instaliran na svakom računalu sa sustavom Windows. Nisu potrebni vanjski alat, certifikacijsko tijelo ni ručni korak s makecertom ili OpenSSLom: jedan poziv funkcije dovoljan je za certifikat prikladan za testiranje potpisivanja
Scenarij u kojem je ova funkcija gotovo uvijek korisna jest CI cjevovod. Test potpisivanja u sklopu provjere ispravnosti treba pravi PFX sa stvarnim privatnim ključem, a pohranjivanje takvog ključa u repozitorij samo po sebi predstavlja sigurnosni problem jer je predani privatni ključ procurio čim taj commit bude objavljen. Pozivanje makecert.exe ili OpenSSL-a iz skripte za izgradnju također funkcionira, ali tada cjevovod ovisi o alatu koji mora biti instaliran, pronađen na putu PATH i usklađen po verziji na svakom agentu za izgradnju. Generiranje certifikata unutar istog procesa koji pokreće test, istim pozivima Win32 CryptoAPI-ja koje Windows već isporučuje, potpuno uklanja tu ovisnost
Što zapravo proizvodi PLCreateSelfSignedCertificate?
PLCreateSelfSignedCertificate proizvodi lozinkom zaštićenu PFX datoteku koja sadrži samopotpisani RSA certifikat i njegov privatni ključ, potpisan algoritmom sha256RSA, uz pet parametara: SubjectName, PFXFileName, PFXPassword, ValidDays i KeyBits, a vraća običnu zastavicu uspjeha tipa Boolean. SubjectName prihvaća puni X.500 niz kao što je 'CN=Alice, O=Example', a goli naziv bez znaka = automatski dobiva prefiks CN=. Vrijednost ValidDays manja od 1 vraća se na 365, a vrijednost KeyBits izvan raspona od 1024 do 16384 vraća se na 2048. PDFlibPas isporučuje ovu funkciju od verzije v3.224.0, dostupnu ne samo iz Delphi jedinice nego i kroz DLL i ActiveX sučelja, a vlastiti komentar dokumentacije jasno navodi gdje prestaje njezina korisnost: svaki uobičajeni preglednik samopotpisani certifikat označava kao nepouzdan dok ga netko izričito ne instalira, pa ono što funkcija proizvede treba tretirati kao certifikat za izvršavanje određene putanje koda, a ne kao potpis kojem bi itko izvan vašeg tima trebao vjerovati
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 duljinu ključa u parametru flags?
CryptGenKey pakira dvije nepovezane postavke u jedan parametar dwFlags. Donja riječ sadržava zastavice ponašanja, među njima CRYPT_EXPORTABLE, dok gornja riječ za ključ razmjene RSA sadržava traženu duljinu ključa u bitovima. Ako se 2048 proslijedi kao da je samo još jedna zastavica, završava u donjoj riječi gdje ne odgovara nijednoj zastavici ponašanja koju CryptoAPI definira, pa poziv generira ključ zadane duljine na koju se davatelj vrati, umjesto duljine koju je pozivatelj mislio zatražiti. Za stvarni RSA ključ od 2048 bitova broj najprije treba pomaknuti u gornju riječ
// 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;
Što se događa ako zaboravite CRYPT_EXPORTABLE?
Ako izostavite CRYPT_EXPORTABLE iz iste vrijednosti zastavica, CryptGenKey i dalje uspijeva, ali privatni ključ koji generira na razini CSP-a označava kao neizvozni. Sve što slijedi također javlja uspjeh: CertCreateSelfSignCertificate vraća valjani kontekst certifikata, a PFXExportCertStoreEx, čak i kada se pozove s EXPORT_PRIVATE_KEYS, također uspijeva i zapisuje PFX datoteku koja se otvara, raščlanjuje i izgleda potpuno uobičajeno. Ono što ne sadrži jest privatni ključ jer mu CSP nije dopustio izlazak iz spremnika ključa, a PFXExportCertStoreEx to odbijanje nikada ne tretira kao razlog za neuspjeh cijelog izvoza
Neuspjeh se pojavljuje tek poslije i potpuno drugdje: poziv za potpisivanje otvara taj PFX, pronalazi certifikat bez pridruženog privatnog ključa i javlja upravo pogrešku kakvu biste dobili od oštećenog ili pogrešnog PFX-a, a ne od zastavice koja nedostaje nekoliko slojeva ranije. Svatko tko otklanja pogrešku samo s pomoću strane za potpisivanje može izgubiti cijelo poslijepodne na pogrešnoj datoteci prije nego što shvati da je stvarna pogreška jedan bit koji nedostaje pri generiranju ključa, u potpuno drugom pozivu funkcije, možda i u potpuno drugoj skripti za izgradnju
Zašto se ProvType mora podudarati između CryptAcquireContextW i certifikata?
ProvType se mora podudarati zato što CertCreateSelfSignCertificate razrješava privatni ključ novog certifikata preko zapisa CRYPT_KEY_PROV_INFO, a jedno polje u tom zapisu, ProvType, mora navesti potpuno istu vrijednost tipa CSP-a koja je proslijeđena funkciji CryptAcquireContextW pri otvaranju spremnika ključa, odnosno PROV_RSA_AES, numerički 24, u implementaciji PDFlibPas. Postavite ProvType na nulu ili na bilo koju konstantu davatelja koja se razlikuje od one kojoj spremnik stvarno pripada i certifikat se i dalje može stvoriti, ali njegova zabilježena veza s privatnim ključem više ne upućuje na spremnik koji ga sadržava, što se poslije pokazuje kao neuspjeh potpisivanja ili izvoza koji nema nikakve veze sa stvarnim kriptografskim sadržajem certifikata
// 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
Sve zajedno: od GUID spremnika do PFX-a zaštićenog lozinkom
Pozivni lanac unutar funkcije PLCreateSelfSignedCertificate slijedi jednu izravnu putanju: otvara novi spremnik ključeva nazvan prema novostvorenom GUID-u kako se istodobna CI pokretanja nikada ne bi sudarila oko naziva spremnika, u njemu generira par RSA ključeva s pomoću dviju prethodno opisanih zastavica, kodira SubjectName u X.500 blob putem funkcije CertStrToNameW i poziva CertCreateSelfSignCertificate s vremenskim prozorom valjanosti izračunatim iz ValidDays i predanim kao obična struktura oblika SYSTEMTIME. Dobiveni kontekst certifikata smješta se u memorijsko spremište certifikata otvoreno funkcijom CertOpenStore i davateljem CERT_STORE_PROV_MEMORY, samo zato da bi PFXExportCertStoreEx imao spremište iz kojeg može izvesti podatke, jer taj API radi s ručkom spremišta, a ne s golim kontekstom certifikata
// 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);
Sam PFXExportCertStoreEx slijedi uobičajenu Win32 konvenciju u dva prolaza: prvi se put poziva s međuspremnikom duljine nula kako bi se saznalo koliko bajtova PFX zahtijeva, zatim se alocira tolika količina memorije, a drugi se put poziva za popunjavanje međuspremnika. Nakon zapisivanja bajtova na disk PDFlibPas briše privremeni spremnik ključeva pomoću CRYPT_DELETEKEYSET umjesto da ga ostavi, jer PFX već sadržava vlastitu kopiju svakog bajta ključnog materijala koji je spremnik sadržavao. Preskočite to čišćenje i svaki poziv funkcije PLCreateSelfSignedCertificate ostavit će napušteni spremnik ključeva s nazivom GUID-a u profilu korisnika koji poziva funkciju, upravo onu vrstu curenja koju će CI agent koji tu funkciju pokreće pri svakoj izgradnji nakupljati mjesecima prije nego što je netko primijeti
Je li samopotpisani certifikat siguran za produkcijsko potpisivanje?
Ne: samopotpisani certifikat siguran je za provjeru putanje koda za potpisivanje, a nije siguran za potpis kojem bi itko izvan tima trebao vjerovati, jer ga ništa ne povezuje s korijenom kojem softver strane koja se oslanja na potpis već vjeruje. Prirodni sljedeći korak za ovakav PFX jest stvarni poziv za potpisivanje, opisan u članku izrada radnog okruženja za usklađenost i potpisivanje u Delphiju s PDFlibPasom, gdje PFX izrađen na ovaj način pokreće dio cjevovoda za potpisivanje koji također izvršava provjeru PDF/A i nadzor raspona ByteRange. Potpisivanje je ipak samo polovica svega što okružuje certifikat, a druga polovica upravo je mjesto na kojem samopotpisani krajnji certifikat treba pasti: članak PAdES potpisivanje i provjera valjanosti u Delphiju s PDFlibPasom opisuje provjere lanca povjerenja koje izvršava validator sukladnosti, a validator koji lanac prati natrag do pouzdanog korijena nema razloga vjerovati certifikatu koji je ova funkcija prije pet minuta stvorila ni iz čega
PLCreateSelfSignedCertificate jedna je od funkcija u API-jima za certifikate i potpisivanje u PDF biblioteci PDFlibPas za Delphi i C++Builder i postoji upravo zbog ovdje opisanog nedostatka: testu potpisivanja potreban je stvarni par ključeva, bez vanjskog alata za njegovo generiranje