Funkce PDFlibPas 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. PDFlibPas 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=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;
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
// 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;
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
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 PDFlibPas. 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
// 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
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
// 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 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, PDFlibPas 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
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 PDFlibPas, 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 PDFlibPas 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 PDFlibPas 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