Technický článek

Vlastnoručně podepsané testovací certifikáty v Delphi s CryptoAPI

Funkce PDF Library for Delphi PLCreateSelfSignedCertificate staví vlastnoručně podepsaný certifikát RSA/SHA-256 a exportuje jej, včetně privátního klíče, rovnou do souboru PFX chráněného heslem, pomocí ničeho jiného než Win32 CryptoAPI, které je už nainstalované na každém stroji s Windows. Žádný externí nástroj, žádná certifikační autorita, žádný ruční krok makecert nebo OpenSSL: jedno volání funkce, jeden certifikát dost dobrý na to, aby prohnal testem podepisování

Scénář, kvůli kterému se tato funkce vyplatí mít, je skoro vždy pipeline CI. Kouřový test podepisování potřebuje skutečné PFX se skutečným privátním klíčem za sebou, a zapsat jeden do repozitáře je vlastní bezpečnostní problém, protože commitnutý privátní klíč je unikly privátní klíč od okamžiku, kdy tento commit přistane. Volání makecert.exe nebo invokace OpenSSL ze skriptu buildu také funguje, ale pak pipeline závisí na nástroji, který se musí nainstalovat, najít na PATH, a udržet verzově konzistentní napříč každým build agentem. Generování certifikátu uvnitř stejného procesu, který spouští test, se stejnými voláními Win32 CryptoAPI, které Windows už dodává, tuto závislost úplně odstraňuje

Co PLCreateSelfSignedCertificate skutečně vyprodukuje?

PLCreateSelfSignedCertificate vyprodukuje heslem chráněný soubor PFX, který drží vlastnoručně podepsaný certifikát RSA a jeho privátní klíč, podepsaný sha256RSA, řízený pěti parametry: SubjectName, PFXFileName, PFXPassword, ValidDays a KeyBits, a vrací obyčejný booleovský příznak úspěchu. SubjectName přijímá plný řetězec X.500 jako 'CN=Alice, O=Example', a holé jméno bez znaku = v něm se automaticky prefixuje CN=. ValidDays pod 1 spadne zpátky na 365, a KeyBits mimo rozsah 1024 až 16384 spadne zpátky na 2048. PDF Library for Delphi tuto funkci dodává od v3.224.0, dosažitelnou nejen z jednotky Delphi, ale i přes plochy DLL a ActiveX, a vlastní dokumentační komentář je přímý o tom, kde přestává být užitečný: každý běžný prohlížeč označí vlastnoručně podepsaný certifikát jako nedůvěryhodný, dokud jej někdo výslovně nenainstaluje, takže berte to, co vyprodukuje, jako certifikát pro procvičení cesty kódu, ne podpis, na který by měl kdokoli mimo váš tým spoléhat

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;

Proč CryptGenKey kóduje délku klíče v parametru flags?

CryptGenKey balí dvě nesouvisející nastavení do jediného parametru dwFlags. Nízké slovo nese behaviorální příznaky, CRYPT_EXPORTABLE mezi nimi, zatímco vysoké slovo, pro klíč výměny klíčů RSA, nese požadovanou délku klíče v bitech. Předání 2048, jako by to byl jen další příznak, jej přistane v nízkém slově místo toho, kde neodpovídá žádnému behaviorálnímu příznaku, který CryptoAPI definuje, takže volání vygeneruje klíč na jakékoli výchozí délce, na kterou poskytovatel spadne zpátky, místo délky, o kterou si volající myslel, že žádá. Získat skutečný 2048bitový klíč RSA znamená nejdřív číslo posunout do vysokého slova

Diagram PDF Library for Delphi: parametr dwFlags CryptGenKey se dělí na vysoké slovo nesoucí KeyBits shl 16 jako délku klíče RSA a nízké slovo s příznaky chování jako CRYPT_EXPORTABLE
Holé 2048 dopadne do půlky příznakového slova, takže poskytovatel potichu spadne na svou výchozí délku klíče
// Délka klíče žije ve vyšších 16 bitech příznaků CryptGenKey;
// nízké slovo nese behaviorální příznaky jako CRYPT_EXPORTABLE.
if not CryptGenKey(hProv, AT_KEYEXCHANGE,
    (Cardinal(KeyBits) shl 16) or CRYPT_EXPORTABLE, hKey) then
  Exit;

Co se stane, když zapomenete CRYPT_EXPORTABLE?

Vypusťte CRYPT_EXPORTABLE ze stejné hodnoty příznaků a CryptGenKey pořád uspěje, ale označí vygenerovaný privátní klíč jako neexportovatelný na úrovni CSP. Vše po směru dál hlásí úspěch: CertCreateSelfSignCertificate vrátí platný kontext certifikátu, a PFXExportCertStoreEx, i volané s EXPORT_PRIVATE_KEYS, přesto uspěje a zapíše soubor PFX, který se otevře, naparsuje a vypadá naprosto obyčejně. Co neobsahuje, je privátní klíč, protože jej CSP odmítl pustit z kontejneru klíče, a PFXExportCertStoreEx toto odmítnutí nikdy nebere jako důvod nechat selhat celý export

Selhání se projeví až později, a úplně jinde: volání podpisu otevře toto PFX, najde certifikát bez připojeného privátního klíče, a nahlásí přesně tu chybu, jakou byste dostali z poškozeného nebo špatného PFX, ne z chybějícího příznaku o tři vrstvy proti proudu. Kdokoli ladí jen ze strany podepisování, může spálit odpoledne nad špatným souborem, dřív než si uvědomí, že skutečná chyba je jeden chybějící bit v okamžiku generování klíče, v úplně jiném volání funkce, možná v úplně jiném skriptu buildu

Diagram PDF Library for Delphi tiché kaskády selhání vyvolané vynecháním CRYPT_EXPORTABLE: generování klíče, tvorba certifikátu i export PFX hlásí úspěch, dokud pozdější podpisové volání nenajde uvnitř PFX žádný soukromý klíč
Vynechání CRYPT_EXPORTABLE vyprodukuje PFX vypadající obyčejně, jehož chybějící privátní klíč se ukáže až při podpisování

Proč se musí ProvType shodovat mezi CryptAcquireContextW a certifikátem?

ProvType se musí shodovat, protože CertCreateSelfSignCertificate rozřešuje privátní klíč nového certifikátu přes záznam CRYPT_KEY_PROV_INFO, a jedno pole v tomto záznamu, ProvType, musí pojmenovat přesně stejnou hodnotu typu CSP předanou CryptAcquireContextW při otevření kontejneru klíče, PROV_RSA_AES, číselně 24, v implementaci PDF Library for Delphi. Nastavte ProvType na nulu, nebo na jakoukoli jinou konstantu poskytovatele, než ke které kontejner skutečně patří, a certifikát se přesto může vytvořit, ale jeho zaznamenaný odkaz zpátky na privátní klíč se už nerozřeší na kontejner, který jej drží, což se projeví později jako selhání podepisování nebo exportu, které nemá nic společného se skutečným kryptografickým obsahem certifikátu

// Typ poskytovatele použitý k otevření kontejneru klíče se musí shodovat
// s typem poskytovatele zaznamenaným v informacích o poskytovateli klíče certifikátu.
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... vygenerovat klíč, sestavit blob jména subjektu, pak:
KeyProvInfo.ProvType := PROV_RSA_AES;   // stejná konstanta, obě volací místa

Sestavení dohromady: od kontejneru GUID k heslem chráněnému PFX

Řetěz volání uvnitř PLCreateSelfSignedCertificate sleduje jednu přímou linii, otevře čerstvý kontejner klíče pojmenovaný podle nově vygenerovaného GUID, aby souběžné běhy CI nikdy nekolidovaly na jménech kontejnerů, vygeneruje uvnitř něj pár klíčů RSA se dvěma příznaky popsanými výše, zakóduje SubjectName do blobu jména X.500 přes CertStrToNameW, a zavolá CertCreateSelfSignCertificate s oknem platnosti spočítaným z ValidDays a předaným jako obyčejná struktura ve tvaru SYSTEMTIME. Výsledný kontext certifikátu jde do úložiště certifikátů v paměti otevřeného pomocí CertOpenStore a CERT_STORE_PROV_MEMORY, čistě proto, aby PFXExportCertStoreEx mělo úložiště, ze kterého exportovat, protože toto API pracuje proti handle úložiště, ne proti holému kontextu certifikátu

// Každé volání otevře jednorázový kontejner pojmenovaný podle čerstvého GUID:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... vygenerovat klíč, vlastnoručně podepsat certifikát, exportovat PFX ...
// pak smazat kontejner, jakmile PFX drží vlastní kopii klíče:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_DELETEKEYSET);

PFXExportCertStoreEx samo dodržuje obyčejnou konvenci Win32 se dvěma průchody: zavolejte ji jednou s bufferem nulové délky, abyste zjistili, kolik bajtů PFX potřebuje, alokujte tolik, pak ji zavolejte znovu, abyste buffer naplnili. Jakmile jsou bajty na disku, PDF Library for Delphi smaže jednorázový kontejner klíče pomocí CRYPT_DELETEKEYSET místo toho, aby jej ponechal za sebou, protože PFX už nese vlastní kopii každého bajtu materiálu klíče, který kontejner držel. Přeskočte tento úklid a každé volání PLCreateSelfSignedCertificate ponechá osiřelý kontejner klíče pojmenovaný podle GUID sedící v profilu volajícího uživatele, což je přesně ten druh úniku, který agent CI spouštějící tuto funkci při každém buildu nahromadí za měsíce, než si toho kdokoli všimne

Diagram PDF Library for Delphi volacího řetězce PLCreateSelfSignedCertificate od kontejneru klíče pojmenovaného podle GUID přes CryptGenKey, CertStrToNameW a CertCreateSelfSignCertificate se odpovídajícím ProvType k dvouprůchodovému exportu PFX a následnému smazání kontejneru
Jednorázový GUID kontejner živí přímočarou řetěz CryptoAPI, který končí v PFX a je hned poté smazán

Je vlastnoručně podepsaný certifikát bezpečný pro produkční podepisování?

Ne: vlastnoručně podepsaný certifikát je bezpečný pro procvičení cesty kódu podepisování a nebezpečný pro podpis, kterému má věřit kdokoli mimo tým, protože nic jej neřetězí zpátky ke kořenu, kterému software spoléhající strany už důvěřuje. Přirozený další krok pro takové PFX je skutečné volání podepisování, popsané v stavbě pracovního nástroje pro shodu a podepisování v Delphi s PDF Library for Delphi, kde PFX postavené tímto způsobem pohání polovinu podepisování v pipeline, která také spouští preflight PDF/A a audity ByteRange. Podepisování je ale jen polovina toho, co sedí kolem certifikátu, a druhá polovina je přesně tam, kde vlastnoručně podepsaný list má selhat: podepisování a validace PAdES v Delphi s PDF Library for Delphi popisuje kontroly řetězu důvěry, které validátor konformity spouští, a validátor, který prochází řetěz zpátky k důvěryhodnému kořeni, nemá důvod věřit certifikátu, který si tato funkce před pěti minutami vymyslela z ničeho

PLCreateSelfSignedCertificate je jedna funkce mezi API pro certifikáty a podepisování v knihovně PDF PDF Library for Delphi pro Delphi a C++Builder, a existuje přesně pro tuto mezeru popsanou zde: test podepisování, který potřebuje za sebou skutečný pár klíčů a nic externího, co by jej vygenerovalo