A PDFlibPas PLCreateSelfSignedCertificate függvénye egy önaláírt RSA/SHA-256 tanúsítványt épít, és exportálja azt, a privátkulccsal együtt, egyenesen egy jelszóval védett PFX-fájlba, semmi mást nem használva, mint a minden Windows-gépen már telepített Win32 CryptoAPI-t. Nincs külső eszköz, nincs tanúsítvány-hatóság, nincs kézi makecert vagy OpenSSL lépés: egy függvényhívás, egy tanúsítvány, amely elég jó egy aláírási teszt meghajtásához
A forgatókönyv, amely ezt a függvényt megéri, szinte mindig egy CI-csővezeték. Egy aláírási füstteszthez egy valódi PFX-re van szükség, valódi privátkulccsal mögötte, és egyet becsekkolni a tárolóba saját biztonsági probléma, mivel egy becsekkolt privátkulcs kiszivárgott privátkulcs attól a pillanattól kezdve, hogy az a commit landol. A makecert.exe-hez vagy egy OpenSSL-híváshoz nyúlás egy build-szkriptből is működik, de akkor a csővezeték egy olyan eszköztől függ, amit telepíteni kell, meg kell találni a PATH-on, és verzió-konzisztensen kell tartani minden build-ügynökön. A tanúsítvány generálása ugyanabban a folyamatban, amely a tesztet futtatja, ugyanazokkal a Win32 CryptoAPI-hívásokkal, amelyeket a Windows már szállít, teljesen eltávolítja azt a függőséget
Mit állít elő ténylegesen a PLCreateSelfSignedCertificate?
A PLCreateSelfSignedCertificate egy jelszóval védett PFX-fájlt állít elő, amely egy önaláírt RSA-tanúsítványt tart a privátkulcsával, sha256RSA-val aláírva, öt paraméter által vezérelve: SubjectName, PFXFileName, PFXPassword, ValidDays, és KeyBits, és egy egyszerű Boolean sikerjelzőt ad vissza. A SubjectName elfogad egy teljes X.500 sztringet, mint 'CN=Alice, O=Example', és egy puszta nevet, amelyben nincs = jel, automatikusan CN=-sel egészít ki. A ValidDays 1 alatt visszaesik 365-re, és a KeyBits az 1024-től 16384-ig terjedő tartományon kívül visszaesik 2048-ra. A PDFlibPas v3.224.0 óta szállítja ezt a függvényt, elérhetően nemcsak a Delphi unitból, hanem a DLL- és ActiveX-felületeken keresztül is, és saját dokumentációs megjegyzése egyenes abban, hol szűnik meg hasznos lenni: minden mainstream megjelenítő nem megbízhatóként jelöli meg az önaláírt tanúsítványt, hacsak valaki kifejezetten nem telepíti azt, így kezeld azt, amit előállít, egy tanúsítványként egy kódútvonal gyakorlásához, ne egy aláírásként, amiben bárkinek a csapaton kívül meg kellene bíznia
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;
Miért kódolja a CryptGenKey a kulcshosszt a flags paraméterben?
A CryptGenKey két, egymással össze nem függő beállítást csomagol egyetlen dwFlags paraméterbe. Az alsó szó viselkedési jelzőket hordoz, köztük a CRYPT_EXPORTABLE-t, míg a felső szó, egy RSA kulcscsere-kulcshoz, a kért kulcshosszt hordozza bitekben. A 2048 átadása, mintha csak egy másik jelző lenne, az alsó szóban landol ehelyett, ahol nem illeszkedik semmilyen viselkedési jelzőhöz, amit a CryptoAPI definiál, így a hívás egy kulcsot generál bármilyen alapértelmezett hosszon, amire a szolgáltató visszaesik, nem azon a hosszon, amit a hívó gondolt, hogy kért. Egy tényleges 2048 bites RSA-kulcs eléréséhez előbb be kell tolni a számot a felső szóba
// 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;
Mi történik, ha elfelejted a CRYPT_EXPORTABLE-t?
Hagyd ki a CRYPT_EXPORTABLE-t ugyanabból a flags-értékből, és a CryptGenKey még mindig sikeres, de a generált privátkulcsot nem-exportálhatóként jelöli meg a CSP-szinten. Minden downstream is sikerről jelent: a CertCreateSelfSignCertificate egy érvényes tanúsítvány-kontextust ad vissza, és a PFXExportCertStoreEx, még az EXPORT_PRIVATE_KEYS-szel is meghívva, mindenképp sikeres, és megír egy PFX-fájlt, amely megnyílik, elemzhető, és teljesen közönségesnek néz ki. Amit nem tartalmaz, az a privátkulcs, mert a CSP megtagadta, hogy kiengedje a kulcstárolójából, és a PFXExportCertStoreEx soha nem kezeli azt a megtagadást okként a teljes export elbuktatására
A hiba csak később mutatkozik meg, és egészen máshol: egy aláíró hívás megnyitja azt a PFX-et, egy tanúsítványt talál privátkulcs csatolása nélkül, és pontosan azt a hibát jelenti, amit egy sérült vagy rossz PFX-ből kapnál, nem egy hiányzó jelzőből három réteggel upstream. Bárki, aki csak az aláírási oldalról debuggol, egy délutánt égethet el a rossz fájlon, mielőtt rájönne, hogy a tényleges hiba egy egyetlen hiányzó bit kulcsgeneráláskor, egy teljesen más függvényhívásban, esetleg egy teljesen más build-szkriptben
Miért kell egyeznie a ProvType-nak a CryptAcquireContextW és a tanúsítvány között?
A ProvType-nak azért kell egyeznie, mert a CertCreateSelfSignCertificate az új tanúsítvány privátkulcsát egy CRYPT_KEY_PROV_INFO rekordon keresztül oldja fel, és annak a rekordnak egy mezője, a ProvType, ugyanazt a CSP-típus-értéket kell megnevezze, amit a CryptAcquireContextW-nek adtak át, amikor a kulcstárolót megnyitották, a PROV_RSA_AES-t, numerikusan 24-et, a PDFlibPas megvalósításában. Állítsd a ProvType-ot nullára, vagy bármely szolgáltató-konstansra, amely nem az, amelyikhez a tároló ténylegesen tartozik, és a tanúsítvány még mindig létrehozható, de a rögzített visszakapcsolása a privátkulcshoz többé nem oldódik fel arra a tárolóra, amely tartja azt, ami később aláírási vagy exportálási hibaként mutatkozik meg, aminek semmi köze a tanúsítvány tényleges kriptográfiai tartalmához
// 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
Összerakva: GUID-tárolótól jelszóval védett PFX-ig
A hívási lánc a PLCreateSelfSignedCertificate-en belül egy egyenes vonalat követ, megnyitva egy friss kulcstárolót egy éppen generált GUID nevével elnevezve, így az egyidejű CI-futtatások soha nem ütköznek tárolónevek fölött, generálva az RSA-kulcspárt belül a fent tárgyalt két jelzővel, kódolva a SubjectName-et egy X.500 névblobba a CertStrToNameW-en keresztül, és meghívva a CertCreateSelfSignCertificate-et egy, a ValidDays-ből számított érvényességi ablakkal, egyszerű SYSTEMTIME-alakú struktúraként átadva. A keletkező tanúsítvány-kontextus egy memóriabeli tanúsítványtárolóba kerül, amelyet CertOpenStore-ral és CERT_STORE_PROV_MEMORY-vel nyitottak meg, tisztán azért, hogy a PFXExportCertStoreEx-nek legyen egy tárolója, amiből exportálhat, mivel az az API egy tároló-handle ellen működik, nem egy puszta tanúsítvány-kontextus ellen
// 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);
Maga a PFXExportCertStoreEx a szokásos Win32 kétátfutásos konvenciót követi: hívd meg egyszer egy nulla hosszúságú pufferrel, hogy megtudd, hány bájtra van szüksége a PFX-nek, allokálj annyit, majd hívd meg újra, hogy kitöltse a puffert. Amint a bájtok a lemezen vannak, a PDFlibPas törli az eldobható kulcstárolót a CRYPT_DELETEKEYSET-tel, ahelyett hogy hátrahagyná, mert a PFX már hordozza a saját másolatát minden bájtnyi kulcsanyagnak, amit a tároló tartott. Hagyd ki azt a takarítást, és minden hívás a PLCreateSelfSignedCertificate-hez egy árva, GUID-elnevezett kulcstárolót hagy ülni a hívó felhasználó profiljában, ami pontosan az a fajta szivárgás, amit egy CI-ügynök, amely minden buildnél futtatja ezt a függvényt, hónapokig felhalmoz, mielőtt bárki észrevenné
Biztonságos-e egy önaláírt tanúsítványt termelési aláíráshoz használni?
Nem: egy önaláírt tanúsítvány biztonságos egy aláírási kódútvonal gyakorlásához, és nem biztonságos egy aláíráshoz, amelyben bárkinek a csapaton kívül meg kellene bíznia, mert semmi nem köti vissza egy gyökérhez, amiben egy bízó fél szoftvere már megbízik. A természetes következő lépés egy ilyen PFX-hez egy tényleges aláíró hívás, amit a megfelelőségi és aláírási munkapad Delphiben a PDFlibPasszal történő építése tárgyal, ahol egy így épített PFX vezérli egy csővezeték aláírási felét, amely PDF/A preflight-ot és ByteRange-audit-okat is futtat. Az aláírás azonban csak fele annak, ami egy tanúsítvány körül van, és a másik fele pontosan az, ahol egy önaláírt levélnek el kell buknia: a PAdES-aláírás és -érvényesítés Delphiben a PDFlibPasszal tárgyalja a bizalmi-lánc ellenőrzéseket, amelyeket egy megfelelőségi validátor futtat, és egy validátor, amely visszafelé bejárja a láncot egy megbízható gyökérig, semmi okot nem lát megbízni egy tanúsítványban, amit ez a függvény öt perccel ezelőtt talált ki a semmiből
A PLCreateSelfSignedCertificate egy függvény a Delphihez és C++Builderhez készült PDFlibPas PDF-könyvtár tanúsítvány- és aláírás-API-i között, és pontosan az itt leírt résért létezik: egy aláírási teszt, amelynek szüksége van egy valódi kulcspárra mögötte, és semmi külsőre annak generálásához