Tehnički članak

Testni samopotpisani certifikati u Delphi-ju s CryptoAPI

Funkcija PLCreateSelfSignedCertificate u biblioteci PDF Library for Delphi 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. PDF Library for Delphi 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=PDF Library for Delphi 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č

PDF Library for Delphi dijagram CryptGenKey dwFlags parametra koji se rastavlja na gornju riječ s KeyBits shl 16 kao duljinom RSA ključa i donju riječ s zastavicama ponašanja poput CRYPT_EXPORTABLE
Goli 2048 završi u dijelu sa zastavicama ponašanja, pa davatelj utiho prelazi na zadanu duljinu ključa
// Duljina ključa živi u gornjih 16 bita CryptGenKey zastavica;
// niža riječ nosi zastavice ponašanja poput 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

PDF Library for Delphi dijagram tihog kaskadnog neuspjeha izazvanog izostavljanjem CRYPT_EXPORTABLE, gdje generiranje ključa, stvaranje certifikata i PFX izvoz svi javljaju uspjeh sve dok kasniji poziv potpisivanja ne nađe da u PFX-u nema privatnog ključa
Izostavljanje CRYPT_EXPORTABLE proizvodi uobičajeno izgledajući PFX čiji nedostajući privatni ključ ispliva tek pri potpisivanju

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 PDF Library for Delphi. 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

// Tip pružatelja usluge korišten za otvaranje spremnika ključa mora se podudarati s
// tipom pružatelja zabilježenim u podacima o pružatelju ključa certifikata.
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... generiraj ključ, izgradi blob imena subjekta, zatim:
KeyProvInfo.ProvType := PROV_RSA_AES;   // ista konstanta, oba mjesta poziva

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

// Svaki poziv otvara jednokratni spremnik imenovan po svježem GUID-u:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... generiraj ključ, samopotpiši certifikat, izvezi PFX ...
// zatim izbriši spremnik nakon što PFX sadrži vlastitu kopiju ključa:
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 PDF Library for Delphi 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

PDF Library for Delphi dijagram lanca poziva PLCreateSelfSignedCertificate od ključnog spremnika s GUID nazivom preko CryptGenKey, CertStrToNameW i CertCreateSelfSignCertificate s odgovarajućim ProvType do dvoprolaznog PFX izvoza, nakon čega slijedi brisanje spremnika
Jednokratni GUID spremnik napaja izravni CryptoAPI lanac koji završava u PFX-u i briše se odmah nakon toga

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 PDF Library for Delphi 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