Odborný článok

Vlastnoručne podpísané testovacie certifikáty v Delphi cez CryptoAPI

Funkcia PLCreateSelfSignedCertificate v PDFlibPas vytvorí vlastnoručne podpísaný certifikát RSA/SHA-256 a exportuje ho spolu so súkromným kľúčom priamo do heslom chráneného súboru PFX, pričom používa výlučne Win32 CryptoAPI, ktoré je nainštalované na každom počítači s Windows. Žiadny externý nástroj, žiadna certifikačná autorita, žiadny ručný krok cez makecert alebo OpenSSL: jedno volanie funkcie, jeden certifikát dostatočne dobrý na to, aby poháňal test podpisovania

Scenár, pre ktorý táto funkcia stojí za to, je takmer vždy CI pipeline. Dymový test podpisovania potrebuje skutočný PFX so skutočným súkromným kľúčom za sebou, a uloženie takého súboru do repozitára je samo osebe bezpečnostný problém, keďže commitnutý súkromný kľúč je od okamihu, keď tento commit pristane, únikom súkromného kľúča. Volanie makecert.exe alebo príkazu OpenSSL zo skriptu zostavy tiež funguje, no potom pipeline závisí od nástroja, ktorý musí byť nainštalovaný, dostupný v PATH a udržiavaný v konzistentnej verzii naprieč každým buildovacím agentom. Generovanie certifikátu priamo v tom istom procese, ktorý spúšťa test, s tými istými volaniami Win32 CryptoAPI, ktoré Windows už dodáva, túto závislosť úplne odstraňuje

Čo presne produkuje PLCreateSelfSignedCertificate?

PLCreateSelfSignedCertificate produkuje heslom chránený súbor PFX obsahujúci vlastnoručne podpísaný certifikát RSA a jeho súkromný kľúč, podpísaný pomocou sha256RSA, riadený piatimi parametrami: SubjectName, PFXFileName, PFXPassword, ValidDays a KeyBits, a vracia jednoduchý booleovský príznak úspechu. SubjectName prijíma úplný reťazec X.500 ako 'CN=Alice, O=Example', a holé meno bez znaku = sa automaticky doplní predponou CN=. ValidDays pod hodnotou 1 sa nahradí predvolenou hodnotou 365 a KeyBits mimo rozsahu od 1024 do 16384 sa nahradí predvolenou hodnotou 2048. PDFlibPas obsahuje túto funkciu od verzie v3.224.0, dostupnú nielen z jednotky pre Delphi, ale aj cez povrch DLL a ActiveX, a jej vlastný komentár v dokumentácii je otvorený v tom, kde jej užitočnosť končí: každý bežný prehliadač označí vlastnoručne podpísaný certifikát ako nedôveryhodný, pokiaľ ho niekto výslovne nenainštaluje, takže s tým, čo táto funkcia produkuje, zaobchádzajte ako s certifikátom na overenie cesty kódu, nie ako s podpisom, na ktorý by sa mal spoliehať niekto mimo vášho tímu

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;

Prečo CryptGenKey kóduje dĺžku kľúča v parametri príznakov?

CryptGenKey zbaľuje dve nesúvisiace nastavenia do jediného parametra dwFlags. Dolné slovo nesie príznaky správania, medzi nimi CRYPT_EXPORTABLE, zatiaľ čo horné slovo nesie pre výmenný kľúč RSA požadovanú dĺžku kľúča v bitoch. Odovzdanie hodnoty 2048, akoby to bol len ďalší príznak, ju umiestni namiesto toho do dolného slova, kde nezodpovedá žiadnemu príznaku správania definovanému CryptoAPI, takže volanie vygeneruje kľúč s takou dĺžkou, na akú sa poskytovateľ predvolene vráti, nie s dĺžkou, o ktorú si volajúci myslel, že žiada. Získanie skutočne 2048-bitového kľúča RSA znamená najprv posunúť číslo do horného slova

// 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;

Čo sa stane, ak zabudnete na CRYPT_EXPORTABLE?

Vynechajte CRYPT_EXPORTABLE z tej istej hodnoty príznakov a CryptGenKey aj tak uspeje, no na úrovni CSP označí vygenerovaný súkromný kľúč ako neexportovateľný. Všetko ďalej v reťazci naďalej hlási úspech: CertCreateSelfSignCertificate vráti platný kontext certifikátu a PFXExportCertStoreEx, aj keď je volaná s EXPORT_PRIVATE_KEYS, aj tak uspeje a zapíše súbor PFX, ktorý sa dá otvoriť, spracovať a vyzerá úplne bežne. Čo neobsahuje, je súkromný kľúč, pretože CSP odmietlo dovoliť mu opustiť kontajner kľúča, a PFXExportCertStoreEx nikdy nepovažuje toto odmietnutie za dôvod na zlyhanie celého exportu

Chyba sa prejaví až neskôr, a úplne inde: volanie podpisovania otvorí tento PFX, nájde certifikát bez pripojeného súkromného kľúča a nahlási presne takú chybu, akú by ste dostali z poškodeného alebo nesprávneho PFX, nie z chýbajúceho príznaku o tri vrstvy vyššie. Ktokoľvek, kto ladí len zo strany podpisovania, môže stráviť celé popoludnie nad nesprávnym súborom, kým si uvedomí, že skutočná chyba je jediný chýbajúci bit v čase generovania kľúča, v úplne inom volaní funkcie, možno v úplne inom skripte zostavy

Prečo sa ProvType musí zhodovať medzi CryptAcquireContextW a certifikátom?

ProvType sa musí zhodovať preto, lebo CertCreateSelfSignCertificate rieši súkromný kľúč nového certifikátu cez záznam CRYPT_KEY_PROV_INFO, a jedno pole v tomto zázname, ProvType, musí pomenovať presne tú istú hodnotu typu CSP, aká bola odovzdaná CryptAcquireContextW pri otváraní kontajnera kľúča, PROV_RSA_AES, numericky 24, v implementácii PDFlibPas. Nastavte ProvType na nulu alebo na akúkoľvek inú konštantu poskytovateľa než tú, ktorej kontajner v skutočnosti patrí, a certifikát sa aj tak dá vytvoriť, no jeho zaznamenaný odkaz späť na súkromný kľúč sa už nevyrieši na kontajner, ktorý ho drží, čo sa neskôr prejaví ako zlyhanie podpisovania alebo exportu, ktoré nemá nič spoločné so skutočným kryptografickým obsahom certifikátu

// 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

Poskladanie celku: od kontajnera s GUID po PFX chránený heslom

Reťazec volaní vnútri PLCreateSelfSignedCertificate sleduje jednu priamu líniu: otvorí nový kontajner kľúča pomenovaný podľa čerstvo vygenerovaného GUID, aby sa súbežné behy CI nikdy nezrazili na názvoch kontajnerov, vygeneruje v ňom pár kľúčov RSA s dvomi príznakmi opísanými vyššie, zakóduje SubjectName do bloku mena X.500 cez CertStrToNameW a zavolá CertCreateSelfSignCertificate s oknom platnosti vypočítaným z ValidDays a odovzdaným ako obyčajná štruktúra v tvare SYSTEMTIME. Výsledný kontext certifikátu ide do pamäťového úložiska certifikátov otvoreného pomocou CertOpenStore a CERT_STORE_PROV_MEMORY, čisto preto, aby PFXExportCertStoreEx malo z čoho exportovať, keďže toto API pracuje s handlom úložiska, nie s holým kontextom certifikátu

// 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 dodržiava bežnú dvojpriechodovú konvenciu Win32: zavolajte ju raz s bufferom nulovej dĺžky, aby ste zistili, koľko bajtov PFX potrebuje, alokujte toľko a zavolajte ju znova na naplnenie bufferu. Akonáhle sú bajty na disku, PDFlibPas odstráni jednorazový kontajner kľúča pomocou CRYPT_DELETEKEYSET namiesto toho, aby ho ponechala za sebou, pretože PFX už nesie vlastnú kópiu každého bajtu materiálu kľúča, ktorý kontajner držal. Vynechajte toto upratanie a každé volanie PLCreateSelfSignedCertificate zanechá osirotený, podľa GUID pomenovaný kontajner kľúča v profile volajúceho používateľa, čo je presne ten druh úniku, ktorý sa v agentovi CI spúšťajúcom túto funkciu pri každej zostave hromadí mesiace, kým si to niekto nevšimne

Je vlastnoručne podpísaný certifikát bezpečný na produkčné podpisovanie?

Nie: vlastnoručne podpísaný certifikát je bezpečný na overenie cesty kódu podpisovania a nebezpečný pre podpis, ktorému by mal dôverovať niekto mimo tímu, pretože nič ho nespája späť s koreňom, ktorému už dôveruje softvér spoliehajúcej sa strany. Prirodzeným ďalším krokom pre takýto PFX je skutočné volanie podpisovania, opísané v článku budovanie pracoviska pre súlad a podpisovanie v Delphi s PDFlibPas, kde takto vytvorený PFX poháňa podpisovaciu polovicu pipeline, ktorá zároveň spúšťa preflight kontrolu PDF/A a audity ByteRange. Podpisovanie je však len polovica toho, čo sa okolo certifikátu odohráva, a druhá polovica je presne to miesto, kde má vlastnoručne podpísaný list zlyhať: podpisovanie a overovanie PAdES v Delphi s PDFlibPas pokrýva kontroly reťazca dôvery, ktoré vykonáva validátor zhody, a validátor, ktorý prejde reťazec späť k dôveryhodnému koreňu, nemá dôvod dôverovať certifikátu, ktorý si táto funkcia pred piatimi minútami vymyslela z ničoho

PLCreateSelfSignedCertificate je jedna z funkcií medzi API pre certifikáty a podpisovanie v PDF knižnici PDFlibPas pre Delphi a C++Builder a existuje presne kvôli tu opísanej medzere: test podpisovania, ktorý potrebuje za sebou skutočný pár kľúčov a nič externé na jeho vygenerovanie