Funkcija PLCreateSelfSignedCertificate v PDFlibPas ustvari samopodpisano potrdilo RSA/SHA-256 in ga skupaj z zasebnim ključem izvozi neposredno v datoteko PFX, zaščiteno z geslom, pri tem pa uporablja samo Win32 CryptoAPI, ki je že nameščen na vsakem računalniku Windows. Brez zunanjega orodja, brez overitelja potrdil, brez ročnega koraka z makecert ali OpenSSL: en klic funkcije in eno potrdilo, dovolj dobro za izvedbo preizkusa podpisovanja
Primer, zaradi katerega je ta funkcija skoraj vedno koristna, je cevovod CI. Preizkus podpisovanja potrebuje pravi PFX z resničnim zasebnim ključem, vendar je shranjevanje take datoteke v repozitorij lasten varnostni problem, saj je zasebni ključ od trenutka objave tega commita razkrit ključ. Klicanje makecert.exe ali OpenSSL iz gradbenega skripta prav tako deluje, vendar je cevovod takrat odvisen od orodja, ki mora biti nameščeno, dosegljivo prek PATH in različicno usklajeno na vsakem gradilnem agentu. Ustvarjanje potrdila v istem procesu, ki izvaja preizkus, z istimi klici Win32 CryptoAPI, ki jih Windows že vsebuje, to odvisnost v celoti odstrani
Kaj dejansko ustvari PLCreateSelfSignedCertificate?
PLCreateSelfSignedCertificate ustvari z geslom zaščiteno datoteko PFX, ki vsebuje samopodpisano potrdilo RSA in njegov zasebni ključ, podpisan z sha256RSA, pri čemer jo določa pet parametrov: SubjectName, PFXFileName, PFXPassword, ValidDays in KeyBits, vrne pa navadno zastavico uspeha tipa Boolean. SubjectName sprejme celoten niz X.500, kot je 'CN=Alice, O=Example', golo ime brez znaka = pa samodejno dobi predpono CN=. Vrednost ValidDays, manjša od 1, se nadomesti z 365, vrednost KeyBits zunaj razpona od 1024 do 16384 pa z 2048. PDFlibPas to funkcijo ponuja od v3.224.0, dostopna pa ni samo iz enote Delphi, temveč tudi prek vmesnikov DLL in ActiveX. Komentar v njeni dokumentaciji jasno pove, kje je meja uporabnosti: vsak običajni pregledovalnik samopodpisano potrdilo označi kot nezaupanja vredno, razen če ga nekdo izrecno namesti, zato rezultat obravnavajte kot potrdilo za izvajanje kode, ne kot podpis, na katerega bi se smel zanesti kdo zunaj vaše ekipe
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;
Zakaj CryptGenKey kodira dolžino ključa v parametru zastavic?
CryptGenKey združi dve nepovezani nastavitvi v en parameter dwFlags. Spodnja beseda vsebuje vedenjske zastavice, med njimi CRYPT_EXPORTABLE, zgornja beseda pa pri ključu za izmenjavo RSA vsebuje zahtevano dolžino ključa v bitih. Če 2048 posredujete kot še eno zastavico, konča v spodnji besedi, kjer se ne ujema z nobeno vedenjsko zastavico, ki jo določa CryptoAPI, zato klic ustvari ključ s privzeto dolžino ponudnika namesto z dolžino, ki jo je klicatelj želel. Za dejanski ključ RSA z 2048 biti je treba število najprej premakniti v zgornjo besedo
// 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;
Kaj se zgodi, če pozabite CRYPT_EXPORTABLE?
Če isto vrednost zastavic prikrajšate za CRYPT_EXPORTABLE, CryptGenKey še vedno uspe, vendar na ravni CSP označi ustvarjeni zasebni ključ kot neizvozljiv. Tudi vsi nadaljnji koraki še naprej poročajo o uspehu: CertCreateSelfSignCertificate vrne veljaven kontekst potrdila, PFXExportCertStoreEx pa uspe tudi ob uporabi EXPORT_PRIVATE_KEYS in zapiše datoteko PFX, ki se odpre, razčleni in je videti povsem običajna. V njej manjka zasebni ključ, ker ga CSP ni dovolil iznesti iz vsebnika ključev, PFXExportCertStoreEx pa te zavrnitve nikoli ne obravnava kot razlog za neuspeh celotnega izvoza
Napaka se pokaže šele pozneje in povsem drugje: klic podpisovanja odpre ta PFX, najde potrdilo brez pripetega zasebnega ključa in sporoči natanko napako, ki bi jo pričakovali pri poškodovanem ali napačnem PFX, ne pa pri manjkajočem bitu tri plasti višje. Kdor napako odpravlja samo s strani podpisovanja, lahko celo popoldne išče težavo v napačni datoteki, preden ugotovi, da je pravi vzrok en manjkajoči bit pri ustvarjanju ključa v povsem drugem klicu funkcije, morda celo v povsem drugem gradbenem skriptu
Zakaj se mora ProvType ujemati med CryptAcquireContextW in potrdilom?
ProvType se mora ujemati, ker CertCreateSelfSignCertificate poišče zasebni ključ novega potrdila prek zapisa CRYPT_KEY_PROV_INFO, pri čemer mora polje ProvType navesti popolnoma isto vrednost vrste CSP, kot je bila posredovana CryptAcquireContextW ob odprtju vsebnika ključev, v izvedbi PDFlibPas PROV_RSA_AES, številčno 24. Če ProvType nastavite na nič ali na katero koli drugo konstanto ponudnika, ki ne pripada dejanskemu vsebniku, se potrdilo lahko še vedno ustvari, vendar njegova zabeležena povezava do zasebnega ključa ne kaže več na vsebnik, ki ga vsebuje, zato se pozneje pokaže napaka pri podpisovanju ali izvozu, ki nima nič skupnega z dejansko kriptografsko vsebino potrdila
// 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
Celoten potek: od vsebnika GUID do PFX, zaščitenega z geslom
Veriga klicev znotraj PLCreateSelfSignedCertificate poteka po eni ravni poti: odpre nov vsebnik ključev, poimenovan z novo ustvarjenim GUID, da se sočasni zagoni CI nikoli ne spopadejo za imena vsebnikov, v njem ustvari par ključev RSA z zgoraj opisanima zastavicama, kodira SubjectName v blok imena X.500 prek CertStrToNameW in pokliče CertCreateSelfSignCertificate z obdobjem veljavnosti, izračunanim iz ValidDays in posredovanim kot navadna struktura v obliki SYSTEMTIME. Nastali kontekst potrdila se vstavi v pomnilniško shrambo potrdil, odprto s CertOpenStore in CERT_STORE_PROV_MEMORY, samo zato, da ima PFXExportCertStoreEx shrambo za izvoz, saj ta API deluje z ročajem shrambe in ne z golim kontekstom potrdila
// 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 sam uporablja običajni dvoprehodni vzorec Win32: prvič ga pokličete z medpomnilnikom dolžine nič, da ugotovite, koliko bajtov potrebuje PFX, nato dodelite toliko prostora in ga pokličete še drugič, da napolni medpomnilnik. Ko so bajti zapisani na disk, PDFlibPas izbriše začasni vsebnik ključev z CRYPT_DELETEKEYSET, namesto da bi ga pustil za seboj, saj PFX že vsebuje lastno kopijo vsakega bajta ključnega materiala, ki ga je vseboval vsebnik. Če čiščenje izpustite, vsak klic PLCreateSelfSignedCertificate pusti osirotel vsebnik ključev z imenom GUID v profilu klicatelja, kar je natanko vrsta uhajanja, ki jo bo agent CI, če to funkcijo izvaja pri vsaki gradnji, kopičil več mesecev, preden jo kdo opazi
Ali je samopodpisano potrdilo varno za produkcijsko podpisovanje?
Ne: samopodpisano potrdilo je varno za preizkušanje poti kode za podpisovanje, ni pa varno za podpis, ki naj bi mu zaupali ljudje zunaj ekipe, ker ga nič ne povezuje s korenom, ki mu programska oprema prejemnika že zaupa. Naravni naslednji korak za tak PFX je dejanski klic podpisovanja, opisan v članku izdelava delovne mize za skladnost in podpisovanje v Delphiju s PDFlibPas, kjer tako ustvarjen PFX poganja del cevovoda za podpisovanje, ki izvaja tudi predhodno preverjanje PDF/A in revizije ByteRange. Podpisovanje je samo polovica vsega, kar obdaja potrdilo, druga polovica pa je prav področje, na katerem mora samopodpisani končni certifikat odpovedati: podpisovanje in preverjanje PAdES v Delphiju s PDFlibPas pokriva preverjanje verige zaupanja, ki ga izvaja validator skladnosti, validator, ki verigo vodi do zaupanja vrednega korena, pa nima razloga zaupati potrdilu, ki ga je ta funkcija pred petimi minutami ustvarila iz nič
PLCreateSelfSignedCertificate je ena od API-funkcij za potrdila in podpisovanje v knjižnici PDF PDFlibPas za Delphi in C++Builder in obstaja natančno za tukaj opisano vrzel: preizkus podpisovanja potrebuje pravi par ključev, za njegovo ustvarjanje pa ne potrebuje ničesar zunanjega